0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31: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 socket
import subprocess import subprocess
import sys import sys
from typing import Literal
from pydantic import BaseModel
from qtpy.QtCore import Signal, Slot
from bec_widgets.widgets.website.website import WebsiteWidget 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(): def get_free_port():
""" """
Get a free port on the local machine. Get a free port on the local machine.
@ -28,6 +37,8 @@ class VSCodeEditor(WebsiteWidget):
A widget to display the VSCode editor. A widget to display the VSCode editor.
""" """
file_saved = Signal(str)
token = "bec" token = "bec"
host = "127.0.0.1" host = "127.0.0.1"
@ -41,6 +52,7 @@ class VSCodeEditor(WebsiteWidget):
self._url = f"http://{self.host}:{self.port}?tkn={self.token}" self._url = f"http://{self.host}:{self.port}?tkn={self.token}"
super().__init__(parent=parent, config=config, client=client, gui_id=gui_id) super().__init__(parent=parent, config=config, client=client, gui_id=gui_id)
self.start_server() self.start_server()
self.bec_dispatcher.connect_slot(self.on_vscode_event, f"vscode-events/{self.gui_id}")
def start_server(self): def start_server(self):
""" """
@ -74,6 +86,92 @@ class VSCodeEditor(WebsiteWidget):
self.set_url(self._url) self.set_url(self._url)
self.wait_until_loaded() 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): def cleanup_vscode(self):
""" """
Cleanup the VSCode editor. 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. 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() self.cleanup_vscode()
return super().cleanup() return super().cleanup()
@ -97,7 +196,7 @@ if __name__ == "__main__": # pragma: no cover
from qtpy.QtWidgets import QApplication from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv) app = QApplication(sys.argv)
widget = VSCodeEditor() widget = VSCodeEditor(gui_id="unknown")
widget.show() widget.show()
app.exec_() app.exec_()
widget.bec_dispatcher.disconnect_all() 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): 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: widget = VSCodeEditor(client=mocked_client)
with mock.patch("bec_widgets.widgets.vscode.vscode.select.select") as mock_select: widget.close()
with mock.patch( widget.deleteLater()
"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) assert (
mock.call(
assert ( shlex.split(
mock.call( f"code serve-web --port {widget.port} --connection-token={widget.token} --accept-server-license-terms"
shlex.split( ),
f"code serve-web --port {widget.port} --connection-token={widget.token} --accept-server-license-terms" text=True,
), stdout=subprocess.PIPE,
text=True, stderr=subprocess.DEVNULL,
stdout=subprocess.PIPE, preexec_fn=os.setsid,
stderr=subprocess.DEVNULL, env=mock.ANY,
preexec_fn=os.setsid, )
env=mock.ANY, in mock_popen.mock_calls
) )
in mock_popen.mock_calls
)
@pytest.fixture @pytest.fixture
def patched_vscode_process(qtbot, vscode_widget): def patched_vscode_process(qtbot, vscode_widget):
with mock.patch("bec_widgets.widgets.vscode.vscode.os.killpg") as mock_killpg: 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: with mock.patch("bec_widgets.widgets.vscode.vscode.os.getpgid") as mock_getpgid:
mock_getpgid.return_value = 123 mock_getpgid.return_value = 123
vscode_widget.process = mock.Mock() vscode_widget.process = mock.Mock()