From ea306868dfa9a1718dcdbd0c89aa264cf4f887b0 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Mon, 19 May 2025 09:24:40 +0200 Subject: [PATCH] wip - subprocess --- .../widgets/editors/monaco/core/index.js | 29 ++-- .../editors/monaco/core/monaco_widget.py | 146 +++++++++++++++++- .../widgets/editors/monaco/monaco_editor.py | 27 +++- 3 files changed, 178 insertions(+), 24 deletions(-) diff --git a/bec_widgets/widgets/editors/monaco/core/index.js b/bec_widgets/widgets/editors/monaco/core/index.js index f782f681..afceaeb3 100644 --- a/bec_widgets/widgets/editors/monaco/core/index.js +++ b/bec_widgets/widgets/editors/monaco/core/index.js @@ -19,24 +19,17 @@ require(["vs/editor/editor.main"], () => { provideCompletionItems: function (model, position, context, token) { return new Promise((resolve, reject) => { const value = model.getValue(); - bridge.requestCompletions( - JSON.stringify({ - code: value, - line: position.lineNumber, - column: position.column, - context: context, - token: token, - }), - (result) => { - try { - const items = JSON.parse(result); - resolve({ suggestions: items }); - } catch (e) { - console.error("Failed to parse completion result:", e); - reject(); - } - } - ); + sendToPython("completion", { + code: value, + line: position.lineNumber, + column: position.column, + context: context, + token: token, + }); + bridge.completion.connect((data) => { + const completionItems = JSON.parse(data); + resolve({ suggestions: completionItems }); + }); }); }, }); diff --git a/bec_widgets/widgets/editors/monaco/core/monaco_widget.py b/bec_widgets/widgets/editors/monaco/core/monaco_widget.py index 0c328377..fcf91894 100644 --- a/bec_widgets/widgets/editors/monaco/core/monaco_widget.py +++ b/bec_widgets/widgets/editors/monaco/core/monaco_widget.py @@ -1,20 +1,150 @@ +import functools import json +import multiprocessing +import threading +import time from pathlib import Path import jedi +from bec_lib.client import BECClient from qtpy.QtCore import Property, QObject, QUrl, Signal, Slot from qtpy.QtWebChannel import * from qtpy.QtWebEngineWidgets import * +class CompletionItemKind: + Text = 1 + Method = 2 + Function = 3 + Constructor = 4 + Field = 5 + Variable = 6 + Class = 7 + Interface = 8 + Module = 9 + Property = 10 + Unit = 11 + Value = 12 + Enum = 13 + Keyword = 14 + Snippet = 15 + Color = 16 + File = 17 + Reference = 18 + Folder = 19 + EnumMember = 20 + Constant = 21 + Struct = 22 + Event = 23 + Operator = 24 + TypeParameter = 25 + + +def completion_worker(request_queue, response_queue): + client = BECClient() + client.start() + while True: + json_str = request_queue.get() + if json_str is None: + continue + if json_str == "exit": + break + + data = json.loads(json_str) + # if data["context"].get("triggerCharacter") != ".": + # response_queue.put(json.dumps([])) + print(client.device_manager.devices) + + print("Received completion request:", data) + start = time.time() + script = jedi.Script(data["code"]) + print("Script created in:", time.time() - start) + + start = time.time() + completions = script.complete(data["line"], data["column"] - 1) + print("Completions created in:", time.time() - start) + start = time.time() + infer_result = script.infer(data["line"], data["column"] - 2) + print("Infer result created in:", time.time() - start) + + if infer_result: + inferred_type = infer_result[0].name + devices = client.device_manager.devices + if inferred_type == "DeviceContainer": + completions = [ + { + "label": device.name, + "kind": 9, + "insertText": device.name, + "documentation": device.name, + } + for device_name, device in devices.items() + ] + response_queue.put(completions) + continue + if inferred_type == "Scans": + completions = [ + { + "label": scan_name, + "kind": CompletionItemKind.Method, + "insertText": scan_name, + "documentation": scan_name, + } + for scan_name, scan in client.scans._available_scans.items() + ] + response_queue.put(completions) + continue + print("Inferred type:", inferred_type) + + if completions: + # sort completions to have private methods at the end + completions = sorted(completions, key=lambda x: (x.name.startswith("_"), x.name)) + + completions = [ + { + "label": completion.name, + "kind": 6, + "insertText": completion.name, + "documentation": completion.description, + } + for completion in completions + ] + # out = {"id": data["id"], "completions": completions} + print("result:", completions, infer_result) + response_queue.put(completions) + client.shutdown() + + class BaseBridge(QObject): initialized = Signal() sendDataChanged = Signal(str, str) + completion = Signal(str) def __init__(self, parent=None): super().__init__(parent=parent) + self.shutdown_event = threading.Event() self.active = False self.queue = [] + self.request_queue = multiprocessing.Queue() + self.response_queue = multiprocessing.Queue() + self.process = multiprocessing.Process( + target=completion_worker, args=(self.request_queue, self.response_queue) + ) + self.process.start() + + self.emitter_thread = threading.Thread(target=self.emitter) + self.emitter_thread.start() + + def emitter(self): + while not self.shutdown_event.is_set(): + try: + data = self.response_queue.get(timeout=0.1) + if data is None: + continue + print("Received data from process:", data) + self.completion.emit(json.dumps(data)) + except multiprocessing.queues.Empty: + continue def send_to_js(self, name, value): if self.active: @@ -26,15 +156,16 @@ class BaseBridge(QObject): @Slot(str, str) def receive_from_js(self, name, value): if name == "completion": - print("Completer received:", value) + self.request_queue.put(value) return data = json.loads(value) self.setProperty(name, data) @Slot(str, result="QVariant") def requestCompletions(self, json_str): - import json - + self.request_queue.put(json_str) + out = self.response_queue.get(timeout=10) + return json.dumps(out) data = json.loads(json_str) if data["context"].get("triggerCharacter") != ".": return json.dumps([]) @@ -80,6 +211,12 @@ class BaseBridge(QObject): self.queue.clear() + def shutdown(self): + self.request_queue.put("exit") + self.shutdown_event.set() + self.emitter_thread.join() + self.process.join() + class EditorBridge(BaseBridge): valueChanged = Signal() @@ -168,3 +305,6 @@ class MonacoWidget(QWebEngineView): def setTheme(self, theme): self._bridge.send_to_js("theme", theme) + + def shutdown(self): + self._bridge.shutdown() diff --git a/bec_widgets/widgets/editors/monaco/monaco_editor.py b/bec_widgets/widgets/editors/monaco/monaco_editor.py index 4f4c1481..2e67d353 100644 --- a/bec_widgets/widgets/editors/monaco/monaco_editor.py +++ b/bec_widgets/widgets/editors/monaco/monaco_editor.py @@ -35,6 +35,13 @@ class MonacoWidget(BECWidget, QWidget): def set_theme(self, theme: str) -> None: self.editor.setTheme(theme) + def cleanup(self) -> None: + """ + Clean up the widget before closing. + """ + self.editor.shutdown() + super().cleanup() + if __name__ == "__main__": qapp = QApplication([]) @@ -43,9 +50,23 @@ if __name__ == "__main__": widget.set_theme("vs-dark") widget.set_text( """ - # This is a comment - def hello_world(): - print("Hello, world!") +import numpy as np +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bec_lib.devicemanager import DeviceContainer + from bec_lib.scans import Scans + dev: DeviceContainer + scans: Scans + +####################################### +########## User Script ##################### +####################################### + + +# This is a comment +def hello_world(): + print("Hello, world!") """ )