Skip to content

Commit e4d4c77

Browse files
authored
Merge pull request #26 from moliva/feat/bracket-completion-trigger
feat: bracket completion trigger
2 parents 2575632 + bd8d3d0 commit e4d4c77

File tree

2 files changed

+143
-34
lines changed

2 files changed

+143
-34
lines changed

src/CompletionProvider.ts

+137-31
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,138 @@ import {
1111
} from './utils';
1212
import type {CamelCaseValues} from './utils';
1313

14-
// check if current character or last character is .
15-
function isTrigger(line: string, position: Position): boolean {
14+
export const COMPLETION_TRIGGERS = ['.', '[', '"', "'"];
15+
16+
type FieldOptions = {
17+
wrappingBracket: boolean;
18+
startsWithQuote: boolean;
19+
endsWithQuote: boolean;
20+
};
21+
22+
/**
23+
* check if current character or last character is any of the completion triggers (i.e. `.`, `[`) and return it
24+
*
25+
* @see COMPLETION_TRIGGERS
26+
*/
27+
function findTrigger(line: string, position: Position): string | undefined {
1628
const i = position.character - 1;
17-
return line[i] === '.' || (i > 1 && line[i - 1] === '.');
29+
30+
for (const trigger of COMPLETION_TRIGGERS) {
31+
if (line[i] === trigger) {
32+
return trigger;
33+
}
34+
if (i > 1 && line[i - 1] === trigger) {
35+
return trigger;
36+
}
37+
}
38+
39+
return undefined;
1840
}
1941

20-
function getWords(line: string, position: Position): string {
42+
/**
43+
* Given the line, position and trigger, returns the identifier referencing the styles spreadsheet and the (partial) field selected with options to help construct the completion item later.
44+
*
45+
*/
46+
function getWords(
47+
line: string,
48+
position: Position,
49+
trigger: string,
50+
): [string, string, FieldOptions?] | undefined {
2151
const text = line.slice(0, position.character);
22-
const index = text.search(/[a-z0-9\._]*$/i);
52+
const index = text.search(/[a-z0-9\._\[\]'"\-]*$/i);
2353
if (index === -1) {
24-
return '';
54+
return undefined;
55+
}
56+
57+
const words = text.slice(index);
58+
59+
if (words === '' || words.indexOf(trigger) === -1) {
60+
return undefined;
61+
}
62+
63+
switch (trigger) {
64+
// process `.` trigger
65+
case '.':
66+
return words.split('.') as [string, string];
67+
// process `[` trigger
68+
case '[': {
69+
const [obj, field] = words.split('[');
70+
71+
let lineAhead = line.slice(position.character);
72+
const endsWithQuote = lineAhead.search(/^["']/) !== -1;
73+
74+
lineAhead = endsWithQuote ? lineAhead.slice(1) : lineAhead;
75+
const wrappingBracket = lineAhead.search(/^\s*\]/) !== -1;
76+
77+
const startsWithQuote =
78+
field.length > 0 && (field[0] === '"' || field[0] === "'");
79+
80+
return [
81+
obj,
82+
field.slice(startsWithQuote ? 1 : 0),
83+
{wrappingBracket, startsWithQuote, endsWithQuote},
84+
];
85+
}
86+
default: {
87+
throw new Error(`Unsupported trigger character ${trigger}`);
88+
}
2589
}
90+
}
2691

27-
return text.slice(index);
92+
function createCompletionItem(
93+
trigger: string,
94+
name: string,
95+
position: Position,
96+
fieldOptions: FieldOptions | undefined,
97+
): CompletionItem {
98+
const nameIncludesDashes = name.includes('-');
99+
const completionField =
100+
trigger === '[' || nameIncludesDashes ? `['${name}']` : name;
101+
102+
let completionItem: CompletionItem;
103+
// in case of items with dashes, we need to replace the `.` and suggest the field using the subscript expression `[`
104+
if (trigger === '.') {
105+
if (nameIncludesDashes) {
106+
const range = lsp.Range.create(
107+
lsp.Position.create(position.line, position.character - 1), // replace the `.` character
108+
position,
109+
);
110+
111+
completionItem = CompletionItem.create(completionField);
112+
completionItem.textEdit = lsp.InsertReplaceEdit.create(
113+
completionField,
114+
range,
115+
range,
116+
);
117+
} else {
118+
completionItem = CompletionItem.create(completionField);
119+
}
120+
} else {
121+
// trigger === '['
122+
const startPositionCharacter =
123+
position.character -
124+
1 - // replace the `[` character
125+
(fieldOptions?.startsWithQuote ? 1 : 0); // replace the starting quote if present
126+
127+
const endPositionCharacter =
128+
position.character +
129+
(fieldOptions?.endsWithQuote ? 1 : 0) + // replace the ending quote if present
130+
(fieldOptions?.wrappingBracket ? 1 : 0); // replace the wrapping bracket if present
131+
132+
const range = lsp.Range.create(
133+
lsp.Position.create(position.line, startPositionCharacter),
134+
lsp.Position.create(position.line, endPositionCharacter),
135+
);
136+
137+
completionItem = CompletionItem.create(completionField);
138+
completionItem.textEdit = lsp.InsertReplaceEdit.create(
139+
completionField,
140+
range,
141+
range,
142+
);
143+
}
144+
145+
return completionItem;
28146
}
29147

30148
export class CSSModulesCompletionProvider {
@@ -57,17 +175,17 @@ export class CSSModulesCompletionProvider {
57175
if (typeof currentLine !== 'string') return null;
58176
const currentDir = getCurrentDirFromUri(textdocument.uri);
59177

60-
if (!isTrigger(currentLine, position)) {
178+
const trigger = findTrigger(currentLine, position);
179+
if (!trigger) {
61180
return [];
62181
}
63182

64-
const words = getWords(currentLine, position);
65-
66-
if (words === '' || words.indexOf('.') === -1) {
183+
const foundFields = getWords(currentLine, position, trigger);
184+
if (!foundFields) {
67185
return [];
68186
}
69187

70-
const [obj, field] = words.split('.');
188+
const [obj, field, fieldOptions] = foundFields;
71189

72190
const importPath = findImportPath(fileContent, obj, currentDir);
73191
if (importPath === '') {
@@ -83,25 +201,13 @@ export class CSSModulesCompletionProvider {
83201
const res = classNames.map(_class => {
84202
const name = this._classTransformer(_class);
85203

86-
let completionItem: CompletionItem;
87-
88-
// in case of items with dashes, we need to replace the `.` and suggest the field using the subscript expression
89-
if (name.includes('-')) {
90-
const arrayAccessor = `['${name}']`;
91-
const range = lsp.Range.create(
92-
lsp.Position.create(position.line, position.character - 1),
93-
position,
94-
);
95-
96-
completionItem = CompletionItem.create(arrayAccessor);
97-
completionItem.textEdit = lsp.InsertReplaceEdit.create(
98-
arrayAccessor,
99-
range,
100-
range,
101-
);
102-
} else {
103-
completionItem = CompletionItem.create(name);
104-
}
204+
const completionItem = createCompletionItem(
205+
trigger,
206+
name,
207+
position,
208+
fieldOptions,
209+
);
210+
105211
return completionItem;
106212
});
107213

src/connection.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as lsp from 'vscode-languageserver/node';
22

3-
import {CSSModulesCompletionProvider} from './CompletionProvider';
3+
import {
4+
COMPLETION_TRIGGERS,
5+
CSSModulesCompletionProvider,
6+
} from './CompletionProvider';
47
import {CSSModulesDefinitionProvider} from './DefinitionProvider';
58
import {textDocuments} from './textDocuments';
69

@@ -42,9 +45,9 @@ export function createConnection(): lsp.Connection {
4245
implementationProvider: true,
4346
completionProvider: {
4447
/**
45-
* only invoke completion once `.` is pressed
48+
* only invoke completion once `.` or `[` are pressed
4649
*/
47-
triggerCharacters: ['.'],
50+
triggerCharacters: COMPLETION_TRIGGERS,
4851
resolveProvider: true,
4952
},
5053
},

0 commit comments

Comments
 (0)