rapydscript-ng 0.7.24 → 0.8.1
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/CHANGELOG.md +28 -0
- package/README.md +80 -26
- package/bin/build.ts +117 -0
- package/bin/package.json +1 -0
- package/bin/rapydscript +65 -62
- package/build_wheels.py +133 -0
- package/editor-plugins/README.md +80 -0
- package/editor-plugins/nvim.lua +321 -0
- package/local-agent.md +28 -0
- package/package.json +4 -5
- package/publish.py +27 -17
- package/release/baselib-plain-pretty.js +448 -326
- package/release/baselib-plain-ugly.js +3 -3
- package/release/compiler.js +9027 -8139
- package/release/signatures.json +27 -27
- package/release/stdlib_modules.json +1 -0
- package/src/ast.pyj +428 -280
- package/src/baselib-builtins.pyj +49 -28
- package/src/baselib-containers.pyj +241 -226
- package/src/baselib-errors.pyj +8 -1
- package/src/baselib-internal.pyj +38 -23
- package/src/baselib-itertools.pyj +13 -7
- package/src/baselib-str.pyj +59 -84
- package/src/compiler.pyj +4 -4
- package/src/errors.pyj +3 -4
- package/src/lib/aes.pyj +46 -32
- package/src/lib/elementmaker.pyj +11 -9
- package/src/lib/encodings.pyj +15 -5
- package/src/lib/gettext.pyj +9 -4
- package/src/lib/math.pyj +106 -45
- package/src/lib/operator.pyj +10 -10
- package/src/lib/pythonize.pyj +2 -1
- package/src/lib/random.pyj +28 -21
- package/src/lib/re.pyj +492 -19
- package/src/lib/traceback.pyj +171 -29
- package/src/lib/uuid.pyj +2 -2
- package/src/output/classes.pyj +28 -27
- package/src/output/codegen.pyj +87 -83
- package/src/output/comments.pyj +8 -8
- package/src/output/exceptions.pyj +20 -21
- package/src/output/functions.pyj +66 -64
- package/src/output/literals.pyj +24 -23
- package/src/output/loops.pyj +84 -142
- package/src/output/modules.pyj +55 -68
- package/src/output/operators.pyj +40 -29
- package/src/output/statements.pyj +21 -15
- package/src/output/stream.pyj +82 -56
- package/src/output/utils.pyj +12 -8
- package/src/parse.pyj +971 -620
- package/src/string_interpolation.pyj +5 -3
- package/src/tokenizer.pyj +188 -148
- package/src/utils.pyj +32 -15
- package/test/_treeshake_mod.pyj +30 -0
- package/test/_treeshake_side_effect.pyj +9 -0
- package/test/ast_serialization.pyj +392 -0
- package/test/async.pyj +95 -0
- package/test/baselib.pyj +1 -2
- package/test/embedded_compiler.pyj +57 -0
- package/test/fmt.pyj +291 -0
- package/test/generic.pyj +16 -3
- package/test/imports.pyj +8 -6
- package/test/internationalization.pyj +38 -37
- package/test/lint.pyj +120 -117
- package/test/lsp.pyj +257 -0
- package/test/repl.pyj +70 -65
- package/test/sourcemaps.pyj +315 -0
- package/test/starargs.pyj +46 -39
- package/test/str.pyj +8 -0
- package/test/traceback.pyj +263 -0
- package/test/treeshaking.pyj +181 -0
- package/test/web_repl_export.pyj +88 -0
- package/tools/ast_serialize.mjs +557 -0
- package/tools/cli.mjs +531 -0
- package/tools/compile.mjs +205 -0
- package/tools/compiler.mjs +159 -0
- package/tools/{completer.js → completer.mjs} +6 -6
- package/tools/{embedded_compiler.js → embedded_compiler.mjs} +17 -13
- package/tools/export.js +109 -56
- package/tools/fmt.mjs +975 -0
- package/tools/{gettext.js → gettext.mjs} +46 -53
- package/tools/ini.mjs +175 -0
- package/tools/{lint.js → lint.mjs} +78 -55
- package/tools/lsp.mjs +964 -0
- package/tools/lsp_protocol.mjs +259 -0
- package/tools/lsp_symbols.mjs +418 -0
- package/tools/{msgfmt.js → msgfmt.mjs} +18 -18
- package/tools/{repl.js → repl.mjs} +56 -42
- package/tools/{self.js → self.mjs} +71 -46
- package/tools/sourcemap.mjs +123 -0
- package/tools/test.mjs +252 -0
- package/tools/treeshake.mjs +400 -0
- package/tools/{utils.js → utils.mjs} +26 -23
- package/tools/{web_repl.js → web_repl.mjs} +15 -13
- package/tools/web_repl_export.mjs +97 -0
- package/tree-sitter/README.md +101 -0
- package/tree-sitter/grammar.js +992 -0
- package/tree-sitter/package.json +43 -0
- package/tree-sitter/queries/highlights.scm +232 -0
- package/tree-sitter/queries/indents.scm +64 -0
- package/tree-sitter/queries/injections.scm +14 -0
- package/tree-sitter/queries/locals.scm +108 -0
- package/tree-sitter/src/grammar.json +4976 -0
- package/tree-sitter/src/node-types.json +2901 -0
- package/tree-sitter/src/parser.c +196465 -0
- package/tree-sitter/src/scanner.c +294 -0
- package/tree-sitter/src/tree_sitter/alloc.h +54 -0
- package/tree-sitter/src/tree_sitter/array.h +330 -0
- package/tree-sitter/src/tree_sitter/parser.h +286 -0
- package/tree-sitter/test/corpus/chaining.txt +99 -0
- package/tree-sitter/test/corpus/classes.txt +147 -0
- package/tree-sitter/test/corpus/comprehensions.txt +155 -0
- package/tree-sitter/test/corpus/containers.txt +242 -0
- package/tree-sitter/test/corpus/control_flow.txt +361 -0
- package/tree-sitter/test/corpus/decorators.txt +64 -0
- package/tree-sitter/test/corpus/existential.txt +102 -0
- package/tree-sitter/test/corpus/functions.txt +293 -0
- package/tree-sitter/test/corpus/imports.txt +117 -0
- package/tree-sitter/test/corpus/literals.txt +135 -0
- package/tree-sitter/test/corpus/operators.txt +296 -0
- package/tree-sitter/test/corpus/statements.txt +189 -0
- package/tree-sitter/test/corpus/strings.txt +90 -0
- package/tree-sitter/test/corpus/subscripts.txt +227 -0
- package/tree-sitter/tree-sitter.json +36 -0
- package/web-repl/env.js +35 -23
- package/web-repl/main.js +8 -8
- package/bin/export +0 -75
- package/bin/web-repl-export +0 -102
- package/tools/cli.js +0 -523
- package/tools/compile.js +0 -184
- package/tools/compiler.js +0 -84
- package/tools/ini.js +0 -65
- package/tools/test.js +0 -110
package/tools/lsp.mjs
ADDED
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
/* vim:fileencoding=utf-8
|
|
2
|
+
*
|
|
3
|
+
* lsp.mjs -- Language Server Protocol server for RapydScript.
|
|
4
|
+
*
|
|
5
|
+
* Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
|
|
6
|
+
*
|
|
7
|
+
* Distributed under terms of the BSD license.
|
|
8
|
+
*
|
|
9
|
+
* Provides: code completion, diagnostics, hover, code actions, document
|
|
10
|
+
* formatting, go-to-definition, find-all-references and rename. Diagnostics
|
|
11
|
+
* reuse the linter (tools/lint.mjs) plus a check for unresolved imports.
|
|
12
|
+
* Formatting reuses the formatter (tools/fmt.mjs). Cross-file references and
|
|
13
|
+
* rename are resolved through the module import graph.
|
|
14
|
+
*
|
|
15
|
+
* The server reuses the single compiler instance created by bin/rapydscript.
|
|
16
|
+
*/
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import crypto from 'crypto';
|
|
22
|
+
import { pathToFileURL, fileURLToPath } from 'url';
|
|
23
|
+
import * as utils from './utils.mjs';
|
|
24
|
+
import { lint_parsed, BUILTINS, WARN, ERROR, MESSAGES } from './lint.mjs';
|
|
25
|
+
import { format_string, organize_imports } from './fmt.mjs';
|
|
26
|
+
import * as sym from './lsp_symbols.mjs';
|
|
27
|
+
import { create_connection, DocumentStore, TextDocument, ResponseError, ErrorCodes } from './lsp_protocol.mjs';
|
|
28
|
+
import { read_pyproject_config } from './ini.mjs';
|
|
29
|
+
|
|
30
|
+
const RapydScript = globalThis.create_rapydscript_compiler();
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// LSP enum constants
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
var DiagnosticSeverity = { Error: 1, Warning: 2, Information: 3, Hint: 4 };
|
|
36
|
+
var CompletionItemKind = {
|
|
37
|
+
Text: 1, Method: 2, Function: 3, Constructor: 4, Field: 5, Variable: 6,
|
|
38
|
+
Class: 7, Interface: 8, Module: 9, Property: 10, Keyword: 14, Constant: 21,
|
|
39
|
+
};
|
|
40
|
+
var SymbolKindLSP = { File: 1, Module: 2, Class: 5, Method: 6, Property: 7, Field: 8, Function: 12, Variable: 13, Constant: 14 };
|
|
41
|
+
var CodeActionKind = { QuickFix: 'quickfix', SourceFixAll: 'source.fixAll', Source: 'source', OrganizeImports: 'source.organizeImports' };
|
|
42
|
+
|
|
43
|
+
function kind_to_completion(kind) {
|
|
44
|
+
switch (kind) {
|
|
45
|
+
case sym.KIND.FUNCTION: return CompletionItemKind.Function;
|
|
46
|
+
case sym.KIND.CLASS: return CompletionItemKind.Class;
|
|
47
|
+
case sym.KIND.METHOD: return CompletionItemKind.Method;
|
|
48
|
+
case sym.KIND.IMPORT: return CompletionItemKind.Module;
|
|
49
|
+
case sym.KIND.IMPORTED_NAME: return CompletionItemKind.Variable;
|
|
50
|
+
case sym.KIND.PARAMETER: return CompletionItemKind.Variable;
|
|
51
|
+
default: return CompletionItemKind.Variable;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// URI <-> path and text normalization
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
function uri_to_path(uri) {
|
|
59
|
+
if (uri.slice(0, 5) === 'file:') return fileURLToPath(uri);
|
|
60
|
+
return uri;
|
|
61
|
+
}
|
|
62
|
+
function path_to_uri(p) { return pathToFileURL(p).href; }
|
|
63
|
+
// The tokenizer normalizes line endings; keep all offset math on normalized text.
|
|
64
|
+
function normalize(text) { return text.replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/\uFEFF/g, ""); }
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Server context: caches, import dirs, document store, workspace.
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
export function create_server_context(opts) {
|
|
70
|
+
opts = opts || {};
|
|
71
|
+
return {
|
|
72
|
+
import_dirs: opts.import_dirs || [], // user --import-path dirs
|
|
73
|
+
libdir: opts.libdir || null, // stdlib dir (src/lib)
|
|
74
|
+
line_length: opts.line_length || 80,
|
|
75
|
+
preferred_quote: opts.preferred_quote || 'single',
|
|
76
|
+
join_lines: opts.join_lines || false,
|
|
77
|
+
docs: new DocumentStore(),
|
|
78
|
+
analysis_cache: Object.create(null), // key -> analysis
|
|
79
|
+
workspace_roots: opts.workspace_roots ? opts.workspace_roots.slice() : [],
|
|
80
|
+
_scan_cache: null, // {files:[...], at:mtime-ish}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Apply a workspace/didChangeConfiguration settings object to ctx.
|
|
85
|
+
// Returns {import_dirs: true} if import_dirs changed (caller should re-run diagnostics).
|
|
86
|
+
export function apply_configuration(ctx, settings) {
|
|
87
|
+
if (!settings) return {};
|
|
88
|
+
var changed = {};
|
|
89
|
+
|
|
90
|
+
if (settings.lineLength !== undefined && settings.lineLength !== null) {
|
|
91
|
+
var ll = typeof settings.lineLength === 'number'
|
|
92
|
+
? Math.floor(settings.lineLength)
|
|
93
|
+
: parseInt(settings.lineLength, 10);
|
|
94
|
+
if (!isNaN(ll) && ll > 0) ctx.line_length = ll;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (settings.preferredQuote === 'single' || settings.preferredQuote === 'double') {
|
|
98
|
+
ctx.preferred_quote = settings.preferredQuote;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (settings.joinLines !== undefined && settings.joinLines !== null) {
|
|
102
|
+
ctx.join_lines = !!settings.joinLines;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (settings.importPath !== undefined && settings.importPath !== null) {
|
|
106
|
+
var new_dirs;
|
|
107
|
+
if (Array.isArray(settings.importPath)) {
|
|
108
|
+
new_dirs = settings.importPath
|
|
109
|
+
.filter(function (p) { return p && typeof p === 'string'; })
|
|
110
|
+
.map(function (p) { return path.resolve(p); });
|
|
111
|
+
} else if (typeof settings.importPath === 'string') {
|
|
112
|
+
new_dirs = utils.get_import_dirs(settings.importPath)
|
|
113
|
+
.map(function (p) { return path.resolve(p); });
|
|
114
|
+
}
|
|
115
|
+
if (new_dirs !== undefined) {
|
|
116
|
+
ctx.import_dirs = new_dirs;
|
|
117
|
+
ctx.analysis_cache = Object.create(null);
|
|
118
|
+
invalidate_workspace_scan(ctx);
|
|
119
|
+
changed.import_dirs = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return changed;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Import search dirs for a given file: user dirs + stdlib + the file's directory.
|
|
127
|
+
function import_dirs_for(ctx, file_path) {
|
|
128
|
+
var dirs = ctx.import_dirs.slice();
|
|
129
|
+
if (ctx.libdir) dirs.push(ctx.libdir);
|
|
130
|
+
if (file_path) dirs.push(path.dirname(file_path));
|
|
131
|
+
return dirs;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Analysis: parse (with error recovery) + build the symbol index. Cached per
|
|
136
|
+
// (uri, text) so hover/definition/references/completion reuse a single parse.
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
export async function analyze(ctx, uri, raw_text) {
|
|
139
|
+
var text = normalize(raw_text);
|
|
140
|
+
var cache_key = crypto.createHash('sha1').update(text).digest('hex');
|
|
141
|
+
var cached = ctx.analysis_cache[uri];
|
|
142
|
+
if (cached && cached.cache_key === cache_key) return cached;
|
|
143
|
+
|
|
144
|
+
var file_path = uri_to_path(uri);
|
|
145
|
+
var toplevel = null, recovered = [];
|
|
146
|
+
try {
|
|
147
|
+
toplevel = await RapydScript.parse(text, {
|
|
148
|
+
filename: file_path,
|
|
149
|
+
for_linting: true,
|
|
150
|
+
recover_errors: true,
|
|
151
|
+
basedir: path.dirname(file_path),
|
|
152
|
+
libdir: ctx.libdir || path.dirname(file_path),
|
|
153
|
+
import_dirs: ctx.import_dirs,
|
|
154
|
+
});
|
|
155
|
+
recovered = toplevel.recovered_errors || [];
|
|
156
|
+
} catch (e) {
|
|
157
|
+
// Even in recovery mode an unexpected internal error can occur; degrade
|
|
158
|
+
// gracefully to a single diagnostic rather than crashing the server.
|
|
159
|
+
if (e instanceof RapydScript.SyntaxError) {
|
|
160
|
+
recovered = [{ message: e.message, line: e.line, col: e.col, pos: e.pos, is_eof: e.is_eof }];
|
|
161
|
+
} else {
|
|
162
|
+
recovered = [{ message: 'Internal parse error: ' + (e && e.message ? e.message : e), line: 1, col: 0, pos: 0 }];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
var index = toplevel ? sym.build_index(toplevel, file_path) : null;
|
|
166
|
+
var doc = new TextDocument(uri, 'rapydscript', 0, text);
|
|
167
|
+
var analysis = {
|
|
168
|
+
uri: uri, file_path: file_path, text: text, cache_key: cache_key,
|
|
169
|
+
toplevel: toplevel, index: index, recovered_errors: recovered, doc: doc,
|
|
170
|
+
};
|
|
171
|
+
ctx.analysis_cache[uri] = analysis;
|
|
172
|
+
return analysis;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Convenience: get the current text for a uri (open document wins over disk).
|
|
176
|
+
async function text_for_uri(ctx, uri) {
|
|
177
|
+
var doc = ctx.docs.get(uri);
|
|
178
|
+
if (doc) return doc.text;
|
|
179
|
+
return normalize(await fs.promises.readFile(uri_to_path(uri), 'utf-8'));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function offset_to_range(doc, span) {
|
|
183
|
+
return { start: doc.position_at(span[0]), end: doc.position_at(span[1]) };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Import resolution (for the unresolved-import diagnostic and go-to-definition)
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
async function resolve_module(ctx, key, from_file, stat_cache) {
|
|
190
|
+
var modpath = key.replace(/\./g, '/');
|
|
191
|
+
var dirs = import_dirs_for(ctx, from_file);
|
|
192
|
+
for (var i = 0; i < dirs.length; i++) {
|
|
193
|
+
if (!dirs[i]) continue;
|
|
194
|
+
var base = dirs[i] + '/' + modpath;
|
|
195
|
+
for (const candidate of [base + '.pyj', base + '/__init__.pyj']) {
|
|
196
|
+
if (await path_exists_cached(candidate, stat_cache)) return path.normalize(candidate);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function path_exists_cached(p, cache) {
|
|
203
|
+
if (cache && Object.prototype.hasOwnProperty.call(cache, p)) return cache[p];
|
|
204
|
+
var ok;
|
|
205
|
+
try { await fs.promises.stat(p); ok = true; }
|
|
206
|
+
catch (e) { ok = false; }
|
|
207
|
+
if (cache) cache[p] = ok;
|
|
208
|
+
return ok;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Diagnostics
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
export async function compute_diagnostics(ctx, uri, raw_text) {
|
|
215
|
+
var a = await analyze(ctx, uri, raw_text);
|
|
216
|
+
var doc = a.doc;
|
|
217
|
+
var out = [];
|
|
218
|
+
|
|
219
|
+
// 1. Syntax errors surfaced by recovery parsing.
|
|
220
|
+
(a.recovered_errors || []).forEach(function (e) {
|
|
221
|
+
var pos = (typeof e.pos === 'number') ? e.pos : doc.offset_at(doc.lsp_position(e.line, e.col));
|
|
222
|
+
var end = (typeof e.endpos === 'number') ? e.endpos : pos + 1;
|
|
223
|
+
out.push({
|
|
224
|
+
range: { start: doc.position_at(pos), end: doc.position_at(Math.max(end, pos + 1)) },
|
|
225
|
+
severity: DiagnosticSeverity.Error, code: 'syntax-err', source: 'rapydscript',
|
|
226
|
+
message: e.message,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// 2. Linter diagnostics -- only when the file parsed cleanly, so we do not
|
|
231
|
+
// emit misleading undefined/unused warnings from a partial AST.
|
|
232
|
+
if (a.toplevel && (!a.recovered_errors || a.recovered_errors.length === 0)) {
|
|
233
|
+
var builtins = utils.merge(BUILTINS);
|
|
234
|
+
var messages = lint_parsed(a.toplevel, a.text, { filename: a.file_path, builtins: builtins, noqa: file_noqa(a.text) });
|
|
235
|
+
messages.forEach(function (m) {
|
|
236
|
+
out.push(lint_message_to_diagnostic(doc, m));
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 3. Unresolved imports.
|
|
241
|
+
if (a.index) {
|
|
242
|
+
var stat_cache = Object.create(null);
|
|
243
|
+
for (const imp of a.index.imports) {
|
|
244
|
+
var resolved = await resolve_module(ctx, imp.key, a.file_path, stat_cache);
|
|
245
|
+
if (!resolved) {
|
|
246
|
+
var span = sym.node_span(imp.node) || [0, 1];
|
|
247
|
+
out.push({
|
|
248
|
+
range: offset_to_range(doc, span),
|
|
249
|
+
severity: DiagnosticSeverity.Error, code: 'import-unresolved', source: 'rapydscript',
|
|
250
|
+
message: 'Unresolved import: "' + imp.key + '" was not found in the import path',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Parse `# noqa: a,b` file-level directives from the first lines (mirrors lint cli).
|
|
259
|
+
function file_noqa(code) {
|
|
260
|
+
var noqa = Object.create(null);
|
|
261
|
+
code.split('\n', 20).forEach(function (line) {
|
|
262
|
+
var lq = line.replace(/\s+/g, '');
|
|
263
|
+
if (lq.slice(0, 6).toLowerCase() === '#noqa:') (lq.split(':', 2)[1] || '').split(',').forEach(function (x) { if (x) noqa[x] = true; });
|
|
264
|
+
});
|
|
265
|
+
return noqa;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function lint_message_to_diagnostic(doc, m) {
|
|
269
|
+
var start_line = (m.start_line || 1) - 1;
|
|
270
|
+
var start_col = (m.start_col === undefined || m.start_col === null) ? 0 : m.start_col;
|
|
271
|
+
var end_line = (m.end_line || m.start_line || 1) - 1;
|
|
272
|
+
var end_col = (m.end_col === undefined || m.end_col === null) ? start_col + 1 : m.end_col;
|
|
273
|
+
return {
|
|
274
|
+
range: { start: { line: start_line, character: start_col }, end: { line: end_line, character: end_col } },
|
|
275
|
+
severity: (m.level === WARN) ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error,
|
|
276
|
+
code: m.ident, source: 'rapydscript', message: m.message,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Formatting
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
export function format_document(ctx, raw_text) {
|
|
284
|
+
var text = normalize(raw_text);
|
|
285
|
+
var formatted = format_string(text, { line_length: ctx.line_length, preferred_quote: ctx.preferred_quote, join_lines: ctx.join_lines });
|
|
286
|
+
if (formatted === text) return [];
|
|
287
|
+
// Single edit that replaces the whole document.
|
|
288
|
+
var doc = new TextDocument('inmem', 'rapydscript', 0, text);
|
|
289
|
+
var end = doc.position_at(text.length);
|
|
290
|
+
return [{ range: { start: { line: 0, character: 0 }, end: end }, newText: formatted }];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Organize imports
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
export function organize_imports_document(ctx, raw_text) {
|
|
297
|
+
var text = normalize(raw_text);
|
|
298
|
+
var organized = organize_imports(text, { line_length: ctx.line_length, preferred_quote: ctx.preferred_quote, join_lines: ctx.join_lines });
|
|
299
|
+
if (organized === text) return [];
|
|
300
|
+
var doc = new TextDocument('inmem', 'rapydscript', 0, text);
|
|
301
|
+
var end = doc.position_at(text.length);
|
|
302
|
+
return [{ range: { start: { line: 0, character: 0 }, end: end }, newText: organized }];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Completion
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
var KEYWORD_LIST = ('and as assert async await break class continue def del do elif else except finally ' +
|
|
309
|
+
'for from global if import in instanceof is new nonlocal not or pass raise return try typeof void while with yield ' +
|
|
310
|
+
'True False None').split(' ');
|
|
311
|
+
|
|
312
|
+
export async function completions(ctx, uri, raw_text, offset) {
|
|
313
|
+
var a = await analyze(ctx, uri, raw_text);
|
|
314
|
+
var items = [];
|
|
315
|
+
var seen = Object.create(null);
|
|
316
|
+
function add(label, kind, detail, doc) {
|
|
317
|
+
if (seen[label + '\0' + kind]) return;
|
|
318
|
+
seen[label + '\0' + kind] = true;
|
|
319
|
+
var it = { label: label, kind: kind };
|
|
320
|
+
if (detail) it.detail = detail;
|
|
321
|
+
if (doc) it.documentation = { kind: 'markdown', value: doc };
|
|
322
|
+
items.push(it);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Member completion: `expr.<here>` where expr is an imported module -> its exports.
|
|
326
|
+
var dotted = dotted_prefix(a.text, offset);
|
|
327
|
+
if (dotted && a.index) {
|
|
328
|
+
var moddef = find_binding_by_name(a.index, dotted.object, offset);
|
|
329
|
+
if (moddef && moddef.kind === sym.KIND.IMPORT && moddef.import_key) {
|
|
330
|
+
var exports = await module_exports(ctx, moddef.import_key, a.file_path);
|
|
331
|
+
exports.forEach(function (name) { add(name, CompletionItemKind.Field, 'from ' + moddef.import_key); });
|
|
332
|
+
}
|
|
333
|
+
return items; // after a dot only member names are relevant
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// In-scope symbols.
|
|
337
|
+
if (a.index) {
|
|
338
|
+
sym.visible_symbols(a.index, offset).forEach(function (v) {
|
|
339
|
+
add(v.name, kind_to_completion(v.def.kind), v.def.kind, v.def.docstring || undefined);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
// Builtins + keywords.
|
|
343
|
+
Object.keys(BUILTINS).forEach(function (name) { add(name, CompletionItemKind.Constant, 'builtin'); });
|
|
344
|
+
KEYWORD_LIST.forEach(function (k) { add(k, CompletionItemKind.Keyword); });
|
|
345
|
+
return items;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Extract `<object>.<partial>` immediately before offset, if the cursor is after a dot.
|
|
349
|
+
function dotted_prefix(text, offset) {
|
|
350
|
+
var i = offset;
|
|
351
|
+
while (i > 0 && /[A-Za-z0-9_$]/.test(text[i - 1])) i--;
|
|
352
|
+
// i is now at the start of the partial identifier
|
|
353
|
+
if (i > 0 && text[i - 1] === '.') {
|
|
354
|
+
var j = i - 1;
|
|
355
|
+
var k = j;
|
|
356
|
+
while (k > 0 && /[A-Za-z0-9_$]/.test(text[k - 1])) k--;
|
|
357
|
+
var object = text.slice(k, j);
|
|
358
|
+
if (object && /[A-Za-z_$]/.test(object[0])) return { object: object, partial: text.slice(i, offset) };
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function find_binding_by_name(index, name, offset) {
|
|
364
|
+
// Prefer a binding visible at the offset; fall back to any binding of that name.
|
|
365
|
+
var vis = sym.visible_symbols(index, offset);
|
|
366
|
+
for (var i = 0; i < vis.length; i++) if (vis[i].name === name) return vis[i].def;
|
|
367
|
+
for (var j = 0; j < index.defs.length; j++) if (index.defs[j].name === name) return index.defs[j];
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function module_exports(ctx, key, from_file) {
|
|
372
|
+
var stat_cache = Object.create(null);
|
|
373
|
+
var file = await resolve_module(ctx, key, from_file, stat_cache);
|
|
374
|
+
if (!file) return [];
|
|
375
|
+
var uri = path_to_uri(file);
|
|
376
|
+
var text;
|
|
377
|
+
try { text = await text_for_uri(ctx, uri); }
|
|
378
|
+
catch (e) { return []; }
|
|
379
|
+
var a = await analyze(ctx, uri, text);
|
|
380
|
+
if (!a.index || !a.toplevel) return [];
|
|
381
|
+
// Exported names = top-level bindings (functions/classes/vars) that are not imports.
|
|
382
|
+
return top_level_export_names(a.index);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function top_level_export_names(index) {
|
|
386
|
+
var root = index.toplevel_scope;
|
|
387
|
+
if (!root) return [];
|
|
388
|
+
var names = [];
|
|
389
|
+
for (var name in root.bindings) {
|
|
390
|
+
var d = root.bindings[name];
|
|
391
|
+
if (d.kind === sym.KIND.IMPORT || d.kind === sym.KIND.IMPORTED_NAME) continue;
|
|
392
|
+
if (name[0] === '_') continue; // convention: underscore-prefixed names are private
|
|
393
|
+
names.push(name);
|
|
394
|
+
}
|
|
395
|
+
return names;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Hover
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
export async function hover(ctx, uri, raw_text, offset) {
|
|
402
|
+
var a = await analyze(ctx, uri, raw_text);
|
|
403
|
+
if (!a.index) return null;
|
|
404
|
+
var loc = sym.locate(a.index, offset);
|
|
405
|
+
if (!loc) return null;
|
|
406
|
+
var def = loc.def || (loc.import_name && loc.import_name.def) || null;
|
|
407
|
+
if (loc.kind === 'member') {
|
|
408
|
+
return { contents: { kind: 'markdown', value: '```rapydscript\n(property) ' + loc.member.property + '\n```' }, range: offset_to_range(a.doc, loc.range) };
|
|
409
|
+
}
|
|
410
|
+
if (!def) {
|
|
411
|
+
// Maybe a builtin.
|
|
412
|
+
var name = a.text.slice(loc.range[0], loc.range[1]);
|
|
413
|
+
if (Object.prototype.hasOwnProperty.call(BUILTINS, name)) {
|
|
414
|
+
return { contents: { kind: 'markdown', value: '```rapydscript\n(builtin) ' + name + '\n```' }, range: offset_to_range(a.doc, loc.range) };
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
var md = '```rapydscript\n(' + def.kind + ') ' + def.name + '\n```';
|
|
419
|
+
if (def.docstring) md += '\n\n' + String(def.docstring).trim();
|
|
420
|
+
if (def.import_key) md += '\n\nimported from `' + def.import_key + '`';
|
|
421
|
+
return { contents: { kind: 'markdown', value: md }, range: offset_to_range(a.doc, loc.range) };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// Go to definition
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
export async function definition(ctx, uri, raw_text, offset) {
|
|
428
|
+
var a = await analyze(ctx, uri, raw_text);
|
|
429
|
+
if (!a.index) return null;
|
|
430
|
+
var loc = sym.locate(a.index, offset);
|
|
431
|
+
if (!loc) return null;
|
|
432
|
+
|
|
433
|
+
// Member access `mod.name` or `from M import ... as ...` original name -> other file.
|
|
434
|
+
if (loc.kind === 'member') {
|
|
435
|
+
var obj = loc.member.object_node;
|
|
436
|
+
var odef = obj._rs_ref ? obj._rs_ref.def : null;
|
|
437
|
+
if (odef && odef.kind === sym.KIND.IMPORT && odef.import_key) {
|
|
438
|
+
return await definition_in_module(ctx, odef.import_key, a.file_path, loc.member.property);
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
if (loc.kind === 'import-name') {
|
|
443
|
+
return await definition_in_module(ctx, loc.import_name.key, a.file_path, loc.import_name.original);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
var def = loc.def;
|
|
447
|
+
if (!def) return null;
|
|
448
|
+
|
|
449
|
+
// For an imported name, jump to its definition in the source module.
|
|
450
|
+
if ((def.kind === sym.KIND.IMPORTED_NAME || def.kind === sym.KIND.IMPORT) && def.import_key) {
|
|
451
|
+
var orig = def.import_original_name || null;
|
|
452
|
+
if (def.kind === sym.KIND.IMPORT) {
|
|
453
|
+
// `import M` -> definition is the module file itself.
|
|
454
|
+
var loc2 = await module_location(ctx, def.import_key, a.file_path);
|
|
455
|
+
if (loc2) return loc2;
|
|
456
|
+
} else if (orig) {
|
|
457
|
+
var d2 = await definition_in_module(ctx, def.import_key, a.file_path, orig);
|
|
458
|
+
if (d2) return d2;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Local definition(s).
|
|
463
|
+
if (def.def_nodes.length) {
|
|
464
|
+
return def.def_nodes.map(function (n) {
|
|
465
|
+
return { uri: uri, range: offset_to_range(a.doc, sym.symbol_range(n, def.name)) };
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function module_location(ctx, key, from_file) {
|
|
472
|
+
var stat_cache = Object.create(null);
|
|
473
|
+
var file = await resolve_module(ctx, key, from_file, stat_cache);
|
|
474
|
+
if (!file) return null;
|
|
475
|
+
return { uri: path_to_uri(file), range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function definition_in_module(ctx, key, from_file, name) {
|
|
479
|
+
var stat_cache = Object.create(null);
|
|
480
|
+
var file = await resolve_module(ctx, key, from_file, stat_cache);
|
|
481
|
+
if (!file) return null;
|
|
482
|
+
var uri = path_to_uri(file);
|
|
483
|
+
var text;
|
|
484
|
+
try { text = await text_for_uri(ctx, uri); } catch (e) { return null; }
|
|
485
|
+
var a = await analyze(ctx, uri, text);
|
|
486
|
+
if (!a.index || !a.toplevel) return null;
|
|
487
|
+
var root = a.index.toplevel_scope;
|
|
488
|
+
var d = root && root.bindings[name];
|
|
489
|
+
if (!d || !d.def_nodes.length) return null;
|
|
490
|
+
return d.def_nodes.map(function (n) { return { uri: uri, range: offset_to_range(a.doc, sym.symbol_range(n, name)) }; });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
// Cross-file target resolution (shared by references and rename)
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// Returns either {local: def} for a file-private symbol, or
|
|
497
|
+
// {exported:{module, name}} for a symbol that participates in the import graph.
|
|
498
|
+
function resolve_target(a, loc) {
|
|
499
|
+
var this_module = a.module_id; // may be undefined for a standalone file
|
|
500
|
+
if (loc.kind === 'member') {
|
|
501
|
+
var obj = loc.member.object_node;
|
|
502
|
+
var odef = obj._rs_ref ? obj._rs_ref.def : null;
|
|
503
|
+
if (odef && odef.kind === sym.KIND.IMPORT && odef.import_key) return { exported: { module: odef.import_key, name: loc.member.property } };
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
if (loc.kind === 'import-name') {
|
|
507
|
+
return { exported: { module: loc.import_name.key, name: loc.import_name.original } };
|
|
508
|
+
}
|
|
509
|
+
var def = loc.def;
|
|
510
|
+
if (!def) return null;
|
|
511
|
+
if (def.kind === sym.KIND.IMPORTED_NAME && def.import_key) {
|
|
512
|
+
if (def.import_alias) {
|
|
513
|
+
// Renaming the alias is a purely local operation.
|
|
514
|
+
if (loc.kind !== 'import-name') return { local: def };
|
|
515
|
+
}
|
|
516
|
+
return { exported: { module: def.import_key, name: def.import_original_name } };
|
|
517
|
+
}
|
|
518
|
+
// A top-level function/class/variable in a module is exported.
|
|
519
|
+
if (def.scope && def.scope.is_toplevel && this_module &&
|
|
520
|
+
(def.kind === sym.KIND.FUNCTION || def.kind === sym.KIND.CLASS || def.kind === sym.KIND.VARIABLE)) {
|
|
521
|
+
return { exported: { module: this_module, name: def.name } };
|
|
522
|
+
}
|
|
523
|
+
return { local: def };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Compute the module id(s) for a file relative to the import dirs.
|
|
527
|
+
function module_ids_for_file(ctx, file_path) {
|
|
528
|
+
var ids = [];
|
|
529
|
+
var dirs = ctx.import_dirs.concat(ctx.libdir ? [ctx.libdir] : []);
|
|
530
|
+
dirs.forEach(function (dir) {
|
|
531
|
+
var rel = path.relative(dir, file_path);
|
|
532
|
+
if (rel.slice(0, 2) === '..' || path.isAbsolute(rel)) return;
|
|
533
|
+
rel = rel.replace(/\\/g, '/');
|
|
534
|
+
if (rel.slice(-4) === '.pyj') rel = rel.slice(0, -4);
|
|
535
|
+
var parts = rel.split('/');
|
|
536
|
+
if (parts[parts.length - 1] === '__init__') parts.pop();
|
|
537
|
+
if (parts.length) ids.push(parts.join('.'));
|
|
538
|
+
});
|
|
539
|
+
return ids;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// Workspace scan (for cross-file references / rename)
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
async function workspace_files(ctx) {
|
|
546
|
+
if (ctx._scan_cache) return ctx._scan_cache;
|
|
547
|
+
var roots = [];
|
|
548
|
+
var add_root = function (d) { if (d && roots.indexOf(d) < 0) roots.push(d); };
|
|
549
|
+
ctx.workspace_roots.forEach(add_root);
|
|
550
|
+
ctx.import_dirs.forEach(add_root);
|
|
551
|
+
ctx.docs.all().forEach(function (doc) { add_root(path.dirname(uri_to_path(doc.uri))); });
|
|
552
|
+
|
|
553
|
+
var files = [];
|
|
554
|
+
var seen = Object.create(null);
|
|
555
|
+
var SKIP = { 'node_modules': 1, '.git': 1, '.hg': 1, '__pycache__': 1 };
|
|
556
|
+
async function walk(dir, depth) {
|
|
557
|
+
if (depth > 24) return;
|
|
558
|
+
var entries;
|
|
559
|
+
try { entries = await fs.promises.readdir(dir, { withFileTypes: true }); }
|
|
560
|
+
catch (e) { return; }
|
|
561
|
+
for (const ent of entries) {
|
|
562
|
+
var full = path.join(dir, ent.name);
|
|
563
|
+
if (ent.isDirectory()) { if (!SKIP[ent.name]) await walk(full, depth + 1); }
|
|
564
|
+
else if (ent.isFile() && ent.name.slice(-4) === '.pyj') {
|
|
565
|
+
var norm = path.normalize(full);
|
|
566
|
+
if (!seen[norm]) { seen[norm] = true; files.push(norm); }
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const r of roots) await walk(r, 0);
|
|
571
|
+
ctx._scan_cache = files;
|
|
572
|
+
return files;
|
|
573
|
+
}
|
|
574
|
+
export function invalidate_workspace_scan(ctx) { ctx._scan_cache = null; }
|
|
575
|
+
|
|
576
|
+
// Analyze a workspace file by path (open document text wins over disk).
|
|
577
|
+
async function analyze_file(ctx, file_path) {
|
|
578
|
+
var uri = path_to_uri(file_path);
|
|
579
|
+
var text;
|
|
580
|
+
try { text = await text_for_uri(ctx, uri); } catch (e) { return null; }
|
|
581
|
+
var a = await analyze(ctx, uri, text);
|
|
582
|
+
a.uri = uri;
|
|
583
|
+
a.module_id = module_ids_for_file(ctx, file_path)[0];
|
|
584
|
+
return a;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Find every occurrence of an exported symbol (module, name) across the workspace.
|
|
588
|
+
// Returns [{uri, doc, range}]. When for_rename is true, aliased local uses are
|
|
589
|
+
// excluded (they keep their alias); only occurrences that literally spell `name`
|
|
590
|
+
// and refer to the target are included.
|
|
591
|
+
async function find_exported_occurrences(ctx, target, for_rename) {
|
|
592
|
+
var out = [];
|
|
593
|
+
var module = target.module, name = target.name;
|
|
594
|
+
var files = await workspace_files(ctx);
|
|
595
|
+
|
|
596
|
+
// Ensure open documents that were never scanned (e.g. untitled dirs) are covered.
|
|
597
|
+
var extra = [];
|
|
598
|
+
ctx.docs.all().forEach(function (doc) { var p = path.normalize(uri_to_path(doc.uri)); if (files.indexOf(p) < 0) extra.push(p); });
|
|
599
|
+
var all = files.concat(extra);
|
|
600
|
+
|
|
601
|
+
for (const file_path of all) {
|
|
602
|
+
var a = await analyze_file(ctx, file_path);
|
|
603
|
+
if (!a || !a.index) continue;
|
|
604
|
+
var push = function (span) { if (span) out.push({ uri: a.uri, doc: a.doc, range: offset_to_range(a.doc, span) }); };
|
|
605
|
+
|
|
606
|
+
// (a) The defining module itself: its top-level binding + local refs.
|
|
607
|
+
if (a.module_id === module) {
|
|
608
|
+
var root = a.index.toplevel_scope;
|
|
609
|
+
var d = root && root.bindings[name];
|
|
610
|
+
if (d) {
|
|
611
|
+
d.def_nodes.forEach(function (n) { push(sym.symbol_range(n, name)); });
|
|
612
|
+
d.ref_nodes.forEach(function (n) { push(sym.symbol_range(n, name)); });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// (b) Importers.
|
|
617
|
+
for (const imp of a.index.imports) {
|
|
618
|
+
if (imp.key !== module) continue;
|
|
619
|
+
if (imp.kind === 'from') {
|
|
620
|
+
(imp.argnames || []).forEach(function (av) {
|
|
621
|
+
if (av.name !== name) return;
|
|
622
|
+
// The original-name token always spells `name`.
|
|
623
|
+
push(sym.symbol_range(av, name));
|
|
624
|
+
var bdef = av._rs_imported_var_def;
|
|
625
|
+
if (bdef && !av.alias) {
|
|
626
|
+
// Non-aliased: local uses also spell `name`.
|
|
627
|
+
bdef.ref_nodes.forEach(function (n) { push(sym.symbol_range(n, name)); });
|
|
628
|
+
}
|
|
629
|
+
// Aliased: alias + its uses keep their own name -> excluded.
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// (c) Member accesses `mod.name` where mod is imported from `module`.
|
|
635
|
+
a.index.member_accesses.forEach(function (ma) {
|
|
636
|
+
if (ma.property !== name) return;
|
|
637
|
+
var odef = ma.object_node._rs_ref ? ma.object_node._rs_ref.def : null;
|
|
638
|
+
if (odef && odef.kind === sym.KIND.IMPORT && odef.import_key === module) push(ma.range);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
// Deduplicate ranges within the same uri.
|
|
642
|
+
return dedup_locations(out);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function dedup_locations(list) {
|
|
646
|
+
var seen = Object.create(null);
|
|
647
|
+
var out = [];
|
|
648
|
+
list.forEach(function (l) {
|
|
649
|
+
var key = l.uri + ':' + l.range.start.line + ':' + l.range.start.character + ':' + l.range.end.line + ':' + l.range.end.character;
|
|
650
|
+
if (seen[key]) return;
|
|
651
|
+
seen[key] = true;
|
|
652
|
+
out.push(l);
|
|
653
|
+
});
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function local_occurrences(a, def) {
|
|
658
|
+
var out = [];
|
|
659
|
+
def.def_nodes.forEach(function (n) { out.push({ uri: a.uri, doc: a.doc, range: offset_to_range(a.doc, sym.symbol_range(n, def.name)) }); });
|
|
660
|
+
def.ref_nodes.forEach(function (n) { out.push({ uri: a.uri, doc: a.doc, range: offset_to_range(a.doc, sym.symbol_range(n, def.name)) }); });
|
|
661
|
+
return dedup_locations(out);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
// References
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
export async function references(ctx, uri, raw_text, offset, include_declaration) {
|
|
668
|
+
var a = await analyze(ctx, uri, raw_text);
|
|
669
|
+
if (!a.index) return [];
|
|
670
|
+
a.uri = uri;
|
|
671
|
+
a.module_id = module_ids_for_file(ctx, a.file_path)[0];
|
|
672
|
+
var loc = sym.locate(a.index, offset);
|
|
673
|
+
if (!loc) return [];
|
|
674
|
+
var target = resolve_target(a, loc);
|
|
675
|
+
if (!target) return [];
|
|
676
|
+
var occ;
|
|
677
|
+
if (target.local) occ = local_occurrences(a, target.local);
|
|
678
|
+
else occ = await find_exported_occurrences(ctx, target.exported, false);
|
|
679
|
+
return occ.map(function (o) { return { uri: o.uri, range: o.range }; });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
// Rename
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
686
|
+
|
|
687
|
+
export async function rename(ctx, uri, raw_text, offset, new_name) {
|
|
688
|
+
if (!IDENT_RE.test(new_name)) throw ResponseError(ErrorCodes.InvalidParams, 'Invalid identifier: "' + new_name + '"');
|
|
689
|
+
var a = await analyze(ctx, uri, raw_text);
|
|
690
|
+
if (!a.index) return null;
|
|
691
|
+
a.uri = uri;
|
|
692
|
+
a.module_id = module_ids_for_file(ctx, a.file_path)[0];
|
|
693
|
+
var loc = sym.locate(a.index, offset);
|
|
694
|
+
if (!loc) return null;
|
|
695
|
+
var target = resolve_target(a, loc);
|
|
696
|
+
if (!target) return null;
|
|
697
|
+
var occ;
|
|
698
|
+
if (target.local) occ = local_occurrences(a, target.local);
|
|
699
|
+
else occ = await find_exported_occurrences(ctx, target.exported, true);
|
|
700
|
+
if (!occ.length) return null;
|
|
701
|
+
|
|
702
|
+
var changes = Object.create(null);
|
|
703
|
+
occ.forEach(function (o) {
|
|
704
|
+
if (!changes[o.uri]) changes[o.uri] = [];
|
|
705
|
+
changes[o.uri].push({ range: o.range, newText: new_name });
|
|
706
|
+
});
|
|
707
|
+
return { changes: changes };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ---------------------------------------------------------------------------
|
|
711
|
+
// Code actions (quick fixes derived from diagnostics)
|
|
712
|
+
// ---------------------------------------------------------------------------
|
|
713
|
+
export async function code_actions(ctx, uri, raw_text, range, diagnostics) {
|
|
714
|
+
var a = await analyze(ctx, uri, raw_text);
|
|
715
|
+
var actions = [];
|
|
716
|
+
(diagnostics || []).forEach(function (d) {
|
|
717
|
+
if (d.code === 'unused-import' || d.code === 'unused-local') {
|
|
718
|
+
// Remove the whole line containing the unused binding.
|
|
719
|
+
var line = d.range.start.line;
|
|
720
|
+
var edit = { range: { start: { line: line, character: 0 }, end: { line: line + 1, character: 0 } }, newText: '' };
|
|
721
|
+
actions.push({
|
|
722
|
+
title: 'Remove unused ' + (d.code === 'unused-import' ? 'import' : 'local'),
|
|
723
|
+
kind: CodeActionKind.QuickFix, diagnostics: [d],
|
|
724
|
+
edit: { changes: single_change(uri, [edit]) },
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
if (d.code && d.code !== 'syntax-err' && d.code !== 'import-unresolved') {
|
|
728
|
+
// Suppress this check on the line with a noqa comment.
|
|
729
|
+
var ln = d.range.start.line;
|
|
730
|
+
var line_text = line_at(a.text, ln);
|
|
731
|
+
var insert_col = line_text.replace(/\s+$/, '').length;
|
|
732
|
+
var noqa_edit = { range: { start: { line: ln, character: insert_col }, end: { line: ln, character: insert_col } }, newText: ' # noqa: ' + d.code };
|
|
733
|
+
actions.push({
|
|
734
|
+
title: 'Ignore ' + d.code + ' on this line (# noqa)',
|
|
735
|
+
kind: CodeActionKind.QuickFix, diagnostics: [d],
|
|
736
|
+
edit: { changes: single_change(uri, [noqa_edit]) },
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
// Source action: format the document.
|
|
741
|
+
var fmt_edits = format_document(ctx, a.text);
|
|
742
|
+
if (fmt_edits.length) actions.push({ title: 'Format document', kind: CodeActionKind.Source, edit: { changes: single_change(uri, fmt_edits) } });
|
|
743
|
+
// Source action: organize imports.
|
|
744
|
+
var org_edits = organize_imports_document(ctx, a.text);
|
|
745
|
+
if (org_edits.length) actions.push({ title: 'Organize imports', kind: CodeActionKind.OrganizeImports, edit: { changes: single_change(uri, org_edits) } });
|
|
746
|
+
return actions;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function single_change(uri, edits) { var c = Object.create(null); c[uri] = edits; return c; }
|
|
750
|
+
|
|
751
|
+
function line_at(text, line) {
|
|
752
|
+
var doc = new TextDocument('x', 'rapydscript', 0, text);
|
|
753
|
+
var start = doc.offset_at({ line: line, character: 0 });
|
|
754
|
+
var end = doc.offset_at({ line: line + 1, character: 0 });
|
|
755
|
+
return text.slice(start, end).replace(/\n$/, '');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Document symbols (outline) -- top-level and nested defs/classes.
|
|
759
|
+
export async function document_symbols(ctx, uri, raw_text) {
|
|
760
|
+
var a = await analyze(ctx, uri, raw_text);
|
|
761
|
+
if (!a.index) return [];
|
|
762
|
+
var out = [];
|
|
763
|
+
a.index.defs.forEach(function (d) {
|
|
764
|
+
if (!d.def_nodes.length) return;
|
|
765
|
+
var kind = (d.kind === sym.KIND.CLASS) ? SymbolKindLSP.Class
|
|
766
|
+
: (d.kind === sym.KIND.FUNCTION) ? SymbolKindLSP.Function
|
|
767
|
+
: (d.kind === sym.KIND.METHOD) ? SymbolKindLSP.Method : null;
|
|
768
|
+
if (!kind) return;
|
|
769
|
+
var n = d.def_nodes[0];
|
|
770
|
+
out.push({ name: d.name, kind: kind, range: offset_to_range(a.doc, sym.symbol_range(n, d.name)), selectionRange: offset_to_range(a.doc, sym.symbol_range(n, d.name)) });
|
|
771
|
+
});
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ===========================================================================
|
|
776
|
+
// Server / CLI
|
|
777
|
+
// ===========================================================================
|
|
778
|
+
function argv_has_flag(flag_names) {
|
|
779
|
+
var args = process.argv.slice(3); // skip: node, script path, mode
|
|
780
|
+
for (var i = 0; i < args.length; i++) {
|
|
781
|
+
var a = args[i];
|
|
782
|
+
if (a === '--') break;
|
|
783
|
+
for (var j = 0; j < flag_names.length; j++) {
|
|
784
|
+
var f = flag_names[j];
|
|
785
|
+
if (a === f || a.startsWith(f + '=')) return true;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export async function cli(argv, base_path, src_path, lib_path) {
|
|
792
|
+
var ll_from_cli = argv_has_flag(['--line-length', '--line_length', '-l']);
|
|
793
|
+
var q_from_cli = argv_has_flag(['--preferred-quote', '--preferred_quote', '-q']);
|
|
794
|
+
|
|
795
|
+
var effective_ll = ll_from_cli ? (parseInt(argv.line_length, 10) || 80) : null;
|
|
796
|
+
var effective_quote = q_from_cli ? argv.preferred_quote : null;
|
|
797
|
+
|
|
798
|
+
if (!ll_from_cli || !q_from_cli) {
|
|
799
|
+
var pyconf = await read_pyproject_config(process.cwd());
|
|
800
|
+
if (!ll_from_cli && pyconf.line_length) effective_ll = pyconf.line_length;
|
|
801
|
+
if (!q_from_cli && pyconf.preferred_quote) effective_quote = pyconf.preferred_quote;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (!effective_ll) effective_ll = 80;
|
|
805
|
+
if (!effective_quote) effective_quote = 'single';
|
|
806
|
+
|
|
807
|
+
var ctx = create_server_context({
|
|
808
|
+
import_dirs: utils.get_import_dirs(argv.import_path).map(function (p) { return path.resolve(p); }),
|
|
809
|
+
libdir: path.join(src_path, 'lib'),
|
|
810
|
+
line_length: effective_ll,
|
|
811
|
+
preferred_quote: effective_quote,
|
|
812
|
+
join_lines: argv.join_lines || false,
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Diagnostics are pushed to the client, debounced per document.
|
|
816
|
+
var diag_timers = Object.create(null);
|
|
817
|
+
function log(msg) { process.stderr.write('[rapydscript-lsp] ' + msg + '\n'); }
|
|
818
|
+
|
|
819
|
+
var connection = create_connection(process.stdin, process.stdout, log);
|
|
820
|
+
var client_capabilities = {};
|
|
821
|
+
|
|
822
|
+
function schedule_diagnostics(uri) {
|
|
823
|
+
if (diag_timers[uri]) clearTimeout(diag_timers[uri]);
|
|
824
|
+
diag_timers[uri] = setTimeout(async function () {
|
|
825
|
+
delete diag_timers[uri];
|
|
826
|
+
var doc = ctx.docs.get(uri);
|
|
827
|
+
if (!doc) return;
|
|
828
|
+
try {
|
|
829
|
+
var diags = await compute_diagnostics(ctx, uri, doc.text);
|
|
830
|
+
connection.send_notification('textDocument/publishDiagnostics', { uri: uri, version: doc.version, diagnostics: diags });
|
|
831
|
+
} catch (e) { log('diagnostics failed for ' + uri + ': ' + (e && e.stack ? e.stack : e)); }
|
|
832
|
+
}, 200);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
connection.on_request('initialize', function (params) {
|
|
836
|
+
client_capabilities = (params && params.capabilities) || {};
|
|
837
|
+
if (params && params.workspaceFolders) params.workspaceFolders.forEach(function (f) { if (f.uri) ctx.workspace_roots.push(uri_to_path(f.uri)); });
|
|
838
|
+
else if (params && params.rootUri) ctx.workspace_roots.push(uri_to_path(params.rootUri));
|
|
839
|
+
else if (params && params.rootPath) ctx.workspace_roots.push(params.rootPath);
|
|
840
|
+
return {
|
|
841
|
+
capabilities: {
|
|
842
|
+
textDocumentSync: { openClose: true, change: 1 /* full */, save: { includeText: false } },
|
|
843
|
+
completionProvider: { triggerCharacters: ['.'] },
|
|
844
|
+
hoverProvider: true,
|
|
845
|
+
definitionProvider: true,
|
|
846
|
+
referencesProvider: true,
|
|
847
|
+
renameProvider: true,
|
|
848
|
+
documentFormattingProvider: true,
|
|
849
|
+
documentSymbolProvider: true,
|
|
850
|
+
codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix, CodeActionKind.Source, CodeActionKind.OrganizeImports] },
|
|
851
|
+
},
|
|
852
|
+
serverInfo: { name: 'rapydscript-lsp', version: '1.0.0' },
|
|
853
|
+
};
|
|
854
|
+
});
|
|
855
|
+
connection.on_notification('initialized', async function () {
|
|
856
|
+
var registrations = [];
|
|
857
|
+
var ws = client_capabilities.workspace || {};
|
|
858
|
+
if (ws.didChangeConfiguration && ws.didChangeConfiguration.dynamicRegistration) {
|
|
859
|
+
registrations.push({
|
|
860
|
+
id: 'rapydscript-config',
|
|
861
|
+
method: 'workspace/didChangeConfiguration',
|
|
862
|
+
registerOptions: {},
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
if (ws.didChangeWatchedFiles && ws.didChangeWatchedFiles.dynamicRegistration) {
|
|
866
|
+
registrations.push({
|
|
867
|
+
id: 'rapydscript-file-watcher',
|
|
868
|
+
method: 'workspace/didChangeWatchedFiles',
|
|
869
|
+
registerOptions: { watchers: [{ globPattern: '**/*.pyj' }] },
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
if (registrations.length) {
|
|
873
|
+
try {
|
|
874
|
+
await connection.send_request('client/registerCapability', { registrations: registrations });
|
|
875
|
+
} catch (e) {
|
|
876
|
+
log('client/registerCapability failed: ' + (e && e.message ? e.message : e));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
connection.on_notification('workspace/didChangeWatchedFiles', function () {
|
|
881
|
+
invalidate_workspace_scan(ctx);
|
|
882
|
+
});
|
|
883
|
+
connection.on_notification('workspace/didChangeConfiguration', function (params) {
|
|
884
|
+
var settings = (params && params.settings && params.settings.rapydscript) || {};
|
|
885
|
+
var changed = apply_configuration(ctx, settings);
|
|
886
|
+
if (changed.import_dirs) {
|
|
887
|
+
ctx.docs.all().forEach(function (doc) { schedule_diagnostics(doc.uri); });
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
connection.on_request('shutdown', function () { return null; });
|
|
891
|
+
connection.on_notification('exit', function () { process.exit(0); });
|
|
892
|
+
|
|
893
|
+
// --- document lifecycle ---
|
|
894
|
+
connection.on_notification('textDocument/didOpen', function (params) {
|
|
895
|
+
var td = params.textDocument;
|
|
896
|
+
ctx.docs.open(td.uri, td.languageId || 'rapydscript', td.version, normalize(td.text));
|
|
897
|
+
invalidate_workspace_scan(ctx);
|
|
898
|
+
schedule_diagnostics(td.uri);
|
|
899
|
+
});
|
|
900
|
+
connection.on_notification('textDocument/didChange', function (params) {
|
|
901
|
+
var doc = ctx.docs.get(params.textDocument.uri);
|
|
902
|
+
if (!doc) return;
|
|
903
|
+
// Full sync: the last content change holds the whole document.
|
|
904
|
+
var changes = params.contentChanges || [];
|
|
905
|
+
if (changes.length) doc.update(normalize(changes[changes.length - 1].text), params.textDocument.version);
|
|
906
|
+
schedule_diagnostics(params.textDocument.uri);
|
|
907
|
+
});
|
|
908
|
+
connection.on_notification('textDocument/didSave', function (params) {
|
|
909
|
+
invalidate_workspace_scan(ctx);
|
|
910
|
+
schedule_diagnostics(params.textDocument.uri);
|
|
911
|
+
});
|
|
912
|
+
connection.on_notification('textDocument/didClose', function (params) {
|
|
913
|
+
ctx.docs.close(params.textDocument.uri);
|
|
914
|
+
delete ctx.analysis_cache[params.textDocument.uri];
|
|
915
|
+
connection.send_notification('textDocument/publishDiagnostics', { uri: params.textDocument.uri, diagnostics: [] });
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// --- helpers to fetch a document's text and an offset from a position ---
|
|
919
|
+
function doc_or_throw(uri) {
|
|
920
|
+
var doc = ctx.docs.get(uri);
|
|
921
|
+
if (!doc) throw ResponseError(ErrorCodes.InvalidParams, 'Unknown document: ' + uri);
|
|
922
|
+
return doc;
|
|
923
|
+
}
|
|
924
|
+
function offset_of(doc, position) { return doc.offset_at(position); }
|
|
925
|
+
|
|
926
|
+
// --- feature requests ---
|
|
927
|
+
connection.on_request('textDocument/completion', async function (params) {
|
|
928
|
+
var doc = doc_or_throw(params.textDocument.uri);
|
|
929
|
+
return await completions(ctx, doc.uri, doc.text, offset_of(doc, params.position));
|
|
930
|
+
});
|
|
931
|
+
connection.on_request('textDocument/hover', async function (params) {
|
|
932
|
+
var doc = doc_or_throw(params.textDocument.uri);
|
|
933
|
+
return await hover(ctx, doc.uri, doc.text, offset_of(doc, params.position));
|
|
934
|
+
});
|
|
935
|
+
connection.on_request('textDocument/definition', async function (params) {
|
|
936
|
+
var doc = doc_or_throw(params.textDocument.uri);
|
|
937
|
+
return await definition(ctx, doc.uri, doc.text, offset_of(doc, params.position));
|
|
938
|
+
});
|
|
939
|
+
connection.on_request('textDocument/references', async function (params) {
|
|
940
|
+
var doc = doc_or_throw(params.textDocument.uri);
|
|
941
|
+
return await references(ctx, doc.uri, doc.text, offset_of(doc, params.position), params.context && params.context.includeDeclaration);
|
|
942
|
+
});
|
|
943
|
+
connection.on_request('textDocument/rename', async function (params) {
|
|
944
|
+
var doc = doc_or_throw(params.textDocument.uri);
|
|
945
|
+
return await rename(ctx, doc.uri, doc.text, offset_of(doc, params.position), params.newName);
|
|
946
|
+
});
|
|
947
|
+
connection.on_request('textDocument/formatting', async function (params) {
|
|
948
|
+
var doc = doc_or_throw(params.textDocument.uri);
|
|
949
|
+
return format_document(ctx, doc.text);
|
|
950
|
+
});
|
|
951
|
+
connection.on_request('textDocument/documentSymbol', async function (params) {
|
|
952
|
+
var doc = doc_or_throw(params.textDocument.uri);
|
|
953
|
+
return await document_symbols(ctx, doc.uri, doc.text);
|
|
954
|
+
});
|
|
955
|
+
connection.on_request('textDocument/codeAction', async function (params) {
|
|
956
|
+
var doc = doc_or_throw(params.textDocument.uri);
|
|
957
|
+
var diags = (params.context && params.context.diagnostics) || [];
|
|
958
|
+
return await code_actions(ctx, doc.uri, doc.text, params.range, diags);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
log('RapydScript language server started');
|
|
962
|
+
// Keep the process alive; stdin 'end' means the client disconnected.
|
|
963
|
+
process.stdin.on('end', function () { process.exit(0); });
|
|
964
|
+
}
|