mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 19:21:50 +02:00
feature(vscode): added support for vscode instructions
This commit is contained in:
@ -5,10 +5,19 @@ import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
from qtpy.QtCore import Signal, Slot
|
||||
|
||||
from bec_widgets.widgets.website.website import WebsiteWidget
|
||||
|
||||
|
||||
class VSCodeInstructionMessage(BaseModel):
|
||||
command: Literal["open", "write", "close", "zenMode", "save", "new", "setCursor"]
|
||||
content: str = ""
|
||||
|
||||
|
||||
def get_free_port():
|
||||
"""
|
||||
Get a free port on the local machine.
|
||||
@ -28,6 +37,8 @@ class VSCodeEditor(WebsiteWidget):
|
||||
A widget to display the VSCode editor.
|
||||
"""
|
||||
|
||||
file_saved = Signal(str)
|
||||
|
||||
token = "bec"
|
||||
host = "127.0.0.1"
|
||||
|
||||
@ -41,6 +52,7 @@ class VSCodeEditor(WebsiteWidget):
|
||||
self._url = f"http://{self.host}:{self.port}?tkn={self.token}"
|
||||
super().__init__(parent=parent, config=config, client=client, gui_id=gui_id)
|
||||
self.start_server()
|
||||
self.bec_dispatcher.connect_slot(self.on_vscode_event, f"vscode-events/{self.gui_id}")
|
||||
|
||||
def start_server(self):
|
||||
"""
|
||||
@ -74,6 +86,92 @@ class VSCodeEditor(WebsiteWidget):
|
||||
self.set_url(self._url)
|
||||
self.wait_until_loaded()
|
||||
|
||||
@Slot(str)
|
||||
def open_file(self, file_path: str):
|
||||
"""
|
||||
Open a file in the VSCode editor.
|
||||
|
||||
Args:
|
||||
file_path: The file path to open
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="open", content=f"file://{file_path}")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot(dict, dict)
|
||||
def on_vscode_event(self, content, _metadata):
|
||||
"""
|
||||
Handle the VSCode event. VSCode events are received as RawMessages.
|
||||
|
||||
Args:
|
||||
content: The content of the event
|
||||
metadata: The metadata of the event
|
||||
"""
|
||||
|
||||
# the message also contains the content but I think is fine for now to just emit the file path
|
||||
if not isinstance(content["data"], dict):
|
||||
return
|
||||
if "uri" not in content["data"]:
|
||||
return
|
||||
if not content["data"]["uri"].startswith("file://"):
|
||||
return
|
||||
file_path = content["data"]["uri"].split("file://")[1]
|
||||
self.file_saved.emit(file_path)
|
||||
|
||||
@Slot()
|
||||
def save_file(self):
|
||||
"""
|
||||
Save the file in the VSCode editor.
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="save")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot()
|
||||
def new_file(self):
|
||||
"""
|
||||
Create a new file in the VSCode editor.
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="new")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot()
|
||||
def close_file(self):
|
||||
"""
|
||||
Close the file in the VSCode editor.
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="close")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot(str)
|
||||
def write_file(self, content: str):
|
||||
"""
|
||||
Write content to the file in the VSCode editor.
|
||||
|
||||
Args:
|
||||
content: The content to write
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="write", content=content)
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot()
|
||||
def zen_mode(self):
|
||||
"""
|
||||
Toggle the Zen mode in the VSCode editor.
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="zenMode")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot(int, int)
|
||||
def set_cursor(self, line: int, column: int):
|
||||
"""
|
||||
Set the cursor in the VSCode editor.
|
||||
|
||||
Args:
|
||||
line: The line number
|
||||
column: The column number
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="setCursor", content=f"{line},{column}")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
def cleanup_vscode(self):
|
||||
"""
|
||||
Cleanup the VSCode editor.
|
||||
@ -87,6 +185,7 @@ class VSCodeEditor(WebsiteWidget):
|
||||
"""
|
||||
Cleanup the widget. This method is called from the dock area when the widget is removed.
|
||||
"""
|
||||
self.bec_dispatcher.disconnect_slot(self.on_vscode_event, f"vscode-events/{self.gui_id}")
|
||||
self.cleanup_vscode()
|
||||
return super().cleanup()
|
||||
|
||||
@ -97,7 +196,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = VSCodeEditor()
|
||||
widget = VSCodeEditor(gui_id="unknown")
|
||||
widget.show()
|
||||
app.exec_()
|
||||
widget.bec_dispatcher.disconnect_all()
|
||||
|
@ -25,42 +25,44 @@ def test_vscode_widget(qtbot, vscode_widget):
|
||||
|
||||
|
||||
def test_start_server(qtbot, mocked_client):
|
||||
with mock.patch("bec_widgets.widgets.vscode.vscode.os.killpg") as mock_killpg:
|
||||
with mock.patch("bec_widgets.widgets.vscode.vscode.os.getpgid") as mock_getpgid:
|
||||
with mock.patch("bec_widgets.widgets.vscode.vscode.subprocess.Popen") as mock_popen:
|
||||
with mock.patch("bec_widgets.widgets.vscode.vscode.select.select") as mock_select:
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.vscode.vscode.get_free_port"
|
||||
) as mock_get_free_port:
|
||||
mock_get_free_port.return_value = 12345
|
||||
mock_process = mock.Mock()
|
||||
mock_process.stdout.fileno.return_value = 1
|
||||
mock_process.poll.return_value = None
|
||||
mock_process.stdout.read.return_value = f"available at http://{VSCodeEditor.host}:{12345}?tkn={VSCodeEditor.token}"
|
||||
mock_popen.return_value = mock_process
|
||||
mock_select.return_value = [[mock_process.stdout], [], []]
|
||||
|
||||
with mock.patch("bec_widgets.widgets.vscode.vscode.subprocess.Popen") as mock_popen:
|
||||
with mock.patch("bec_widgets.widgets.vscode.vscode.select.select") as mock_select:
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.vscode.vscode.get_free_port"
|
||||
) as mock_get_free_port:
|
||||
mock_get_free_port.return_value = 12345
|
||||
mock_process = mock.Mock()
|
||||
mock_process.stdout.fileno.return_value = 1
|
||||
mock_process.poll.return_value = None
|
||||
mock_process.stdout.read.return_value = (
|
||||
f"available at http://{VSCodeEditor.host}:{12345}?tkn={VSCodeEditor.token}"
|
||||
)
|
||||
mock_popen.return_value = mock_process
|
||||
mock_select.return_value = [[mock_process.stdout], [], []]
|
||||
widget = VSCodeEditor(client=mocked_client)
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
|
||||
widget = VSCodeEditor(client=mocked_client)
|
||||
|
||||
assert (
|
||||
mock.call(
|
||||
shlex.split(
|
||||
f"code serve-web --port {widget.port} --connection-token={widget.token} --accept-server-license-terms"
|
||||
),
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
preexec_fn=os.setsid,
|
||||
env=mock.ANY,
|
||||
)
|
||||
in mock_popen.mock_calls
|
||||
)
|
||||
assert (
|
||||
mock.call(
|
||||
shlex.split(
|
||||
f"code serve-web --port {widget.port} --connection-token={widget.token} --accept-server-license-terms"
|
||||
),
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
preexec_fn=os.setsid,
|
||||
env=mock.ANY,
|
||||
)
|
||||
in mock_popen.mock_calls
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patched_vscode_process(qtbot, vscode_widget):
|
||||
with mock.patch("bec_widgets.widgets.vscode.vscode.os.killpg") as mock_killpg:
|
||||
mock_killpg.reset_mock()
|
||||
with mock.patch("bec_widgets.widgets.vscode.vscode.os.getpgid") as mock_getpgid:
|
||||
mock_getpgid.return_value = 123
|
||||
vscode_widget.process = mock.Mock()
|
||||
|
Reference in New Issue
Block a user