0
0
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:
2024-09-19 20:56:27 +02:00
parent 923867947f
commit f5f1f6c304
2 changed files with 131 additions and 30 deletions

View File

@ -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()

View File

@ -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()