Skip to content

Commit

Permalink
Improvement to the import auto complete (#2936)
Browse files Browse the repository at this point in the history
- fix #2481 Autocomplete directive names
- Stop crashing when completing invalid dir
- Autocompleting dir with non letter char (e.g. `-`) will autocomplete
correctly
- Playground autocomplete imports
<img width="579" alt="image"
src="https://github.com/microsoft/typespec/assets/1031227/08e9e516-6472-4ab2-b3a5-30b4a8ce722d">
  • Loading branch information
timotheeguerin authored Feb 20, 2024
1 parent b4b0bbb commit 753ca1a
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
73 changes: 55 additions & 18 deletions packages/compiler/src/server/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
} from "vscode-languageserver";
import { getDeprecationDetails } from "../core/deprecation.js";
import {
CompilerHost,
IdentifierNode,
Node,
NodePackage,
Program,
StringLiteralNode,
SymbolFlags,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -113,18 +116,27 @@ function addKeywordCompletion(area: keyof KeywordArea, completions: CompletionLi
}
}

async function loadPackageJson(host: CompilerHost, path: string): Promise<NodePackage> {
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
) {
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) {
Expand All @@ -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),
Expand All @@ -165,37 +170,56 @@ async function addImportCompletion(context: CompletionContext, node: StringLiter
}
}

async function tryListItemInDir(host: CompilerHost, path: string): Promise<string[]> {
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
) {
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;
}
Expand Down Expand Up @@ -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:
Expand Down
68 changes: 60 additions & 8 deletions packages/compiler/test/server/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
],
{
Expand All @@ -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 } },
},
},
],
{
Expand All @@ -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 } },
},
},
],
{
Expand All @@ -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 } },
},
},
],
{
Expand Down Expand Up @@ -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[],
Expand Down
9 changes: 9 additions & 0 deletions packages/playground/src/browser-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ export async function createBrowserHost(
for (const [key, value] of Object.entries<any>(_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) {
Expand Down
10 changes: 8 additions & 2 deletions packages/playground/src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 753ca1a

Please sign in to comment.