Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions src/CompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,45 @@ import {
TextDocument,
Position,
CompletionItem,
CompletionItemKind
CompletionItemKind,
} from "vscode";
import * as path from "path";
import * as _ from "lodash";
import { getAllClassNames, getCurrentLine, dashesCamelCase, isKebabCaseClassName, createBracketCompletionItem } from "./utils";
import {
getAllClassNames,
getCurrentLine,
dashesCamelCase,
isKebabCaseClassName,
createBracketCompletionItem,
} from "./utils";
import { findImportModule, resolveImportPath } from "./utils/path";
import { AliasFromUserOptions, ExtensionOptions } from "./options";
import { getRealPathAlias } from "./path-alias";

// check if current character or last character is .
// or if current character is " or ' after a [
function isTrigger(line: string, position: Position): boolean {
const i = position.character - 1;
return line[i] === "." || (i > 1 && line[i - 1] === ".");
const isDotSyntax = line[i] === "." || (i > 1 && line[i - 1] === ".");
if (isDotSyntax) {
return true;
}

const isBracketSyntax =
line[i - 1] === "[" && (line[i] === '"' || line[i] === "'");
if (isBracketSyntax) {
return true;
}

return false;
}

function getWords(line: string, position: Position): string {
const text = line.slice(0, position.character);
// support optional chain https://github.com/tc39/proposal-optional-chaining
// covert ?. to .
const convertText = text.replace(/(\?\.)/g, '.');
const index = convertText.search(/[a-zA-Z0-9._]*$/);
const convertText = text.replace(/(\?\.)/g, ".");
const index = convertText.search(/[a-zA-Z0-9._["']*$/);
if (index === -1) {
return "";
}
Expand Down Expand Up @@ -61,12 +79,13 @@ export class CSSModuleCompletionProvider implements CompletionItemProvider {
return Promise.resolve([]);
}

const splitRegex = /\.|\["|\['/;
const words = getWords(currentLine, position);
if (words === "" || words.indexOf(".") === -1) {
if (words === "" || !splitRegex.test(words)) {
return Promise.resolve([]);
}

const [obj, field] = words.split(".");
const [obj, field] = words.split(splitRegex);

const importModule = findImportModule(document.getText(), obj);
const importPath = await resolveImportPath(
Expand All @@ -88,7 +107,7 @@ export class CSSModuleCompletionProvider implements CompletionItemProvider {
}
if (isKebabCaseClassName(name)) {
return createBracketCompletionItem(name, position);
}
}
return new CompletionItem(name, CompletionItemKind.Variable);
})
);
Expand Down
37 changes: 26 additions & 11 deletions src/DefinitionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
genImportRegExp,
resolveImportPath,
} from "./utils/path";
import { AliasFromUserOptions, CamelCaseValues, ExtensionOptions } from "./options";
import {
AliasFromUserOptions,
CamelCaseValues,
ExtensionOptions,
} from "./options";
import * as path from "path";
import * as fs from "fs";
import * as _ from "lodash";
Expand All @@ -31,18 +35,25 @@ interface Keyword {
}

function getWords(line: string, position: Position): string {
const splitRegex = /\.|\["|\['/;

const headText = line.slice(0, position.character);
const startIndex = headText.search(/[a-zA-Z0-9._]*$/);
const startIndex = headText.search(/[a-zA-Z0-9._["']*$/);
// not found or not clicking object field
if (startIndex === -1 || headText.slice(startIndex).indexOf(".") === -1) {
if (startIndex === -1 || !splitRegex.test(headText.slice(startIndex))) {
return "";
}

const match = /^([a-zA-Z0-9._]*)/.exec(line.slice(startIndex));
const match = /^([a-zA-Z0-9._["']*)/.exec(line.slice(startIndex));
if (match === null) {
return "";
}

if (match[1].includes('["') || match[1].includes("['")) {
// Remove " or ' from end
return match[1].slice(0, -1);
}

return match[1];
}

Expand Down Expand Up @@ -79,22 +90,24 @@ function getPosition(
/**
* This is a simple solution for definition match.
* Only guarantee keyword not follow normal characters
*
*
* if we want match [.main] classname
* escaped dot char first and then use RegExp to match
* more detail -> https://github.com/clinyong/vscode-css-modules/pull/41#discussion_r696247941
*
*
* 1. .main, // valid
* 2. .main // valid
*
*
* 3. .main-sub // invalid
* 4. .main09 // invalid
* 5. .main_bem // invalid
* 6. .mainsuffix // invalid
*
*
* @TODO Refact by new tokenizer later
*/
const keyWordMatchReg = new RegExp(`${keyWord.replace(/^\./, '\\.')}(?![_0-9a-zA-Z-])`);
const keyWordMatchReg = new RegExp(
`${keyWord.replace(/^\./, "\\.")}(?![_0-9a-zA-Z-])`
);

for (let i = 0; i < lines.length; i++) {
const originalLine = lines[i];
Expand Down Expand Up @@ -161,12 +174,14 @@ function isImportLineMatch(
}

function getKeyword(currentLine: string, position: Position): Keyword | null {
const splitRegex = /\.|\["|\['/;

const words = getWords(currentLine, position);
if (words === "" || words.indexOf(".") === -1) {
if (words === "" || !splitRegex.test(words)) {
return null;
}

const [obj, field] = words.split(".");
const [obj, field] = words.split(splitRegex);
if (!obj || !field) {
// probably a spread operator
return null;
Expand Down
16 changes: 9 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ export function activate(context: ExtensionContext): void {
{ language: "astro", scheme: "file" },
];
const options = readOptions();
context.subscriptions.push(
languages.registerCompletionItemProvider(
mode,
new CSSModuleCompletionProvider(options),
"."
)
);
context.subscriptions.push(
languages.registerCompletionItemProvider(
mode,
new CSSModuleCompletionProvider(options),
".",
"\"",
"'"
)
);
context.subscriptions.push(
languages.registerDefinitionProvider(
mode,
Expand Down
10 changes: 10 additions & 0 deletions src/test/fixtures/sample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ v?.a; // don't match anything
styles1?. // support optional chain
const es1 = {};
es1. // don't match styles1

// Support autocompletion with bracket notation
styles1["
styles1['
styles1[""]
styles1['']

// issue-#70
console.log(styles1["body"]);
console.log(styles1['body']);
37 changes: 35 additions & 2 deletions src/test/suite/CompletionProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ const uri4 = vscode.Uri.file(SAMPLE_TSX_FILE);
const uri5 = vscode.Uri.file(SAMPLE_TS_FILE);
const uri6 = vscode.Uri.file(SAMPLE_ASTRO_FILE);

function testCompletion(position: vscode.Position, itemCount: number, fixtureFile?: vscode.Uri) {
function testCompletion(
position: vscode.Position,
itemCount: number,
fixtureFile?: vscode.Uri
) {
return vscode.workspace.openTextDocument(fixtureFile || uri).then((text) => {
const provider = new CSSModuleCompletionProvider(readOptions());
return provider.provideCompletionItems(text, position).then((items) => {
Expand Down Expand Up @@ -74,6 +78,34 @@ test("test optional chain valid match", () => {
});
});

test("test bracket notation with double quotes and no closing quote valid match", () => {
const position = new vscode.Position(17, 9);
return Promise.resolve(testCompletion(position, 5)).catch((err) => {
assert.ok(false, `error in OpenTextDocument ${err}`);
});
});

test("test bracket notation with single quotes and no closing quote valid match", () => {
const position = new vscode.Position(18, 9);
return Promise.resolve(testCompletion(position, 5)).catch((err) => {
assert.ok(false, `error in OpenTextDocument ${err}`);
});
});

test("test bracket notation with double quotes and closing quote valid match", () => {
const position = new vscode.Position(19, 9);
return Promise.resolve(testCompletion(position, 5)).catch((err) => {
assert.ok(false, `error in OpenTextDocument ${err}`);
});
});

test("test bracket notation with single quotes and closing quote valid match", () => {
const position = new vscode.Position(20, 9);
return Promise.resolve(testCompletion(position, 5)).catch((err) => {
assert.ok(false, `error in OpenTextDocument ${err}`);
});
});

test("test exact Match", () => {
const position = new vscode.Position(14, 4);
return Promise.resolve(testCompletion(position, 0)).catch((err) => {
Expand Down Expand Up @@ -112,7 +144,8 @@ test("test camelCase:false style and kebab-case completion", () => {
return Promise.resolve(
testCompletionWithCase(position, false, [
(items) => assert.strictEqual(1, items.length),
(items) => assert.strictEqual(`['sidebar_without-header']`, items[0].insertText),
(items) =>
assert.strictEqual(`['sidebar_without-header']`, items[0].insertText),
])
).catch((err) => {
assert.ok(false, `error in OpenTextDocument ${err}`);
Expand Down
14 changes: 14 additions & 0 deletions src/test/suite/DefinitionProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,17 @@ test("ignore spread syntax", async () => {
);
assert.deepStrictEqual(result, null);
});

test("test bracket definition with double quotes jump to definition", () => {
const position = new vscode.Position(23, 22);
return Promise.resolve(testDefinition(position, 2, 1)).catch((err) =>
assert.ok(false, `error in OpenTextDocument ${err}`)
);
});

test("test bracket definition with single quotes jump to definition", () => {
const position = new vscode.Position(24, 22);
return Promise.resolve(testDefinition(position, 2, 1)).catch((err) =>
assert.ok(false, `error in OpenTextDocument ${err}`)
);
});