qtmonaco 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ **/*_venv
2
+ **/.idea
3
+ *.log
4
+ **/__pycache__
5
+ **/.DS_Store
6
+ **/out
7
+ **/.vscode
8
+ **/.pytest_cache
9
+ **/*.egg*
10
+ .python-version
11
+
12
+ # file writer data
13
+ **.h5
14
+
15
+ # Byte-compiled / optimized / DLL files
16
+ __pycache__/
17
+ *.py[cod]
18
+ *$py.class
19
+
20
+ # C extensions
21
+ *.so
22
+
23
+ # Distribution / packaging
24
+ .Python
25
+ build/
26
+ develop-eggs/
27
+ dist/
28
+ downloads/
29
+ eggs/
30
+ .eggs/
31
+ lib/
32
+ lib64/
33
+ parts/
34
+ sdist/
35
+ var/
36
+ wheels/
37
+ share/python-wheels/
38
+ *.egg-info/
39
+ .installed.cfg
40
+ *.egg
41
+ MANIFEST
42
+ *.rcc
43
+ node_modules
44
+
45
+ # PyInstaller
46
+ # Usually these files are written by a python script from a template
47
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
48
+ *.manifest
49
+ *.spec
50
+
51
+ # Installer logs
52
+ pip-log.txt
53
+ pip-delete-this-directory.txt
54
+
55
+ # Unit test / coverage reports
56
+ htmlcov/
57
+ .tox/
58
+ .nox/
59
+ .coverage
60
+ .coverage.*
61
+ .cache
62
+ nosetests.xml
63
+ coverage.xml
64
+ *.cover
65
+ *.py,cover
66
+ .hypothesis/
67
+ .pytest_cache/
68
+ cover/
69
+
70
+ # Output from end2end testing
71
+ tests/reference_failures/
72
+
73
+ # Translations
74
+ *.mo
75
+ *.pot
76
+
77
+ # Django stuff:
78
+ *.log
79
+ local_settings.py
80
+ db.sqlite3
81
+ db.sqlite3-journal
82
+
83
+ # Flask stuff:
84
+ instance/
85
+ .webassets-cache
86
+
87
+ # Scrapy stuff:
88
+ .scrapy
89
+
90
+ # Sphinx documentation
91
+ docs/**/_build/
92
+ docs/**/autodoc/
93
+ docs/**/_autosummary/
94
+
95
+ # PyBuilder
96
+ .pybuilder/
97
+ target/
98
+
99
+ # Jupyter Notebook
100
+ .ipynb_checkpoints
101
+
102
+ # IPython
103
+ profile_default/
104
+ ipython_config.py
105
+
106
+ **.prof
107
+
108
+ # pyenv
109
+ # For a library or package, you might want to ignore these files since the code is
110
+ # intended to run in multiple environments; otherwise, check them in:
111
+ # .python-version
112
+
113
+ # pipenv
114
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
115
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
116
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
117
+ # install all needed dependencies.
118
+ #Pipfile.lock
119
+
120
+ # poetry
121
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
122
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
123
+ # commonly ignored for libraries.
124
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
125
+ #poetry.lock
126
+
127
+ # pdm
128
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
129
+ #pdm.lock
130
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
131
+ # in version control.
132
+ # https://pdm.fming.dev/#use-with-ide
133
+ .pdm.toml
134
+
135
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
136
+ __pypackages__/
137
+
138
+ # Celery stuff
139
+ celerybeat-schedule
140
+ celerybeat.pid
141
+
142
+ # SageMath parsed files
143
+ *.sage.py
144
+
145
+ # Environments
146
+ .env
147
+ .venv
148
+ env/
149
+ venv/
150
+ ENV/
151
+ env.bak/
152
+ venv.bak/
153
+
154
+ # Spyder project settings
155
+ .spyderproject
156
+ .spyproject
157
+
158
+ # Rope project settings
159
+ .ropeproject
160
+
161
+ # mkdocs documentation
162
+ /site
163
+
164
+ # mypy
165
+ .mypy_cache/
166
+ .dmypy.json
167
+ dmypy.json
168
+
169
+ # Pyre type checker
170
+ .pyre/
171
+
172
+ # pytype static type analyzer
173
+ .pytype/
174
+
175
+ # Cython debug symbols
176
+ cython_debug/
177
+
178
+ # PyCharm
179
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
180
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
181
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
182
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
183
+ #.idea/
@@ -0,0 +1,25 @@
1
+ # CHANGELOG
2
+
3
+
4
+ ## v0.1.1 (2025-07-11)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Fix release step
9
+ ([`182c7de`](https://github.com/bec-project/qtmonaco/commit/182c7dec74c46437e1a56d03c53b9992b135ad90))
10
+
11
+
12
+ ## v0.1.0 (2025-07-11)
13
+
14
+ ### Continuous Integration
15
+
16
+ - Add pypi build and upload
17
+ ([`25ea54a`](https://github.com/bec-project/qtmonaco/commit/25ea54a80e6fd54a215cb3679ca80e8e49c8e97b))
18
+
19
+ - Init
20
+ ([`0b5ef18`](https://github.com/bec-project/qtmonaco/commit/0b5ef1858bc95ec93e6c8abb993b1e01f8772cac))
21
+
22
+ ### Features
23
+
24
+ - Add qt bridge
25
+ ([`7361271`](https://github.com/bec-project/qtmonaco/commit/73612710b57424234900863606fe3aa74ad6754f))
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: qtmonaco
3
+ Version: 0.1.1
4
+ Summary: Qt Monaco is a Python library for working with the Monaco Editor in Qt applications.
5
+ Project-URL: Bug Tracker, https://github.com/bec-project/qtmonaco/issues
6
+ Project-URL: Homepage, https://github.com/bec-project/qtmonaco
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Topic :: Scientific/Engineering
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: pyside6~=6.8.2
12
+ Requires-Dist: python-lsp-server[all,websockets]~=1.12
13
+ Requires-Dist: qtpy~=2.4
14
+ Provides-Extra: dev
15
+ Requires-Dist: coverage~=7.0; extra == 'dev'
16
+ Requires-Dist: isort>=5.13.2,~=5.13; extra == 'dev'
17
+ Requires-Dist: pytest-cov~=6.1.1; extra == 'dev'
18
+ Requires-Dist: pytest-qt~=4.4; extra == 'dev'
19
+ Requires-Dist: pytest-random-order~=1.1; extra == 'dev'
20
+ Requires-Dist: pytest-timeout~=2.2; extra == 'dev'
21
+ Requires-Dist: pytest-xvfb~=3.0; extra == 'dev'
22
+ Requires-Dist: pytest~=8.0; extra == 'dev'
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate QRC and RCC files from dist directory for PySide6.
4
+
5
+ This script creates a Qt Resource Collection (.qrc) file from the dist directory
6
+ and automatically compiles it to a binary RCC file for use in PySide6 applications.
7
+
8
+ Usage:
9
+ python generate_qrc.py
10
+ """
11
+
12
+ import os
13
+ import subprocess
14
+ import xml.etree.ElementTree as ET
15
+ from typing import List
16
+
17
+
18
+ def get_file_list(build_dir: str) -> List[str]:
19
+ """
20
+ Get list of all files in build directory, excluding certain extensions.
21
+
22
+ Args:
23
+ build_dir: Path to build directory
24
+
25
+ Returns:
26
+ List of file paths relative to build_dir
27
+ """
28
+ exclude_extensions = {".map", ".txt", ".md"}
29
+
30
+ files = []
31
+ for root, dirs, filenames in os.walk(build_dir):
32
+ # Skip hidden directories and common build artifacts
33
+ dirs[:] = [
34
+ d for d in dirs if not d.startswith(".") and d not in {"node_modules", "__pycache__"}
35
+ ]
36
+
37
+ for filename in filenames:
38
+ file_path = os.path.join(root, filename)
39
+ relative_path = os.path.relpath(file_path, build_dir)
40
+
41
+ # Skip files with excluded extensions
42
+ if not any(relative_path.endswith(ext) for ext in exclude_extensions):
43
+ files.append(relative_path)
44
+
45
+ return sorted(files)
46
+
47
+
48
+ def create_qrc_content(files: List[str], build_dir: str) -> str:
49
+ """
50
+ Create QRC XML content from file list.
51
+
52
+ Args:
53
+ files: List of file paths relative to build directory
54
+ build_dir: Build directory path to include in file paths
55
+
56
+ Returns:
57
+ QRC XML content as string
58
+ """
59
+ # Create root element
60
+ root = ET.Element("RCC")
61
+ root.set("version", "1.0")
62
+
63
+ # Create qresource element
64
+ qresource = ET.SubElement(root, "qresource")
65
+ qresource.set("prefix", "/monaco")
66
+
67
+ # Add each file with build directory prefix
68
+ for file_path in files:
69
+ file_element = ET.SubElement(qresource, "file")
70
+ # Include build directory in the path
71
+ full_path = os.path.join("dist", file_path)
72
+ file_element.text = str(full_path).replace("\\", "/") # Use forward slashes for Qt
73
+
74
+ # Pretty print the XML
75
+ ET.indent(root, space=" ")
76
+
77
+ # Create XML declaration and return
78
+ xml_str = ET.tostring(root, encoding="unicode", xml_declaration=False)
79
+ return f'<?xml version="1.0" encoding="UTF-8"?>\n{xml_str}\n'
80
+
81
+
82
+ def compile_to_rcc(qrc_file: str, rcc_file: str) -> bool:
83
+ """
84
+ Compile QRC file to binary RCC file using pyside6-rcc.
85
+
86
+ Args:
87
+ qrc_file: Path to QRC file
88
+ rcc_file: Path to output RCC file
89
+
90
+ Returns:
91
+ True if compilation was successful, False otherwise
92
+ """
93
+ try:
94
+ cmd = ["pyside6-rcc", qrc_file, "-o", rcc_file, "--binary"]
95
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
96
+ return True
97
+ except subprocess.CalledProcessError as e:
98
+ print(f"Error compiling QRC to RCC: {e}")
99
+ print(f"stderr: {e.stderr}")
100
+ return False
101
+ except FileNotFoundError:
102
+ print("Error: pyside6-rcc not found. Make sure PySide6 is installed.")
103
+ return False
104
+
105
+
106
+ def main():
107
+
108
+ build_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "dist")
109
+ rcc_build_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "build")
110
+ qrc_file = "monaco_resources.qrc"
111
+ rcc_file = os.path.join(rcc_build_dir, "monaco.rcc")
112
+
113
+ # Validate build directory
114
+ if not os.path.exists(build_dir):
115
+ print(f"Error: Build directory '{build_dir}' does not exist")
116
+ print("Make sure to run 'npm run build' first")
117
+ return 1
118
+
119
+ # Get file list
120
+ files = get_file_list(build_dir)
121
+
122
+ if not files:
123
+ print(f"Warning: No files found in '{build_dir}'")
124
+ return 1
125
+
126
+ print(f"Found {len(files)} files in '{build_dir}':")
127
+ for file_path in files[:5]: # Show first 5 files
128
+ print(f" {file_path}")
129
+ if len(files) > 5:
130
+ print(f" ... and {len(files) - 5} more files")
131
+
132
+ # Generate QRC content
133
+ qrc_content = create_qrc_content(files, build_dir)
134
+
135
+ # Write QRC file
136
+ with open(qrc_file, "w", encoding="utf-8") as f:
137
+ f.write(qrc_content)
138
+
139
+ print(f"\n✓ Generated QRC file: {qrc_file}")
140
+
141
+ # Compile to RCC
142
+ print("Compiling to RCC file...")
143
+ os.makedirs(rcc_build_dir, exist_ok=True) # Ensure build directory exists
144
+ if compile_to_rcc(qrc_file, rcc_file):
145
+ print(f"✓ Generated RCC file: {rcc_file}")
146
+
147
+ # Clean up QRC file since we only need the RCC
148
+ os.remove(qrc_file)
149
+ print("✓ Cleaned up temporary QRC file")
150
+
151
+ print("\n🎉 Success!")
152
+ return 0
153
+ print("✗ Failed to compile RCC file")
154
+ return 1
155
+
156
+
157
+ if __name__ == "__main__":
158
+ exit(main())
@@ -0,0 +1,102 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "qtmonaco"
7
+ version = "0.1.1"
8
+ description = "Qt Monaco is a Python library for working with the Monaco Editor in Qt applications."
9
+ requires-python = ">=3.10"
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Programming Language :: Python :: 3",
13
+ "Topic :: Scientific/Engineering",
14
+ ]
15
+ dependencies = [
16
+ "PySide6~=6.8.2",
17
+ "qtpy~=2.4",
18
+ "python-lsp-server[all,websockets] ~= 1.12",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "coverage~=7.0",
24
+ "isort~=5.13, >=5.13.2",
25
+ "pytest-qt~=4.4",
26
+ "pytest-random-order~=1.1",
27
+ "pytest-timeout~=2.2",
28
+ "pytest-xvfb~=3.0",
29
+ "pytest~=8.0",
30
+ "pytest-cov~=6.1.1",
31
+ ]
32
+
33
+ [project.urls]
34
+ "Bug Tracker" = "https://github.com/bec-project/qtmonaco/issues"
35
+ Homepage = "https://github.com/bec-project/qtmonaco"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ include = ["*"]
39
+ exclude = ["docs/**", "tests/**"]
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ include = ["*"]
43
+ exclude = ["docs/**", "tests/**"]
44
+
45
+ [tool.black]
46
+ line-length = 100
47
+ skip-magic-trailing-comma = true
48
+
49
+ [tool.isort]
50
+ profile = "black"
51
+ line_length = 100
52
+ multi_line_output = 3
53
+ include_trailing_comma = true
54
+ known_first_party = ["bec_widgets"]
55
+
56
+ [tool.semantic_release]
57
+ build_command = "pip install build wheel && python -m build"
58
+ version_toml = ["pyproject.toml:project.version"]
59
+
60
+ [tool.semantic_release.commit_author]
61
+ env = "GIT_COMMIT_AUTHOR"
62
+ default = "semantic-release <semantic-release>"
63
+
64
+ [tool.semantic_release.commit_parser_options]
65
+ allowed_tags = [
66
+ "build",
67
+ "chore",
68
+ "ci",
69
+ "docs",
70
+ "feat",
71
+ "fix",
72
+ "perf",
73
+ "style",
74
+ "refactor",
75
+ "test",
76
+ ]
77
+ minor_tags = ["feat"]
78
+ patch_tags = ["fix", "perf"]
79
+ default_bump_level = 0
80
+
81
+ [tool.semantic_release.remote]
82
+ name = "origin"
83
+ type = "github"
84
+ ignore_token_for_push = true
85
+
86
+ [tool.semantic_release.remote.token]
87
+ env = "GH_TOKEN"
88
+
89
+ [tool.semantic_release.publish]
90
+ dist_glob_patterns = ["dist/*"]
91
+ upload_to_vcs_release = true
92
+
93
+ [tool.coverage.report]
94
+ skip_empty = true # exclude empty *files*, e.g. __init__.py, from the report
95
+ exclude_lines = [
96
+ "pragma: no cover",
97
+ "if TYPE_CHECKING:",
98
+ "return NotImplemented",
99
+ "raise NotImplementedError",
100
+ "\\.\\.\\.",
101
+ 'if __name__ == "__main__":',
102
+ ]
File without changes
@@ -0,0 +1,65 @@
1
+ import json
2
+
3
+ from qtpy.QtCore import QObject, Signal, Slot
4
+
5
+
6
+ class BaseBridge(QObject):
7
+ initialized = Signal()
8
+ sendDataChanged = Signal(str, str)
9
+ completion = Signal(str)
10
+
11
+ def __init__(self, parent=None):
12
+ super().__init__(parent=parent)
13
+ self._initialized = False
14
+ self._buffer = []
15
+ self.initialized.connect(self._process_startup_buffer)
16
+
17
+ def _process_startup_buffer(self):
18
+ """
19
+ Process the buffer of data that was sent before the bridge was initialized.
20
+ This is useful for sending initial data to the JavaScript side.
21
+ """
22
+ for name, value in self._buffer:
23
+ self._send_to_js(name, value)
24
+ self._buffer.clear()
25
+
26
+ # Update the local buffer by reading the current state
27
+ # This is mostly to ensure that we are in sync with the JS side
28
+ self._send_to_js("read", "")
29
+
30
+ def _send_to_js(self, name, value):
31
+ if not self._initialized:
32
+ self._buffer.append((name, value))
33
+ return
34
+ data = json.dumps(value)
35
+ self.sendDataChanged.emit(name, data)
36
+
37
+ @Slot(str, str)
38
+ def receive_from_js(self, name, value):
39
+ data = json.loads(value)
40
+
41
+ if name == "bridge_initialized":
42
+ self._initialized = data
43
+ self.initialized.emit()
44
+ return
45
+ if name == "setValue":
46
+ self.on_value_changed(data)
47
+ return
48
+ print(f"Received from JS: {name} = {data}")
49
+ self.setProperty(name, data)
50
+
51
+ @property
52
+ def bridge_initialized(self):
53
+ return self._initialized
54
+
55
+ @bridge_initialized.setter
56
+ def bridge_initialized(self, value):
57
+ if self._initialized != value:
58
+ self._initialized = value
59
+ self.initialized.emit()
60
+
61
+ def on_value_changed(self, value):
62
+ """
63
+ Placeholder method to handle value changes.
64
+ This can be overridden in subclasses to implement specific behavior.
65
+ """
@@ -0,0 +1,200 @@
1
+ import json
2
+ from typing import Literal
3
+
4
+ from qtpy.QtCore import QObject, Signal, Slot
5
+
6
+
7
+ class Connector(QObject):
8
+ javascript_data_sent = Signal(str, str)
9
+ javascript_data_received = Signal(str, str)
10
+
11
+ """
12
+ A base class for connecting Python and JavaScript.
13
+ This class provides a mechanism to send and receive data between Python and JavaScript.
14
+ It uses Qt's signal-slot mechanism to handle communication.
15
+ """
16
+
17
+ def __init__(self, parent=None):
18
+ super().__init__(parent=parent)
19
+ self._initialized = False
20
+ self._buffer = []
21
+
22
+ def _process_startup_buffer(self):
23
+ """
24
+ Process the buffer of data that was sent before the bridge was initialized.
25
+ This is useful for sending initial data to the JavaScript side.
26
+ """
27
+ for name, value in self._buffer:
28
+ self.send(name, value)
29
+ self._buffer.clear()
30
+
31
+ # Update the local buffer by reading the current state
32
+ # This is mostly to ensure that we are in sync with the JS side
33
+ self.send("read", "")
34
+
35
+ def send(self, name, value):
36
+ """
37
+ Send data to the JavaScript side.
38
+ Args:
39
+ name (str): The name of the data to send.
40
+ value (any): The value to send, which will be serialized to JSON.
41
+ """
42
+ if not self._initialized:
43
+ self._buffer.append((name, value))
44
+ return
45
+ data = json.dumps(value)
46
+ self.javascript_data_sent.emit(name, data)
47
+
48
+ @Slot(str, str)
49
+ def _receive(self, name: str, value: str):
50
+ """
51
+ Receive data from the JavaScript side.
52
+ This method is called when the JavaScript side sends data to the Python side.
53
+ Args:
54
+
55
+
56
+ """
57
+ self.javascript_data_received.emit(name, value)
58
+
59
+ def set_initialized(self):
60
+ """
61
+ Set the initialized state of the connector.
62
+ This method is used to indicate that the connector is ready to send and receive data.
63
+ Args:
64
+ value (bool): The new initialized state.
65
+ """
66
+ self._initialized = True
67
+ self._process_startup_buffer()
68
+
69
+
70
+ class EditorBridge(QObject):
71
+ text_changed = Signal()
72
+ language_changed = Signal()
73
+ theme_changed = Signal()
74
+ initialized = Signal()
75
+ completion = Signal(str)
76
+
77
+ def __init__(self, parent=None):
78
+ super().__init__(parent=parent)
79
+ self._connector = Connector(parent=self)
80
+ self._connector.javascript_data_received.connect(self.on_new_data_received)
81
+ self._value = ""
82
+ self._language = ""
83
+ self._theme = ""
84
+ self._readonly = False
85
+ self._initialized = False
86
+ self._buffer = []
87
+ self.initialized.connect(self._connector.set_initialized)
88
+
89
+ def on_new_data_received(self, name: str, value: str):
90
+ """
91
+ Handle new data received from JavaScript.
92
+ This method is called when the JavaScript side sends data to the Python side.
93
+ """
94
+ data = json.loads(value)
95
+ if hasattr(self, name):
96
+ method = getattr(self, name)
97
+ if callable(method):
98
+ method(data)
99
+ return
100
+ if hasattr(self, name):
101
+ setattr(self, name, data)
102
+ return
103
+ else:
104
+ print(f"Warning: No method or property named '{name}' in EditorBridge.")
105
+
106
+ @property
107
+ def bridge_initialized(self):
108
+ return self._initialized
109
+
110
+ @bridge_initialized.setter
111
+ def bridge_initialized(self, value):
112
+ if self._initialized != value:
113
+ self._initialized = value
114
+ self.initialized.emit()
115
+
116
+ def on_value_changed(self, value):
117
+ """Handle value changes from the JavaScript side."""
118
+ self._current_text(value)
119
+
120
+ def _current_text(self, value: str):
121
+ if self._value == value:
122
+ return
123
+ self._value = value
124
+ self.text_changed.emit()
125
+
126
+ def set_text(self, value: str):
127
+ """
128
+ Set the value in the editor.
129
+
130
+ Args:
131
+ value (str): The new value to set in the editor.
132
+ """
133
+ if self._value == value:
134
+ return
135
+ if self._readonly:
136
+ raise ValueError("Editor is in read-only mode, cannot set value.")
137
+ if not isinstance(value, str):
138
+ raise TypeError("Value must be a string.")
139
+ self._value = value
140
+ self.text_changed.emit()
141
+
142
+ def get_language(self):
143
+ return self._language
144
+
145
+ def set_language(self, language):
146
+ self._language = language
147
+ self._connector.send("language", language)
148
+ self.language_changed.emit()
149
+
150
+ def get_theme(self):
151
+ return self._theme
152
+
153
+ def set_theme(self, theme):
154
+ self._theme = theme
155
+ self._connector.send("theme", theme)
156
+ self.theme_changed.emit()
157
+
158
+ def set_readonly(self, read_only: bool):
159
+ """Set the editor to read-only mode."""
160
+ self._connector.send("readonly", read_only)
161
+ self._readonly = read_only
162
+
163
+ @property
164
+ def value(self):
165
+ return self._value
166
+
167
+ @property
168
+ def language(self):
169
+ return self._language
170
+
171
+ @property
172
+ def theme(self):
173
+ return self._theme
174
+
175
+ def set_host(self, host: str):
176
+ """
177
+ Set the host for the editor.
178
+
179
+ Args:
180
+ host (str): The host URL for the editor.
181
+ """
182
+ self._connector.send("lsp_url", host)
183
+
184
+ def set_cursor(
185
+ self,
186
+ line: int,
187
+ column: int = 1,
188
+ move_to_position: Literal[None, "center", "top", "position"] = None,
189
+ ):
190
+ """
191
+ Set the cursor position in the editor.
192
+
193
+ Args:
194
+ line (int): Line number (1-based).
195
+ column (int): Column number (1-based), defaults to 1.
196
+ move_to_position (Literal[None, "center", "top", "position"], optional): Position to move the cursor to.
197
+ """
198
+ self._connector.send(
199
+ "set_cursor", {"line": line, "column": column, "moveToPosition": move_to_position}
200
+ )
@@ -0,0 +1,10 @@
1
+ import logging
2
+
3
+ from qtpy.QtWebEngineCore import QWebEnginePage
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class MonacoPage(QWebEnginePage):
9
+ def javaScriptConsoleMessage(self, level, message, line, source):
10
+ print(f"[JS Console] {level.name} at line {line} in {source}: {message}")
@@ -0,0 +1,134 @@
1
+ from typing import Literal
2
+
3
+ from qtpy.QtCore import Signal
4
+ from qtpy.QtWebChannel import QWebChannel
5
+ from qtpy.QtWebEngineWidgets import QWebEngineView
6
+
7
+ from qtmonaco.editor_bridge import EditorBridge
8
+ from qtmonaco.monaco_page import MonacoPage
9
+ from qtmonaco.resource_loader import get_monaco_base_url, get_monaco_html
10
+
11
+
12
+ def get_pylsp_host() -> str:
13
+ """
14
+ Get the host address for the PyLSP server.
15
+ This function initializes the PyLSP server if it is not already running
16
+ and returns the host address in the format 'localhost:port'.
17
+ Returns:
18
+ str: The host address of the PyLSP server.
19
+ """
20
+ # lazy import to only load when needed
21
+ # pylint: disable=import-outside-toplevel
22
+ from qtmonaco.pylsp_provider import pylsp_server
23
+
24
+ if not pylsp_server.is_running():
25
+ pylsp_server.start()
26
+
27
+ return f"localhost:{pylsp_server.port}"
28
+
29
+
30
+ class Monaco(QWebEngineView):
31
+ initialized = Signal()
32
+ textChanged = Signal(str)
33
+
34
+ def __init__(self, parent=None):
35
+ super().__init__(parent=parent)
36
+
37
+ self.pylsp_host = get_pylsp_host()
38
+
39
+ self._setup_page()
40
+ self._setup_bridge()
41
+ self._load_editor()
42
+
43
+ def _setup_page(self):
44
+ """Initialize the web engine page."""
45
+ page = MonacoPage(parent=self)
46
+ self.setPage(page)
47
+
48
+ def _setup_bridge(self):
49
+ """Initialize the bridge for communication with JavaScript."""
50
+ self._channel = QWebChannel(self)
51
+ self._bridge = EditorBridge()
52
+
53
+ self.page().setWebChannel(self._channel)
54
+ self._channel.registerObject("connector", self._bridge._connector)
55
+
56
+ self._bridge.initialized.connect(self._set_host)
57
+ self._bridge.initialized.connect(self.initialized)
58
+ self._bridge.text_changed.connect(lambda: self.textChanged.emit(self._bridge.value))
59
+
60
+ def _load_editor(self):
61
+ """Load the Monaco Editor HTML content."""
62
+ raw_html = get_monaco_html()
63
+ base_url = get_monaco_base_url()
64
+ self.setHtml(raw_html, base_url)
65
+
66
+ def _set_host(self):
67
+ """Set the LSP host once the bridge is initialized."""
68
+ self._bridge.set_host(self.pylsp_host)
69
+
70
+ # Public API methods
71
+ def text(self):
72
+ return self._bridge.value
73
+
74
+ def set_text(self, text: str):
75
+ self._bridge.set_text(text)
76
+
77
+ def set_cursor(
78
+ self,
79
+ line: int,
80
+ column: int = 1,
81
+ move_to_position: Literal[None, "center", "top", "position"] = None,
82
+ ):
83
+ """Set the cursor position in the editor.
84
+
85
+ Args:
86
+ line (int): Line number (1-based)
87
+ column (int): Column number (1-based), defaults to 1
88
+ """
89
+ self._bridge.set_cursor(line, column, move_to_position)
90
+
91
+ def get_language(self) -> str:
92
+ """Get the current programming language for syntax highlighting in the editor."""
93
+ return self._bridge.get_language()
94
+
95
+ def set_language(self, language: str):
96
+ """Set the programming language for syntax highlighting in the editor.
97
+
98
+ Args:
99
+ language (str): The programming language to set (e.g., "python", "javascript").
100
+ """
101
+ self._bridge.set_language(language)
102
+
103
+ def get_theme(self):
104
+ return self._bridge.get_theme()
105
+
106
+ def set_theme(self, theme: str):
107
+ """Set the theme for the Monaco editor.
108
+
109
+ Args:
110
+ theme (str): The theme to apply (e.g., "vs", "vs-dark").
111
+ """
112
+ self._bridge.set_theme(theme)
113
+
114
+ def set_read_only(self, read_only: bool):
115
+ """Set the editor to read-only mode."""
116
+ self._bridge.set_readonly(read_only)
117
+
118
+ def shutdown(self):
119
+ if hasattr(self._bridge, "shutdown"):
120
+ self._bridge.shutdown()
121
+
122
+
123
+ if __name__ == "__main__":
124
+ import logging
125
+ import sys
126
+
127
+ from qtpy.QtWidgets import QApplication
128
+
129
+ logging.basicConfig(level=logging.INFO)
130
+
131
+ app = QApplication(sys.argv)
132
+ editor = Monaco()
133
+ editor.show()
134
+ sys.exit(app.exec_())
@@ -0,0 +1,61 @@
1
+ import atexit
2
+ import logging
3
+ import signal
4
+ import socket
5
+ import subprocess
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class PyLSPProvider:
11
+ """A provider for the PyLSP server."""
12
+
13
+ def __init__(self):
14
+ self.port = None
15
+ self.server_process = None
16
+ atexit.register(self.stop)
17
+ signal.signal(signal.SIGINT, self._handle_signal)
18
+ signal.signal(signal.SIGTERM, self._handle_signal)
19
+
20
+ def _find_free_port(self):
21
+ """Find a free port for the PyLSP server."""
22
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
23
+ s.bind(("localhost", 0))
24
+ self.port = s.getsockname()[1]
25
+ return self.port
26
+
27
+ def start(self):
28
+ """Start the PyLSP server."""
29
+ if self.port is None:
30
+ self._find_free_port()
31
+ # Here you would start the PyLSP server using the found port
32
+ logger.info(f"Starting PyLSP server on port {self.port}")
33
+ self.server_process = subprocess.Popen(
34
+ ["pylsp", "--ws", "--port", str(self.port)],
35
+ stdout=subprocess.PIPE,
36
+ stderr=subprocess.PIPE,
37
+ )
38
+
39
+ def stop(self):
40
+ """Stop the PyLSP server."""
41
+ if not self.server_process:
42
+ return
43
+
44
+ self.server_process.terminate()
45
+ try:
46
+ self.server_process.wait(timeout=5)
47
+ except subprocess.TimeoutExpired:
48
+ self.server_process.kill()
49
+ self.server_process = None
50
+ self.port = None
51
+
52
+ def _handle_signal(self, signum, frame):
53
+ """Handle termination signals."""
54
+ self.stop()
55
+
56
+ def is_running(self):
57
+ """Check if the PyLSP server is running."""
58
+ return self.server_process is not None and self.server_process.poll() is None
59
+
60
+
61
+ pylsp_server = PyLSPProvider()
@@ -0,0 +1,28 @@
1
+ import os
2
+
3
+ from qtpy.QtCore import QFile, QIODevice, QResource, QUrl
4
+
5
+ QResource.registerResource(
6
+ os.path.join(os.path.dirname(os.path.dirname(__file__)), "build/monaco.rcc")
7
+ )
8
+
9
+
10
+ def load_resource_html(resource_path: str) -> str:
11
+ """Load HTML content from Qt resources."""
12
+ file = QFile(resource_path)
13
+ if file.open(QIODevice.OpenModeFlag.ReadOnly):
14
+ content = file.readAll()
15
+ file.close()
16
+ return content.toStdString()
17
+ else:
18
+ raise FileNotFoundError(f"Resource not found: {resource_path}")
19
+
20
+
21
+ def get_monaco_html():
22
+ """Get Monaco Editor HTML content from Qt resources."""
23
+ return load_resource_html(":/monaco/dist/index.html")
24
+
25
+
26
+ def get_monaco_base_url():
27
+ """Get the base URL for Monaco Editor resources."""
28
+ return QUrl("qrc:/monaco/dist/")