microsoft/typespec

Public

mirrored from https://github.com/microsoft/typespecAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
3e669c74c1e16a47afe2b98706bdcf8dad4434af

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/compiler/server/serverlib.ts

1371lines · modecode

1import { TextDocument } from "vscode-languageserver-textdocument";
2import {
3 CompletionList,
4 CompletionParams,
5 DefinitionParams,
6 Diagnostic as VSDiagnostic,
7 DiagnosticSeverity,
8 DiagnosticTag,
9 DidChangeWatchedFilesParams,
10 DocumentFormattingParams,
11 DocumentHighlight,
12 DocumentHighlightKind,
13 DocumentHighlightParams,
14 DocumentSymbol,
15 DocumentSymbolParams,
16 FileEvent,
17 FoldingRange,
18 FoldingRangeParams,
19 Hover,
20 HoverParams,
21 InitializedParams,
22 InitializeParams,
23 InitializeResult,
24 Location,
25 MarkupContent,
26 MarkupKind,
27 ParameterInformation,
28 PrepareRenameParams,
29 PublishDiagnosticsParams,
30 Range,
31 ReferenceParams,
32 RenameParams,
33 SemanticTokens,
34 SemanticTokensBuilder,
35 SemanticTokensLegend,
36 SemanticTokensParams,
37 ServerCapabilities,
38 SignatureHelp,
39 SignatureHelpParams,
40 TextDocumentChangeEvent,
41 TextDocumentIdentifier,
42 TextDocumentSyncKind,
43 TextEdit,
44 WorkspaceEdit,
45 WorkspaceFolder,
46 WorkspaceFoldersChangeEvent,
47} from "vscode-languageserver/node.js";
48import {
49 defaultConfig,
50 findTypeSpecConfigPath,
51 loadTypeSpecConfigFile,
52} from "../config/config-loader.js";
53import { TypeSpecConfig } from "../config/types.js";
54import { codePointBefore, isIdentifierContinue } from "../core/charcode.js";
55import {
56 compilerAssert,
57 createSourceFile,
58 formatDiagnostic,
59 getSourceLocation,
60} from "../core/diagnostics.js";
61import { formatTypeSpec } from "../core/formatter.js";
62import { getTypeName } from "../core/helpers/type-name-utils.js";
63import { CompilerOptions } from "../core/options.js";
64import { getNodeAtPosition, visitChildren } from "../core/parser.js";
65import {
66 ensureTrailingDirectorySeparator,
67 getDirectoryPath,
68 joinPaths,
69} from "../core/path-utils.js";
70import { compile as compileProgram, Program } from "../core/program.js";
71import {
72 createScanner,
73 isKeyword,
74 isPunctuation,
75 skipTrivia,
76 skipWhiteSpace,
77 Token,
78} from "../core/scanner.js";
79import {
80 AugmentDecoratorStatementNode,
81 CompilerHost,
82 DecoratorDeclarationStatementNode,
83 DecoratorExpressionNode,
84 Diagnostic as TypeSpecDiagnostic,
85 DiagnosticTarget,
86 IdentifierNode,
87 Node,
88 SourceFile,
89 StringLiteralNode,
90 SyntaxKind,
91 TextRange,
92 TypeSpecScriptNode,
93} from "../core/types.js";
94import {
95 doIO,
96 getNormalizedRealPath,
97 getSourceFileKindFromExt,
98 loadFile,
99 resolveTspMain,
100} from "../core/util.js";
101import { resolveCompletion } from "./completion.js";
102import { getSymbolStructure } from "./symbol-structure.js";
103import { getParameterDocumentation, getTypeDetails } from "./type-details.js";
104
105export interface ServerHost {
106 compilerHost: CompilerHost;
107 throwInternalErrors?: boolean;
108 getOpenDocumentByURL(url: string): TextDocument | undefined;
109 sendDiagnostics(params: PublishDiagnosticsParams): void;
110 log(message: string): void;
111}
112
113export interface Server {
114 readonly pendingMessages: readonly string[];
115 readonly workspaceFolders: readonly ServerWorkspaceFolder[];
116 compile(document: TextDocument | TextDocumentIdentifier): Promise<Program | undefined>;
117 initialize(params: InitializeParams): Promise<InitializeResult>;
118 initialized(params: InitializedParams): void;
119 workspaceFoldersChanged(e: WorkspaceFoldersChangeEvent): Promise<void>;
120 watchedFilesChanged(params: DidChangeWatchedFilesParams): void;
121 formatDocument(params: DocumentFormattingParams): Promise<TextEdit[]>;
122 gotoDefinition(params: DefinitionParams): Promise<Location[]>;
123 complete(params: CompletionParams): Promise<CompletionList>;
124 findReferences(params: ReferenceParams): Promise<Location[]>;
125 findDocumentHighlight(params: DocumentHighlightParams): Promise<DocumentHighlight[]>;
126 prepareRename(params: PrepareRenameParams): Promise<Range | undefined>;
127 rename(params: RenameParams): Promise<WorkspaceEdit>;
128 getSemanticTokens(params: SemanticTokensParams): Promise<SemanticToken[]>;
129 buildSemanticTokens(params: SemanticTokensParams): Promise<SemanticTokens>;
130 checkChange(change: TextDocumentChangeEvent<TextDocument>): Promise<void>;
131 getHover(params: HoverParams): Promise<Hover>;
132 getSignatureHelp(params: SignatureHelpParams): Promise<SignatureHelp | undefined>;
133 getFoldingRanges(getFoldingRanges: FoldingRangeParams): Promise<FoldingRange[]>;
134 getDocumentSymbols(params: DocumentSymbolParams): Promise<DocumentSymbol[]>;
135 documentClosed(change: TextDocumentChangeEvent<TextDocument>): void;
136 log(message: string, details?: any): void;
137}
138
139export interface ServerSourceFile extends SourceFile {
140 // Keep track of the open document (if any) associated with a source file.
141 readonly document?: TextDocument;
142}
143
144export interface ServerWorkspaceFolder extends WorkspaceFolder {
145 // Remember path to URL conversion for workspace folders. This path must
146 // be resolved and normalized as other paths and have a trailing separator
147 // character so that we can test if a path is within a workspace using
148 // startsWith.
149 path: string;
150}
151
152export enum SemanticTokenKind {
153 Namespace,
154 Type,
155 Class,
156 Enum,
157 Interface,
158 Struct,
159 TypeParameter,
160 Parameter,
161 Variable,
162 Property,
163 EnumMember,
164 Event,
165 Function,
166 Method,
167 Macro,
168 Keyword,
169 Comment,
170 String,
171 Number,
172 Regexp,
173 Operator,
174}
175
176export interface SemanticToken {
177 kind: SemanticTokenKind;
178 pos: number;
179 end: number;
180}
181
182interface CachedFile {
183 type: "file";
184 file: SourceFile;
185 version?: number;
186
187 // Cache additional data beyond the raw text of the source file. Currently
188 // used only for JSON.parse result of package.json.
189 data?: any;
190}
191
192interface CachedError {
193 type: "error";
194 error: unknown;
195 data?: any;
196 version?: undefined;
197}
198
199const serverOptions: CompilerOptions = {
200 noEmit: true,
201 designTimeBuild: true,
202 parseOptions: {
203 comments: true,
204 docs: true,
205 },
206};
207
208export function createServer(host: ServerHost): Server {
209 // Remember original URL when we convert it to a local path so that we can
210 // get it back. We can't convert it back because things like URL-encoding
211 // could give us back an equivalent but non-identical URL but the original
212 // URL is used as a key into the opened documents and so we must reproduce
213 // it exactly.
214 const pathToURLMap = new Map<string, string>();
215
216 // Cache all file I/O. Only open documents are sent over the LSP pipe. When
217 // the compiler reads a file that isn't open, we use this cache to avoid
218 // hitting the disk. Entries are invalidated when LSP client notifies us of
219 // a file change.
220 const fileSystemCache = createFileSystemCache();
221 const compilerHost = createCompilerHost();
222
223 const oldPrograms = new Map<string, Program>();
224
225 let workspaceFolders: ServerWorkspaceFolder[] = [];
226 let isInitialized = false;
227 let pendingMessages: string[] = [];
228
229 return {
230 get pendingMessages() {
231 return pendingMessages;
232 },
233 get workspaceFolders() {
234 return workspaceFolders;
235 },
236 compile,
237 initialize,
238 initialized,
239 workspaceFoldersChanged,
240 watchedFilesChanged,
241 formatDocument,
242 gotoDefinition,
243 documentClosed,
244 complete,
245 findReferences,
246 findDocumentHighlight,
247 prepareRename,
248 rename,
249 getSemanticTokens,
250 buildSemanticTokens,
251 checkChange,
252 getFoldingRanges,
253 getHover,
254 getSignatureHelp,
255 getDocumentSymbols,
256 log,
257 };
258
259 async function initialize(params: InitializeParams): Promise<InitializeResult> {
260 const tokenLegend: SemanticTokensLegend = {
261 tokenTypes: Object.keys(SemanticTokenKind)
262 .filter((x) => Number.isNaN(Number(x)))
263 .map((x) => x.slice(0, 1).toLocaleLowerCase() + x.slice(1)),
264 tokenModifiers: [],
265 };
266
267 const capabilities: ServerCapabilities = {
268 textDocumentSync: TextDocumentSyncKind.Incremental,
269 definitionProvider: true,
270 foldingRangeProvider: true,
271 hoverProvider: true,
272 documentSymbolProvider: true,
273 documentHighlightProvider: true,
274 completionProvider: {
275 resolveProvider: false,
276 triggerCharacters: [".", "@", "/"],
277 },
278 semanticTokensProvider: {
279 full: true,
280 legend: tokenLegend,
281 },
282 referencesProvider: true,
283 renameProvider: {
284 prepareProvider: true,
285 },
286 documentFormattingProvider: true,
287 signatureHelpProvider: {
288 triggerCharacters: ["(", ",", "<"],
289 retriggerCharacters: [")"],
290 },
291 };
292
293 if (params.capabilities.workspace?.workspaceFolders) {
294 for (const w of params.workspaceFolders ?? []) {
295 workspaceFolders.push({
296 ...w,
297 path: ensureTrailingDirectorySeparator(await fileURLToRealPath(w.uri)),
298 });
299 }
300 capabilities.workspace = {
301 workspaceFolders: {
302 supported: true,
303 changeNotifications: true,
304 },
305 };
306 } else if (params.rootUri) {
307 workspaceFolders = [
308 {
309 name: "<root>",
310 uri: params.rootUri,
311 path: ensureTrailingDirectorySeparator(await fileURLToRealPath(params.rootUri)),
312 },
313 ];
314 } else if (params.rootPath) {
315 workspaceFolders = [
316 {
317 name: "<root>",
318 uri: compilerHost.pathToFileURL(params.rootPath),
319 path: ensureTrailingDirectorySeparator(
320 await getNormalizedRealPath(compilerHost, params.rootPath)
321 ),
322 },
323 ];
324 }
325
326 log("Workspace Folders", workspaceFolders);
327 return { capabilities };
328 }
329
330 function initialized(params: InitializedParams): void {
331 isInitialized = true;
332 log("Initialization complete.");
333 }
334
335 async function workspaceFoldersChanged(e: WorkspaceFoldersChangeEvent) {
336 log("Workspace Folders Changed", e);
337 const map = new Map(workspaceFolders.map((f) => [f.uri, f]));
338 for (const folder of e.removed) {
339 map.delete(folder.uri);
340 }
341 for (const folder of e.added) {
342 map.set(folder.uri, {
343 ...folder,
344 path: ensureTrailingDirectorySeparator(await fileURLToRealPath(folder.uri)),
345 });
346 }
347 workspaceFolders = Array.from(map.values());
348 log("Workspace Folders", workspaceFolders);
349 }
350
351 function watchedFilesChanged(params: DidChangeWatchedFilesParams) {
352 fileSystemCache.notify(params.changes);
353 }
354
355 type CompileCallback<T> = (
356 program: Program,
357 document: TextDocument,
358 script: TypeSpecScriptNode
359 ) => (T | undefined) | Promise<T | undefined>;
360
361 async function compile(
362 document: TextDocument | TextDocumentIdentifier
363 ): Promise<Program | undefined>;
364
365 async function compile<T>(
366 document: TextDocument | TextDocumentIdentifier,
367 callback: CompileCallback<T>
368 ): Promise<T | undefined>;
369
370 async function compile<T>(
371 document: TextDocument | TextDocumentIdentifier,
372 callback?: CompileCallback<T>
373 ): Promise<T | Program | undefined> {
374 const path = await getPath(document);
375 const mainFile = await getMainFileForDocument(path);
376 const config = await getConfig(mainFile, path);
377
378 const options = {
379 ...serverOptions,
380 emit: config.emit,
381 options: config.options,
382 };
383
384 if (!upToDate(document)) {
385 return undefined;
386 }
387
388 let program: Program;
389 try {
390 program = await compileProgram(compilerHost, mainFile, options, oldPrograms.get(mainFile));
391 oldPrograms.set(mainFile, program);
392 if (!upToDate(document)) {
393 return undefined;
394 }
395
396 if (mainFile !== path && !program.sourceFiles.has(path)) {
397 // If the file that changed wasn't imported by anything from the main
398 // file, retry using the file itself as the main file.
399 program = await compileProgram(compilerHost, path, options, oldPrograms.get(path));
400 oldPrograms.set(path, program);
401 }
402
403 if (!upToDate(document)) {
404 return undefined;
405 }
406
407 if (callback) {
408 const doc = "version" in document ? document : host.getOpenDocumentByURL(document.uri);
409 compilerAssert(doc, "Failed to get document.");
410 const path = await getPath(doc);
411 const script = program.sourceFiles.get(path);
412 compilerAssert(script, "Failed to get script.");
413 return await callback(program, doc, script);
414 }
415
416 return program;
417 } catch (err: any) {
418 if (host.throwInternalErrors) {
419 throw err;
420 }
421 host.sendDiagnostics({
422 uri: document.uri,
423 diagnostics: [
424 {
425 severity: DiagnosticSeverity.Error,
426 range: Range.create(0, 0, 0, 0),
427 message:
428 `Internal compiler error!\nFile issue at https://github.com/microsoft/typespec\n\n` +
429 err.stack,
430 },
431 ],
432 });
433
434 return undefined;
435 }
436 }
437
438 async function getConfig(mainFile: string, path: string): Promise<TypeSpecConfig> {
439 const configPath = await findTypeSpecConfigPath(compilerHost, mainFile);
440 if (!configPath) {
441 return { ...defaultConfig, projectRoot: getDirectoryPath(mainFile) };
442 }
443
444 const cached = await fileSystemCache.get(configPath);
445 if (cached?.data) {
446 return cached.data;
447 }
448
449 const config = await loadTypeSpecConfigFile(compilerHost, configPath);
450 await fileSystemCache.setData(configPath, config);
451 return config;
452 }
453
454 async function getScript(document: TextDocument | TextDocumentIdentifier) {
455 const file = await compilerHost.readFile(await getPath(document));
456 const cached = compilerHost.parseCache?.get(file);
457 return cached ?? (await compile<TypeSpecScriptNode>(document, (_, __, script) => script));
458 }
459
460 async function getFoldingRanges(params: FoldingRangeParams): Promise<FoldingRange[]> {
461 const ast = await getScript(params.textDocument);
462 if (!ast) {
463 return [];
464 }
465 const file = ast.file;
466 const ranges: FoldingRange[] = [];
467 let rangeStartSingleLines = -1;
468 for (let i = 0; i < ast.comments.length; i++) {
469 const comment = ast.comments[i];
470 if (
471 comment.kind === SyntaxKind.LineComment &&
472 i + 1 < ast.comments.length &&
473 ast.comments[i + 1].kind === SyntaxKind.LineComment &&
474 ast.comments[i + 1].pos === skipWhiteSpace(file.text, comment.end)
475 ) {
476 if (rangeStartSingleLines === -1) {
477 rangeStartSingleLines = comment.pos;
478 }
479 } else if (rangeStartSingleLines !== -1) {
480 addRange(rangeStartSingleLines, comment.end);
481 rangeStartSingleLines = -1;
482 } else {
483 addRange(comment.pos, comment.end);
484 }
485 }
486 visitChildren(ast, addRangesForNode);
487 function addRangesForNode(node: Node) {
488 if (node.kind === SyntaxKind.Doc) {
489 return; // fold doc comments as regular comments
490 }
491 let nodeStart = node.pos;
492 if ("decorators" in node && node.decorators.length > 0) {
493 const decoratorEnd = node.decorators[node.decorators.length - 1].end;
494 addRange(nodeStart, decoratorEnd);
495 nodeStart = skipTrivia(file.text, decoratorEnd);
496 }
497
498 addRange(nodeStart, node.end);
499 visitChildren(node, addRangesForNode);
500 }
501 return ranges;
502 function addRange(startPos: number, endPos: number) {
503 const start = file.getLineAndCharacterOfPosition(startPos);
504 const end = file.getLineAndCharacterOfPosition(endPos);
505 if (start.line !== end.line) {
506 ranges.push({
507 startLine: start.line,
508 startCharacter: start.character,
509 endLine: end.line,
510 endCharacter: end.character,
511 });
512 }
513 }
514 }
515
516 async function getDocumentSymbols(params: DocumentSymbolParams): Promise<DocumentSymbol[]> {
517 const ast = await getScript(params.textDocument);
518 if (!ast) {
519 return [];
520 }
521
522 return getSymbolStructure(ast);
523 }
524
525 async function findDocumentHighlight(
526 params: DocumentHighlightParams
527 ): Promise<DocumentHighlight[]> {
528 let highlights: DocumentHighlight[] = [];
529 await compile(params.textDocument, (program, document, file) => {
530 const identifiers = findReferenceIdentifiers(
531 program,
532 file,
533 document.offsetAt(params.position),
534 [file]
535 );
536 highlights = identifiers.map((identifier) => ({
537 range: getRange(identifier, file.file),
538 kind: DocumentHighlightKind.Read,
539 }));
540 });
541 return highlights;
542 }
543
544 async function checkChange(change: TextDocumentChangeEvent<TextDocument>) {
545 const program = await compile(change.document);
546 if (!program) {
547 return;
548 }
549
550 // Group diagnostics by file.
551 //
552 // Initialize diagnostics for all source files in program to empty array
553 // as we must send an empty array when a file has no diagnostics or else
554 // stale diagnostics from a previous run will stick around in the IDE.
555 //
556 const diagnosticMap: Map<TextDocument, VSDiagnostic[]> = new Map();
557 diagnosticMap.set(change.document, []);
558 for (const each of program.sourceFiles.values()) {
559 const document = (each.file as ServerSourceFile)?.document;
560 if (document) {
561 diagnosticMap.set(document, []);
562 }
563 }
564
565 for (const each of program.diagnostics) {
566 let document: TextDocument | undefined;
567
568 const location = getSourceLocation(each.target);
569 if (location?.file) {
570 document = (location.file as ServerSourceFile).document;
571 } else {
572 // https://github.com/Microsoft/language-server-protocol/issues/256
573 //
574 // LSP does not currently allow sending a diagnostic with no location so
575 // we report diagnostics with no location on the document that changed to
576 // trigger.
577 document = change.document;
578 }
579
580 if (!document || !upToDate(document)) {
581 continue;
582 }
583
584 const start = document.positionAt(location?.pos ?? 0);
585 const end = document.positionAt(location?.end ?? 0);
586 const range = Range.create(start, end);
587 const severity = convertSeverity(each.severity);
588 const diagnostic = VSDiagnostic.create(range, each.message, severity, each.code, "TypeSpec");
589 if (each.code === "deprecated") {
590 diagnostic.tags = [DiagnosticTag.Deprecated];
591 }
592 const diagnostics = diagnosticMap.get(document);
593 compilerAssert(
594 diagnostics,
595 "Diagnostic reported against a source file that was not added to the program."
596 );
597 diagnostics.push(diagnostic);
598 }
599
600 for (const [document, diagnostics] of diagnosticMap) {
601 sendDiagnostics(document, diagnostics);
602 }
603 }
604
605 async function getHover(params: HoverParams): Promise<Hover> {
606 const docString = await compile(params.textDocument, (program, document, file) => {
607 const id = getNodeAtPosition(file, document.offsetAt(params.position));
608 const sym =
609 id?.kind === SyntaxKind.Identifier ? program.checker.resolveIdentifier(id) : undefined;
610 if (sym) {
611 const type = sym.type ?? program.checker.getTypeForNode(sym.declarations[0]);
612 return getTypeDetails(program, type);
613 }
614 return undefined;
615 });
616
617 const markdown: MarkupContent = {
618 kind: MarkupKind.Markdown,
619 value: docString ?? "",
620 };
621 return {
622 contents: markdown,
623 };
624 }
625
626 async function getSignatureHelp(params: SignatureHelpParams): Promise<SignatureHelp | undefined> {
627 return await compile(params.textDocument, (program, document, file) => {
628 const nodeAtPosition = getNodeAtPosition(file, document.offsetAt(params.position));
629 const data = nodeAtPosition && findDecoratorOrParameter(nodeAtPosition);
630 if (data === undefined) {
631 return undefined;
632 }
633 const { node, argumentIndex } = data;
634 const sym = program.checker.resolveIdentifier(
635 node.target.kind === SyntaxKind.MemberExpression ? node.target.id : node.target
636 );
637
638 const decoratorDeclNode: DecoratorDeclarationStatementNode | undefined =
639 sym?.declarations.find(
640 (x): x is DecoratorDeclarationStatementNode =>
641 x.kind === SyntaxKind.DecoratorDeclarationStatement
642 );
643
644 if (decoratorDeclNode === undefined) {
645 return undefined;
646 }
647 const type = program.checker.getTypeForNode(decoratorDeclNode);
648 compilerAssert(type.kind === "Decorator", "Expected type to be a decorator.");
649
650 const parameterDocs = getParameterDocumentation(program, type);
651 let labelPrefix = "";
652 const parameters: ParameterInformation[] = [];
653 if (node.kind === SyntaxKind.AugmentDecoratorStatement) {
654 const targetType = decoratorDeclNode.target.type
655 ? program.checker.getTypeForNode(decoratorDeclNode.target.type)
656 : undefined;
657
658 parameters.push({
659 label: `${decoratorDeclNode.target.id.sv}: ${
660 targetType ? getTypeName(targetType) : "unknown"
661 }`,
662 });
663
664 labelPrefix = "@";
665 }
666
667 parameters.push(
668 ...type.parameters.map((x) => {
669 const info: ParameterInformation = {
670 // prettier-ignore
671 label: `${x.rest ? "..." : ""}${x.name}${x.optional ? "?" : ""}: ${getTypeName(x.type)}`,
672 };
673 const doc = parameterDocs.get(x.name);
674 if (doc) {
675 info.documentation = { kind: MarkupKind.Markdown, value: doc };
676 }
677 return info;
678 })
679 );
680
681 const help: SignatureHelp = {
682 signatures: [
683 {
684 label: `${labelPrefix}${type.name}(${parameters.map((x) => x.label).join(", ")})`,
685 parameters,
686 activeParameter: Math.min(parameters.length - 1, argumentIndex),
687 },
688 ],
689 activeSignature: 0,
690 activeParameter: 0,
691 };
692
693 const doc = getTypeDetails(program, type, {
694 includeSignature: false,
695 includeParameterTags: false,
696 });
697 if (doc) {
698 help.signatures[0].documentation = { kind: MarkupKind.Markdown, value: doc };
699 }
700
701 return help;
702 });
703 }
704
705 async function formatDocument(params: DocumentFormattingParams): Promise<TextEdit[]> {
706 const document = host.getOpenDocumentByURL(params.textDocument.uri);
707 if (document === undefined) {
708 return [];
709 }
710 const formattedText = formatTypeSpec(document.getText(), {
711 tabWidth: params.options.tabSize,
712 useTabs: !params.options.insertSpaces,
713 });
714 return [minimalEdit(document, formattedText)];
715 }
716
717 function minimalEdit(document: TextDocument, string1: string): TextEdit {
718 const string0 = document.getText();
719 // length of common prefix
720 let i = 0;
721 while (i < string0.length && i < string1.length && string0[i] === string1[i]) {
722 ++i;
723 }
724 // length of common suffix
725 let j = 0;
726 while (
727 i + j < string0.length &&
728 i + j < string1.length &&
729 string0[string0.length - j - 1] === string1[string1.length - j - 1]
730 ) {
731 ++j;
732 }
733 const newText = string1.substring(i, string1.length - j);
734 const pos0 = document.positionAt(i);
735 const pos1 = document.positionAt(string0.length - j);
736
737 return TextEdit.replace(Range.create(pos0, pos1), newText);
738 }
739
740 async function gotoDefinition(params: DefinitionParams): Promise<Location[]> {
741 const sym = await compile(params.textDocument, (program, document, file) => {
742 const id = getNodeAtPosition(file, document.offsetAt(params.position));
743 return id?.kind === SyntaxKind.Identifier ? program.checker.resolveIdentifier(id) : undefined;
744 });
745
746 return getLocations(sym?.declarations);
747 }
748 async function complete(params: CompletionParams): Promise<CompletionList> {
749 const completions: CompletionList = {
750 isIncomplete: false,
751 items: [],
752 };
753 await compile(params.textDocument, async (program, document, file) => {
754 const node = getCompletionNodeAtPosition(file, document.offsetAt(params.position));
755
756 await resolveCompletion(
757 {
758 program,
759 file,
760 completions,
761 params,
762 },
763 node
764 );
765 });
766 return completions;
767 }
768
769 async function findReferences(params: ReferenceParams): Promise<Location[]> {
770 const identifiers = await compile(params.textDocument, (program, document, file) =>
771 findReferenceIdentifiers(program, file, document.offsetAt(params.position))
772 );
773 return getLocations(identifiers);
774 }
775
776 async function prepareRename(params: PrepareRenameParams): Promise<Range | undefined> {
777 return await compile(params.textDocument, (_, document, file) => {
778 const id = getNodeAtPosition(file, document.offsetAt(params.position));
779 return id?.kind === SyntaxKind.Identifier ? getLocation(id)?.range : undefined;
780 });
781 }
782
783 async function rename(params: RenameParams): Promise<WorkspaceEdit> {
784 const changes: Record<string, TextEdit[]> = {};
785 await compile(params.textDocument, (program, document, file) => {
786 const identifiers = findReferenceIdentifiers(
787 program,
788 file,
789 document.offsetAt(params.position)
790 );
791 for (const id of identifiers) {
792 const location = getLocation(id);
793 if (!location) {
794 continue;
795 }
796 const change = TextEdit.replace(location.range, params.newName);
797 if (location.uri in changes) {
798 changes[location.uri].push(change);
799 } else {
800 changes[location.uri] = [change];
801 }
802 }
803 });
804 return { changes };
805 }
806
807 function findReferenceIdentifiers(
808 program: Program,
809 file: TypeSpecScriptNode,
810 pos: number,
811 searchFiles: Iterable<TypeSpecScriptNode> = program.sourceFiles.values()
812 ): IdentifierNode[] {
813 const id = getNodeAtPosition(file, pos);
814 if (id?.kind !== SyntaxKind.Identifier) {
815 return [];
816 }
817
818 const sym = program.checker.resolveIdentifier(id);
819 if (!sym) {
820 return [id];
821 }
822
823 const references: IdentifierNode[] = [];
824 for (const searchFile of searchFiles) {
825 visitChildren(searchFile, function visit(node) {
826 if (node.kind === SyntaxKind.Identifier) {
827 const s = program.checker.resolveIdentifier(node);
828 if (s === sym || (sym.type && s?.type === sym.type)) {
829 references.push(node);
830 }
831 }
832 visitChildren(node, visit);
833 });
834 }
835 return references;
836 }
837
838 async function getSemanticTokens(params: SemanticTokensParams): Promise<SemanticToken[]> {
839 const ignore = -1;
840 const defer = -2;
841
842 const ast = await getScript(params.textDocument);
843 if (!ast) {
844 return [];
845 }
846 const file = ast.file;
847 const tokens = mapTokens();
848 classifyNode(ast);
849 return Array.from(tokens.values()).filter((t) => t.kind !== undefined);
850
851 function mapTokens() {
852 const tokens = new Map<number, SemanticToken>();
853 const scanner = createScanner(file, () => {});
854
855 while (scanner.scan() !== Token.EndOfFile) {
856 const kind = classifyToken(scanner.token);
857 if (kind === ignore) {
858 continue;
859 }
860 tokens.set(scanner.tokenPosition, {
861 kind: kind === defer ? undefined! : kind,
862 pos: scanner.tokenPosition,
863 end: scanner.position,
864 });
865 }
866 return tokens;
867 }
868
869 function classifyToken(token: Token): SemanticTokenKind | typeof defer | typeof ignore {
870 switch (token) {
871 case Token.Identifier:
872 return defer;
873 case Token.StringLiteral:
874 return SemanticTokenKind.String;
875 case Token.NumericLiteral:
876 return SemanticTokenKind.Number;
877 case Token.MultiLineComment:
878 case Token.SingleLineComment:
879 return SemanticTokenKind.Comment;
880 default:
881 if (isKeyword(token)) {
882 return SemanticTokenKind.Keyword;
883 }
884 if (isPunctuation(token)) {
885 return SemanticTokenKind.Operator;
886 }
887 return ignore;
888 }
889 }
890
891 function classifyNode(node: Node) {
892 switch (node.kind) {
893 case SyntaxKind.DirectiveExpression:
894 classify(node.target, SemanticTokenKind.Keyword);
895 break;
896 case SyntaxKind.TemplateParameterDeclaration:
897 classify(node.id, SemanticTokenKind.TypeParameter);
898 break;
899 case SyntaxKind.ModelProperty:
900 case SyntaxKind.UnionVariant:
901 classify(node.id, SemanticTokenKind.Property);
902 break;
903 case SyntaxKind.AliasStatement:
904 classify(node.id, SemanticTokenKind.Struct);
905 break;
906 case SyntaxKind.ModelStatement:
907 classify(node.id, SemanticTokenKind.Struct);
908 break;
909 case SyntaxKind.ScalarStatement:
910 classify(node.id, SemanticTokenKind.Type);
911 break;
912 case SyntaxKind.EnumStatement:
913 classify(node.id, SemanticTokenKind.Enum);
914 break;
915 case SyntaxKind.EnumMember:
916 classify(node.id, SemanticTokenKind.EnumMember);
917 break;
918 case SyntaxKind.NamespaceStatement:
919 classify(node.id, SemanticTokenKind.Namespace);
920 break;
921 case SyntaxKind.InterfaceStatement:
922 classify(node.id, SemanticTokenKind.Interface);
923 break;
924 case SyntaxKind.OperationStatement:
925 classify(node.id, SemanticTokenKind.Function);
926 break;
927 case SyntaxKind.DecoratorDeclarationStatement:
928 classify(node.id, SemanticTokenKind.Function);
929 break;
930 case SyntaxKind.FunctionDeclarationStatement:
931 classify(node.id, SemanticTokenKind.Function);
932 break;
933 case SyntaxKind.FunctionParameter:
934 classify(node.id, SemanticTokenKind.Parameter);
935 break;
936 case SyntaxKind.AugmentDecoratorStatement:
937 classifyReference(node.targetType, SemanticTokenKind.Type);
938 classifyReference(node.target, SemanticTokenKind.Macro);
939 break;
940 case SyntaxKind.DecoratorExpression:
941 classifyReference(node.target, SemanticTokenKind.Macro);
942 break;
943
944 case SyntaxKind.TypeReference:
945 classifyReference(node.target);
946 break;
947 case SyntaxKind.MemberExpression:
948 classifyReference(node);
949 break;
950 case SyntaxKind.ProjectionStatement:
951 classifyReference(node.selector);
952 classify(node.id, SemanticTokenKind.Variable);
953 break;
954 case SyntaxKind.Projection:
955 classify(node.directionId, SemanticTokenKind.Keyword);
956 break;
957 case SyntaxKind.ProjectionParameterDeclaration:
958 classifyReference(node.id, SemanticTokenKind.Parameter);
959 break;
960 case SyntaxKind.ProjectionCallExpression:
961 classifyReference(node.target, SemanticTokenKind.Function);
962 for (const arg of node.arguments) {
963 classifyReference(arg);
964 }
965 break;
966 case SyntaxKind.ProjectionMemberExpression:
967 classifyReference(node.id);
968 break;
969 }
970 visitChildren(node, classifyNode);
971 }
972
973 function classify(node: IdentifierNode | StringLiteralNode, kind: SemanticTokenKind) {
974 const token = tokens.get(node.pos);
975 if (token && token.kind === undefined) {
976 token.kind = kind;
977 }
978 }
979
980 function classifyReference(node: Node, kind = SemanticTokenKind.Type) {
981 switch (node.kind) {
982 case SyntaxKind.MemberExpression:
983 classifyIdentifier(node.base, SemanticTokenKind.Namespace);
984 classifyIdentifier(node.id, kind);
985 break;
986 case SyntaxKind.ProjectionMemberExpression:
987 classifyReference(node.base, SemanticTokenKind.Namespace);
988 classifyIdentifier(node.id, kind);
989 break;
990 case SyntaxKind.TypeReference:
991 classifyIdentifier(node.target, kind);
992 break;
993 case SyntaxKind.Identifier:
994 classify(node, kind);
995 break;
996 }
997 }
998
999 function classifyIdentifier(node: Node, kind: SemanticTokenKind) {
1000 if (node.kind === SyntaxKind.Identifier) {
1001 classify(node, kind);
1002 }
1003 }
1004 }
1005
1006 async function buildSemanticTokens(params: SemanticTokensParams): Promise<SemanticTokens> {
1007 const builder = new SemanticTokensBuilder();
1008 const tokens = await getSemanticTokens(params);
1009 const file = await compilerHost.readFile(await getPath(params.textDocument));
1010 const starts = file.getLineStarts();
1011
1012 for (const token of tokens) {
1013 const start = file.getLineAndCharacterOfPosition(token.pos);
1014 const end = file.getLineAndCharacterOfPosition(token.end);
1015
1016 for (let pos = token.pos, line = start.line; line <= end.line; line++) {
1017 const endPos = line === end.line ? token.end : starts[line + 1];
1018 const character = line === start.line ? start.character : 0;
1019 builder.push(line, character, endPos - pos, token.kind, 0);
1020 pos = endPos;
1021 }
1022 }
1023
1024 return builder.build();
1025 }
1026
1027 function documentClosed(change: TextDocumentChangeEvent<TextDocument>) {
1028 // clear diagnostics on file close
1029 sendDiagnostics(change.document, []);
1030 }
1031
1032 function getLocations(targets: readonly DiagnosticTarget[] | undefined): Location[] {
1033 return targets?.map(getLocation).filter((x): x is Location => !!x) ?? [];
1034 }
1035
1036 function getLocation(target: DiagnosticTarget): Location | undefined {
1037 const location = getSourceLocation(target);
1038 if (location.isSynthetic) {
1039 return undefined;
1040 }
1041
1042 return {
1043 uri: getURL(location.file.path),
1044 range: getRange(location, location.file),
1045 };
1046 }
1047
1048 function getRange(location: TextRange, file: SourceFile): Range {
1049 const start = file.getLineAndCharacterOfPosition(location.pos);
1050 const end = file.getLineAndCharacterOfPosition(location.end);
1051 return Range.create(start, end);
1052 }
1053
1054 function convertSeverity(severity: "warning" | "error"): DiagnosticSeverity {
1055 switch (severity) {
1056 case "warning":
1057 return DiagnosticSeverity.Warning;
1058 case "error":
1059 return DiagnosticSeverity.Error;
1060 }
1061 }
1062
1063 function log(message: string, details: any = undefined) {
1064 message = `[${new Date().toLocaleTimeString()}] ${message}`;
1065 if (details) {
1066 message += ": " + JSON.stringify(details, undefined, 2);
1067 }
1068
1069 if (!isInitialized) {
1070 pendingMessages.push(message);
1071 return;
1072 }
1073
1074 for (const pending of pendingMessages) {
1075 host.log(pending);
1076 }
1077
1078 pendingMessages = [];
1079 host.log(message);
1080 }
1081
1082 function sendDiagnostics(document: TextDocument, diagnostics: VSDiagnostic[]) {
1083 host.sendDiagnostics({
1084 uri: document.uri,
1085 version: document.version,
1086 diagnostics,
1087 });
1088 }
1089
1090 /**
1091 * Determine if the given document is the latest version.
1092 *
1093 * A document can become out-of-date if a change comes in during an async
1094 * operation.
1095 */
1096 function upToDate(document: TextDocument | TextDocumentIdentifier) {
1097 if (!("version" in document)) {
1098 return true;
1099 }
1100 return document.version === host.getOpenDocumentByURL(document.uri)?.version;
1101 }
1102
1103 /**
1104 * Infer the appropriate entry point (a.k.a. "main file") for analyzing a
1105 * change to the file at the given path. This is necessary because different
1106 * results can be obtained from compiling the same file with different entry
1107 * points.
1108 *
1109 * Walk directory structure upwards looking for package.json with tspMain or
1110 * main.tsp file. Stop search when reaching a workspace root. If a root is
1111 * reached without finding an entry point, use the given path as its own
1112 * entry point.
1113 *
1114 * Untitled documents are always treated as their own entry points as they
1115 * do not exist in a directory that could pull them in via another entry
1116 * point.
1117 */
1118 async function getMainFileForDocument(path: string) {
1119 if (path.startsWith("untitled:")) {
1120 return path;
1121 }
1122
1123 let dir = getDirectoryPath(path);
1124 const options = { allowFileNotFound: true };
1125
1126 while (inWorkspace(dir)) {
1127 let mainFile = "main.tsp";
1128 let pkg: any;
1129 const pkgPath = joinPaths(dir, "package.json");
1130 const cached = await fileSystemCache.get(pkgPath);
1131
1132 if (cached) {
1133 pkg = cached.data;
1134 } else {
1135 [pkg] = await loadFile(
1136 compilerHost,
1137 pkgPath,
1138 JSON.parse,
1139 logMainFileSearchDiagnostic,
1140 options
1141 );
1142 await fileSystemCache.setData(pkgPath, pkg ?? {});
1143 }
1144
1145 const tspMain = resolveTspMain(pkg);
1146 if (typeof tspMain === "string") {
1147 mainFile = tspMain;
1148 }
1149
1150 const candidate = joinPaths(dir, mainFile);
1151 const stat = await doIO(
1152 () => compilerHost.stat(candidate),
1153 candidate,
1154 logMainFileSearchDiagnostic,
1155 options
1156 );
1157
1158 if (stat?.isFile()) {
1159 return candidate;
1160 }
1161
1162 const parentDir = getDirectoryPath(dir);
1163 if (parentDir === dir) {
1164 break;
1165 }
1166 dir = parentDir;
1167 }
1168
1169 return path;
1170
1171 function logMainFileSearchDiagnostic(diagnostic: TypeSpecDiagnostic) {
1172 log(
1173 `Unexpected diagnostic while looking for main file of ${path}`,
1174 formatDiagnostic(diagnostic)
1175 );
1176 }
1177 }
1178
1179 function inWorkspace(path: string) {
1180 path = ensureTrailingDirectorySeparator(path);
1181 return workspaceFolders.some((f) => path.startsWith(f.path));
1182 }
1183
1184 async function getPath(document: TextDocument | TextDocumentIdentifier) {
1185 if (isUntitled(document.uri)) {
1186 return document.uri;
1187 }
1188 const path = await fileURLToRealPath(document.uri);
1189 pathToURLMap.set(path, document.uri);
1190 return path;
1191 }
1192
1193 function getURL(path: string) {
1194 if (isUntitled(path)) {
1195 return path;
1196 }
1197 return pathToURLMap.get(path) ?? compilerHost.pathToFileURL(path);
1198 }
1199
1200 function isUntitled(pathOrUrl: string) {
1201 return pathOrUrl.startsWith("untitled:");
1202 }
1203
1204 function getOpenDocument(path: string) {
1205 const url = getURL(path);
1206 return url ? host.getOpenDocumentByURL(url) : undefined;
1207 }
1208
1209 async function fileURLToRealPath(url: string) {
1210 return getNormalizedRealPath(compilerHost, compilerHost.fileURLToPath(url));
1211 }
1212
1213 function createFileSystemCache() {
1214 const cache = new Map<string, CachedFile | CachedError>();
1215 let changes: FileEvent[] = [];
1216 return {
1217 async get(path: string) {
1218 for (const change of changes) {
1219 const path = await fileURLToRealPath(change.uri);
1220 cache.delete(path);
1221 }
1222 changes = [];
1223 return cache.get(path);
1224 },
1225 set(path: string, entry: CachedFile | CachedError) {
1226 cache.set(path, entry);
1227 },
1228 async setData(path: string, data: any) {
1229 const entry = await this.get(path);
1230 if (entry) {
1231 entry.data = data;
1232 }
1233 },
1234 notify(changes: FileEvent[]) {
1235 changes.push(...changes);
1236 },
1237 };
1238 }
1239
1240 function createCompilerHost(): CompilerHost {
1241 const base = host.compilerHost;
1242 return {
1243 ...base,
1244 parseCache: new WeakMap(),
1245 readFile,
1246 stat,
1247 getSourceFileKind,
1248 };
1249
1250 async function readFile(path: string): Promise<ServerSourceFile> {
1251 const document = getOpenDocument(path);
1252 const cached = await fileSystemCache.get(path);
1253
1254 // Try cache
1255 if (cached && (!document || document.version === cached.version)) {
1256 if (cached.type === "error") {
1257 throw cached.error;
1258 }
1259 return cached.file;
1260 }
1261
1262 // Try open document, although this is cheap, the instance still needs
1263 // to be cached so that the compiler can reuse parse and bind results.
1264 if (document) {
1265 const file = {
1266 document,
1267 ...createSourceFile(document.getText(), path),
1268 };
1269 fileSystemCache.set(path, { type: "file", file, version: document.version });
1270 return file;
1271 }
1272
1273 // Hit the disk and cache
1274 try {
1275 const file = await base.readFile(path);
1276 fileSystemCache.set(path, { type: "file", file });
1277 return file;
1278 } catch (error) {
1279 fileSystemCache.set(path, { type: "error", error });
1280 throw error;
1281 }
1282 }
1283
1284 async function stat(path: string): Promise<{ isDirectory(): boolean; isFile(): boolean }> {
1285 // if we have an open document for the path or a cache entry, then we know
1286 // it's a file and not a directory and needn't hit the disk.
1287 if (getOpenDocument(path) || (await fileSystemCache.get(path))?.type === "file") {
1288 return {
1289 isFile() {
1290 return true;
1291 },
1292 isDirectory() {
1293 return false;
1294 },
1295 };
1296 }
1297 return await base.stat(path);
1298 }
1299
1300 function getSourceFileKind(path: string) {
1301 const document = getOpenDocument(path);
1302 if (document?.languageId === "typespec") {
1303 return "typespec";
1304 }
1305 return getSourceFileKindFromExt(path);
1306 }
1307 }
1308}
1309
1310type DecoratorNode = DecoratorExpressionNode | AugmentDecoratorStatementNode;
1311
1312function findDecoratorOrParameter(
1313 node: Node
1314): { node: DecoratorNode; argumentIndex: number } | undefined {
1315 if (node.kind === SyntaxKind.DecoratorExpression) {
1316 return { node, argumentIndex: node.arguments.length };
1317 }
1318
1319 if (node.kind === SyntaxKind.AugmentDecoratorStatement) {
1320 return { node, argumentIndex: node.arguments.length + 1 };
1321 }
1322
1323 let current: Node | undefined = node;
1324 while (current) {
1325 if (current.parent?.kind === SyntaxKind.DecoratorExpression) {
1326 return {
1327 node: current.parent,
1328 argumentIndex: current.parent.arguments.indexOf(current as any),
1329 };
1330 }
1331 if (current.parent?.kind === SyntaxKind.AugmentDecoratorStatement) {
1332 return {
1333 node: current.parent,
1334 argumentIndex:
1335 current === current.parent?.targetType
1336 ? 0
1337 : current.parent.arguments.indexOf(current as any) + 1,
1338 };
1339 }
1340 current = current.parent;
1341 }
1342 return undefined;
1343}
1344
1345/**
1346 * Resolve the node that should be auto completed at the given position.
1347 * It will try to guess what node it could be as during auto complete the ast might not be complete.
1348 * @internal
1349 */
1350export function getCompletionNodeAtPosition(
1351 script: TypeSpecScriptNode,
1352 position: number,
1353 filter: (node: Node) => boolean = (node: Node) => true
1354): Node | undefined {
1355 const realNode = getNodeAtPosition(script, position, filter);
1356 if (realNode?.kind === SyntaxKind.StringLiteral) {
1357 return realNode;
1358 }
1359 // If we're not immediately after an identifier character, then advance
1360 // the position past any trivia. This is done because a zero-width
1361 // inserted missing identifier that the user is now trying to complete
1362 // starts after the trivia following the cursor.
1363 const cp = codePointBefore(script.file.text, position);
1364 if (!cp || !isIdentifierContinue(cp)) {
1365 const newPosition = skipTrivia(script.file.text, position);
1366 if (newPosition !== position) {
1367 return getNodeAtPosition(script, newPosition, filter);
1368 }
1369 }
1370 return realNode;
1371}