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.
- package/dist/clangd/worker.js +21 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/lsp/cpp/server.d.ts +2 -0
- package/dist/lsp/cpp/server.js +57 -0
- package/dist/lsp/index.d.ts +5 -0
- package/dist/lsp/index.js +4 -0
- package/dist/lsp/python/main.worker.d.ts +1 -0
- package/dist/lsp/python/main.worker.js +92 -0
- package/dist/lsp/python/package/arcturus_python_lsp/__init__.py +3 -0
- package/dist/lsp/python/package/arcturus_python_lsp/server.py +462 -0
- package/dist/lsp/python/server.d.ts +2 -0
- package/dist/lsp/python/server.js +54 -0
- package/dist/lsp/registry.d.ts +2 -0
- package/dist/lsp/registry.js +12 -0
- package/dist/lsp/runtime.d.ts +3 -0
- package/dist/lsp/runtime.js +30 -0
- package/dist/lsp/types.d.ts +15 -0
- package/dist/lsp/types.js +1 -0
- package/package.json +1 -1
package/dist/clangd/worker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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,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 @@
|
|
|
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,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,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,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 {};
|