@@ -7,10 +7,12 @@ import {
7
7
ExtensionContext , languages , Location , Position , Range , TextDocument , Uri , window ,
8
8
workspace ,
9
9
} from "vscode" ;
10
+ import type ClassAttributeMatcher from "./common/class-attribute-matcher" ;
10
11
import CssClassDefinition from "./common/css-class-definition" ;
11
12
import Fetcher from "./fetcher" ;
12
13
import Notifier from "./notifier" ;
13
14
import ParseEngineGateway from "./parse-engine-gateway" ;
15
+ import ClassAttributeExtractor from "./parse-engines/common/class-attribute-extractor" ;
14
16
import IParseOptions from "./parse-engines/common/parse-options" ;
15
17
16
18
enum Command {
@@ -129,128 +131,14 @@ async function cache() {
129
131
130
132
const registerCompletionProvider = (
131
133
languageSelector : string ,
132
- matcherMode : CompletionMatcherMode ,
134
+ matcher : ClassAttributeMatcher ,
133
135
classPrefix = "" ,
134
136
) => languages . registerCompletionItemProvider ( languageSelector , {
135
137
provideCompletionItems ( document : TextDocument , position : Position ) : CompletionItem [ ] {
136
- const start : Position = new Position ( position . line , 0 ) ;
137
- const range : Range = new Range ( start , position ) ;
138
- const text : string = document . getText ( range ) ;
139
-
140
- // Classes already written in the completion target. These classes are excluded.
141
- const classesOnAttribute : string [ ] = [ ] ;
142
-
143
- switch ( matcherMode . type ) {
144
- case "regexp" : {
145
- const { classMatchRegex, splitChar = " " } = matcherMode ;
146
- // Check if the cursor is on a class attribute and retrieve all the css rules in this class attribute.
147
- // Unless matched, completion isn't provided at the position.
148
- const rawClasses : RegExpMatchArray | null = text . match ( classMatchRegex ) ;
149
- if ( ! rawClasses || rawClasses . length === 1 ) {
150
- return [ ] ;
151
- }
152
-
153
- // Will store the classes found on the class attribute.
154
- classesOnAttribute . push ( ...rawClasses [ 1 ] . split ( splitChar ) ) ;
155
- break ;
156
- }
157
- case "javascript" : {
158
- const REGEXP1 = / c l a s s N a m e = (?: { ? " | { ? ' | { ? ` ) ( [ - \w , @ \\ : \[ \] ] * $ ) / ;
159
- const REGEXP2 = / c l a s s = (?: { ? " | { ? ' ) ( [ - \w , @ \\ : \[ \] ] * $ ) / ;
160
-
161
- let matched = false ;
162
-
163
- // Apply two regexp rules.
164
- for ( const regexp of [ REGEXP1 , REGEXP2 ] ) {
165
- const rawClasses = text . match ( regexp ) ;
166
- if ( ! rawClasses || rawClasses . length === 1 ) {
167
- continue ;
168
- }
169
-
170
- matched = true ;
171
- classesOnAttribute . push ( ...rawClasses [ 1 ] . split ( " " ) ) ;
172
- }
173
-
174
- // Special case for `className={}`,
175
- // e.g. `className={"widget " + (p ? "widget--modified" : "")}.
176
- // The completion is provided if the position is in the braces and in a string literal.
177
- const attributeIndex = text . lastIndexOf ( "className={" ) ;
178
- if ( attributeIndex >= 0 ) {
179
- const start = attributeIndex + "className={" . length ;
180
- let index = start ;
181
-
182
- // Stack to find matching braces and quotes.
183
- // Whenever an open brace or opening quote is found, push it.
184
- // When the closer is found, pop it.
185
- let stack : string [ ] = [ ] ;
186
-
187
- const inQuote = ( ) => {
188
- const top = stack . at ( - 1 ) ;
189
- return top === "\"" || top === "'" || top === "`" ;
190
- } ;
191
-
192
- for ( ; index < text . length ; index ++ ) {
193
- const char = text [ index ] ;
194
- if ( stack . length === 0 && char === "}" ) {
195
- break ;
196
- }
197
- switch ( char ) {
198
- case "{" :
199
- stack . push ( "{" ) ;
200
- break ;
201
-
202
- case "}" : {
203
- const last = stack . at ( - 1 ) ;
204
- if ( last === "{" || last === "${" ) {
205
- stack . pop ( ) ;
206
- }
207
- break ;
208
- }
209
- case "\"" :
210
- case "'" :
211
- case "`" :
212
- if ( stack . at ( - 1 ) === char ) {
213
- stack . pop ( ) ;
214
- } else {
215
- stack . push ( char ) ;
216
- }
217
- break ;
218
-
219
- // Escape sequence (e.g. `\"`.)
220
- case "\\" :
221
- if ( inQuote ( ) && index + 1 < text . length ) {
222
- index ++ ;
223
- }
224
- break ;
225
-
226
- // String interpolation (`${...}`.)
227
- case "$" :
228
- if ( stack . at ( - 1 ) === "`" && index + 1 < text . length && text [ index + 1 ] === "{" ) {
229
- stack . push ( "${" ) ;
230
- index ++ ;
231
- }
232
- break ;
233
- }
234
- }
235
-
236
- if ( index === text . length && inQuote ( ) ) {
237
- matched = true ;
238
-
239
- // Roughly extract all tokens that look like css name.
240
- // (E.g. in `className={"a" + (b ? "" : "")}`, both "a" and "b" are matched.)
241
- const wordMatches = text . slice ( start ) . match ( / [ - \w , @ \\ : \[ \] ] + / g) ;
242
- if ( wordMatches != null && wordMatches . length >= 1 ) {
243
- classesOnAttribute . push ( ...wordMatches ) ;
244
- }
245
- }
246
- }
247
-
248
- if ( ! matched ) {
249
- // Unless any rule is matched, completion isn't provided at the position.
250
- return [ ] ;
251
- }
252
- break ;
253
- }
138
+ // Check if the cursor is on class attribute and collect class names on the attribute.
139
+ const classesOnAttribute = ClassAttributeExtractor . extract ( document , position , matcher ) ;
140
+ if ( classesOnAttribute == null ) {
141
+ return [ ] ;
254
142
}
255
143
256
144
const wordRangeAtPosition = document . getWordRangeAtPosition ( position , / [ - \w , @ \\ : \[ \] ] + / ) ;
@@ -284,28 +172,12 @@ const registerCompletionProvider = (
284
172
} ,
285
173
} , ...completionTriggerChars ) ;
286
174
287
- type CompletionMatcherMode =
288
- {
289
- type : "regexp"
290
- classMatchRegex : RegExp
291
- classPrefix ?: string
292
- splitChar ?: string
293
- } | {
294
- type : "javascript"
295
- }
296
-
297
- const registerDefinitionProvider = ( languageSelector : string , classMatchRegex : RegExp ) => languages . registerDefinitionProvider ( languageSelector , {
175
+ const registerDefinitionProvider = ( languageSelector : string , matcher : ClassAttributeMatcher ) => languages . registerDefinitionProvider ( languageSelector , {
298
176
provideDefinition ( document , position , _token ) {
299
- // Check if the cursor is on a class attribute and retrieve all the css rules in this class attribute
300
- {
301
- const start : Position = new Position ( position . line , 0 ) ;
302
- const range : Range = new Range ( start , position ) ;
303
- const text : string = document . getText ( range ) ;
304
-
305
- const rawClasses : RegExpMatchArray | null = text . match ( classMatchRegex ) ;
306
- if ( ! rawClasses || rawClasses . length === 1 ) {
307
- return ;
308
- }
177
+ // Check if the cursor is on class attribute.
178
+ const classesOnAttribute = ClassAttributeExtractor . extract ( document , position , matcher ) ;
179
+ if ( classesOnAttribute == null ) {
180
+ return ;
309
181
}
310
182
311
183
const range : Range | undefined = document . getWordRangeAtPosition ( position , / [ - \w , @ \\ : \[ \] ] + / ) ;
@@ -347,8 +219,8 @@ const registerJavaScriptProviders = (disposables: Disposable[]) =>
347
219
workspace . getConfiguration ( )
348
220
. get < string [ ] > ( Configuration . JavaScriptLanguages )
349
221
?. forEach ( ( extension ) => {
350
- disposables . push ( registerCompletionProvider ( extension , { type : "javascript " } ) ) ;
351
- disposables . push ( registerDefinitionProvider ( extension , / c l a s s (?: N a m e ) ? = (?: \{ ? [ " ' ` ] ) ( [ - \w , @ \\ : \[ \] ] * $ ) / ) ) ;
222
+ disposables . push ( registerCompletionProvider ( extension , { type : "jsx " } ) ) ;
223
+ disposables . push ( registerDefinitionProvider ( extension , { type : "jsx" } ) ) ;
352
224
} ) ;
353
225
354
226
function registerEmmetProviders ( disposables : Disposable [ ] ) {
0 commit comments