microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
bead6d153df3e7bd00100e72ae51a740bba955e6

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/compiler/server/serverlib.ts

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