diff --git a/.chronus/changes/ide-completion-improvments-2024-1-17-3-52-31.md b/.chronus/changes/ide-completion-improvments-2024-1-17-3-52-31.md new file mode 100644 index 0000000000..4374c768b6 --- /dev/null +++ b/.chronus/changes/ide-completion-improvments-2024-1-17-3-52-31.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/compiler" +--- + +[IDE] Autocompleting file or folder with non alpha numeric charachter completes correctly diff --git a/.chronus/changes/ide-completion-improvments-2024-1-17-3-53-42.md b/.chronus/changes/ide-completion-improvments-2024-1-17-3-53-42.md new file mode 100644 index 0000000000..9240b7b5de --- /dev/null +++ b/.chronus/changes/ide-completion-improvments-2024-1-17-3-53-42.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/compiler" +--- + +[IDE] Fix crashing when trying to autocomplete an invalid folder diff --git a/.chronus/changes/ide-completion-improvments-2024-1-17-4-14-46.md b/.chronus/changes/ide-completion-improvments-2024-1-17-4-14-46.md new file mode 100644 index 0000000000..34238bb2fc --- /dev/null +++ b/.chronus/changes/ide-completion-improvments-2024-1-17-4-14-46.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/playground" +--- + +Autocomplete installed libraries in `import` statements diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index 8676d7929a..04fc633ad6 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -9,8 +9,10 @@ import { } from "vscode-languageserver"; import { getDeprecationDetails } from "../core/deprecation.js"; import { + CompilerHost, IdentifierNode, Node, + NodePackage, Program, StringLiteralNode, SymbolFlags, @@ -52,6 +54,7 @@ export async function resolveCompletion( addKeywordCompletion("namespace", completions); break; case SyntaxKind.Identifier: + addDirectiveCompletion(context, node); addIdentifierCompletion(context, node); break; case SyntaxKind.StringLiteral: @@ -113,6 +116,17 @@ function addKeywordCompletion(area: keyof KeywordArea, completions: CompletionLi } } +async function loadPackageJson(host: CompilerHost, path: string): Promise { + const [libPackageJson] = await loadFile(host, path, JSON.parse, () => {}); + return libPackageJson; +} +/** Check if the folder given has a package.json which has a tspMain. */ +async function isTspLibraryPackage(host: CompilerHost, dir: string) { + const libPackageJson = await loadPackageJson(host, resolvePath(dir, "package.json")); + + return resolveTspMain(libPackageJson) !== undefined; +} + async function addLibraryImportCompletion( { program, file, completions }: CompletionContext, node: StringLiteralNode @@ -120,11 +134,9 @@ async function addLibraryImportCompletion( const documentPath = file.file.path; const projectRoot = await findProjectRoot(program.host.stat, documentPath); if (projectRoot !== undefined) { - const [packagejson] = await loadFile( + const packagejson = await loadPackageJson( program.host, - resolvePath(projectRoot, "package.json"), - JSON.parse, - program.reportDiagnostic + resolvePath(projectRoot, "package.json") ); let dependencies: string[] = []; if (packagejson.dependencies !== undefined) { @@ -134,15 +146,8 @@ async function addLibraryImportCompletion( dependencies = dependencies.concat(Object.keys(packagejson.peerDependencies)); } for (const dependency of dependencies) { - const nodeProjectRoot = resolvePath(projectRoot, "node_modules", dependency); - const [libPackageJson] = await loadFile( - program.host, - resolvePath(nodeProjectRoot, "package.json"), - JSON.parse, - program.reportDiagnostic - ); - - if (resolveTspMain(libPackageJson) !== undefined) { + const dependencyDir = resolvePath(projectRoot, "node_modules", dependency); + if (await isTspLibraryPackage(program.host, dependencyDir)) { const range = { start: file.file.getLineAndCharacterOfPosition(node.pos + 1), end: file.file.getLineAndCharacterOfPosition(node.end - 1), @@ -165,6 +170,17 @@ async function addImportCompletion(context: CompletionContext, node: StringLiter } } +async function tryListItemInDir(host: CompilerHost, path: string): Promise { + try { + return await host.readDir(path); + } catch (e: any) { + if (e.code === "ENOENT") { + return []; + } + throw e; + } +} + async function addRelativePathCompletion( { program, completions, file }: CompletionContext, node: StringLiteralNode @@ -172,30 +188,38 @@ async function addRelativePathCompletion( const documentPath = file.file.path; const documentFile = getBaseFileName(documentPath); const documentDir = getDirectoryPath(documentPath); - const nodevalueDir = hasTrailingDirectorySeparator(node.value) + const currentRelativePath = hasTrailingDirectorySeparator(node.value) ? node.value : getDirectoryPath(node.value); - const mainTypeSpec = resolvePath(documentDir, nodevalueDir); - const files = (await program.host.readDir(mainTypeSpec)).filter( + const currentAbsolutePath = resolvePath(documentDir, currentRelativePath); + const files = (await tryListItemInDir(program.host, currentAbsolutePath)).filter( (x) => x !== documentFile && x !== "node_modules" ); + + const lastSlash = node.value.lastIndexOf("/"); + const offset = lastSlash === -1 ? 0 : lastSlash + 1; + const range = { + start: file.file.getLineAndCharacterOfPosition(node.pos + 1 + offset), + end: file.file.getLineAndCharacterOfPosition(node.end - 1), + }; for (const file of files) { const extension = getAnyExtensionFromPath(file); + switch (extension) { case ".tsp": case ".js": case ".mjs": completions.items.push({ label: file, - commitCharacters: [], kind: CompletionItemKind.File, + textEdit: TextEdit.replace(range, file), }); break; case "": completions.items.push({ label: file, - commitCharacters: [], kind: CompletionItemKind.Folder, + textEdit: TextEdit.replace(range, file), }); break; } @@ -254,6 +278,19 @@ function addIdentifierCompletion( } } +const directiveNames = ["suppress", "deprecated"]; +function addDirectiveCompletion({ completions }: CompletionContext, node: IdentifierNode) { + if (!(node.parent?.kind === SyntaxKind.DirectiveExpression && node.parent.target === node)) { + return; + } + for (const directive of directiveNames) { + completions.items.push({ + label: directive, + kind: CompletionItemKind.Keyword, + }); + } +} + function getCompletionItemKind(program: Program, target: Type): CompletionItemKind { switch (target.node?.kind) { case SyntaxKind.EnumStatement: diff --git a/packages/compiler/test/server/completion.test.ts b/packages/compiler/test/server/completion.test.ts index 6c02727b62..ab923d83ec 100644 --- a/packages/compiler/test/server/completion.test.ts +++ b/packages/compiler/test/server/completion.test.ts @@ -129,25 +129,35 @@ describe("compiler: server: completion", () => { const completions = await complete(` import "./ā”† `, undefined, { "test/bar.tsp": "", "test/foo.tsp": "", - "test/foo/test.tsp": "", + "test/dir/test.tsp": "", }); + const range = { start: { line: 0, character: 11 }, end: { line: 0, character: 11 } }; check( completions, [ { label: "bar.tsp", - commitCharacters: [], kind: CompletionItemKind.File, + textEdit: { + newText: "bar.tsp", + range, + }, }, { label: "foo.tsp", - commitCharacters: [], kind: CompletionItemKind.File, + textEdit: { + newText: "foo.tsp", + range, + }, }, { - label: "foo", - commitCharacters: [], + label: "dir", kind: CompletionItemKind.Folder, + textEdit: { + newText: "dir", + range, + }, }, ], { @@ -166,9 +176,12 @@ describe("compiler: server: completion", () => { completions, [ { - commitCharacters: [], kind: 19, label: "main", + textEdit: { + newText: "main", + range: { start: { line: 0, character: 11 }, end: { line: 0, character: 11 } }, + }, }, ], { @@ -185,9 +198,12 @@ describe("compiler: server: completion", () => { completions, [ { - commitCharacters: [], kind: 17, label: "foo.tsp", + textEdit: { + newText: "foo.tsp", + range: { start: { line: 0, character: 24 }, end: { line: 0, character: 24 } }, + }, }, ], { @@ -204,9 +220,12 @@ describe("compiler: server: completion", () => { completions, [ { - commitCharacters: [], kind: 17, label: "foo.tsp", + textEdit: { + newText: "foo.tsp", + range: { start: { line: 0, character: 15 }, end: { line: 0, character: 15 } }, + }, }, ], { @@ -962,6 +981,39 @@ describe("compiler: server: completion", () => { ]); }); + describe("directives", () => { + it("complete directives when starting with `#`", async () => { + const completions = await complete( + ` + #ā”† + model Bar {} + ` + ); + + check(completions, [ + { + label: "suppress", + kind: CompletionItemKind.Keyword, + }, + { + label: "deprecated", + kind: CompletionItemKind.Keyword, + }, + ]); + }); + + it("doesn't complete when in the argument section", async () => { + const completions = await complete( + ` + #suppress sā”† + model Bar {} + ` + ); + + check(completions, []); + }); + }); + function check( list: CompletionList, expectedItems: CompletionItem[], diff --git a/packages/playground/src/browser-host.ts b/packages/playground/src/browser-host.ts index c00378dbf1..8915f04670 100644 --- a/packages/playground/src/browser-host.ts +++ b/packages/playground/src/browser-host.ts @@ -38,6 +38,15 @@ export async function createBrowserHost( for (const [key, value] of Object.entries(_TypeSpecLibrary_.jsSourceFiles)) { addJsImport(`/test/node_modules/${libName}/${key}`, value); } + virtualFs.set( + `/test/package.json`, + JSON.stringify({ + name: "playground-pkg", + dependencies: Object.fromEntries( + Object.values(libraries).map((x) => [x.name, x.packageJson.version]) + ), + }) + ); } function addJsImport(path: string, value: any) { diff --git a/packages/playground/src/services.ts b/packages/playground/src/services.ts index ddb4a8ad36..c7787bb322 100644 --- a/packages/playground/src/services.ts +++ b/packages/playground/src/services.ts @@ -287,12 +287,18 @@ export async function registerMonacoLanguage(host: BrowserHost) { const suggestions: monaco.languages.CompletionItem[] = []; for (const item of result.items) { + let itemRange = range; + let insertText = item.insertText!; + if (item.textEdit && "range" in item.textEdit) { + itemRange = monacoRange(item.textEdit.range); + insertText = item.textEdit.newText; + } suggestions.push({ label: item.label, kind: item.kind as any, documentation: item.documentation, - insertText: item.insertText!, - range, + insertText, + range: itemRange, commitCharacters: item.commitCharacters ?? lsConfig.capabilities.completionProvider!.allCommitCharacters, tags: item.tags,