Skip to main content

nodejs/
lsp.rs

1//! Builtin / keyword / method corpus for offline documentation.
2//!
3//! Single source of truth for the reference manual (`gen-docs` → `docs/reference.html`).
4//! Every entry mirrors something the runtime actually recognizes:
5//!   * "Keyword"      → the `KEYWORDS` set in `parser.rs` (plus the operator
6//!     keywords `typeof`/`void`/`delete`/`instanceof`/`in`/`new`/`this`).
7//!   * "Global"       → the global identifiers resolved in `builtins.rs`
8//!     (`undefined`/`NaN`/`Infinity`/`globalThis`) and the free functions /
9//!     constructors in its dispatch table (`parseInt`, `parseFloat`, `isNaN`,
10//!     `isFinite`, `String`, `Number`, `Boolean`, `Array`, `Error`, …).
11//!   * "console"      → `console.log`/`error`/`warn`/`info`/`debug`.
12//!   * "Math"/"JSON"/"Object"/"Number"/"String static"/"Array static" → the
13//!     namespace dispatch arms in `builtins.rs` (`call_builtin`, `call_math`,
14//!     the `Math.*` const table, `Object.*`, `Number.*`, `Array.*`).
15//!   * "Array method"/"String method"/"Number method" → the per-type dispatch
16//!     tables (`array_method`, `string_method`, and the number-method arms).
17//!
18//! Only names the crate implements appear here — no classes, regex, Map/Set,
19//! generators, or modules, none of which the lexer/parser/builtins support.
20//!
21//! The same corpus also backs the Language Server (`node --lsp`): completion and
22//! hover render from it, while diagnostics come from the runtime's own
23//! `parser::parse` (a syntax error maps to the reported line). No output ever
24//! reaches the terminal — JSON-RPC on stdio only. Structure follows the sibling
25//! `-rs` interpreters' `lsp.rs` (see `pythonrs/src/lsp.rs`).
26
27use std::collections::HashMap;
28
29use lsp_server::{Connection, ErrorCode, ExtractError, Message, Request, Response};
30use lsp_types::notification::{
31    DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification as _,
32    PublishDiagnostics,
33};
34use lsp_types::request::{Completion, HoverRequest, Request as _};
35use lsp_types::{
36    CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
37    Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
38    DidOpenTextDocumentParams, Hover, HoverContents, HoverParams, HoverProviderCapability,
39    MarkupContent, MarkupKind, Position, PublishDiagnosticsParams, Range, ServerCapabilities,
40    TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, Uri,
41};
42
43/// The builtin corpus: (name, chapter, one-line doc, runnable example).
44const CORPUS: &[(&str, &str, &str, &str)] = &[
45    // ── Keyword ──
46    (
47        "var",
48        "Keyword",
49        "declare a function-scoped (hoisted) variable",
50        "var x = 1; x   // => 1",
51    ),
52    (
53        "let",
54        "Keyword",
55        "declare a block-scoped variable",
56        "let x = 2; x   // => 2",
57    ),
58    (
59        "const",
60        "Keyword",
61        "declare a block-scoped binding that cannot be reassigned",
62        "const x = 3; x   // => 3",
63    ),
64    (
65        "function",
66        "Keyword",
67        "define a function (declaration or expression)",
68        "function f() { return 9; } f()   // => 9",
69    ),
70    (
71        "return",
72        "Keyword",
73        "return a value from the current function (undefined if omitted)",
74        "(function () { return 7; })()   // => 7",
75    ),
76    (
77        "if",
78        "Keyword",
79        "conditional branch on a truthy test",
80        "let x; if (true) x = 1; x   // => 1",
81    ),
82    (
83        "else",
84        "Keyword",
85        "fallback branch of an if",
86        "let x; if (false) x = 1; else x = 2; x   // => 2",
87    ),
88    (
89        "while",
90        "Keyword",
91        "loop while the condition is truthy",
92        "let i = 0; while (i < 3) i++; i   // => 3",
93    ),
94    (
95        "do",
96        "Keyword",
97        "do/while loop: run the body once, then repeat while truthy",
98        "let i = 0; do { i++; } while (i < 3); i   // => 3",
99    ),
100    (
101        "for",
102        "Keyword",
103        "C-style loop, or for/of and for/in iteration",
104        "let s = 0; for (let i = 0; i < 3; i++) s += i; s   // => 3",
105    ),
106    (
107        "of",
108        "Keyword",
109        "iterate values of an iterable: `for (x of iterable)`",
110        "let s = 0; for (const n of [1, 2, 3]) s += n; s   // => 6",
111    ),
112    (
113        "in",
114        "Keyword",
115        "for/in key iteration, and the property-membership operator",
116        "\"a\" in { a: 1 }   // => true",
117    ),
118    (
119        "switch",
120        "Keyword",
121        "multi-way branch on a discriminant with case labels",
122        "let x; switch (2) { case 2: x = \"b\"; break; } x   // => 'b'",
123    ),
124    (
125        "case",
126        "Keyword",
127        "a labeled branch inside a switch",
128        "let x; switch (1) { case 1: x = \"a\"; } x   // => 'a'",
129    ),
130    (
131        "default",
132        "Keyword",
133        "the fallback branch of a switch",
134        "let x; switch (9) { default: x = \"d\"; } x   // => 'd'",
135    ),
136    (
137        "break",
138        "Keyword",
139        "exit the nearest enclosing loop or switch",
140        "let x; for (const n of [1, 2, 3]) { if (n === 2) break; x = n; } x   // => 1",
141    ),
142    (
143        "continue",
144        "Keyword",
145        "skip to the next iteration of the nearest loop",
146        "let s = 0; for (const n of [1, 2, 3]) { if (n === 2) continue; s += n; } s   // => 4",
147    ),
148    (
149        "new",
150        "Keyword",
151        "construct an instance: `new Ctor(args)`",
152        "new Error(\"boom\").message   // => 'boom'",
153    ),
154    (
155        "this",
156        "Keyword",
157        "the receiver of the current call",
158        "({ n: 5, get() { return this.n; } }).get()   // => 5",
159    ),
160    (
161        "typeof",
162        "Keyword",
163        "the type tag of a value as a string",
164        "typeof 3   // => 'number'",
165    ),
166    (
167        "void",
168        "Keyword",
169        "evaluate an expression and yield undefined",
170        "void 0   // => undefined",
171    ),
172    (
173        "delete",
174        "Keyword",
175        "remove a property from an object",
176        "const o = { a: 1 }; delete o.a; o.a   // => undefined",
177    ),
178    (
179        "instanceof",
180        "Keyword",
181        "test whether an object was built by a constructor",
182        "new Error(\"e\") instanceof Error   // => true",
183    ),
184    (
185        "throw",
186        "Keyword",
187        "raise an exception value",
188        "try { throw \"x\"; } catch (e) { e }   // => 'x'",
189    ),
190    (
191        "try",
192        "Keyword",
193        "run a block, routing exceptions to catch/finally",
194        "let x; try { x = 1; } finally { x = 2; } x   // => 2",
195    ),
196    (
197        "catch",
198        "Keyword",
199        "handle an exception thrown in the try block",
200        "try { null.x; } catch (e) { \"caught\" }   // => 'caught'",
201    ),
202    (
203        "finally",
204        "Keyword",
205        "block that always runs after try/catch",
206        "let x; try { x = 1; } finally { x = 3; } x   // => 3",
207    ),
208    (
209        "true",
210        "Keyword",
211        "the boolean true literal",
212        "true && 1   // => 1",
213    ),
214    (
215        "false",
216        "Keyword",
217        "the boolean false literal",
218        "false || 2   // => 2",
219    ),
220    (
221        "null",
222        "Keyword",
223        "the intentional-absence value",
224        "null ?? 5   // => 5",
225    ),
226    // ── Global ──
227    (
228        "undefined",
229        "Global",
230        "the value of an unassigned binding or missing property",
231        "typeof undefined   // => 'undefined'",
232    ),
233    (
234        "NaN",
235        "Global",
236        "the not-a-number float; unequal to itself",
237        "NaN === NaN   // => false",
238    ),
239    (
240        "Infinity",
241        "Global",
242        "the positive-infinity float",
243        "1 / 0 === Infinity   // => true",
244    ),
245    (
246        "globalThis",
247        "Global",
248        "the global object",
249        "typeof globalThis   // => 'object'",
250    ),
251    (
252        "parseInt",
253        "Global",
254        "parse the leading integer of a string (optional radix)",
255        "parseInt(\"42px\")   // => 42",
256    ),
257    (
258        "parseFloat",
259        "Global",
260        "parse the leading floating-point number of a string",
261        "parseFloat(\"3.14abc\")   // => 3.14",
262    ),
263    (
264        "isNaN",
265        "Global",
266        "true if the coerced number is NaN",
267        "isNaN(\"x\")   // => true",
268    ),
269    (
270        "isFinite",
271        "Global",
272        "true if the coerced number is finite",
273        "isFinite(10)   // => true",
274    ),
275    (
276        "String",
277        "Global",
278        "convert a value to its string form",
279        "String(123)   // => '123'",
280    ),
281    (
282        "Number",
283        "Global",
284        "convert a value to a number",
285        "Number(\"3.5\")   // => 3.5",
286    ),
287    (
288        "Boolean",
289        "Global",
290        "convert a value to its truthiness",
291        "Boolean(\"\")   // => false",
292    ),
293    (
294        "Array",
295        "Global",
296        "build an array from the given elements",
297        "Array(1, 2, 3)   // => [ 1, 2, 3 ]",
298    ),
299    (
300        "Error",
301        "Global",
302        "construct a generic error with a message",
303        "new Error(\"boom\").message   // => 'boom'",
304    ),
305    (
306        "TypeError",
307        "Global",
308        "construct a type error (name is 'TypeError')",
309        "new TypeError(\"bad\").name   // => 'TypeError'",
310    ),
311    (
312        "RangeError",
313        "Global",
314        "construct a range error (name is 'RangeError')",
315        "new RangeError(\"oob\").name   // => 'RangeError'",
316    ),
317    // ── console ──
318    (
319        "console.log",
320        "console",
321        "write args to stdout, space-separated, ending in a newline",
322        "console.log(\"hi\", 1)   // prints: hi 1",
323    ),
324    (
325        "console.error",
326        "console",
327        "write args to stderr",
328        "console.error(\"oops\")   // prints oops to stderr",
329    ),
330    (
331        "console.warn",
332        "console",
333        "write args to stderr (warning channel)",
334        "console.warn(\"careful\")   // prints careful to stderr",
335    ),
336    (
337        "console.info",
338        "console",
339        "write args to stdout (info channel)",
340        "console.info(\"note\")   // prints note",
341    ),
342    (
343        "console.debug",
344        "console",
345        "write args to stdout (debug channel)",
346        "console.debug(\"dbg\")   // prints dbg",
347    ),
348    // ── Math ──
349    (
350        "Math.PI",
351        "Math",
352        "the ratio of a circle's circumference to its diameter",
353        "Math.PI   // => 3.141592653589793",
354    ),
355    (
356        "Math.abs",
357        "Math",
358        "absolute value of a number",
359        "Math.abs(-5)   // => 5",
360    ),
361    (
362        "Math.floor",
363        "Math",
364        "largest integer <= x",
365        "Math.floor(3.7)   // => 3",
366    ),
367    (
368        "Math.ceil",
369        "Math",
370        "smallest integer >= x",
371        "Math.ceil(3.2)   // => 4",
372    ),
373    (
374        "Math.round",
375        "Math",
376        "round to the nearest integer (ties toward +Infinity)",
377        "Math.round(2.5)   // => 3",
378    ),
379    (
380        "Math.trunc",
381        "Math",
382        "the integer part of x, dropping any fraction",
383        "Math.trunc(-4.7)   // => -4",
384    ),
385    (
386        "Math.sign",
387        "Math",
388        "the sign of x as -1, 0, or 1",
389        "Math.sign(-8)   // => -1",
390    ),
391    (
392        "Math.sqrt",
393        "Math",
394        "square root of x",
395        "Math.sqrt(16)   // => 4",
396    ),
397    (
398        "Math.cbrt",
399        "Math",
400        "cube root of x",
401        "Math.cbrt(27)   // => 3",
402    ),
403    (
404        "Math.pow",
405        "Math",
406        "x raised to the power y",
407        "Math.pow(2, 10)   // => 1024",
408    ),
409    (
410        "Math.exp",
411        "Math",
412        "e raised to the power x",
413        "Math.exp(0)   // => 1",
414    ),
415    (
416        "Math.log",
417        "Math",
418        "natural logarithm of x",
419        "Math.log(1)   // => 0",
420    ),
421    (
422        "Math.max",
423        "Math",
424        "largest of the arguments",
425        "Math.max(3, 1, 2)   // => 3",
426    ),
427    (
428        "Math.min",
429        "Math",
430        "smallest of the arguments",
431        "Math.min(3, 1, 2)   // => 1",
432    ),
433    (
434        "Math.hypot",
435        "Math",
436        "the square root of the sum of squares of the arguments",
437        "Math.hypot(3, 4)   // => 5",
438    ),
439    (
440        "Math.sin",
441        "Math",
442        "sine of x (radians)",
443        "Math.sin(0)   // => 0",
444    ),
445    (
446        "Math.cos",
447        "Math",
448        "cosine of x (radians)",
449        "Math.cos(0)   // => 1",
450    ),
451    (
452        "Math.tan",
453        "Math",
454        "tangent of x (radians)",
455        "Math.tan(0)   // => 0",
456    ),
457    (
458        "Math.random",
459        "Math",
460        "a pseudo-random float in [0, 1)",
461        "Math.random() < 1   // => true",
462    ),
463    // ── JSON ──
464    (
465        "JSON.stringify",
466        "JSON",
467        "serialize a value to a JSON string",
468        "JSON.stringify({ a: 1 })   // => '{\"a\":1}'",
469    ),
470    (
471        "JSON.parse",
472        "JSON",
473        "parse a JSON string into a value",
474        "JSON.parse(\"[1,2]\")   // => [ 1, 2 ]",
475    ),
476    // ── Object ──
477    (
478        "Object.keys",
479        "Object",
480        "an array of an object's own enumerable keys",
481        "Object.keys({ a: 1, b: 2 })   // => [ 'a', 'b' ]",
482    ),
483    (
484        "Object.values",
485        "Object",
486        "an array of an object's own enumerable values",
487        "Object.values({ a: 1, b: 2 })   // => [ 1, 2 ]",
488    ),
489    (
490        "Object.entries",
491        "Object",
492        "an array of [key, value] pairs",
493        "Object.entries({ a: 1 })   // => [ [ 'a', 1 ] ]",
494    ),
495    (
496        "Object.assign",
497        "Object",
498        "copy source properties onto a target object (in place)",
499        "Object.assign({ a: 1 }, { b: 2 })   // => { a: 1, b: 2 }",
500    ),
501    (
502        "Object.fromEntries",
503        "Object",
504        "build an object from [key, value] pairs",
505        "Object.fromEntries([[\"a\", 1]])   // => { a: 1 }",
506    ),
507    (
508        "Object.freeze",
509        "Object",
510        "make an object immutable and return it",
511        "Object.freeze({ a: 1 }).a   // => 1",
512    ),
513    // ── Number ──
514    (
515        "Number.isInteger",
516        "Number",
517        "true if the value is an integer number",
518        "Number.isInteger(3)   // => true",
519    ),
520    (
521        "Number.isNaN",
522        "Number",
523        "true only if the value is exactly NaN (no coercion)",
524        "Number.isNaN(NaN)   // => true",
525    ),
526    (
527        "Number.isFinite",
528        "Number",
529        "true if the value is a finite number (no coercion)",
530        "Number.isFinite(10)   // => true",
531    ),
532    (
533        "Number.isSafeInteger",
534        "Number",
535        "true if the value is an integer within +/-2^53-1",
536        "Number.isSafeInteger(2 ** 53)   // => false",
537    ),
538    (
539        "Number.parseInt",
540        "Number",
541        "same as the global parseInt",
542        "Number.parseInt(\"20\", 10)   // => 20",
543    ),
544    (
545        "Number.parseFloat",
546        "Number",
547        "same as the global parseFloat",
548        "Number.parseFloat(\"1.5\")   // => 1.5",
549    ),
550    // ── String static ──
551    (
552        "String.fromCharCode",
553        "String static",
554        "a string built from the given UTF-16 code units",
555        "String.fromCharCode(65, 66)   // => 'AB'",
556    ),
557    // ── Array static ──
558    (
559        "Array.isArray",
560        "Array static",
561        "true if the value is an array",
562        "Array.isArray([1, 2])   // => true",
563    ),
564    (
565        "Array.from",
566        "Array static",
567        "build an array from an iterable or array-like",
568        "Array.from(\"ab\")   // => [ 'a', 'b' ]",
569    ),
570    (
571        "Array.of",
572        "Array static",
573        "build an array from the given arguments",
574        "Array.of(1, 2, 3)   // => [ 1, 2, 3 ]",
575    ),
576    // ── Array method ──
577    (
578        "push",
579        "Array method",
580        "append items to the end; returns the new length",
581        "const a = [1]; a.push(2); a   // => [ 1, 2 ]",
582    ),
583    (
584        "pop",
585        "Array method",
586        "remove and return the last item",
587        "[1, 2, 3].pop()   // => 3",
588    ),
589    (
590        "shift",
591        "Array method",
592        "remove and return the first item",
593        "[1, 2, 3].shift()   // => 1",
594    ),
595    (
596        "unshift",
597        "Array method",
598        "prepend items; returns the new length",
599        "const a = [2]; a.unshift(1); a   // => [ 1, 2 ]",
600    ),
601    (
602        "map",
603        "Array method",
604        "a new array of the results of calling fn on each item",
605        "[1, 2, 3].map(x => x * 2)   // => [ 2, 4, 6 ]",
606    ),
607    (
608        "filter",
609        "Array method",
610        "a new array of the items for which fn is truthy",
611        "[1, 2, 3, 4].filter(x => x % 2 === 0)   // => [ 2, 4 ]",
612    ),
613    (
614        "forEach",
615        "Array method",
616        "call fn on each item for effect; returns undefined",
617        "let s = 0; [1, 2, 3].forEach(x => s += x); s   // => 6",
618    ),
619    (
620        "reduce",
621        "Array method",
622        "fold the array to a single value with an accumulator",
623        "[1, 2, 3].reduce((a, b) => a + b, 0)   // => 6",
624    ),
625    (
626        "join",
627        "Array method",
628        "concatenate items into a string with a separator",
629        "[1, 2, 3].join(\"-\")   // => '1-2-3'",
630    ),
631    (
632        "slice",
633        "Array method",
634        "a shallow copy of a [start, end) sub-range",
635        "[1, 2, 3, 4].slice(1, 3)   // => [ 2, 3 ]",
636    ),
637    (
638        "splice",
639        "Array method",
640        "remove/insert items in place; returns the removed items",
641        "const a = [1, 2, 3]; a.splice(1, 1); a   // => [ 1, 3 ]",
642    ),
643    (
644        "concat",
645        "Array method",
646        "a new array joining this array with more arrays/values",
647        "[1].concat([2, 3])   // => [ 1, 2, 3 ]",
648    ),
649    (
650        "indexOf",
651        "Array method",
652        "index of the first matching item, or -1",
653        "[1, 2, 3].indexOf(2)   // => 1",
654    ),
655    (
656        "lastIndexOf",
657        "Array method",
658        "index of the last matching item, or -1",
659        "[1, 2, 1].lastIndexOf(1)   // => 2",
660    ),
661    (
662        "includes",
663        "Array method",
664        "true if the array contains the value",
665        "[1, 2, 3].includes(2)   // => true",
666    ),
667    (
668        "find",
669        "Array method",
670        "the first item for which fn is truthy, else undefined",
671        "[1, 2, 3].find(x => x > 1)   // => 2",
672    ),
673    (
674        "findIndex",
675        "Array method",
676        "the index of the first item for which fn is truthy, else -1",
677        "[1, 2, 3].findIndex(x => x > 1)   // => 1",
678    ),
679    (
680        "some",
681        "Array method",
682        "true if fn is truthy for any item",
683        "[1, 2, 3].some(x => x > 2)   // => true",
684    ),
685    (
686        "every",
687        "Array method",
688        "true if fn is truthy for every item",
689        "[1, 2, 3].every(x => x > 0)   // => true",
690    ),
691    (
692        "reverse",
693        "Array method",
694        "reverse the array in place",
695        "[1, 2, 3].reverse()   // => [ 3, 2, 1 ]",
696    ),
697    (
698        "sort",
699        "Array method",
700        "sort in place (default: by string order)",
701        "[3, 1, 2].sort()   // => [ 1, 2, 3 ]",
702    ),
703    (
704        "flat",
705        "Array method",
706        "a new array with sub-arrays flattened one level (or by depth)",
707        "[1, [2, [3]]].flat()   // => [ 1, 2, [ 3 ] ]",
708    ),
709    (
710        "flatMap",
711        "Array method",
712        "map each item then flatten the result one level",
713        "[1, 2].flatMap(x => [x, x])   // => [ 1, 1, 2, 2 ]",
714    ),
715    (
716        "fill",
717        "Array method",
718        "overwrite a range with a value in place",
719        "[1, 2, 3].fill(0)   // => [ 0, 0, 0 ]",
720    ),
721    (
722        "at",
723        "Array method",
724        "the item at an index, allowing negative indexing",
725        "[1, 2, 3].at(-1)   // => 3",
726    ),
727    // ── String method ──
728    (
729        "toUpperCase",
730        "String method",
731        "a copy with all cased characters uppercased",
732        "\"abc\".toUpperCase()   // => 'ABC'",
733    ),
734    (
735        "toLowerCase",
736        "String method",
737        "a copy with all cased characters lowercased",
738        "\"ABC\".toLowerCase()   // => 'abc'",
739    ),
740    (
741        "charAt",
742        "String method",
743        "the character at an index",
744        "\"hi\".charAt(1)   // => 'i'",
745    ),
746    (
747        "charCodeAt",
748        "String method",
749        "the UTF-16 code unit at an index",
750        "\"A\".charCodeAt(0)   // => 65",
751    ),
752    (
753        "codePointAt",
754        "String method",
755        "the Unicode code point at an index",
756        "\"A\".codePointAt(0)   // => 65",
757    ),
758    (
759        "slice",
760        "String method",
761        "a substring over a [start, end) range (negatives allowed)",
762        "\"hello\".slice(1, 3)   // => 'el'",
763    ),
764    (
765        "substring",
766        "String method",
767        "a substring over a [start, end) range (no negatives)",
768        "\"hello\".substring(0, 2)   // => 'he'",
769    ),
770    (
771        "split",
772        "String method",
773        "an array of substrings split on a separator",
774        "\"a,b,c\".split(\",\")   // => [ 'a', 'b', 'c' ]",
775    ),
776    (
777        "trim",
778        "String method",
779        "a copy with leading and trailing whitespace removed",
780        "\"  hi  \".trim()   // => 'hi'",
781    ),
782    (
783        "trimStart",
784        "String method",
785        "a copy with leading whitespace removed",
786        "\"  hi\".trimStart()   // => 'hi'",
787    ),
788    (
789        "trimEnd",
790        "String method",
791        "a copy with trailing whitespace removed",
792        "\"hi  \".trimEnd()   // => 'hi'",
793    ),
794    (
795        "replace",
796        "String method",
797        "a copy with the first match of a substring replaced",
798        "\"aaa\".replace(\"a\", \"b\")   // => 'baa'",
799    ),
800    (
801        "replaceAll",
802        "String method",
803        "a copy with every match of a substring replaced",
804        "\"aaa\".replaceAll(\"a\", \"b\")   // => 'bbb'",
805    ),
806    (
807        "repeat",
808        "String method",
809        "the string repeated n times",
810        "\"ab\".repeat(3)   // => 'ababab'",
811    ),
812    (
813        "startsWith",
814        "String method",
815        "true if the string starts with the prefix",
816        "\"hello\".startsWith(\"he\")   // => true",
817    ),
818    (
819        "endsWith",
820        "String method",
821        "true if the string ends with the suffix",
822        "\"hello\".endsWith(\"lo\")   // => true",
823    ),
824    (
825        "padStart",
826        "String method",
827        "pad on the left to a target length",
828        "\"5\".padStart(3, \"0\")   // => '005'",
829    ),
830    (
831        "padEnd",
832        "String method",
833        "pad on the right to a target length",
834        "\"5\".padEnd(3, \"0\")   // => '500'",
835    ),
836    // ── Number method ──
837    (
838        "toFixed",
839        "Number method",
840        "a fixed-point string with n digits after the decimal point",
841        "(3.14159).toFixed(2)   // => '3.14'",
842    ),
843    (
844        "toPrecision",
845        "Number method",
846        "a string with n significant digits",
847        "(123.456).toPrecision(4)   // => '123.5'",
848    ),
849    (
850        "toString",
851        "Number method",
852        "the string form of a number in an optional radix",
853        "(255).toString(16)   // => 'ff'",
854    ),
855];
856
857/// The builtin corpus, exposed for offline doc generation (`gen-docs`) and any
858/// editor tooling that wants the same (name, chapter, doc, example) rows.
859pub fn corpus() -> &'static [(&'static str, &'static str, &'static str, &'static str)] {
860    CORPUS
861}
862
863/// Open document text keyed by URI, kept current from the sync notifications so
864/// hover can look up the identifier under the cursor.
865type Docs = HashMap<String, String>;
866
867/// Entry point for `node --lsp`.
868pub fn run() -> Result<(), String> {
869    spawn_orphan_guard();
870    let (conn, io_threads) = Connection::stdio();
871    let (init_id, _params) = conn
872        .initialize_start()
873        .map_err(|e| format!("lsp initialize: {e}"))?;
874    let init_result = serde_json::json!({
875        "capabilities": server_capabilities(),
876        "serverInfo": { "name": "nodejs", "version": env!("CARGO_PKG_VERSION") },
877    });
878    conn.sender
879        .send(Response::new_ok(init_id, init_result).into())
880        .map_err(|e| format!("lsp send: {e}"))?;
881
882    let mut docs: Docs = HashMap::new();
883    for msg in &conn.receiver {
884        match msg {
885            Message::Request(req) => {
886                if conn
887                    .handle_shutdown(&req)
888                    .map_err(|e| format!("lsp shutdown: {e}"))?
889                {
890                    break;
891                }
892                dispatch_request(&conn, &docs, req);
893            }
894            Message::Notification(not) => dispatch_notification(&conn, &mut docs, not),
895            Message::Response(_) => {}
896        }
897    }
898    drop(conn);
899    io_threads.join().map_err(|_| "lsp io join".to_string())?;
900    Ok(())
901}
902
903fn server_capabilities() -> ServerCapabilities {
904    ServerCapabilities {
905        text_document_sync: Some(TextDocumentSyncCapability::Options(
906            TextDocumentSyncOptions {
907                open_close: Some(true),
908                change: Some(TextDocumentSyncKind::FULL),
909                ..Default::default()
910            },
911        )),
912        completion_provider: Some(CompletionOptions {
913            resolve_provider: Some(false),
914            ..Default::default()
915        }),
916        hover_provider: Some(HoverProviderCapability::Simple(true)),
917        ..Default::default()
918    }
919}
920
921fn handle<P, R>(conn: &Connection, req: Request, f: impl FnOnce(P) -> R)
922where
923    P: serde::de::DeserializeOwned,
924    R: serde::Serialize,
925{
926    let method = req.method.clone();
927    let id = req.id.clone();
928    match req.extract::<P>(&method) {
929        Ok((id, params)) => {
930            let value = serde_json::to_value(f(params)).unwrap_or(serde_json::Value::Null);
931            let _ = conn.sender.send(Response::new_ok(id, value).into());
932        }
933        Err(ExtractError::JsonError { error, .. }) => {
934            let _ = conn.sender.send(
935                Response::new_err(id, ErrorCode::InvalidParams as i32, error.to_string()).into(),
936            );
937        }
938        Err(ExtractError::MethodMismatch(_)) => unreachable!("method matched before extract"),
939    }
940}
941
942fn dispatch_request(conn: &Connection, docs: &Docs, req: Request) {
943    match req.method.as_str() {
944        Completion::METHOD => handle(conn, req, |_p: CompletionParams| completions()),
945        HoverRequest::METHOD => handle(conn, req, |p: HoverParams| hover(docs, &p)),
946        _ => {
947            let _ = conn.sender.send(
948                Response::new_err(req.id, ErrorCode::MethodNotFound as i32, "unhandled".into())
949                    .into(),
950            );
951        }
952    }
953}
954
955fn dispatch_notification(conn: &Connection, docs: &mut Docs, not: lsp_server::Notification) {
956    match not.method.as_str() {
957        DidOpenTextDocument::METHOD => {
958            if let Ok(p) = serde_json::from_value::<DidOpenTextDocumentParams>(not.params) {
959                let uri = p.text_document.uri;
960                docs.insert(uri.as_str().to_string(), p.text_document.text.clone());
961                publish_diagnostics(conn, &uri, &p.text_document.text);
962            }
963        }
964        DidChangeTextDocument::METHOD => {
965            if let Ok(p) = serde_json::from_value::<DidChangeTextDocumentParams>(not.params) {
966                if let Some(change) = p.content_changes.into_iter().last() {
967                    let uri = p.text_document.uri;
968                    docs.insert(uri.as_str().to_string(), change.text.clone());
969                    publish_diagnostics(conn, &uri, &change.text);
970                }
971            }
972        }
973        DidCloseTextDocument::METHOD => {
974            if let Ok(p) = serde_json::from_value::<DidCloseTextDocumentParams>(not.params) {
975                let uri = p.text_document.uri;
976                docs.remove(uri.as_str());
977                publish_diagnostics(conn, &uri, "");
978            }
979        }
980        _ => {}
981    }
982}
983
984fn completions() -> CompletionResponse {
985    let items = CORPUS
986        .iter()
987        .map(|(name, chapter, doc, _example)| CompletionItem {
988            label: name.to_string(),
989            kind: Some(if *chapter == "Keyword" {
990                CompletionItemKind::KEYWORD
991            } else if chapter.contains("method") {
992                CompletionItemKind::METHOD
993            } else {
994                CompletionItemKind::FUNCTION
995            }),
996            detail: Some((*doc).to_string()),
997            ..Default::default()
998        })
999        .collect();
1000    CompletionResponse::Array(items)
1001}
1002
1003/// Hover: look up the identifier under the cursor in the corpus and render its
1004/// chapter, doc, and example. Falls back to a short banner when the cursor is
1005/// not on a known name.
1006fn hover(docs: &Docs, params: &HoverParams) -> Hover {
1007    let pos = params.text_document_position_params.position;
1008    let uri = params
1009        .text_document_position_params
1010        .text_document
1011        .uri
1012        .as_str();
1013    let word = docs
1014        .get(uri)
1015        .and_then(|text| word_at(text, pos))
1016        .unwrap_or_default();
1017
1018    let matches: Vec<&(&str, &str, &str, &str)> =
1019        CORPUS.iter().filter(|(name, ..)| *name == word).collect();
1020
1021    let body = if matches.is_empty() {
1022        "**node-js** — JavaScript on the fusevm bytecode VM + Cranelift JIT.".to_string()
1023    } else {
1024        let mut out = String::new();
1025        for (name, chapter, doc, example) in matches {
1026            out.push_str(&format!(
1027                "**`{name}`** — _{chapter}_\n\n{doc}\n\n```javascript\n{example}\n```\n\n"
1028            ));
1029        }
1030        out.trim_end().to_string()
1031    };
1032
1033    Hover {
1034        contents: HoverContents::Markup(MarkupContent {
1035            kind: MarkupKind::Markdown,
1036            value: body,
1037        }),
1038        range: None,
1039    }
1040}
1041
1042/// Extract the identifier (`[A-Za-z0-9_$]+`) spanning the given position, if any.
1043fn word_at(text: &str, pos: Position) -> Option<String> {
1044    let line = text.lines().nth(pos.line as usize)?;
1045    let chars: Vec<char> = line.chars().collect();
1046    let col = (pos.character as usize).min(chars.len());
1047    let is_word = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
1048
1049    let mut start = col;
1050    while start > 0 && is_word(chars[start - 1]) {
1051        start -= 1;
1052    }
1053    let mut end = col;
1054    while end < chars.len() && is_word(chars[end]) {
1055        end += 1;
1056    }
1057    if start == end {
1058        return None;
1059    }
1060    Some(chars[start..end].iter().collect())
1061}
1062
1063fn publish_diagnostics(conn: &Connection, uri: &Uri, text: &str) {
1064    let params = PublishDiagnosticsParams {
1065        uri: uri.clone(),
1066        diagnostics: compute_diagnostics(text),
1067        version: None,
1068    };
1069    let not = lsp_server::Notification::new(PublishDiagnostics::METHOD.to_string(), params);
1070    let _ = conn.sender.send(not.into());
1071}
1072
1073/// Parse the whole document with the runtime's own parser; a syntax error maps
1074/// to a single diagnostic on the line named in its `(line N)` suffix.
1075fn compute_diagnostics(text: &str) -> Vec<Diagnostic> {
1076    if text.trim().is_empty() {
1077        return Vec::new();
1078    }
1079    match crate::parser::parse(text) {
1080        Ok(_) => Vec::new(),
1081        Err(e) => {
1082            let line = parse_error_line(&e).saturating_sub(1);
1083            vec![Diagnostic {
1084                range: Range {
1085                    start: Position { line, character: 0 },
1086                    end: Position {
1087                        line,
1088                        character: 200,
1089                    },
1090                },
1091                severity: Some(DiagnosticSeverity::ERROR),
1092                message: e,
1093                ..Default::default()
1094            }]
1095        }
1096    }
1097}
1098
1099/// Extract the (1-based) line number from a node-js parser error, which embeds
1100/// it as `… (line N)`. Defaults to line 1 when no such marker is present.
1101fn parse_error_line(e: &str) -> u32 {
1102    e.rsplit_once("(line ")
1103        .and_then(|(_, rest)| rest.split(|c: char| !c.is_ascii_digit()).next())
1104        .and_then(|n| n.parse().ok())
1105        .unwrap_or(1)
1106}
1107
1108/// Exit if reparented to pid 1 (the editor died) so we never leak.
1109fn spawn_orphan_guard() {
1110    std::thread::spawn(|| {
1111        #[cfg(target_os = "linux")]
1112        // SAFETY: prctl(PR_SET_PDEATHSIG, ...) only registers a signal disposition.
1113        unsafe {
1114            libc::prctl(
1115                libc::PR_SET_PDEATHSIG,
1116                libc::SIGKILL as libc::c_ulong,
1117                0,
1118                0,
1119                0,
1120            );
1121        }
1122        loop {
1123            std::thread::sleep(std::time::Duration::from_secs(2));
1124            // SAFETY: getppid takes no arguments and never fails.
1125            if unsafe { libc::getppid() } == 1 {
1126                std::process::exit(0);
1127            }
1128        }
1129    });
1130}