microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e07d86c87a444d4145b98d85fa416eca819e251c

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/compiler/server/serverlib.ts

1429lines · modecode

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