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:
@ -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()
|
||||||
|
@ -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()
|
||||||
|
Reference in New Issue
Block a user