mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 19:21:50 +02:00
wip - feat: add monaco editor
This commit is contained in:
@ -40,6 +40,7 @@ _Widgets = {
|
|||||||
"Image": "Image",
|
"Image": "Image",
|
||||||
"LogPanel": "LogPanel",
|
"LogPanel": "LogPanel",
|
||||||
"Minesweeper": "Minesweeper",
|
"Minesweeper": "Minesweeper",
|
||||||
|
"MonacoWidget": "MonacoWidget",
|
||||||
"MotorMap": "MotorMap",
|
"MotorMap": "MotorMap",
|
||||||
"MultiWaveform": "MultiWaveform",
|
"MultiWaveform": "MultiWaveform",
|
||||||
"PositionIndicator": "PositionIndicator",
|
"PositionIndicator": "PositionIndicator",
|
||||||
@ -1879,6 +1880,68 @@ class LogPanel(RPCBase):
|
|||||||
class Minesweeper(RPCBase): ...
|
class Minesweeper(RPCBase): ...
|
||||||
|
|
||||||
|
|
||||||
|
class MonacoWidget(RPCBase):
|
||||||
|
"""A simple Monaco editor widget"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_text(self, text: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the text in the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The text to set in the editor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def get_text(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the current text from the Monaco editor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_language(self, language: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the programming language for syntax highlighting in the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
language (str): The programming language to set (e.g., "python", "javascript").
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_theme(self, theme: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the theme for the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
theme (str): The theme to set (e.g., "vs-dark", "light").
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_read_only(self, read_only: bool) -> None:
|
||||||
|
"""
|
||||||
|
Set the Monaco editor to read-only mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
read_only (bool): If True, the editor will be read-only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_cursor(
|
||||||
|
self,
|
||||||
|
line: int,
|
||||||
|
column: int = 1,
|
||||||
|
move_to_position: Literal[None, "center", "top", "position"] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Set the cursor position in the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line (int): Line number (1-based).
|
||||||
|
column (int): Column number (1-based), defaults to 1.
|
||||||
|
move_to_position (Literal[None, "center", "top", "position"], optional): Position to move the cursor to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class MotorMap(RPCBase):
|
class MotorMap(RPCBase):
|
||||||
"""Motor map widget for plotting motor positions in 2D including a trace of the last points."""
|
"""Motor map widget for plotting motor positions in 2D including a trace of the last points."""
|
||||||
|
|
||||||
|
0
bec_widgets/widgets/editors/monaco/__init__.py
Normal file
0
bec_widgets/widgets/editors/monaco/__init__.py
Normal file
0
bec_widgets/widgets/editors/monaco/core/__init__.py
Normal file
0
bec_widgets/widgets/editors/monaco/core/__init__.py
Normal file
65
bec_widgets/widgets/editors/monaco/core/bridge_base.py
Normal file
65
bec_widgets/widgets/editors/monaco/core/bridge_base.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from qtpy.QtCore import QObject, Signal, Slot
|
||||||
|
|
||||||
|
|
||||||
|
class BaseBridge(QObject):
|
||||||
|
initialized = Signal()
|
||||||
|
sendDataChanged = Signal(str, str)
|
||||||
|
completion = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self._initialized = False
|
||||||
|
self._buffer = []
|
||||||
|
self.initialized.connect(self._process_startup_buffer)
|
||||||
|
|
||||||
|
def _process_startup_buffer(self):
|
||||||
|
"""
|
||||||
|
Process the buffer of data that was sent before the bridge was initialized.
|
||||||
|
This is useful for sending initial data to the JavaScript side.
|
||||||
|
"""
|
||||||
|
for name, value in self._buffer:
|
||||||
|
self._send_to_js(name, value)
|
||||||
|
self._buffer.clear()
|
||||||
|
|
||||||
|
# Update the local buffer by reading the current state
|
||||||
|
# This is mostly to ensure that we are in sync with the JS side
|
||||||
|
self._send_to_js("read", "")
|
||||||
|
|
||||||
|
def _send_to_js(self, name, value):
|
||||||
|
if not self._initialized:
|
||||||
|
self._buffer.append((name, value))
|
||||||
|
return
|
||||||
|
data = json.dumps(value)
|
||||||
|
self.sendDataChanged.emit(name, data)
|
||||||
|
|
||||||
|
@Slot(str, str)
|
||||||
|
def receive_from_js(self, name, value):
|
||||||
|
data = json.loads(value)
|
||||||
|
|
||||||
|
if name == "bridge_initialized":
|
||||||
|
self._initialized = data
|
||||||
|
self.initialized.emit()
|
||||||
|
return
|
||||||
|
if name == "setValue":
|
||||||
|
self.on_value_changed(data)
|
||||||
|
return
|
||||||
|
print(f"Received from JS: {name} = {data}")
|
||||||
|
self.setProperty(name, data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bridge_initialized(self):
|
||||||
|
return self._initialized
|
||||||
|
|
||||||
|
@bridge_initialized.setter
|
||||||
|
def bridge_initialized(self, value):
|
||||||
|
if self._initialized != value:
|
||||||
|
self._initialized = value
|
||||||
|
self.initialized.emit()
|
||||||
|
|
||||||
|
def on_value_changed(self, value):
|
||||||
|
"""
|
||||||
|
Placeholder method to handle value changes.
|
||||||
|
This can be overridden in subclasses to implement specific behavior.
|
||||||
|
"""
|
101
bec_widgets/widgets/editors/monaco/core/editor_bridge.py
Normal file
101
bec_widgets/widgets/editors/monaco/core/editor_bridge.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from qtpy.QtCore import Signal
|
||||||
|
|
||||||
|
from bec_widgets.widgets.editors.monaco.core.bridge_base import BaseBridge
|
||||||
|
|
||||||
|
|
||||||
|
class EditorBridge(BaseBridge):
|
||||||
|
valueChanged = Signal()
|
||||||
|
languageChanged = Signal()
|
||||||
|
themeChanged = Signal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self._value = ""
|
||||||
|
self._language = ""
|
||||||
|
self._theme = ""
|
||||||
|
self._readonly = False
|
||||||
|
|
||||||
|
def on_value_changed(self, value):
|
||||||
|
"""Handle value changes from the JavaScript side."""
|
||||||
|
self.setValue(value)
|
||||||
|
|
||||||
|
def getValue(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def setValue(self, value: str):
|
||||||
|
"""
|
||||||
|
Set the value in the editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (str): The new value to set in the editor.
|
||||||
|
"""
|
||||||
|
if self._value == value:
|
||||||
|
return
|
||||||
|
if self._readonly:
|
||||||
|
raise ValueError("Editor is in read-only mode, cannot set value.")
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError("Value must be a string.")
|
||||||
|
self._value = value
|
||||||
|
self.valueChanged.emit()
|
||||||
|
|
||||||
|
def getLanguage(self):
|
||||||
|
return self._language
|
||||||
|
|
||||||
|
def setLanguage(self, language):
|
||||||
|
self._language = language
|
||||||
|
self._send_to_js("language", language)
|
||||||
|
self.languageChanged.emit()
|
||||||
|
|
||||||
|
def getTheme(self):
|
||||||
|
return self._theme
|
||||||
|
|
||||||
|
def setTheme(self, theme):
|
||||||
|
self._theme = theme
|
||||||
|
self._send_to_js("theme", theme)
|
||||||
|
self.themeChanged.emit()
|
||||||
|
|
||||||
|
def setReadOnly(self, read_only: bool):
|
||||||
|
"""Set the editor to read-only mode."""
|
||||||
|
self._send_to_js("readOnly", read_only)
|
||||||
|
self._readonly = read_only
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def language(self):
|
||||||
|
return self._language
|
||||||
|
|
||||||
|
@property
|
||||||
|
def theme(self):
|
||||||
|
return self._theme
|
||||||
|
|
||||||
|
def setHost(self, host: str):
|
||||||
|
"""
|
||||||
|
Set the host for the editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host (str): The host URL for the editor.
|
||||||
|
"""
|
||||||
|
self._send_to_js("lsp_url", host)
|
||||||
|
|
||||||
|
def setCursor(
|
||||||
|
self,
|
||||||
|
line: int,
|
||||||
|
column: int = 1,
|
||||||
|
move_to_position: Literal[None, "center", "top", "position"] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Set the cursor position in the editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line (int): Line number (1-based).
|
||||||
|
column (int): Column number (1-based), defaults to 1.
|
||||||
|
move_to_position (Literal[None, "center", "top", "position"], optional): Position to move the cursor to.
|
||||||
|
"""
|
||||||
|
self._send_to_js(
|
||||||
|
"setCursor", {"line": line, "column": column, "moveToPosition": move_to_position}
|
||||||
|
)
|
BIN
bec_widgets/widgets/editors/monaco/core/monaco.rcc
Normal file
BIN
bec_widgets/widgets/editors/monaco/core/monaco.rcc
Normal file
Binary file not shown.
9
bec_widgets/widgets/editors/monaco/core/monaco_page.py
Normal file
9
bec_widgets/widgets/editors/monaco/core/monaco_page.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from qtpy.QtWebEngineCore import QWebEnginePage
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class MonacoPage(QWebEnginePage):
|
||||||
|
def javaScriptConsoleMessage(self, level, message, line, source):
|
||||||
|
logger.debug(f"[JS Console] {level.name} at line {line} in {source}: {message}")
|
122
bec_widgets/widgets/editors/monaco/core/monaco_web_view.py
Normal file
122
bec_widgets/widgets/editors/monaco/core/monaco_web_view.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from qtpy.QtCore import Signal
|
||||||
|
from qtpy.QtWebChannel import QWebChannel
|
||||||
|
from qtpy.QtWebEngineWidgets import QWebEngineView
|
||||||
|
|
||||||
|
from bec_widgets.widgets.editors.monaco.core.editor_bridge import EditorBridge
|
||||||
|
from bec_widgets.widgets.editors.monaco.core.monaco_page import MonacoPage
|
||||||
|
from bec_widgets.widgets.editors.monaco.core.resource_loader import (
|
||||||
|
get_monaco_base_url,
|
||||||
|
get_monaco_html,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pylsp_host() -> str:
|
||||||
|
"""
|
||||||
|
Get the host address for the PyLSP server.
|
||||||
|
This function initializes the PyLSP server if it is not already running
|
||||||
|
and returns the host address in the format 'localhost:port'.
|
||||||
|
Returns:
|
||||||
|
str: The host address of the PyLSP server.
|
||||||
|
"""
|
||||||
|
# lazy import to only load when needed
|
||||||
|
from bec_widgets.widgets.editors.monaco.core.pylsp_provider import pylsp_server
|
||||||
|
|
||||||
|
if not pylsp_server.is_running():
|
||||||
|
pylsp_server.start()
|
||||||
|
|
||||||
|
return f"localhost:{pylsp_server.port}"
|
||||||
|
|
||||||
|
|
||||||
|
class MonacoWebView(QWebEngineView):
|
||||||
|
initialized = Signal()
|
||||||
|
textChanged = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
|
self.pylsp_host = get_pylsp_host()
|
||||||
|
|
||||||
|
self._setup_page()
|
||||||
|
self._setup_bridge()
|
||||||
|
self._load_editor()
|
||||||
|
|
||||||
|
def _setup_page(self):
|
||||||
|
"""Initialize the web engine page."""
|
||||||
|
page = MonacoPage(parent=self)
|
||||||
|
self.setPage(page)
|
||||||
|
|
||||||
|
def _setup_bridge(self):
|
||||||
|
"""Initialize the bridge for communication with JavaScript."""
|
||||||
|
self._channel = QWebChannel(self)
|
||||||
|
self._bridge = EditorBridge()
|
||||||
|
|
||||||
|
self.page().setWebChannel(self._channel)
|
||||||
|
self._channel.registerObject("bridge", self._bridge)
|
||||||
|
|
||||||
|
self._bridge.initialized.connect(self._set_host)
|
||||||
|
self._bridge.initialized.connect(self.initialized)
|
||||||
|
self._bridge.valueChanged.connect(lambda: self.textChanged.emit(self._bridge.value))
|
||||||
|
|
||||||
|
def _load_editor(self):
|
||||||
|
"""Load the Monaco Editor HTML content."""
|
||||||
|
raw_html = get_monaco_html()
|
||||||
|
base_url = get_monaco_base_url()
|
||||||
|
self.setHtml(raw_html, base_url)
|
||||||
|
|
||||||
|
def _set_host(self):
|
||||||
|
"""Set the LSP host once the bridge is initialized."""
|
||||||
|
self._bridge.setHost(self.pylsp_host)
|
||||||
|
|
||||||
|
# Public API methods
|
||||||
|
def text(self):
|
||||||
|
return self._bridge.value
|
||||||
|
|
||||||
|
def set_text(self, text: str):
|
||||||
|
self._bridge.setValue(text)
|
||||||
|
|
||||||
|
def set_cursor(
|
||||||
|
self,
|
||||||
|
line: int,
|
||||||
|
column: int = 1,
|
||||||
|
move_to_position: Literal[None, "center", "top", "position"] = None,
|
||||||
|
):
|
||||||
|
"""Set the cursor position in the editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line (int): Line number (1-based)
|
||||||
|
column (int): Column number (1-based), defaults to 1
|
||||||
|
"""
|
||||||
|
self._bridge.setCursor(line, column, move_to_position)
|
||||||
|
|
||||||
|
def get_language(self) -> str:
|
||||||
|
"""Get the current programming language for syntax highlighting in the editor."""
|
||||||
|
return self._bridge.getLanguage()
|
||||||
|
|
||||||
|
def set_language(self, language: str):
|
||||||
|
"""Set the programming language for syntax highlighting in the editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
language (str): The programming language to set (e.g., "python", "javascript").
|
||||||
|
"""
|
||||||
|
self._bridge.setLanguage(language)
|
||||||
|
|
||||||
|
def get_theme(self):
|
||||||
|
return self._bridge.getTheme()
|
||||||
|
|
||||||
|
def set_theme(self, theme: str):
|
||||||
|
"""Set the theme for the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
theme (str): The theme to apply (e.g., "vs", "vs-dark").
|
||||||
|
"""
|
||||||
|
self._bridge.setTheme(theme)
|
||||||
|
|
||||||
|
def set_read_only(self, read_only: bool):
|
||||||
|
"""Set the editor to read-only mode."""
|
||||||
|
self._bridge.setReadOnly(read_only)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
if hasattr(self._bridge, "shutdown"):
|
||||||
|
self._bridge.shutdown()
|
62
bec_widgets/widgets/editors/monaco/core/pylsp_provider.py
Normal file
62
bec_widgets/widgets/editors/monaco/core/pylsp_provider.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import atexit
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class PyLSPProvider:
|
||||||
|
"""A provider for the PyLSP server."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.port = None
|
||||||
|
self.server_process = None
|
||||||
|
atexit.register(self.stop)
|
||||||
|
signal.signal(signal.SIGINT, self._handle_signal)
|
||||||
|
signal.signal(signal.SIGTERM, self._handle_signal)
|
||||||
|
|
||||||
|
def _find_free_port(self):
|
||||||
|
"""Find a free port for the PyLSP server."""
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.bind(("localhost", 0))
|
||||||
|
self.port = s.getsockname()[1]
|
||||||
|
return self.port
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the PyLSP server."""
|
||||||
|
if self.port is None:
|
||||||
|
self._find_free_port()
|
||||||
|
# Here you would start the PyLSP server using the found port
|
||||||
|
logger.info(f"Starting PyLSP server on port {self.port}")
|
||||||
|
self.server_process = subprocess.Popen(
|
||||||
|
["pylsp", "--ws", "--port", str(self.port)],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the PyLSP server."""
|
||||||
|
if not self.server_process:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.server_process.terminate()
|
||||||
|
try:
|
||||||
|
self.server_process.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self.server_process.kill()
|
||||||
|
self.server_process = None
|
||||||
|
self.port = None
|
||||||
|
|
||||||
|
def _handle_signal(self, signum, frame):
|
||||||
|
"""Handle termination signals."""
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def is_running(self):
|
||||||
|
"""Check if the PyLSP server is running."""
|
||||||
|
return self.server_process is not None and self.server_process.poll() is None
|
||||||
|
|
||||||
|
|
||||||
|
pylsp_server = PyLSPProvider()
|
26
bec_widgets/widgets/editors/monaco/core/resource_loader.py
Normal file
26
bec_widgets/widgets/editors/monaco/core/resource_loader.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from qtpy.QtCore import QFile, QIODevice, QResource, QUrl
|
||||||
|
|
||||||
|
QResource.registerResource(os.path.join(os.path.dirname(__file__), "monaco.rcc"))
|
||||||
|
|
||||||
|
|
||||||
|
def load_resource_html(resource_path: str) -> str:
|
||||||
|
"""Load HTML content from Qt resources."""
|
||||||
|
file = QFile(resource_path)
|
||||||
|
if file.open(QIODevice.OpenModeFlag.ReadOnly):
|
||||||
|
content = file.readAll()
|
||||||
|
file.close()
|
||||||
|
return content.toStdString()
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError(f"Resource not found: {resource_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_monaco_html():
|
||||||
|
"""Get Monaco Editor HTML content from Qt resources."""
|
||||||
|
return load_resource_html(":/monaco/dist/index.html")
|
||||||
|
|
||||||
|
|
||||||
|
def get_monaco_base_url():
|
||||||
|
"""Get the base URL for Monaco Editor resources."""
|
||||||
|
return QUrl("qrc:/monaco/dist/")
|
144
bec_widgets/widgets/editors/monaco/monaco_widget.py
Normal file
144
bec_widgets/widgets/editors/monaco/monaco_widget.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.colors import get_theme_name
|
||||||
|
from bec_widgets.widgets.editors.monaco.core.monaco_web_view import MonacoWebView
|
||||||
|
|
||||||
|
|
||||||
|
class MonacoWidget(BECWidget, QWidget):
|
||||||
|
"""
|
||||||
|
A simple Monaco editor widget
|
||||||
|
"""
|
||||||
|
|
||||||
|
PLUGIN = True
|
||||||
|
ICON_NAME = "code"
|
||||||
|
USER_ACCESS = [
|
||||||
|
"set_text",
|
||||||
|
"get_text",
|
||||||
|
"set_language",
|
||||||
|
"set_theme",
|
||||||
|
"set_read_only",
|
||||||
|
"set_cursor",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
parent=parent, client=client, gui_id=gui_id, config=config, theme_update=True, **kwargs
|
||||||
|
)
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.editor = MonacoWebView(self)
|
||||||
|
layout.addWidget(self.editor)
|
||||||
|
self.setLayout(layout)
|
||||||
|
self.editor.initialized.connect(self.apply_theme)
|
||||||
|
|
||||||
|
def apply_theme(self, theme: str | None = None) -> None:
|
||||||
|
"""
|
||||||
|
Apply the current theme to the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
theme (str, optional): The theme to apply. If None, the current theme will be used.
|
||||||
|
"""
|
||||||
|
if theme is None:
|
||||||
|
theme = get_theme_name()
|
||||||
|
editor_theme = "vs" if theme == "light" else "vs-dark"
|
||||||
|
self.set_theme(editor_theme)
|
||||||
|
|
||||||
|
def set_text(self, text: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the text in the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The text to set in the editor.
|
||||||
|
"""
|
||||||
|
self.editor.set_text(text)
|
||||||
|
|
||||||
|
def get_text(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the current text from the Monaco editor.
|
||||||
|
"""
|
||||||
|
return self.editor.text()
|
||||||
|
|
||||||
|
def set_cursor(
|
||||||
|
self,
|
||||||
|
line: int,
|
||||||
|
column: int = 1,
|
||||||
|
move_to_position: Literal[None, "center", "top", "position"] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Set the cursor position in the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line (int): Line number (1-based).
|
||||||
|
column (int): Column number (1-based), defaults to 1.
|
||||||
|
move_to_position (Literal[None, "center", "top", "position"], optional): Position to move the cursor to.
|
||||||
|
"""
|
||||||
|
self.editor.set_cursor(line, column, move_to_position)
|
||||||
|
|
||||||
|
def set_language(self, language: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the programming language for syntax highlighting in the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
language (str): The programming language to set (e.g., "python", "javascript").
|
||||||
|
"""
|
||||||
|
self.editor.set_language(language)
|
||||||
|
|
||||||
|
def set_read_only(self, read_only: bool) -> None:
|
||||||
|
"""
|
||||||
|
Set the Monaco editor to read-only mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
read_only (bool): If True, the editor will be read-only.
|
||||||
|
"""
|
||||||
|
self.editor.set_read_only(read_only)
|
||||||
|
|
||||||
|
def set_theme(self, theme: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the theme for the Monaco editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
theme (str): The theme to set (e.g., "vs-dark", "light").
|
||||||
|
"""
|
||||||
|
self.editor.set_theme(theme)
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
"""
|
||||||
|
Clean up the widget before closing.
|
||||||
|
"""
|
||||||
|
self.editor.shutdown()
|
||||||
|
super().cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
qapp = QApplication([])
|
||||||
|
widget = MonacoWidget()
|
||||||
|
# set the default size
|
||||||
|
widget.resize(800, 600)
|
||||||
|
widget.set_language("python")
|
||||||
|
widget.set_theme("github-dark")
|
||||||
|
widget.set_text(
|
||||||
|
"""
|
||||||
|
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!")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
widget.show()
|
||||||
|
qapp.exec_()
|
@ -0,0 +1 @@
|
|||||||
|
{'files': ['monaco_widget.py']}
|
54
bec_widgets/widgets/editors/monaco/monaco_widget_plugin.py
Normal file
54
bec_widgets/widgets/editors/monaco/monaco_widget_plugin.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
|
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||||
|
|
||||||
|
DOM_XML = """
|
||||||
|
<ui language='c++'>
|
||||||
|
<widget class='MonacoWidget' name='monaco_widget'>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MonacoWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._form_editor = None
|
||||||
|
|
||||||
|
def createWidget(self, parent):
|
||||||
|
t = MonacoWidget(parent)
|
||||||
|
return t
|
||||||
|
|
||||||
|
def domXml(self):
|
||||||
|
return DOM_XML
|
||||||
|
|
||||||
|
def group(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def icon(self):
|
||||||
|
return designer_material_icon(MonacoWidget.ICON_NAME)
|
||||||
|
|
||||||
|
def includeFile(self):
|
||||||
|
return "monaco_widget"
|
||||||
|
|
||||||
|
def initialize(self, form_editor):
|
||||||
|
self._form_editor = form_editor
|
||||||
|
|
||||||
|
def isContainer(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isInitialized(self):
|
||||||
|
return self._form_editor is not None
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return "MonacoWidget"
|
||||||
|
|
||||||
|
def toolTip(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def whatsThis(self):
|
||||||
|
return self.toolTip()
|
15
bec_widgets/widgets/editors/monaco/register_monaco_widget.py
Normal file
15
bec_widgets/widgets/editors/monaco/register_monaco_widget.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
def main(): # pragma: no cover
|
||||||
|
from qtpy import PYSIDE6
|
||||||
|
|
||||||
|
if not PYSIDE6:
|
||||||
|
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||||
|
return
|
||||||
|
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||||
|
|
||||||
|
from bec_widgets.widgets.editors.monaco.monaco_widget_plugin import MonacoWidgetPlugin
|
||||||
|
|
||||||
|
QPyDesignerCustomWidgetCollection.addCustomWidget(MonacoWidgetPlugin())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
main()
|
@ -23,6 +23,7 @@ dependencies = [
|
|||||||
"PySide6~=6.8.2",
|
"PySide6~=6.8.2",
|
||||||
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
||||||
"qtpy~=2.4",
|
"qtpy~=2.4",
|
||||||
|
"python-lsp-server[all,websockets] ~= 1.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
39
tests/unit_tests/test_monaco_editor.py
Normal file
39
tests/unit_tests/test_monaco_editor.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def monaco_widget(qtbot):
|
||||||
|
widget = MonacoWidget()
|
||||||
|
qtbot.addWidget(widget)
|
||||||
|
qtbot.waitExposed(widget)
|
||||||
|
yield widget
|
||||||
|
|
||||||
|
|
||||||
|
def test_monaco_widget_set_text(monaco_widget: MonacoWidget, qtbot):
|
||||||
|
"""
|
||||||
|
Test that the MonacoWidget can set text correctly.
|
||||||
|
"""
|
||||||
|
test_text = "Hello, Monaco!"
|
||||||
|
monaco_widget.set_text(test_text)
|
||||||
|
qtbot.waitUntil(lambda: monaco_widget.get_text() == test_text, timeout=1000)
|
||||||
|
assert monaco_widget.get_text() == test_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_monaco_widget_readonly(monaco_widget: MonacoWidget, qtbot):
|
||||||
|
"""
|
||||||
|
Test that the MonacoWidget can be set to read-only mode.
|
||||||
|
"""
|
||||||
|
monaco_widget.set_text("Initial text")
|
||||||
|
qtbot.waitUntil(lambda: monaco_widget.get_text() == "Initial text", timeout=1000)
|
||||||
|
monaco_widget.set_read_only(True)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
monaco_widget.set_text("This should not change")
|
||||||
|
|
||||||
|
monaco_widget.set_read_only(False) # Set back to editable
|
||||||
|
qtbot.wait(100)
|
||||||
|
monaco_widget.set_text("Attempting to change text")
|
||||||
|
qtbot.waitUntil(lambda: monaco_widget.get_text() == "Attempting to change text", timeout=1000)
|
||||||
|
assert monaco_widget.get_text() == "Attempting to change text"
|
Reference in New Issue
Block a user