Skip to content

Commit

Permalink
feat(nxls): path completion for files and directories (#1326)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cammisuli authored Aug 23, 2022
1 parent 5769144 commit 816a4d8
Show file tree
Hide file tree
Showing 18 changed files with 339 additions and 18 deletions.
1 change: 1 addition & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"request": "attach",
"name": "Attach to LSP Server",
"port": 6009,
"timeout": 30000,
"restart": true,
"sourceMaps": true,
"skipFiles": [
Expand Down
71 changes: 71 additions & 0 deletions apps/nxls/src/completions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
ASTNode,
CompletionItem,
JSONSchema,
TextDocument,
} from 'vscode-json-languageservice';
import {
CompletionType,
hasCompletionGlob,
hasCompletionType,
X_COMPLETION_GLOB,
X_COMPLETION_TYPE,
} from '@nx-console/json-schema';
import { pathCompletion } from './path-completion';
import { targetCompletion } from './target-completion';

export async function getCompletionItems(
workingPath: string | undefined,
schema: JSONSchema,
node: ASTNode,
document: TextDocument
): Promise<CompletionItem[]> {
if (!workingPath) {
return [];
}

const items = completionItems(workingPath, node, document);

if (hasCompletionType(schema)) {
const completion = schema[X_COMPLETION_TYPE];
if (hasCompletionGlob(schema)) {
return items(completion, schema[X_COMPLETION_GLOB]);
}

return items(completion);
} else {
return [];
}
}

function completionItems(
workingPath: string,
node: ASTNode,
document: TextDocument
) {
return async (
completion: CompletionType,
glob?: string
): Promise<CompletionItem[]> => {
switch (completion) {
case 'file': {
return pathCompletion(workingPath, node, document, {
glob: glob ?? '**/*.*',
searchType: 'file',
});
}
case 'directory': {
return pathCompletion(workingPath, node, document, {
glob: glob ?? '*',
searchType: 'directory',
});
}
case 'target': {
return targetCompletion(workingPath, node, document);
}
default: {
return [];
}
}
};
}
125 changes: 125 additions & 0 deletions apps/nxls/src/completions/path-completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import fastGlob from 'fast-glob';
import {
ASTNode,
CompletionItem,
CompletionItemKind,
TextDocument,
} from 'vscode-json-languageservice';
import {
isObjectNode,
isPropertyNode,
isStringNode,
} from '../utils/node-types';

export async function pathCompletion(
workingPath: string | undefined,
node: ASTNode,
document: TextDocument,
options?: {
glob: string;
searchType: 'file' | 'directory';
supportsInterpolation?: boolean;
}
): Promise<CompletionItem[]> {
const items: CompletionItem[] = [];

if (!workingPath) {
return items;
}

const { supportsInterpolation, glob, searchType } = {
supportsInterpolation: false,
...options,
};

if (!isStringNode(node)) {
return items;
}

const projectRoot = findProjectRoot(node);

const files = await fastGlob([workingPath + '/**/' + glob], {
ignore: ['**/node_modules/**'],
dot: true,
onlyFiles: searchType === 'file',
onlyDirectories: searchType === 'directory',
objectMode: true,
});

for (const file of files) {
if (
supportsInterpolation &&
file.path.startsWith(workingPath + '/' + projectRoot)
) {
const label =
'{projectRoot}' +
file.path.replace(workingPath + '/' + projectRoot, '');

items.push(addCompletionPathItem(label, file.path, node, document));
}

if (file.path.startsWith(workingPath)) {
const label = file.path.replace(workingPath + '/', '');
items.push(addCompletionPathItem(label, file.path, node, document));

if (supportsInterpolation) {
const label = '{workspaceRoot}' + file.path.replace(workingPath, '');
items.push(addCompletionPathItem(label, file.path, node, document));
}
}
}

return items;
}

/**
* Get the first `root` property from the current node to determine `${projectRoot}`
* @param node
* @returns
*/
function findProjectRoot(node: ASTNode): string {
if (isObjectNode(node)) {
for (const child of node.children) {
if (isPropertyNode(child)) {
if (
(child.keyNode.value === 'root' ||
child.keyNode.value === 'sourceRoot') &&
isStringNode(child.valueNode)
) {
return child.valueNode?.value;
}
}
}
}

if (node.parent) {
return findProjectRoot(node.parent);
}

return '';
}

function addCompletionPathItem(
label: string,
path: string,
node: ASTNode,
document: TextDocument
): CompletionItem {
const startPosition = document.positionAt(node.offset);
const endPosition = document.positionAt(node.offset + node.length);
label = `"${label}"`;
return {
label,
kind: CompletionItemKind.File,
insertText: label,
insertTextFormat: 2,
textEdit: {
newText: label,
range: {
start: startPosition,
end: endPosition,
},
},
detail: path,
};
}
13 changes: 13 additions & 0 deletions apps/nxls/src/completions/target-completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {
ASTNode,
CompletionItem,
TextDocument,
} from 'vscode-json-languageservice';

export async function targetCompletion(
workingPath: string | undefined,
node: ASTNode,
document: TextDocument
): Promise<CompletionItem[]> {
return [];
}
53 changes: 43 additions & 10 deletions apps/nxls/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@nx-console/json-schema';
import {
ClientCapabilities,
CompletionList,
getLanguageService,
TextDocument,
} from 'vscode-json-languageservice';
Expand All @@ -18,8 +19,12 @@ import {
TextDocumentSyncKind,
} from 'vscode-languageserver/node';
import { URI, Utils } from 'vscode-uri';
import { getLanguageModelCache } from './languageModelCache';
import { getSchemaRequestService } from './runtime';
import { getCompletionItems } from './completions';
import { getLanguageModelCache } from './utils/language-model-cache';
import { getSchemaRequestService } from './utils/runtime';
import { mergeArrays } from './utils/merge-arrays';

let WORKING_PATH: string | undefined = undefined;

const workspaceContext = {
resolveRelativePath: (relativePath: string, resource: string) => {
Expand Down Expand Up @@ -54,12 +59,16 @@ connection.onInitialize(async (params) => {
});

try {
const workingPath =
WORKING_PATH =
workspacePath ??
params.rootPath ??
URI.parse(params.rootUri ?? '').fsPath;
// get schemas
const collections = await getExecutors(workingPath, projects, false);

if (!WORKING_PATH) {
throw 'Unable to determine workspace path';
}

const collections = await getExecutors(WORKING_PATH, projects, false);
const workspaceSchema = getWorkspaceJsonSchema(collections);
const projectSchema = getProjectJsonSchema(collections);
languageService.configure({
Expand Down Expand Up @@ -101,11 +110,35 @@ connection.onCompletion(async (completionParams) => {
}

const { jsonAst, document } = getJsonDocument(changedDocument);
return languageService.doComplete(
document,
completionParams.position,
jsonAst
);

const completionResults =
(await languageService.doComplete(
document,
completionParams.position,
jsonAst
)) ?? CompletionList.create([]);

const offset = document.offsetAt(completionParams.position);
const node = jsonAst.getNodeFromOffset(offset);
if (!node) {
return completionResults;
}

const schemas = await languageService.getMatchingSchemas(document, jsonAst);
for (const s of schemas) {
if (s.node === node) {
const pathItems = await getCompletionItems(
WORKING_PATH,
s.schema,
node,
document
);
mergeArrays(completionResults.items, pathItems);
break;
}
}

return completionResults;
});

connection.onHover(async (hoverParams) => {
Expand Down
File renamed without changes.
8 changes: 8 additions & 0 deletions apps/nxls/src/utils/merge-arrays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Combines the second array with the first array, without having to loop or change the reference of the first array.
* @param arr1
* @param arr2
*/
export function mergeArrays(arr1: Array<unknown>, arr2: Array<unknown>) {
Array.prototype.push.apply(arr1, arr2);
}
18 changes: 18 additions & 0 deletions apps/nxls/src/utils/node-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
ASTNode,
ObjectASTNode,
PropertyASTNode,
StringASTNode,
} from 'vscode-json-languageservice';

export function isPropertyNode(node?: ASTNode): node is PropertyASTNode {
return node?.type === 'property';
}

export function isObjectNode(node?: ASTNode): node is ObjectASTNode {
return node?.type === 'object';
}

export function isStringNode(node?: ASTNode): node is StringASTNode {
return node?.type === 'string';
}
File renamed without changes.
3 changes: 2 additions & 1 deletion apps/nxls/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "ESNext",
"esModuleInterop": true,
"types": ["node"]
},
"exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"],
"include": ["**/*.ts"]
"include": ["**/*.ts", "../../libs/json-schema/src/lib/completion-type.ts"]
}
1 change: 1 addition & 0 deletions libs/json-schema/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './lib/workspace-json-schema';
export * from './lib/project-json-schema';
export * from './lib/completion-type';
19 changes: 19 additions & 0 deletions libs/json-schema/src/lib/completion-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { JSONSchema } from 'vscode-json-languageservice';
import { hasKey } from '@nx-console/utils/shared';

export const X_COMPLETION_TYPE = 'x-completion-type' as const;
export const X_COMPLETION_GLOB = 'x-completion-glob' as const;

export type CompletionType = 'file' | 'directory' | 'target';

export function hasCompletionType(
schema: JSONSchema
): schema is JSONSchema & { [X_COMPLETION_TYPE]: CompletionType } {
return hasKey(schema, X_COMPLETION_TYPE);
}

export function hasCompletionGlob(
schema: JSONSchema
): schema is JSONSchema & { [X_COMPLETION_GLOB]: string } {
return hasKey(schema, X_COMPLETION_GLOB);
}
Loading

0 comments on commit 816a4d8

Please sign in to comment.