Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import autocomplete #347

Merged
merged 7 commits into from Nov 7, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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<CompletionItem[]> {
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 [];
}
Expand Up @@ -11,6 +11,6 @@ export const SERVER_CAPABILITIES: ServerCapabilities = {
documentSymbolProvider: false,
completionProvider: {
resolveProvider: false,
"triggerCharacters": [ ":", '"', "=" ],
triggerCharacters: [ ":", '"', "=", "/" ],
},
};
@@ -0,0 +1,4 @@
@block randoBlock from "../";
@export utils from "../blocks/";
chriseppstein marked this conversation as resolved.
Show resolved Hide resolved
@block aBlock from "../blocks/ut";
@export bBlock from "./";
109 changes: 109 additions & 0 deletions packages/@css-blocks/language-server/src/test/server-test.ts
Expand Up @@ -351,4 +351,113 @@ export class LanguageServerServerTest {
target: pathToUri("fixtures/ember-classic/styles/blocks/utils.block.css"),
}]);
}

@test async "it returns the expected completions for a block/export path in a block file"() {
const textDocumentsByUri = new Map<string, TextDocument>();
const importCompletionsFixtureUri = pathToUri("fixtures/ember-classic/styles/components/import-completions.block.css");
textDocumentsByUri.set(importCompletionsFixtureUri, createTextDocumentMock(importCompletionsFixtureUri));
this.documents = createTextDocumentsMock(textDocumentsByUri);

this.startServer();

// directory completions
const params1: TextDocumentPositionParams = {
textDocument: {
uri: importCompletionsFixtureUri,
},
position: {
line: 0,
character: 27,
},
};

const response1 = await this.mockClientConnection.sendRequest(CompletionRequest.type, params1);

assert.deepEqual(
response1, [
{
kind: 19,
label: "blocks",
},
{
kind: 19,
label: "components",
},
],
"it returns the expected folder completions");

// file name completions
const params2: TextDocumentPositionParams = {
textDocument: {
uri: importCompletionsFixtureUri,
},
position: {
line: 1,
character: 30,
},
};

const response2 = await this.mockClientConnection.sendRequest(CompletionRequest.type, params2);

assert.deepEqual(
response2, [
{
"kind": 17,
"label": "block-with-errors.block.css",
},
{
"kind": 17,
"label": "utils.block.css",
},
],
"it returns the expected file completions");

// partially typed filename completion
const params3: TextDocumentPositionParams = {
textDocument: {
uri: importCompletionsFixtureUri,
},
position: {
line: 2,
character: 32,
},
};

const response3 = await this.mockClientConnection.sendRequest(CompletionRequest.type, params3);

assert.deepEqual(
response3, [
{
kind: 17,
label: "block-with-errors.block.css",
},
{
kind: 17,
label: "utils.block.css",
},
],
"it returns the expected file completions");

// local directory reference
const params4: TextDocumentPositionParams = {
textDocument: {
uri: importCompletionsFixtureUri,
},
position: {
line: 3,
character: 23,
},
};

const response4 = await this.mockClientConnection.sendRequest(CompletionRequest.type, params4);

assert.deepEqual(
response4, [
{
kind: 17,
label: "a.block.css",
},
],
"it returns the expected file completions");
}
}
96 changes: 96 additions & 0 deletions packages/@css-blocks/language-server/src/util/blockUtils.ts
@@ -1,7 +1,12 @@
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 { postcss } from "opticss";
import * as path from "path";
import { CompletionItem, CompletionItemKind, Position, TextDocument } from "vscode-languageserver";
import { URI } from "vscode-uri";

const IMPORT_PATH_REGEX = /from\s+(['"])([^'"]+)/;

// TODO: Currently we are only supporting css. This should eventually support all
// of the file types supported by css blocks
Expand Down Expand Up @@ -29,3 +34,94 @@ 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;
}

interface PathCompletionCandidateInfo {
pathName: string;
stats: fs.Stats;
}

function maybeCompletionItem(completionCandidateInfo: PathCompletionCandidateInfo): CompletionItem | null {
let completionKind: CompletionItemKind | undefined;

if (completionCandidateInfo.stats.isDirectory()) {
completionKind = CompletionItemKind.Folder;
} else if (completionCandidateInfo.stats.isFile() && completionCandidateInfo.pathName.indexOf(".block.") >= 0) {
completionKind = CompletionItemKind.File;
}

if (completionKind) {
return {
label: path.basename(completionCandidateInfo.pathName),
kind: completionKind,
};
}

return null;
}

async function getImportPathCompletions(documentUri: string, relativeImportPath: string): Promise<CompletionItem[]> {
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 relativeScanDir = relativeImportPath.endsWith(path.sep) ? relativeImportPath : relativeImportPath.substring(0, relativeImportPath.lastIndexOf(path.sep) + 1);
let absoluteScanDir = path.resolve(blockDirPath, relativeScanDir);
let blockFsPath = URI.parse(documentUri).fsPath;
let pathNames: string[] = await new Promise(r => {
fs.readdir(absoluteScanDir, (_, paths) => r(paths || []));
chriseppstein marked this conversation as resolved.
Show resolved Hide resolved
});

let fileInfos: PathCompletionCandidateInfo[] = await Promise.all(pathNames.map(pathName => {
return new Promise(r => {
let absolutePath = `${absoluteScanDir}/${pathName}`;
fs.stat(absolutePath, (_, stats) => r({ pathName: absolutePath, stats }));
});
}));

return fileInfos.reduce(
(completionItems: CompletionItem[], fileInfo) => {
if (fileInfo.pathName === blockFsPath) {
return completionItems;
}

let completionItem = maybeCompletionItem(fileInfo);

if (completionItem) {
completionItems.push(completionItem);
}

return completionItems;
},
[]);
}

// TODO: handle other completion cases (extending imported block, etc). Right
// this is only providing completions for import path;
export async function getBlockCompletions(document: TextDocument, position: Position): Promise<CompletionItem[]> {
let text = document.getText();
let lineAtCursor = text.split(/\r?\n/)[position.line];
let importPathMatches = lineAtCursor.match(IMPORT_PATH_REGEX);

if (importPathMatches && shouldCompleteImportPath(importPathMatches, position, lineAtCursor)) {
let relativeImportPath = importPathMatches[2];
return await getImportPathCompletions(document.uri, relativeImportPath);
}

return [];
}