@esportsplus/typescript 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +91 -0
  2. package/bin/tsc-lsp +3 -0
  3. package/build/cli/diagnostics.d.ts +5 -1
  4. package/build/cli/diagnostics.js +13 -8
  5. package/build/cli/tsc.d.ts +3 -1
  6. package/build/cli/tsc.js +77 -87
  7. package/build/compiler/coordinator.d.ts +1 -2
  8. package/build/compiler/coordinator.js +33 -22
  9. package/build/compiler/imports.d.ts +2 -0
  10. package/build/compiler/imports.js +9 -2
  11. package/build/compiler/language-service.d.ts +15 -4
  12. package/build/compiler/language-service.js +56 -24
  13. package/build/compiler/plugins/vite.js +7 -5
  14. package/build/compiler/sourcemap.d.ts +3 -1
  15. package/build/compiler/sourcemap.js +23 -5
  16. package/build/jsonc.d.ts +2 -0
  17. package/build/jsonc.js +85 -0
  18. package/build/lsp/bin.d.ts +1 -0
  19. package/build/lsp/bin.js +2 -0
  20. package/build/lsp/diagnostics.d.ts +10 -0
  21. package/build/lsp/diagnostics.js +69 -0
  22. package/build/lsp/index.d.ts +2 -0
  23. package/build/lsp/index.js +2 -0
  24. package/build/lsp/server.d.ts +4 -0
  25. package/build/lsp/server.js +130 -0
  26. package/build/lsp/workspace.d.ts +14 -0
  27. package/build/lsp/workspace.js +46 -0
  28. package/build/probe/adapter.d.ts +14 -0
  29. package/build/probe/adapter.js +42 -0
  30. package/build/probe/async/channel.d.ts +4 -0
  31. package/build/probe/async/channel.js +560 -0
  32. package/build/probe/async/value.d.ts +9 -0
  33. package/build/probe/async/value.js +16 -0
  34. package/build/probe/channels.d.ts +4 -0
  35. package/build/probe/channels.js +16 -0
  36. package/build/probe/exceptions/channel.d.ts +5 -0
  37. package/build/probe/exceptions/channel.js +669 -0
  38. package/build/probe/exceptions/jsdoc.d.ts +8 -0
  39. package/build/probe/exceptions/jsdoc.js +34 -0
  40. package/build/probe/exceptions/value.d.ts +39 -0
  41. package/build/probe/exceptions/value.js +158 -0
  42. package/build/probe/kernel/analyze.d.ts +8 -0
  43. package/build/probe/kernel/analyze.js +68 -0
  44. package/build/probe/kernel/ast.d.ts +11 -0
  45. package/build/probe/kernel/ast.js +49 -0
  46. package/build/probe/kernel/config.d.ts +5 -0
  47. package/build/probe/kernel/config.js +209 -0
  48. package/build/probe/kernel/fixpoint.d.ts +6 -0
  49. package/build/probe/kernel/fixpoint.js +206 -0
  50. package/build/probe/kernel/format.d.ts +4 -0
  51. package/build/probe/kernel/format.js +32 -0
  52. package/build/probe/kernel/graph.d.ts +4 -0
  53. package/build/probe/kernel/graph.js +507 -0
  54. package/build/probe/kernel/ids.d.ts +8 -0
  55. package/build/probe/kernel/ids.js +66 -0
  56. package/build/probe/kernel/program.d.ts +8 -0
  57. package/build/probe/kernel/program.js +13 -0
  58. package/build/probe/kernel/types.d.ts +134 -0
  59. package/build/probe/kernel/types.js +1 -0
  60. package/build/probe/overlay/base/async.jsonc +13 -0
  61. package/build/probe/overlay/base/exceptions.jsonc +54 -0
  62. package/build/probe/overlay/base/resources.jsonc +57 -0
  63. package/build/probe/overlay/load.d.ts +18 -0
  64. package/build/probe/overlay/load.js +302 -0
  65. package/build/probe/overlay/presets/express.jsonc +25 -0
  66. package/build/probe/overlay/presets/node.jsonc +17 -0
  67. package/build/probe/resources/channel.d.ts +4 -0
  68. package/build/probe/resources/channel.js +798 -0
  69. package/build/probe/resources/value.d.ts +9 -0
  70. package/build/probe/resources/value.js +36 -0
  71. package/build/tsconfig.d.ts +2 -0
  72. package/build/tsconfig.js +150 -0
  73. package/package.json +14 -8
  74. package/tsconfig.base.json +19 -0
@@ -1,5 +1,6 @@
1
1
  import type { SourceFile } from 'typescript/unstable/ast';
2
- import { type Checker, type Program } from 'typescript/unstable/sync';
2
+ import type { FileSystem } from 'typescript/unstable/fs';
3
+ import { API, type Checker, type Program, type Project, type Snapshot } from 'typescript/unstable/sync';
3
4
  type ScratchResult = {
4
5
  checker: Checker;
5
6
  program: Program;
@@ -9,20 +10,30 @@ type UpdateResult = {
9
10
  checker: Checker;
10
11
  program: Program;
11
12
  };
13
+ type OpenProject = {
14
+ api: API;
15
+ dispose: () => void;
16
+ project: Project;
17
+ snapshot: Snapshot;
18
+ };
19
+ declare function open(configPath: string, overlay?: FileSystem): OpenProject;
12
20
  declare const dispose: (root?: string) => void;
13
21
  declare const findConfig: (startDir: string) => string | null;
14
- declare const invalidate: (root: string, fileName: string) => void;
22
+ declare const invalidate: (configFileName: string, fileName: string) => void;
15
23
  declare const parse: (fileName: string, content: string) => SourceFile;
16
24
  declare const scratch: (fileName: string, content: string) => ScratchResult;
17
- declare const update: (root: string, fileName: string, content: string) => UpdateResult;
25
+ declare const update: (configFileName: string, fileName: string, content: string) => UpdateResult;
26
+ declare const updateMany: (configFileName: string, updates: Map<string, string>) => UpdateResult;
18
27
  declare const _default: {
19
28
  dispose: typeof dispose;
20
29
  findConfig: typeof findConfig;
21
30
  invalidate: typeof invalidate;
31
+ open: typeof open;
22
32
  parse: typeof parse;
23
33
  scratch: typeof scratch;
24
34
  update: typeof update;
35
+ updateMany: typeof updateMany;
25
36
  };
26
37
  export default _default;
27
- export { dispose, findConfig, invalidate, parse, scratch, update };
38
+ export { dispose, findConfig, invalidate, open, parse, scratch, update, updateMany };
28
39
  export type { ScratchResult, UpdateResult };
@@ -31,12 +31,9 @@ function advanceScratch(id, content) {
31
31
  entry.snapshot = snapshot;
32
32
  return entry;
33
33
  }
34
- function createEntry(root) {
35
- let configFileName = findConfig(root);
36
- if (!configFileName) {
37
- throw new Error(`${PACKAGE_NAME}: tsconfig.json not found`);
38
- }
39
- let contents = new Map(), api = new API({ cwd: root, fs: overlayFileSystem(contents) }), snapshot = api.updateSnapshot({ openProjects: [configFileName] }), project = resolveProject(snapshot, configFileName);
34
+ function createEntry(configFileName) {
35
+ let root = path.dirname(configFileName);
36
+ let contents = new Map(), opened = open(configFileName, overlayFileSystem(contents)), { api, project, snapshot } = opened;
40
37
  for (let diagnostic of project.program.getConfigFileParsingDiagnostics()) {
41
38
  if (diagnostic.category === DiagnosticCategory.Error) {
42
39
  snapshot.dispose();
@@ -64,17 +61,39 @@ function disposeScratch(entry) {
64
61
  }
65
62
  entry.api.close();
66
63
  }
67
- function getEntry(root) {
68
- let entry = cache.get(root);
64
+ function getEntry(configFileName) {
65
+ let entry = cache.get(configFileName);
69
66
  if (!entry) {
70
- entry = createEntry(root);
71
- cache.set(root, entry);
67
+ entry = createEntry(configFileName);
68
+ cache.set(configFileName, entry);
72
69
  }
73
70
  return entry;
74
71
  }
75
72
  function normalize(fileName) {
76
73
  return fileName.replace(backslashes, '/');
77
74
  }
75
+ function open(configPath, overlay) {
76
+ let configFileName = normalize(path.resolve(configPath)), entry = overlay === undefined ? getEntry(configFileName) : undefined, api = entry?.api ?? new API({ cwd: path.dirname(configFileName), fs: overlay }), snapshot = entry?.snapshot ?? api.updateSnapshot({ openProjects: [configFileName] }), project = entry?.project ?? resolveProject(snapshot, configFileName);
77
+ if (entry) {
78
+ return {
79
+ api,
80
+ dispose: () => dispose(configFileName),
81
+ project,
82
+ snapshot
83
+ };
84
+ }
85
+ return {
86
+ api,
87
+ dispose: () => {
88
+ if (!snapshot.isDisposed()) {
89
+ snapshot.dispose();
90
+ }
91
+ api.close();
92
+ },
93
+ project,
94
+ snapshot,
95
+ };
96
+ }
78
97
  function overlayFileSystem(contents) {
79
98
  return {
80
99
  fileExists: (fileName) => {
@@ -101,7 +120,7 @@ function overlayFileSystem(contents) {
101
120
  }
102
121
  catch {
103
122
  }
104
- return { directories, files: [...files, ...extra] };
123
+ return { directories, files: [...new Set([...files, ...extra])] };
105
124
  },
106
125
  readFile: (fileName) => {
107
126
  let content = contents.get(normalize(fileName));
@@ -192,8 +211,8 @@ const findConfig = (startDir) => {
192
211
  current = parent;
193
212
  }
194
213
  };
195
- const invalidate = (root, fileName) => {
196
- let entry = cache.get(root);
214
+ const invalidate = (configFileName, fileName) => {
215
+ let entry = cache.get(configFileName);
197
216
  if (!entry) {
198
217
  return;
199
218
  }
@@ -211,11 +230,20 @@ const scratch = (fileName, content) => {
211
230
  }
212
231
  return { checker: entry.project.checker, program: entry.project.program, sourceFile: source };
213
232
  };
214
- const update = (root, fileName, content) => {
215
- let entry = getEntry(root), id = normalize(fileName);
216
- entry.contents.set(id, content);
217
- if (entry.seen.has(id)) {
218
- entry.pending.add(id);
233
+ const update = (configFileName, fileName, content) => {
234
+ return updateMany(configFileName, new Map([[fileName, content]]));
235
+ };
236
+ const updateMany = (configFileName, updates) => {
237
+ let entry = getEntry(configFileName), ids = [];
238
+ for (let [fileName, content] of updates) {
239
+ let id = normalize(fileName);
240
+ entry.contents.set(id, content);
241
+ ids.push(id);
242
+ }
243
+ if (ids.every(id => entry.seen.has(id))) {
244
+ for (let i = 0, n = ids.length; i < n; i++) {
245
+ entry.pending.add(ids[i]);
246
+ }
219
247
  let changed = [...entry.pending];
220
248
  entry.pending.clear();
221
249
  advance(entry, changed);
@@ -223,13 +251,17 @@ const update = (root, fileName, content) => {
223
251
  else {
224
252
  recreate(entry);
225
253
  entry.pending.clear();
226
- entry.seen.add(id);
254
+ for (let i = 0, n = ids.length; i < n; i++) {
255
+ entry.seen.add(ids[i]);
256
+ }
227
257
  }
228
- let source = entry.project.program.getSourceFile(id);
229
- if (!source || source.text !== content) {
230
- throw new Error(`${PACKAGE_NAME}: failed to load ${fileName} into the program`);
258
+ for (let i = 0, n = ids.length; i < n; i++) {
259
+ let id = ids[i], source = entry.project.program.getSourceFile(id), content = entry.contents.get(id);
260
+ if (!source || source.text !== content) {
261
+ throw new Error(`${PACKAGE_NAME}: failed to load ${id} into the program`);
262
+ }
231
263
  }
232
264
  return { checker: entry.project.checker, program: entry.project.program };
233
265
  };
234
- export default { dispose, findConfig, invalidate, parse, scratch, update };
235
- export { dispose, findConfig, invalidate, parse, scratch, update };
266
+ export default { dispose, findConfig, invalidate, open, parse, scratch, update, updateMany };
267
+ export { dispose, findConfig, invalidate, open, parse, scratch, update, updateMany };
@@ -6,17 +6,19 @@ const FILE_REGEX = /\.[tj]sx?$/;
6
6
  let contexts = new Map();
7
7
  export default ({ name, onWatchChange, plugins }) => {
8
8
  return ({ root } = {}) => {
9
+ let tsconfig = null;
9
10
  return {
10
11
  closeBundle() {
11
- languageService.dispose(root || '');
12
+ languageService.dispose(tsconfig ?? '');
12
13
  contexts.delete(root || '');
13
14
  },
14
15
  closeWatcher() {
15
- languageService.dispose(root || '');
16
+ languageService.dispose(tsconfig ?? '');
16
17
  contexts.delete(root || '');
17
18
  },
18
19
  configResolved(config) {
19
20
  root ??= config.root;
21
+ tsconfig = languageService.findConfig(root);
20
22
  },
21
23
  enforce: 'pre',
22
24
  name: `${name}/compiler/vite`,
@@ -25,13 +27,13 @@ export default ({ name, onWatchChange, plugins }) => {
25
27
  return null;
26
28
  }
27
29
  try {
28
- let normalizedId = id.replace(DIRECTORY_SEPARATOR_REGEX, '/'), { checker, program } = languageService.update(root || '', normalizedId, code), sourceFile = program.getSourceFile(normalizedId) ?? languageService.parse(normalizedId, code);
30
+ let normalizedId = id.replace(DIRECTORY_SEPARATOR_REGEX, '/'), configPath = tsconfig ?? languageService.findConfig(root || ''), { checker, program } = languageService.update(configPath ?? '', normalizedId, code), sourceFile = program.getSourceFile(normalizedId) ?? languageService.parse(normalizedId, code);
29
31
  let key = root || '', ctx = contexts.get(key);
30
32
  if (!ctx) {
31
33
  ctx = new Map();
32
34
  contexts.set(key, ctx);
33
35
  }
34
- let result = coordinator.transform(plugins, code, sourceFile, { checker, program }, key, ctx);
36
+ let result = coordinator.transform(plugins, code, sourceFile, { checker, configPath: configPath ?? undefined, program }, key, ctx);
35
37
  if (!result.changed) {
36
38
  return null;
37
39
  }
@@ -46,7 +48,7 @@ export default ({ name, onWatchChange, plugins }) => {
46
48
  if (FILE_REGEX.test(id)) {
47
49
  onWatchChange?.();
48
50
  contexts.delete(root || '');
49
- languageService.invalidate(root || '', id);
51
+ languageService.invalidate(tsconfig ?? '', id);
50
52
  }
51
53
  }
52
54
  };
@@ -28,6 +28,7 @@ type SourceMapV3 = {
28
28
  sourcesContent?: (string | null)[];
29
29
  version: 3;
30
30
  };
31
+ declare function resolveOffset(mapping: PositionMapping, offset: number): number;
31
32
  declare const buildGeneration: (beforeText: string, edits: Edit[]) => OffsetAnchor[];
32
33
  declare const composeEmittedMap: (map: SourceMapV3, mapping: PositionMapping, transformedText: string, originalText: string) => SourceMapV3;
33
34
  declare const decode: (mappings: string) => number[][][];
@@ -43,8 +44,9 @@ declare const _default: {
43
44
  decode: typeof decode;
44
45
  encode: typeof encode;
45
46
  originalPositionFor: typeof originalPositionFor;
47
+ resolveOffset: typeof resolveOffset;
46
48
  toSourceMapV3: typeof toSourceMapV3;
47
49
  };
48
50
  export default _default;
49
- export { buildGeneration, composeEmittedMap, decode, encode, originalPositionFor, toSourceMapV3 };
51
+ export { buildGeneration, composeEmittedMap, decode, encode, originalPositionFor, resolveOffset, toSourceMapV3 };
50
52
  export type { Edit, OffsetAnchor, PositionMapping, Segment, SourceMapV3 };
@@ -115,6 +115,24 @@ function resolveOffset(mapping, offset) {
115
115
  }
116
116
  return result;
117
117
  }
118
+ function resolveOffsets(mapping) {
119
+ const cursors = mapping.generations.map(() => 0);
120
+ return (offset) => {
121
+ let result = offset;
122
+ for (let i = mapping.generations.length - 1; i >= 0; i--) {
123
+ const anchors = mapping.generations[i];
124
+ while (cursors[i] + 1 < anchors.length &&
125
+ result >= anchors[cursors[i] + 1].afterStart) {
126
+ cursors[i] += 1;
127
+ }
128
+ const anchor = anchors[cursors[i]];
129
+ result = anchor.identity
130
+ ? anchor.beforeStart + (result - anchor.afterStart)
131
+ : anchor.beforeStart;
132
+ }
133
+ return result;
134
+ };
135
+ }
118
136
  function segmentAt(starts, offset, genColumn) {
119
137
  let position = offsetToLineCol(starts, offset);
120
138
  return { genColumn, originalColumn: position.column, originalLine: position.line, source: 0 };
@@ -223,16 +241,16 @@ const originalPositionFor = (mapping, transformedText, originalText, line, colum
223
241
  return offsetToLineCol(lineStarts(originalText), resolveOffset(mapping, base + column));
224
242
  };
225
243
  const toSourceMapV3 = (mapping, transformedText, originalText, source) => {
226
- let oStarts = lineStarts(originalText), segments = [], tStarts = lineStarts(transformedText);
244
+ let oStarts = lineStarts(originalText), resolve = resolveOffsets(mapping), segments = [], tStarts = lineStarts(transformedText);
227
245
  for (let line = 0, n = tStarts.length; line < n; line++) {
228
- let end = line + 1 < n ? tStarts[line + 1] - 1 : transformedText.length, previous = resolveOffset(mapping, tStarts[line]), start = tStarts[line];
246
+ let end = line + 1 < n ? tStarts[line + 1] - 1 : transformedText.length, previous = resolve(tStarts[line]), start = tStarts[line];
229
247
  let row = [segmentAt(oStarts, previous, 0)];
230
248
  for (let offset = start + 1; offset < end; offset++) {
231
249
  let current = charClass(transformedText.charCodeAt(offset));
232
250
  if (current !== CLASS_OTHER && current === charClass(transformedText.charCodeAt(offset - 1))) {
233
251
  continue;
234
252
  }
235
- let resolved = resolveOffset(mapping, offset);
253
+ let resolved = resolve(offset);
236
254
  if (resolved === previous) {
237
255
  continue;
238
256
  }
@@ -243,5 +261,5 @@ const toSourceMapV3 = (mapping, transformedText, originalText, source) => {
243
261
  }
244
262
  return { mappings: encode(segmentsToRaw(segments)), names: [], sources: [source], version: 3 };
245
263
  };
246
- export default { buildGeneration, composeEmittedMap, decode, encode, originalPositionFor, toSourceMapV3 };
247
- export { buildGeneration, composeEmittedMap, decode, encode, originalPositionFor, toSourceMapV3 };
264
+ export default { buildGeneration, composeEmittedMap, decode, encode, originalPositionFor, resolveOffset, toSourceMapV3 };
265
+ export { buildGeneration, composeEmittedMap, decode, encode, originalPositionFor, resolveOffset, toSourceMapV3 };
@@ -0,0 +1,2 @@
1
+ declare const stripJsonc: (text: string) => string;
2
+ export { stripJsonc };
package/build/jsonc.js ADDED
@@ -0,0 +1,85 @@
1
+ const stripJsonc = (text) => {
2
+ let escaped = false, inBlockComment = false, inLineComment = false, inString = false, stripped = '';
3
+ for (let i = 0, n = text.length; i < n; i++) {
4
+ let char = text[i], next = text[i + 1];
5
+ if (inLineComment) {
6
+ if (char === '\n') {
7
+ inLineComment = false;
8
+ stripped += char;
9
+ }
10
+ continue;
11
+ }
12
+ if (inBlockComment) {
13
+ if (char === '*' && next === '/') {
14
+ inBlockComment = false;
15
+ i++;
16
+ }
17
+ continue;
18
+ }
19
+ if (inString) {
20
+ stripped += char;
21
+ if (escaped) {
22
+ escaped = false;
23
+ }
24
+ else if (char === '\\') {
25
+ escaped = true;
26
+ }
27
+ else if (char === '"') {
28
+ inString = false;
29
+ }
30
+ continue;
31
+ }
32
+ if (char === '"') {
33
+ inString = true;
34
+ stripped += char;
35
+ continue;
36
+ }
37
+ if (char === '/' && next === '/') {
38
+ inLineComment = true;
39
+ i++;
40
+ continue;
41
+ }
42
+ if (char === '/' && next === '*') {
43
+ inBlockComment = true;
44
+ i++;
45
+ continue;
46
+ }
47
+ stripped += char;
48
+ }
49
+ escaped = false;
50
+ inString = false;
51
+ let result = '';
52
+ for (let i = 0, n = stripped.length; i < n; i++) {
53
+ let char = stripped[i];
54
+ if (inString) {
55
+ result += char;
56
+ if (escaped) {
57
+ escaped = false;
58
+ }
59
+ else if (char === '\\') {
60
+ escaped = true;
61
+ }
62
+ else if (char === '"') {
63
+ inString = false;
64
+ }
65
+ continue;
66
+ }
67
+ if (char === '"') {
68
+ inString = true;
69
+ result += char;
70
+ continue;
71
+ }
72
+ if (char === ',') {
73
+ let j = i + 1;
74
+ while (j < n && /\s/.test(stripped[j])) {
75
+ j++;
76
+ }
77
+ if (stripped[j] === ']' || stripped[j] === '}') {
78
+ continue;
79
+ }
80
+ }
81
+ result += char;
82
+ }
83
+ return result;
84
+ };
85
+ export { stripJsonc };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { startServer } from './server.js';
2
+ startServer();
@@ -0,0 +1,10 @@
1
+ import { DiagnosticSeverity } from 'vscode-languageserver/node';
2
+ import type { CodeAction, Diagnostic as LspDiagnostic, Hover } from 'vscode-languageserver/node';
3
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
4
+ import type { Diagnostic as AnalyzeDiagnostic } from '../probe/kernel/types.js';
5
+ export type DocumentResolver = (fileName: string) => TextDocument | undefined;
6
+ declare function toLspDiagnostic(diagnostic: AnalyzeDiagnostic, severity: DiagnosticSeverity, resolve: DocumentResolver, uriOf: (fileName: string) => string): LspDiagnostic;
7
+ declare function groupByFile(diagnostics: ReadonlyArray<AnalyzeDiagnostic>, severity: DiagnosticSeverity, resolve: DocumentResolver, uriOf: (fileName: string) => string): Map<string, LspDiagnostic[]>;
8
+ declare function hoverAt(diagnostics: ReadonlyArray<AnalyzeDiagnostic>, offset: number, document: TextDocument): Hover | undefined;
9
+ declare function codeActionsAt(diagnostics: ReadonlyArray<AnalyzeDiagnostic>, startOffset: number, endOffset: number, document: TextDocument, uriOf: (fileName: string) => string): CodeAction[];
10
+ export { codeActionsAt, DiagnosticSeverity, groupByFile, hoverAt, toLspDiagnostic };
@@ -0,0 +1,69 @@
1
+ import { CodeActionKind, DiagnosticSeverity, MarkupKind } from 'vscode-languageserver/node';
2
+ function rangeOf(location, document) {
3
+ if (document) {
4
+ return { start: document.positionAt(location.pos), end: document.positionAt(location.end) };
5
+ }
6
+ let start = { line: Math.max(0, location.line - 1), character: Math.max(0, location.column - 1) };
7
+ return { start, end: { line: start.line, character: start.character + 1 } };
8
+ }
9
+ function relatedOf(diagnostic, resolve, uriOf) {
10
+ return diagnostic.related.map((related) => ({
11
+ location: { range: rangeOf(related.location, resolve(related.location.fileName)), uri: uriOf(related.location.fileName) },
12
+ message: related.message,
13
+ }));
14
+ }
15
+ function toLspDiagnostic(diagnostic, severity, resolve, uriOf) {
16
+ return {
17
+ code: diagnostic.channel,
18
+ message: diagnostic.message,
19
+ range: rangeOf(diagnostic.location, resolve(diagnostic.location.fileName)),
20
+ relatedInformation: relatedOf(diagnostic, resolve, uriOf),
21
+ severity,
22
+ source: 'analyze',
23
+ };
24
+ }
25
+ function groupByFile(diagnostics, severity, resolve, uriOf) {
26
+ let grouped = new Map();
27
+ for (let i = 0, n = diagnostics.length; i < n; i++) {
28
+ let diagnostic = diagnostics[i], fileName = diagnostic.location.fileName, list = grouped.get(fileName);
29
+ if (!list) {
30
+ list = [];
31
+ grouped.set(fileName, list);
32
+ }
33
+ list.push(toLspDiagnostic(diagnostic, severity, resolve, uriOf));
34
+ }
35
+ return grouped;
36
+ }
37
+ function hoverAt(diagnostics, offset, document) {
38
+ let hits = diagnostics.filter((diagnostic) => offset >= diagnostic.location.pos && offset <= diagnostic.location.end);
39
+ if (hits.length === 0) {
40
+ return undefined;
41
+ }
42
+ let lines = [];
43
+ for (let i = 0, n = hits.length; i < n; i++) {
44
+ let diagnostic = hits[i];
45
+ lines.push(`**${diagnostic.channel}** — ${diagnostic.message}`);
46
+ for (let related of diagnostic.related) {
47
+ lines.push(`- ${related.message}`);
48
+ }
49
+ }
50
+ return { contents: { kind: MarkupKind.Markdown, value: lines.join('\n\n') }, range: rangeOf(hits[0].location, document) };
51
+ }
52
+ function codeActionsAt(diagnostics, startOffset, endOffset, document, uriOf) {
53
+ let actions = [];
54
+ for (let diagnostic of diagnostics) {
55
+ if (diagnostic.location.end < startOffset || diagnostic.location.pos > endOffset) {
56
+ continue;
57
+ }
58
+ for (let fix of diagnostic.fixes ?? []) {
59
+ let changes = {};
60
+ for (let edit of fix.edits) {
61
+ let uri = uriOf(edit.fileName), range = { start: document.positionAt(edit.pos), end: document.positionAt(edit.end) };
62
+ (changes[uri] ??= []).push({ newText: edit.newText, range });
63
+ }
64
+ actions.push({ edit: { changes }, kind: CodeActionKind.QuickFix, title: fix.title });
65
+ }
66
+ }
67
+ return actions;
68
+ }
69
+ export { codeActionsAt, DiagnosticSeverity, groupByFile, hoverAt, toLspDiagnostic };
@@ -0,0 +1,2 @@
1
+ export { createServer, startServer } from './server.js';
2
+ export { AnalyzeWorkspace } from './workspace.js';
@@ -0,0 +1,2 @@
1
+ export { createServer, startServer } from './server.js';
2
+ export { AnalyzeWorkspace } from './workspace.js';
@@ -0,0 +1,4 @@
1
+ import type { Connection } from 'vscode-languageserver/node';
2
+ declare function createServer(connection: Connection): void;
3
+ declare function startServer(): void;
4
+ export { createServer, startServer };
@@ -0,0 +1,130 @@
1
+ import * as NodeFS from 'node:fs';
2
+ import * as NodePath from 'node:path';
3
+ import { CodeActionKind, createConnection, ProposedFeatures, StreamMessageReader, StreamMessageWriter, TextDocuments, TextDocumentSyncKind } from 'vscode-languageserver/node';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { TextDocument } from 'vscode-languageserver-textdocument';
6
+ import { codeActionsAt, DiagnosticSeverity, groupByFile, hoverAt } from './diagnostics.js';
7
+ import { AnalyzeWorkspace } from './workspace.js';
8
+ function pathKey(fileName) {
9
+ let resolved = NodePath.resolve(fileName);
10
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
11
+ }
12
+ function findTsconfig(rootDir) {
13
+ let candidate = NodePath.join(rootDir, 'tsconfig.json');
14
+ return NodeFS.existsSync(candidate) ? candidate : undefined;
15
+ }
16
+ function rootFromInitialize(params) {
17
+ let folder = params.workspaceFolders?.[0]?.uri ?? params.rootUri ?? undefined;
18
+ return folder ? fileURLToPath(folder) : undefined;
19
+ }
20
+ function createServer(connection) {
21
+ let analyzed = new Map(), documents = new TextDocuments(TextDocument), pending = new Set(), published = new Set(), timer, workspace;
22
+ function runAnalysis() {
23
+ timer = undefined;
24
+ if (!workspace || !workspace.config) {
25
+ return;
26
+ }
27
+ let changed = [...pending];
28
+ pending.clear();
29
+ try {
30
+ workspace.refresh(changed);
31
+ }
32
+ catch (error) {
33
+ connection.console.error(`analyze: snapshot refresh failed — ${error.message}`);
34
+ return;
35
+ }
36
+ let result = workspace.analyze();
37
+ if (!result) {
38
+ return;
39
+ }
40
+ analyzed = new Map();
41
+ for (let diagnostic of result.diagnostics) {
42
+ let key = pathKey(diagnostic.location.fileName), list = analyzed.get(key);
43
+ if (!list) {
44
+ list = [];
45
+ analyzed.set(key, list);
46
+ }
47
+ list.push(diagnostic);
48
+ }
49
+ let openByPath = new Map(documents.all().map((document) => [pathKey(fileURLToPath(document.uri)), document])), resolve = (fileName) => openByPath.get(pathKey(fileName)), severity = workspace.config.severity === 'warning' ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error, uriOf = (fileName) => openByPath.get(pathKey(fileName))?.uri ?? pathToFileURL(fileName).toString(), grouped = groupByFile(result.diagnostics, severity, resolve, uriOf), next = new Set();
50
+ for (let [fileName, diagnostics] of grouped) {
51
+ let uri = uriOf(fileName);
52
+ next.add(uri);
53
+ connection.sendDiagnostics({ diagnostics, uri });
54
+ }
55
+ for (let uri of published) {
56
+ if (!next.has(uri)) {
57
+ connection.sendDiagnostics({ diagnostics: [], uri });
58
+ }
59
+ }
60
+ published = next;
61
+ }
62
+ function schedule(changed) {
63
+ for (let i = 0, n = changed.length; i < n; i++) {
64
+ pending.add(changed[i]);
65
+ }
66
+ if (timer !== undefined) {
67
+ clearTimeout(timer);
68
+ }
69
+ timer = setTimeout(runAnalysis, 300);
70
+ }
71
+ connection.onInitialize((params) => {
72
+ let root = rootFromInitialize(params);
73
+ if (root) {
74
+ let tsconfig = findTsconfig(root);
75
+ if (tsconfig) {
76
+ workspace = new AnalyzeWorkspace(tsconfig);
77
+ }
78
+ }
79
+ return {
80
+ capabilities: {
81
+ codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix] },
82
+ hoverProvider: true,
83
+ textDocumentSync: TextDocumentSyncKind.Incremental,
84
+ },
85
+ };
86
+ });
87
+ connection.onInitialized(() => {
88
+ if (workspace) {
89
+ schedule([]);
90
+ }
91
+ });
92
+ connection.onShutdown(() => {
93
+ workspace?.dispose();
94
+ workspace = undefined;
95
+ });
96
+ connection.onHover((params) => {
97
+ let document = documents.get(params.textDocument.uri);
98
+ if (!document) {
99
+ return null;
100
+ }
101
+ let diagnostics = analyzed.get(pathKey(fileURLToPath(params.textDocument.uri)));
102
+ return diagnostics ? hoverAt(diagnostics, document.offsetAt(params.position), document) ?? null : null;
103
+ });
104
+ connection.onCodeAction((params) => {
105
+ let document = documents.get(params.textDocument.uri);
106
+ if (!document) {
107
+ return [];
108
+ }
109
+ let target = pathKey(fileURLToPath(params.textDocument.uri)), diagnostics = analyzed.get(target);
110
+ if (!diagnostics) {
111
+ return [];
112
+ }
113
+ let uriOf = (fileName) => (pathKey(fileName) === target ? params.textDocument.uri : pathToFileURL(fileName).toString());
114
+ return codeActionsAt(diagnostics, document.offsetAt(params.range.start), document.offsetAt(params.range.end), document, uriOf);
115
+ });
116
+ documents.onDidOpen((event) => schedule([fileURLToPath(event.document.uri)]));
117
+ documents.onDidSave((event) => {
118
+ let fileName = fileURLToPath(event.document.uri);
119
+ if (workspace && NodePath.basename(fileName) === 'tsconfig.json') {
120
+ workspace.reloadConfig();
121
+ }
122
+ schedule([fileName]);
123
+ });
124
+ documents.listen(connection);
125
+ connection.listen();
126
+ }
127
+ function startServer() {
128
+ createServer(createConnection(ProposedFeatures.all, new StreamMessageReader(process.stdin), new StreamMessageWriter(process.stdout)));
129
+ }
130
+ export { createServer, startServer };
@@ -0,0 +1,14 @@
1
+ import type { AnalyzeResult } from '../probe/kernel/analyze.js';
2
+ import type { AnalyzeConfig } from '../probe/kernel/types.js';
3
+ declare class AnalyzeWorkspace {
4
+ private api;
5
+ private configPath;
6
+ private snapshot;
7
+ config: AnalyzeConfig | undefined;
8
+ constructor(tsconfigPath: string);
9
+ reloadConfig(): void;
10
+ refresh(changed: ReadonlyArray<string>): void;
11
+ analyze(): AnalyzeResult | undefined;
12
+ dispose(): void;
13
+ }
14
+ export { AnalyzeWorkspace };