Skip to content

Commit bc0316d

Browse files
author
Jackson Dean
committed
feat: Implement autocomplete for import paths.
1 parent 3912c56 commit bc0316d

File tree

6 files changed

+108
-7
lines changed

6 files changed

+108
-7
lines changed

packages/@css-blocks/language-server/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"dependencies": {
1616
"@css-blocks/core": "^0.24.0",
1717
"@glimmer/syntax": "^0.42.1",
18+
"glob": "^7.1.5",
1819
"opticss": "^0.6.2",
1920
"vscode-languageserver": "^5.2.1",
2021
"vscode-uri": "^2.0.3"

packages/@css-blocks/language-server/src/completionProviders/emberCompletionProvider.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ import { BlockFactory } from "@css-blocks/core/dist/src";
22
import { CompletionItem, TextDocumentPositionParams, TextDocuments } from "vscode-languageserver";
33

44
import { PathTransformer } from "../pathTransformers/PathTransformer";
5+
import { getBlockCompletions, isBlockFile } from "../util/blockUtils";
56
import { getHbsCompletions } from "../util/hbsCompletionProvider";
67
import { isTemplateFile } from "../util/hbsUtils";
78

89
export async function emberCompletionProvider(documents: TextDocuments, factory: BlockFactory, params: TextDocumentPositionParams, pathTransformer: PathTransformer): Promise<CompletionItem[]> {
910
const document = documents.get(params.textDocument.uri);
10-
if (document) {
11-
if (isTemplateFile(document.uri)) {
12-
return await getHbsCompletions(document, params.position, factory, pathTransformer);
13-
}
11+
12+
if (!document) {
13+
return [];
14+
}
15+
16+
if (isTemplateFile(document.uri)) {
17+
return await getHbsCompletions(document, params.position, factory, pathTransformer);
18+
} else if (isBlockFile(document.uri)) {
19+
return await getBlockCompletions(document, params.position);
20+
} else {
21+
return [];
1422
}
15-
return [];
1623
}

packages/@css-blocks/language-server/src/documentLinksProviders/blockLinkProvider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { URI } from "vscode-uri";
55

66
import { isBlockFile } from "../util/blockUtils";
77

8-
const LINK_REGEX = /from\s+(['"])([^'"]+)\1;/;
8+
export const LINK_REGEX = /from\s+(['"])([^'"]+)\1;?/;
99

1010
export async function blockLinksProvider(documents: TextDocuments, params: DocumentLinkParams): Promise<DocumentLink[]> {
1111
let { uri } = params.textDocument;

packages/@css-blocks/language-server/src/serverCapabilities.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ export const SERVER_CAPABILITIES: ServerCapabilities = {
1111
documentSymbolProvider: false,
1212
completionProvider: {
1313
resolveProvider: false,
14-
"triggerCharacters": [ ":", '"', "=" ],
14+
triggerCharacters: [ ":", '"', "=", ".", "/" ],
1515
},
1616
};

packages/@css-blocks/language-server/src/util/blockUtils.ts

+82
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { CssBlockError, Syntax } from "@css-blocks/core/dist/src";
22
import { BlockParser } from "@css-blocks/core/dist/src/BlockParser/BlockParser";
3+
import * as fs from "fs";
4+
import * as glob from "glob";
35
import { postcss } from "opticss";
46
import * as path from "path";
7+
import { CompletionItem, CompletionItemKind, Position, TextDocument } from "vscode-languageserver";
8+
import { URI } from "vscode-uri";
9+
10+
import { LINK_REGEX } from "../documentLinksProviders/blockLinkProvider";
511

612
// TODO: Currently we are only supporting css. This should eventually support all
713
// of the file types supported by css blocks
@@ -29,3 +35,79 @@ export async function parseBlockErrors(parser: BlockParser, blockFsPath: string,
2935

3036
return errors;
3137
}
38+
39+
/**
40+
* If the cursor line has an import path, we check to see if the current position
41+
* of the cursor in the line is within the bounds of the import path to decide
42+
* whether to provide import path completions.
43+
*/
44+
function shouldCompleteImportPath(importPathMatches: RegExpMatchArray, position: Position, lineText: string): boolean {
45+
let relativeImportPath = importPathMatches[2];
46+
let relativeImportPathStartLinePosition = lineText.indexOf(relativeImportPath);
47+
let relativeImportPathEndLinePosition = relativeImportPathStartLinePosition + relativeImportPath.length;
48+
return relativeImportPathStartLinePosition <= position.character && relativeImportPathEndLinePosition >= position.character;
49+
}
50+
51+
async function getImportPathCompletions(documentUri: string, relativeImportPath: string): Promise<CompletionItem[]> {
52+
let completionItems: CompletionItem[] = [];
53+
54+
// if the user has only typed leading dots, don't complete anything.
55+
if (/^\.+$/.test(relativeImportPath)) {
56+
return completionItems;
57+
}
58+
59+
let blockDirPath = path.dirname(URI.parse(documentUri).fsPath);
60+
let absoluteImportPath = path.resolve(blockDirPath, relativeImportPath);
61+
let globPatternSuffix = relativeImportPath.endsWith("/") ? "/*" : "*";
62+
let blockSyntax = path.extname(documentUri);
63+
64+
return new Promise(outerResolve => {
65+
glob(`${absoluteImportPath}${globPatternSuffix}`, async (_, pathNames) => {
66+
let items: (CompletionItem | null)[] = await Promise.all(pathNames.map(pathName => {
67+
return new Promise(innerResolve => {
68+
fs.stat(pathName, (_, stats) => {
69+
let completionKind: CompletionItemKind | undefined;
70+
71+
if (stats.isDirectory()) {
72+
completionKind = CompletionItemKind.Folder;
73+
} else if (stats.isFile() && path.extname(pathName) === blockSyntax) {
74+
completionKind = CompletionItemKind.File;
75+
}
76+
77+
if (!completionKind) {
78+
innerResolve(null);
79+
}
80+
81+
innerResolve({
82+
label: path.basename(pathName),
83+
kind: completionKind,
84+
});
85+
});
86+
});
87+
}));
88+
89+
// NOTE: it seems typescript is not happy with items.filter(Boolean)
90+
items.forEach(item => {
91+
if (item) {
92+
completionItems.push(item);
93+
}
94+
});
95+
96+
outerResolve(completionItems);
97+
});
98+
});
99+
}
100+
101+
// TODO: handle other completion cases (extending imported block, etc);
102+
export async function getBlockCompletions(document: TextDocument, position: Position): Promise<CompletionItem[]> {
103+
let text = document.getText();
104+
let lineAtCursor = text.split(/\r?\n/)[position.line];
105+
let importPathMatches = lineAtCursor.match(LINK_REGEX);
106+
107+
if (importPathMatches && shouldCompleteImportPath(importPathMatches, position, lineAtCursor)) {
108+
let relativeImportPath = importPathMatches[2];
109+
return await getImportPathCompletions(document.uri, relativeImportPath);
110+
}
111+
112+
return [];
113+
}

yarn.lock

+11
Original file line numberDiff line numberDiff line change
@@ -8532,6 +8532,17 @@ glob@^7.1.4:
85328532
once "^1.3.0"
85338533
path-is-absolute "^1.0.0"
85348534

8535+
glob@^7.1.5:
8536+
version "7.1.5"
8537+
resolved "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0"
8538+
dependencies:
8539+
fs.realpath "^1.0.0"
8540+
inflight "^1.0.4"
8541+
inherits "2"
8542+
minimatch "^3.0.4"
8543+
once "^1.3.0"
8544+
path-is-absolute "^1.0.0"
8545+
85358546
global-dirs@^0.1.0, global-dirs@^0.1.1:
85368547
version "0.1.1"
85378548
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"

0 commit comments

Comments
 (0)