From bc0316d7e1e978a97b3105bfe38b9a921a19f782 Mon Sep 17 00:00:00 2001 From: Jackson Dean Date: Mon, 4 Nov 2019 17:12:39 -0800 Subject: [PATCH] feat: Implement autocomplete for import paths. --- .../@css-blocks/language-server/package.json | 1 + .../emberCompletionProvider.ts | 17 ++-- .../blockLinkProvider.ts | 2 +- .../language-server/src/serverCapabilities.ts | 2 +- .../language-server/src/util/blockUtils.ts | 82 +++++++++++++++++++ yarn.lock | 11 +++ 6 files changed, 108 insertions(+), 7 deletions(-) diff --git a/packages/@css-blocks/language-server/package.json b/packages/@css-blocks/language-server/package.json index 7c6e645eb..7bdf7a1f8 100644 --- a/packages/@css-blocks/language-server/package.json +++ b/packages/@css-blocks/language-server/package.json @@ -15,6 +15,7 @@ "dependencies": { "@css-blocks/core": "^0.24.0", "@glimmer/syntax": "^0.42.1", + "glob": "^7.1.5", "opticss": "^0.6.2", "vscode-languageserver": "^5.2.1", "vscode-uri": "^2.0.3" diff --git a/packages/@css-blocks/language-server/src/completionProviders/emberCompletionProvider.ts b/packages/@css-blocks/language-server/src/completionProviders/emberCompletionProvider.ts index 3af59d80c..532a4e3b3 100644 --- a/packages/@css-blocks/language-server/src/completionProviders/emberCompletionProvider.ts +++ b/packages/@css-blocks/language-server/src/completionProviders/emberCompletionProvider.ts @@ -2,15 +2,22 @@ import { BlockFactory } from "@css-blocks/core/dist/src"; import { CompletionItem, TextDocumentPositionParams, TextDocuments } from "vscode-languageserver"; import { PathTransformer } from "../pathTransformers/PathTransformer"; +import { getBlockCompletions, isBlockFile } from "../util/blockUtils"; import { getHbsCompletions } from "../util/hbsCompletionProvider"; import { isTemplateFile } from "../util/hbsUtils"; export async function emberCompletionProvider(documents: TextDocuments, factory: BlockFactory, params: TextDocumentPositionParams, pathTransformer: PathTransformer): Promise { const document = documents.get(params.textDocument.uri); - if (document) { - if (isTemplateFile(document.uri)) { - return await getHbsCompletions(document, params.position, factory, pathTransformer); - } + + if (!document) { + return []; + } + + if (isTemplateFile(document.uri)) { + return await getHbsCompletions(document, params.position, factory, pathTransformer); + } else if (isBlockFile(document.uri)) { + return await getBlockCompletions(document, params.position); + } else { + return []; } - return []; } diff --git a/packages/@css-blocks/language-server/src/documentLinksProviders/blockLinkProvider.ts b/packages/@css-blocks/language-server/src/documentLinksProviders/blockLinkProvider.ts index 752d7ed0c..501a8bc53 100644 --- a/packages/@css-blocks/language-server/src/documentLinksProviders/blockLinkProvider.ts +++ b/packages/@css-blocks/language-server/src/documentLinksProviders/blockLinkProvider.ts @@ -5,7 +5,7 @@ import { URI } from "vscode-uri"; import { isBlockFile } from "../util/blockUtils"; -const LINK_REGEX = /from\s+(['"])([^'"]+)\1;/; +export const LINK_REGEX = /from\s+(['"])([^'"]+)\1;?/; export async function blockLinksProvider(documents: TextDocuments, params: DocumentLinkParams): Promise { let { uri } = params.textDocument; diff --git a/packages/@css-blocks/language-server/src/serverCapabilities.ts b/packages/@css-blocks/language-server/src/serverCapabilities.ts index 56ea4fe0f..3a949b720 100644 --- a/packages/@css-blocks/language-server/src/serverCapabilities.ts +++ b/packages/@css-blocks/language-server/src/serverCapabilities.ts @@ -11,6 +11,6 @@ export const SERVER_CAPABILITIES: ServerCapabilities = { documentSymbolProvider: false, completionProvider: { resolveProvider: false, - "triggerCharacters": [ ":", '"', "=" ], + triggerCharacters: [ ":", '"', "=", ".", "/" ], }, }; diff --git a/packages/@css-blocks/language-server/src/util/blockUtils.ts b/packages/@css-blocks/language-server/src/util/blockUtils.ts index c6cfb52c2..064da4f3b 100644 --- a/packages/@css-blocks/language-server/src/util/blockUtils.ts +++ b/packages/@css-blocks/language-server/src/util/blockUtils.ts @@ -1,7 +1,13 @@ import { CssBlockError, Syntax } from "@css-blocks/core/dist/src"; import { BlockParser } from "@css-blocks/core/dist/src/BlockParser/BlockParser"; +import * as fs from "fs"; +import * as glob from "glob"; import { postcss } from "opticss"; import * as path from "path"; +import { CompletionItem, CompletionItemKind, Position, TextDocument } from "vscode-languageserver"; +import { URI } from "vscode-uri"; + +import { LINK_REGEX } from "../documentLinksProviders/blockLinkProvider"; // TODO: Currently we are only supporting css. This should eventually support all // of the file types supported by css blocks @@ -29,3 +35,79 @@ export async function parseBlockErrors(parser: BlockParser, blockFsPath: string, return errors; } + +/** + * If the cursor line has an import path, we check to see if the current position + * of the cursor in the line is within the bounds of the import path to decide + * whether to provide import path completions. + */ +function shouldCompleteImportPath(importPathMatches: RegExpMatchArray, position: Position, lineText: string): boolean { + let relativeImportPath = importPathMatches[2]; + let relativeImportPathStartLinePosition = lineText.indexOf(relativeImportPath); + let relativeImportPathEndLinePosition = relativeImportPathStartLinePosition + relativeImportPath.length; + return relativeImportPathStartLinePosition <= position.character && relativeImportPathEndLinePosition >= position.character; +} + +async function getImportPathCompletions(documentUri: string, relativeImportPath: string): Promise { + let completionItems: CompletionItem[] = []; + + // if the user has only typed leading dots, don't complete anything. + if (/^\.+$/.test(relativeImportPath)) { + return completionItems; + } + + let blockDirPath = path.dirname(URI.parse(documentUri).fsPath); + let absoluteImportPath = path.resolve(blockDirPath, relativeImportPath); + let globPatternSuffix = relativeImportPath.endsWith("/") ? "/*" : "*"; + let blockSyntax = path.extname(documentUri); + + return new Promise(outerResolve => { + glob(`${absoluteImportPath}${globPatternSuffix}`, async (_, pathNames) => { + let items: (CompletionItem | null)[] = await Promise.all(pathNames.map(pathName => { + return new Promise(innerResolve => { + fs.stat(pathName, (_, stats) => { + let completionKind: CompletionItemKind | undefined; + + if (stats.isDirectory()) { + completionKind = CompletionItemKind.Folder; + } else if (stats.isFile() && path.extname(pathName) === blockSyntax) { + completionKind = CompletionItemKind.File; + } + + if (!completionKind) { + innerResolve(null); + } + + innerResolve({ + label: path.basename(pathName), + kind: completionKind, + }); + }); + }); + })); + + // NOTE: it seems typescript is not happy with items.filter(Boolean) + items.forEach(item => { + if (item) { + completionItems.push(item); + } + }); + + outerResolve(completionItems); + }); + }); +} + +// TODO: handle other completion cases (extending imported block, etc); +export async function getBlockCompletions(document: TextDocument, position: Position): Promise { + let text = document.getText(); + let lineAtCursor = text.split(/\r?\n/)[position.line]; + let importPathMatches = lineAtCursor.match(LINK_REGEX); + + if (importPathMatches && shouldCompleteImportPath(importPathMatches, position, lineAtCursor)) { + let relativeImportPath = importPathMatches[2]; + return await getImportPathCompletions(document.uri, relativeImportPath); + } + + return []; +} diff --git a/yarn.lock b/yarn.lock index d12379180..60333139e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8532,6 +8532,17 @@ glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.5: + version "7.1.5" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^0.1.0, global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"