wasm-idle 0.5.0 → 0.6.0

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.
@@ -26,6 +26,7 @@ const stdinReady = async () => {
26
26
  }
27
27
  };
28
28
  let writer = null;
29
+ let clangdRuntime = null;
29
30
  const stdout = (charCode) => {
30
31
  const json = jsonStream.insert(charCode);
31
32
  if (json && writer)
@@ -36,7 +37,22 @@ const onAbort = () => {
36
37
  writer?.end();
37
38
  self.reportError?.('clangd aborted');
38
39
  };
40
+ const syncWorkspaceFile = (filePath) => {
41
+ if (!clangdRuntime)
42
+ return;
43
+ const normalizedFilePath = filePath.startsWith(CLANGD_WORKSPACE_PATH)
44
+ ? filePath
45
+ : `${CLANGD_WORKSPACE_PATH}/${filePath.replace(/^\/+/, '')}`;
46
+ const lastSlash = normalizedFilePath.lastIndexOf('/');
47
+ const directoryPath = lastSlash > 0 ? normalizedFilePath.slice(0, lastSlash) : CLANGD_WORKSPACE_PATH;
48
+ clangdRuntime.FS.mkdirTree(directoryPath);
49
+ clangdRuntime.FS.writeFile(normalizedFilePath, '');
50
+ };
39
51
  self.addEventListener('message', async (event) => {
52
+ if (event.data?.type === 'sync-file' && typeof event.data?.name === 'string') {
53
+ syncWorkspaceFile(event.data.name);
54
+ return;
55
+ }
40
56
  if (event.data?.type !== 'init')
41
57
  return;
42
58
  const baseUrl = normalizeClangdBaseUrl(event.data.baseUrl);
@@ -91,7 +107,7 @@ self.addEventListener('message', async (event) => {
91
107
  const wasmBlob = new Blob([wasmBytes.buffer], { type: 'application/wasm' });
92
108
  const wasmDataUrl = URL.createObjectURL(wasmBlob);
93
109
  const { default: Clangd } = await jsModule;
94
- const clangd = await Clangd({
110
+ clangdRuntime = await Clangd({
95
111
  thisProgram: '/usr/bin/clangd',
96
112
  mainScriptUrlOrBlob: jsDataUrl,
97
113
  locateFile: (path, prefix) => path.endsWith('.wasm') ? wasmDataUrl : `${prefix}${path}`,
@@ -102,14 +118,14 @@ self.addEventListener('message', async (event) => {
102
118
  onExit: onAbort,
103
119
  onAbort
104
120
  });
105
- clangd.FS.mkdirTree(CLANGD_WORKSPACE_PATH);
106
- clangd.FS.writeFile(CLANGD_CPP_FILE_PATH, '');
107
- clangd.FS.writeFile(`${CLANGD_WORKSPACE_PATH}/.clangd`, JSON.stringify({
121
+ clangdRuntime.FS.mkdirTree(CLANGD_WORKSPACE_PATH);
122
+ syncWorkspaceFile(CLANGD_CPP_FILE_PATH);
123
+ clangdRuntime.FS.writeFile(`${CLANGD_WORKSPACE_PATH}/.clangd`, JSON.stringify({
108
124
  CompileFlags: {
109
125
  Add: createClangdCompileFlags()
110
126
  }
111
127
  }));
112
- clangd.callMain([]);
128
+ clangdRuntime.callMain([]);
113
129
  writer = new BrowserMessageWriter(self);
114
130
  const reader = new BrowserMessageReader(self);
115
131
  reader.listen((data) => {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import Terminal from './terminal';
2
2
  export { cppDebugLanguageAdapter, pythonDebugLanguageAdapter, evaluateDebugExpression, createDebugSessionController, MonacoDebugView, attachMonacoDebugActions, selectInlineDebugLocals } from './debug';
3
+ export { getEditorLanguageServer, getCppLanguageServer, getPythonLanguageServer, resolveCppLanguageServerBaseUrl, resolvePythonLanguageServerBaseUrl } from './lsp';
3
4
  import playground, { createPlaygroundBinding } from './playground';
4
5
  export default Terminal;
5
6
  export { createPlaygroundBinding, playground };
@@ -9,3 +10,4 @@ export type { DebugSessionController, DebugSessionControllerOptions, DebugWatchV
9
10
  export type { PlaygroundRuntimeAssets, RuntimeAssetConfig, RuntimeAssetLoadRequest, RuntimeAssetLoader, RuntimeAssetLoaderResult } from './playground/assets';
10
11
  export type { BoundSandbox, PlaygroundBinding, PlaygroundTerminalProps, SandboxProgress, SandboxRuntimeAssets } from './playground/sandbox';
11
12
  export type { DebugLanguageAdapter } from './debug';
13
+ export type { EditorLanguageServerHandle, EditorLanguageServerRuntimeOptions } from './lsp';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import Terminal from './terminal';
2
2
  export { cppDebugLanguageAdapter, pythonDebugLanguageAdapter, evaluateDebugExpression, createDebugSessionController, MonacoDebugView, attachMonacoDebugActions, selectInlineDebugLocals } from './debug';
3
+ export { getEditorLanguageServer, getCppLanguageServer, getPythonLanguageServer, resolveCppLanguageServerBaseUrl, resolvePythonLanguageServerBaseUrl } from './lsp';
3
4
  import playground, { createPlaygroundBinding } from './playground';
4
5
  export default Terminal;
5
6
  export { createPlaygroundBinding, playground };
@@ -0,0 +1,2 @@
1
+ import type { EditorLanguageServerRuntimeOptions, EditorLanguageServerHandle } from '../types';
2
+ export declare function getCppLanguageServer(options?: string | EditorLanguageServerRuntimeOptions): Promise<EditorLanguageServerHandle>;
@@ -0,0 +1,57 @@
1
+ import { resolveCppLanguageServerBaseUrl } from '../runtime';
2
+ import ClangdWorker from '../../clangd/worker?worker';
3
+ import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-jsonrpc/browser';
4
+ const currentUrl = () => globalThis.location?.href || '';
5
+ async function createServer(baseUrl) {
6
+ let resolveReady = () => { };
7
+ let rejectReady = (_error) => { };
8
+ const ready = new Promise((resolve, reject) => {
9
+ resolveReady = resolve;
10
+ rejectReady = reject;
11
+ });
12
+ const worker = new ClangdWorker();
13
+ const cleanup = () => {
14
+ worker.removeEventListener('message', readyListener);
15
+ worker.removeEventListener('error', errorListener);
16
+ };
17
+ const readyListener = (event) => {
18
+ switch (event.data?.type) {
19
+ case 'ready': {
20
+ cleanup();
21
+ resolveReady();
22
+ break;
23
+ }
24
+ case 'error': {
25
+ cleanup();
26
+ rejectReady(new Error(event.data?.message || 'clangd failed to initialize'));
27
+ break;
28
+ }
29
+ }
30
+ };
31
+ const errorListener = (event) => {
32
+ cleanup();
33
+ rejectReady(event.error || new Error(event.message || 'clangd worker failed'));
34
+ };
35
+ worker.addEventListener('message', readyListener);
36
+ worker.addEventListener('error', errorListener);
37
+ worker.postMessage({ type: 'init', baseUrl });
38
+ await ready;
39
+ return worker;
40
+ }
41
+ export async function getCppLanguageServer(options) {
42
+ const baseUrl = resolveCppLanguageServerBaseUrl(options, currentUrl());
43
+ const worker = await createServer(baseUrl);
44
+ const reader = new BrowserMessageReader(worker);
45
+ const writer = new BrowserMessageWriter(worker);
46
+ return {
47
+ transport: { reader, writer },
48
+ syncFile: (path) => {
49
+ worker.postMessage({ type: 'sync-file', name: path });
50
+ },
51
+ dispose: () => {
52
+ worker.terminate();
53
+ reader.dispose();
54
+ writer.dispose();
55
+ }
56
+ };
57
+ }
@@ -0,0 +1,5 @@
1
+ export { getEditorLanguageServer } from './registry';
2
+ export { resolveCppLanguageServerBaseUrl, resolvePythonLanguageServerBaseUrl } from './runtime';
3
+ export { getCppLanguageServer } from './cpp/server';
4
+ export { getPythonLanguageServer } from './python/server';
5
+ export type { EditorLanguageServerHandle, EditorLanguageServerRuntimeOptions } from './types';
@@ -0,0 +1,4 @@
1
+ export { getEditorLanguageServer } from './registry';
2
+ export { resolveCppLanguageServerBaseUrl, resolvePythonLanguageServerBaseUrl } from './runtime';
3
+ export { getCppLanguageServer } from './cpp/server';
4
+ export { getPythonLanguageServer } from './python/server';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,92 @@
1
+ /// <reference lib="WebWorker" />
2
+ import { loadPyodide } from 'pyodide';
3
+ import packageInitSource from './package/arcturus_python_lsp/__init__.py?raw';
4
+ import packageServerSource from './package/arcturus_python_lsp/server.py?raw';
5
+ let pyodide = null;
6
+ let bridge = null;
7
+ let initPromise = null;
8
+ let configuredPyodideBaseUrl = '/pyodide/';
9
+ function toErrorMessage(error) {
10
+ return error instanceof Error ? error.message : String(error);
11
+ }
12
+ function normalizeBaseUrl(baseUrl) {
13
+ return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
14
+ }
15
+ function emitTransportMessage(payload) {
16
+ try {
17
+ self.postMessage(JSON.parse(payload));
18
+ }
19
+ catch (error) {
20
+ console.error('Failed to emit Python LSP payload', error, payload);
21
+ }
22
+ }
23
+ function installPythonServerPackage(currentPyodide) {
24
+ currentPyodide.FS.mkdirTree('/arcturus_lsp/arcturus_python_lsp');
25
+ currentPyodide.FS.writeFile('/arcturus_lsp/arcturus_python_lsp/__init__.py', packageInitSource);
26
+ currentPyodide.FS.writeFile('/arcturus_lsp/arcturus_python_lsp/server.py', packageServerSource);
27
+ }
28
+ async function bootstrapPythonBridge(pyodideBaseUrl) {
29
+ if (bridge)
30
+ return;
31
+ if (initPromise)
32
+ return initPromise;
33
+ initPromise = (async () => {
34
+ self.postMessage({ type: 'progress', stage: 'load-pyodide' });
35
+ pyodide = await loadPyodide({ indexURL: normalizeBaseUrl(pyodideBaseUrl) });
36
+ pyodide.registerJsModule('arcturus_lsp_bridge', {
37
+ emit: emitTransportMessage
38
+ });
39
+ await pyodide.loadPackage('jedi');
40
+ installPythonServerPackage(pyodide);
41
+ await pyodide.runPythonAsync(`
42
+ import sys
43
+
44
+ if "/arcturus_lsp" not in sys.path:
45
+ sys.path.insert(0, "/arcturus_lsp")
46
+
47
+ from arcturus_python_lsp import create_bridge
48
+
49
+ _arcturus_python_lsp_bridge = create_bridge()
50
+ `);
51
+ const bridgeProxy = pyodide.globals.get('_arcturus_python_lsp_bridge');
52
+ bridge = (payload) => {
53
+ bridgeProxy(payload);
54
+ };
55
+ self.postMessage({ type: 'ready' });
56
+ })().catch((error) => {
57
+ const message = toErrorMessage(error);
58
+ console.error('Python LSP bootstrap failed', error);
59
+ self.postMessage({ type: 'error', error: message });
60
+ initPromise = null;
61
+ bridge = null;
62
+ throw error;
63
+ });
64
+ return initPromise;
65
+ }
66
+ self.addEventListener('message', (event) => {
67
+ const payload = event.data;
68
+ if (!payload || typeof payload !== 'object')
69
+ return;
70
+ if (payload.type === 'init') {
71
+ configuredPyodideBaseUrl =
72
+ typeof payload.pyodideBaseUrl === 'string' ? payload.pyodideBaseUrl : '/pyodide/';
73
+ void bootstrapPythonBridge(configuredPyodideBaseUrl).catch((error) => {
74
+ console.error('Failed to initialize Python LSP worker', error);
75
+ });
76
+ return;
77
+ }
78
+ if ('type' in payload)
79
+ return;
80
+ const serialized = JSON.stringify(payload);
81
+ if (bridge) {
82
+ bridge(serialized);
83
+ return;
84
+ }
85
+ void bootstrapPythonBridge(configuredPyodideBaseUrl)
86
+ .then(() => {
87
+ bridge?.(serialized);
88
+ })
89
+ .catch((error) => {
90
+ console.error('Failed to forward LSP message to Python bridge', error);
91
+ });
92
+ });
@@ -0,0 +1,3 @@
1
+ from .server import create_bridge
2
+
3
+ __all__ = ["create_bridge"]
@@ -0,0 +1,462 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import json
5
+ from pathlib import Path
6
+ from urllib.parse import unquote, urlparse
7
+
8
+ from jedi import Project, Script
9
+
10
+ from arcturus_lsp_bridge import emit
11
+
12
+ SERVER_NAME = "arcturus-python-lsp"
13
+ SERVER_VERSION = "0.2.0"
14
+ WORKSPACE_ROOT = "/workspace"
15
+
16
+ TEXT_DOCUMENT_SYNC_FULL = 1
17
+ DIAGNOSTIC_SEVERITY_ERROR = 1
18
+
19
+ COMPLETION_ITEM_KIND = {
20
+ "text": 1,
21
+ "method": 2,
22
+ "function": 3,
23
+ "constructor": 4,
24
+ "field": 5,
25
+ "variable": 6,
26
+ "class": 7,
27
+ "interface": 8,
28
+ "module": 9,
29
+ "property": 10,
30
+ "unit": 11,
31
+ "value": 12,
32
+ "enum": 13,
33
+ "keyword": 14,
34
+ "snippet": 15,
35
+ "color": 16,
36
+ "file": 17,
37
+ "reference": 18,
38
+ "folder": 19,
39
+ "enumMember": 20,
40
+ "constant": 21,
41
+ "struct": 22,
42
+ "event": 23,
43
+ "operator": 24,
44
+ "typeParameter": 25,
45
+ }
46
+
47
+ SYMBOL_KIND = {
48
+ "file": 1,
49
+ "module": 2,
50
+ "namespace": 3,
51
+ "package": 4,
52
+ "class": 5,
53
+ "method": 6,
54
+ "property": 7,
55
+ "field": 8,
56
+ "constructor": 9,
57
+ "enum": 10,
58
+ "interface": 11,
59
+ "function": 12,
60
+ "variable": 13,
61
+ "constant": 14,
62
+ }
63
+
64
+ COMPLETION_KIND_MAP = {
65
+ "module": COMPLETION_ITEM_KIND["module"],
66
+ "class": COMPLETION_ITEM_KIND["class"],
67
+ "instance": COMPLETION_ITEM_KIND["variable"],
68
+ "function": COMPLETION_ITEM_KIND["function"],
69
+ "param": COMPLETION_ITEM_KIND["variable"],
70
+ "path": COMPLETION_ITEM_KIND["file"],
71
+ "keyword": COMPLETION_ITEM_KIND["keyword"],
72
+ "statement": COMPLETION_ITEM_KIND["keyword"],
73
+ "property": COMPLETION_ITEM_KIND["property"],
74
+ }
75
+
76
+ SYMBOL_KIND_MAP = {
77
+ "module": SYMBOL_KIND["module"],
78
+ "class": SYMBOL_KIND["class"],
79
+ "function": SYMBOL_KIND["function"],
80
+ "statement": SYMBOL_KIND["variable"],
81
+ "instance": SYMBOL_KIND["variable"],
82
+ "param": SYMBOL_KIND["variable"],
83
+ }
84
+
85
+
86
+ def _uri_to_path(uri: str) -> str:
87
+ if uri.startswith("file://"):
88
+ return unquote(urlparse(uri).path)
89
+
90
+ parsed = urlparse(uri)
91
+ if parsed.scheme and parsed.path:
92
+ return unquote(parsed.path)
93
+
94
+ return uri
95
+
96
+
97
+ def _path_to_uri(path: Path) -> str:
98
+ return path.as_posix()
99
+
100
+
101
+ class Document:
102
+ def __init__(
103
+ self,
104
+ uri: str,
105
+ source: str,
106
+ version: int | None = None,
107
+ language_id: str | None = None,
108
+ ) -> None:
109
+ self.uri = uri
110
+ self.source = source
111
+ self.version = version
112
+ self.language_id = language_id
113
+ self.path = _uri_to_path(uri)
114
+
115
+ @property
116
+ def lines(self) -> list[str]:
117
+ lines = self.source.splitlines(True)
118
+ return lines or [""]
119
+
120
+ def offset_at(self, position: dict) -> int:
121
+ line = max(int(position.get("line", 0)), 0)
122
+ character = max(int(position.get("character", 0)), 0)
123
+ lines = self.lines
124
+ if line >= len(lines):
125
+ return len(self.source)
126
+
127
+ offset = sum(len(lines[idx]) for idx in range(line))
128
+ return offset + min(character, len(lines[line]))
129
+
130
+ def apply_change(self, change: dict) -> None:
131
+ if "range" not in change or change["range"] is None:
132
+ self.source = change.get("text", "")
133
+ return
134
+
135
+ change_range = change["range"]
136
+ start = self.offset_at(change_range["start"])
137
+ end = self.offset_at(change_range["end"])
138
+ self.source = self.source[:start] + change.get("text", "") + self.source[end:]
139
+
140
+
141
+ class ArcturusPythonLsp:
142
+ def __init__(self) -> None:
143
+ self.workspace_root = WORKSPACE_ROOT
144
+ Path(self.workspace_root).mkdir(parents=True, exist_ok=True)
145
+ self.project = Project(
146
+ path=self.workspace_root,
147
+ smart_sys_path=True,
148
+ load_unsafe_extensions=False,
149
+ )
150
+ self.documents: dict[str, Document] = {}
151
+ self.shutdown_requested = False
152
+
153
+ def handle(self, payload: str) -> None:
154
+ message = json.loads(payload)
155
+ if message.get("jsonrpc") != "2.0":
156
+ return
157
+
158
+ method = message.get("method")
159
+ if not method:
160
+ return
161
+
162
+ if "id" in message:
163
+ self._handle_request(message["id"], method, message.get("params") or {})
164
+ else:
165
+ self._handle_notification(method, message.get("params") or {})
166
+
167
+ def _emit(self, body: dict) -> None:
168
+ emit(json.dumps(body))
169
+
170
+ def _respond(self, msg_id, result=None, error: dict | None = None) -> None:
171
+ response = {"jsonrpc": "2.0", "id": msg_id}
172
+ if error is not None:
173
+ response["error"] = error
174
+ else:
175
+ response["result"] = result
176
+ self._emit(response)
177
+
178
+ def _notify(self, method: str, params: dict) -> None:
179
+ self._emit({"jsonrpc": "2.0", "method": method, "params": params})
180
+
181
+ def _refresh_project(self, root_uri: str | None) -> None:
182
+ if root_uri:
183
+ self.workspace_root = _uri_to_path(root_uri)
184
+
185
+ Path(self.workspace_root).mkdir(parents=True, exist_ok=True)
186
+ self.project = Project(
187
+ path=self.workspace_root,
188
+ smart_sys_path=True,
189
+ load_unsafe_extensions=False,
190
+ )
191
+
192
+ def _document(self, uri: str) -> Document:
193
+ return self.documents[uri]
194
+
195
+ def _mirror_document(self, uri: str) -> None:
196
+ document = self._document(uri)
197
+ path = Path(document.path)
198
+ path.parent.mkdir(parents=True, exist_ok=True)
199
+ path.write_text(document.source, encoding="utf-8")
200
+
201
+ def _script(self, uri: str) -> Script:
202
+ document = self._document(uri)
203
+ return Script(code=document.source, path=document.path, project=self.project)
204
+
205
+ def _range_for_name(self, name) -> dict | None:
206
+ if name.line is None or name.column is None:
207
+ return None
208
+
209
+ return {
210
+ "start": {"line": name.line - 1, "character": name.column},
211
+ "end": {"line": name.line - 1, "character": name.column + len(name.name)},
212
+ }
213
+
214
+ def _location_for_name(self, name, fallback_uri: str) -> dict | None:
215
+ range_value = self._range_for_name(name)
216
+ if range_value is None:
217
+ return None
218
+
219
+ module_path = getattr(name, "module_path", None)
220
+ uri = fallback_uri
221
+ if module_path is not None:
222
+ uri = _path_to_uri(Path(module_path))
223
+
224
+ return {"uri": uri, "range": range_value}
225
+
226
+ def _completion_kind(self, name_type: str) -> int:
227
+ return COMPLETION_KIND_MAP.get(name_type, COMPLETION_ITEM_KIND["text"])
228
+
229
+ def _symbol_kind(self, name_type: str) -> int:
230
+ return SYMBOL_KIND_MAP.get(name_type, SYMBOL_KIND["variable"])
231
+
232
+ def _publish_diagnostics(self, uri: str) -> None:
233
+ document = self._document(uri)
234
+ diagnostics: list[dict] = []
235
+
236
+ try:
237
+ ast.parse(document.source, filename=document.path)
238
+ except SyntaxError as error:
239
+ line = max((error.lineno or 1) - 1, 0)
240
+ column = max((error.offset or 1) - 1, 0)
241
+ diagnostics.append(
242
+ {
243
+ "range": {
244
+ "start": {"line": line, "character": column},
245
+ "end": {"line": line, "character": column + 1},
246
+ },
247
+ "severity": DIAGNOSTIC_SEVERITY_ERROR,
248
+ "source": SERVER_NAME,
249
+ "message": error.msg or "Syntax error",
250
+ }
251
+ )
252
+
253
+ self._notify(
254
+ "textDocument/publishDiagnostics",
255
+ {"uri": uri, "diagnostics": diagnostics},
256
+ )
257
+
258
+ def _hover_text(self, definitions) -> str | None:
259
+ chunks: list[str] = []
260
+ for definition in definitions:
261
+ description = getattr(definition, "description", "") or definition.name
262
+ doc = definition.docstring(raw=False).strip()
263
+ block = description if not doc else f"{description}\n\n{doc}"
264
+ if block and block not in chunks:
265
+ chunks.append(block)
266
+
267
+ if not chunks:
268
+ return None
269
+
270
+ return "\n\n---\n\n".join(chunks)
271
+
272
+ def _initialize(self, params: dict) -> dict:
273
+ root_uri = params.get("rootUri")
274
+ if not root_uri and params.get("workspaceFolders"):
275
+ root_uri = params["workspaceFolders"][0].get("uri")
276
+ self._refresh_project(root_uri)
277
+ return {
278
+ "capabilities": {
279
+ "textDocumentSync": TEXT_DOCUMENT_SYNC_FULL,
280
+ "completionProvider": {
281
+ "triggerCharacters": [".", "(", "[", '"', "'"],
282
+ "resolveProvider": False,
283
+ },
284
+ "hoverProvider": True,
285
+ "definitionProvider": True,
286
+ "documentSymbolProvider": True,
287
+ "signatureHelpProvider": {"triggerCharacters": ["(", ","]},
288
+ },
289
+ "serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
290
+ }
291
+
292
+ def _shutdown(self):
293
+ self.shutdown_requested = True
294
+ return None
295
+
296
+ def _completion(self, params: dict) -> dict:
297
+ position = params["position"]
298
+ completions = self._script(params["textDocument"]["uri"]).complete(
299
+ line=position["line"] + 1,
300
+ column=position["character"],
301
+ )
302
+ items = []
303
+ for completion in completions:
304
+ item = {
305
+ "label": completion.name_with_symbols or completion.name,
306
+ "kind": self._completion_kind(completion.type),
307
+ "detail": completion.type,
308
+ "insertText": completion.complete or completion.name,
309
+ }
310
+ doc = completion.docstring(raw=False).strip()
311
+ if doc:
312
+ item["documentation"] = doc
313
+ items.append(item)
314
+ return {"isIncomplete": False, "items": items}
315
+
316
+ def _signature_help(self, params: dict) -> dict | None:
317
+ position = params["position"]
318
+ signatures = self._script(params["textDocument"]["uri"]).get_signatures(
319
+ line=position["line"] + 1,
320
+ column=position["character"],
321
+ )
322
+ if not signatures:
323
+ return None
324
+
325
+ items = []
326
+ for signature in signatures:
327
+ items.append(
328
+ {
329
+ "label": signature.to_string(),
330
+ "documentation": signature.docstring(raw=False).strip() or None,
331
+ "parameters": [
332
+ {"label": parameter.to_string()} for parameter in signature.params
333
+ ],
334
+ }
335
+ )
336
+
337
+ return {"signatures": items, "activeSignature": 0, "activeParameter": 0}
338
+
339
+ def _hover(self, params: dict) -> dict | None:
340
+ position = params["position"]
341
+ definitions = self._script(params["textDocument"]["uri"]).infer(
342
+ line=position["line"] + 1,
343
+ column=position["character"],
344
+ )
345
+ hover_text = self._hover_text(definitions)
346
+ if not hover_text:
347
+ return None
348
+ return {"contents": {"kind": "markdown", "value": hover_text}}
349
+
350
+ def _definition(self, params: dict) -> list[dict] | None:
351
+ position = params["position"]
352
+ definitions = self._script(params["textDocument"]["uri"]).goto(
353
+ line=position["line"] + 1,
354
+ column=position["character"],
355
+ follow_imports=True,
356
+ follow_builtin_imports=True,
357
+ )
358
+ locations = [
359
+ location
360
+ for location in (
361
+ self._location_for_name(definition, params["textDocument"]["uri"])
362
+ for definition in definitions
363
+ )
364
+ if location is not None
365
+ ]
366
+ return locations or None
367
+
368
+ def _document_symbols(self, params: dict) -> list[dict]:
369
+ names = self._script(params["textDocument"]["uri"]).get_names(
370
+ all_scopes=True,
371
+ definitions=True,
372
+ references=False,
373
+ )
374
+ symbols = []
375
+ for name in names:
376
+ location = self._location_for_name(name, params["textDocument"]["uri"])
377
+ if location is None:
378
+ continue
379
+ symbols.append(
380
+ {
381
+ "name": name.name,
382
+ "kind": self._symbol_kind(name.type),
383
+ "location": location,
384
+ "containerName": getattr(name, "full_name", None) or name.name,
385
+ }
386
+ )
387
+ return symbols
388
+
389
+ def _handle_request(self, msg_id, method: str, params: dict) -> None:
390
+ try:
391
+ handlers = {
392
+ "initialize": self._initialize,
393
+ "shutdown": self._shutdown,
394
+ "textDocument/completion": self._completion,
395
+ "textDocument/signatureHelp": self._signature_help,
396
+ "textDocument/hover": self._hover,
397
+ "textDocument/definition": self._definition,
398
+ "textDocument/documentSymbol": self._document_symbols,
399
+ }
400
+ handler = handlers.get(method)
401
+ if handler is None:
402
+ self._respond(
403
+ msg_id,
404
+ error={"code": -32601, "message": f"Method not found: {method}"},
405
+ )
406
+ return
407
+ self._respond(msg_id, result=handler(params))
408
+ except Exception as error:
409
+ self._respond(
410
+ msg_id,
411
+ error={"code": -32603, "message": str(error) or "Internal server error"},
412
+ )
413
+
414
+ def _handle_notification(self, method: str, params: dict) -> None:
415
+ try:
416
+ if method == "initialized":
417
+ return
418
+ if method == "exit":
419
+ self.shutdown_requested = True
420
+ return
421
+ if method == "textDocument/didOpen":
422
+ text_document = params["textDocument"]
423
+ self.documents[text_document["uri"]] = Document(
424
+ uri=text_document["uri"],
425
+ source=text_document.get("text", ""),
426
+ version=text_document.get("version"),
427
+ language_id=text_document.get("languageId"),
428
+ )
429
+ self._mirror_document(text_document["uri"])
430
+ self._publish_diagnostics(text_document["uri"])
431
+ return
432
+ if method == "textDocument/didChange":
433
+ text_document = params["textDocument"]
434
+ document = self._document(text_document["uri"])
435
+ for change in params.get("contentChanges", []):
436
+ document.apply_change(change)
437
+ document.version = text_document.get("version")
438
+ self._mirror_document(text_document["uri"])
439
+ self._publish_diagnostics(text_document["uri"])
440
+ return
441
+ if method == "textDocument/didSave":
442
+ text_document = params["textDocument"]
443
+ document = self._document(text_document["uri"])
444
+ if "text" in params and params["text"] is not None:
445
+ document.source = params["text"]
446
+ self._mirror_document(text_document["uri"])
447
+ self._publish_diagnostics(text_document["uri"])
448
+ return
449
+ if method == "textDocument/didClose":
450
+ text_document = params["textDocument"]
451
+ self.documents.pop(text_document["uri"], None)
452
+ self._notify(
453
+ "textDocument/publishDiagnostics",
454
+ {"uri": text_document["uri"], "diagnostics": []},
455
+ )
456
+ except Exception:
457
+ return
458
+
459
+
460
+ def create_bridge():
461
+ server = ArcturusPythonLsp()
462
+ return server.handle
@@ -0,0 +1,2 @@
1
+ import type { EditorLanguageServerHandle, EditorLanguageServerRuntimeOptions } from '../types';
2
+ export declare function getPythonLanguageServer(options?: string | EditorLanguageServerRuntimeOptions): Promise<EditorLanguageServerHandle>;
@@ -0,0 +1,54 @@
1
+ import { resolvePythonLanguageServerBaseUrl } from '../runtime';
2
+ import PythonLspWorker from './main.worker?worker';
3
+ import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-jsonrpc/browser';
4
+ const currentUrl = () => globalThis.location?.href || '';
5
+ async function createServer(pyodideBaseUrl) {
6
+ let resolveReady = () => { };
7
+ let rejectReady = (_error) => { };
8
+ const ready = new Promise((resolve, reject) => {
9
+ resolveReady = resolve;
10
+ rejectReady = reject;
11
+ });
12
+ const worker = new PythonLspWorker();
13
+ const cleanup = () => {
14
+ worker.removeEventListener('message', readyListener);
15
+ worker.removeEventListener('error', errorListener);
16
+ };
17
+ const readyListener = (event) => {
18
+ switch (event.data?.type) {
19
+ case 'ready': {
20
+ cleanup();
21
+ resolveReady();
22
+ break;
23
+ }
24
+ case 'error': {
25
+ cleanup();
26
+ rejectReady(new Error(event.data?.error || 'Python LSP worker failed to initialize'));
27
+ break;
28
+ }
29
+ }
30
+ };
31
+ const errorListener = (event) => {
32
+ cleanup();
33
+ rejectReady(event.error || new Error(event.message || 'Python LSP worker failed'));
34
+ };
35
+ worker.addEventListener('message', readyListener);
36
+ worker.addEventListener('error', errorListener);
37
+ worker.postMessage({ type: 'init', pyodideBaseUrl });
38
+ await ready;
39
+ return worker;
40
+ }
41
+ export async function getPythonLanguageServer(options) {
42
+ const pyodideBaseUrl = resolvePythonLanguageServerBaseUrl(options, currentUrl());
43
+ const worker = await createServer(pyodideBaseUrl);
44
+ const reader = new BrowserMessageReader(worker);
45
+ const writer = new BrowserMessageWriter(worker);
46
+ return {
47
+ transport: { reader, writer },
48
+ dispose: () => {
49
+ worker.terminate();
50
+ reader.dispose();
51
+ writer.dispose();
52
+ }
53
+ };
54
+ }
@@ -0,0 +1,2 @@
1
+ import type { EditorLanguageServerRuntimeOptions, EditorLanguageServerHandle } from './types';
2
+ export declare function getEditorLanguageServer(language: string, options?: string | EditorLanguageServerRuntimeOptions): Promise<EditorLanguageServerHandle | null>;
@@ -0,0 +1,12 @@
1
+ import { getCppLanguageServer } from './cpp/server';
2
+ import { getPythonLanguageServer } from './python/server';
3
+ export async function getEditorLanguageServer(language, options) {
4
+ const normalized = language.toLowerCase();
5
+ if (normalized === 'c' || normalized === 'cpp') {
6
+ return getCppLanguageServer(options);
7
+ }
8
+ if (normalized === 'python') {
9
+ return getPythonLanguageServer(options);
10
+ }
11
+ return null;
12
+ }
@@ -0,0 +1,3 @@
1
+ import type { EditorLanguageServerRuntimeOptions } from './types';
2
+ export declare function resolveCppLanguageServerBaseUrl(options: string | EditorLanguageServerRuntimeOptions | undefined, currentUrl?: string): string;
3
+ export declare function resolvePythonLanguageServerBaseUrl(options: string | EditorLanguageServerRuntimeOptions | undefined, currentUrl?: string): string;
@@ -0,0 +1,30 @@
1
+ const normalizeBaseUrl = (baseUrl, currentUrl = '') => {
2
+ const normalized = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
3
+ return currentUrl ? new URL(normalized, currentUrl).href : normalized;
4
+ };
5
+ const normalizeRootUrl = (rootUrl) => rootUrl.endsWith('/') ? rootUrl.slice(0, -1) : rootUrl;
6
+ const resolveRuntimeRootBaseUrl = (rootUrl, suffix, currentUrl = '') => normalizeBaseUrl(`${normalizeRootUrl(rootUrl) || ''}${suffix}`, currentUrl);
7
+ export function resolveCppLanguageServerBaseUrl(options, currentUrl = '') {
8
+ if (typeof options === 'string') {
9
+ return resolveRuntimeRootBaseUrl(options, '/clangd/', currentUrl);
10
+ }
11
+ if (options?.cpp?.baseUrl) {
12
+ return normalizeBaseUrl(options.cpp.baseUrl, currentUrl);
13
+ }
14
+ if (options?.rootUrl) {
15
+ return resolveRuntimeRootBaseUrl(options.rootUrl, '/clangd/', currentUrl);
16
+ }
17
+ return normalizeBaseUrl('/clangd/', currentUrl);
18
+ }
19
+ export function resolvePythonLanguageServerBaseUrl(options, currentUrl = '') {
20
+ if (typeof options === 'string') {
21
+ return resolveRuntimeRootBaseUrl(options, '/pyodide/', currentUrl);
22
+ }
23
+ if (options?.python?.baseUrl) {
24
+ return normalizeBaseUrl(options.python.baseUrl, currentUrl);
25
+ }
26
+ if (options?.rootUrl) {
27
+ return resolveRuntimeRootBaseUrl(options.rootUrl, '/pyodide/', currentUrl);
28
+ }
29
+ return normalizeBaseUrl('/pyodide/', currentUrl);
30
+ }
@@ -0,0 +1,15 @@
1
+ import type { MessageTransports } from '@hancomac/monaco-languageclient';
2
+ export interface EditorLanguageServerHandle {
3
+ transport: string | MessageTransports;
4
+ syncFile?: (path: string) => void;
5
+ dispose: () => void;
6
+ }
7
+ export interface EditorLanguageServerRuntimeOptions {
8
+ rootUrl?: string;
9
+ cpp?: {
10
+ baseUrl?: string;
11
+ };
12
+ python?: {
13
+ baseUrl?: string;
14
+ };
15
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wasm-idle",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "A simple web app to run C++/Python/Java code in the browser",
5
5
  "license": "MIT",
6
6
  "type": "module",