microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
064161d8277a88b3facccca58df87aa332bf9187

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/compiler/server/serverlib.ts

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