@@ -7,10 +7,12 @@ import {
77 ExtensionContext , languages , Location , Position , Range , TextDocument , Uri , window ,
88 workspace ,
99} from "vscode" ;
10+ import type ClassAttributeMatcher from "./common/class-attribute-matcher" ;
1011import CssClassDefinition from "./common/css-class-definition" ;
1112import Fetcher from "./fetcher" ;
1213import Notifier from "./notifier" ;
1314import ParseEngineGateway from "./parse-engine-gateway" ;
15+ import ClassAttributeExtractor from "./parse-engines/common/class-attribute-extractor" ;
1416import IParseOptions from "./parse-engines/common/parse-options" ;
1517
1618enum Command {
@@ -129,128 +131,14 @@ async function cache() {
129131
130132const registerCompletionProvider = (
131133 languageSelector : string ,
132- matcherMode : CompletionMatcherMode ,
134+ matcher : ClassAttributeMatcher ,
133135 classPrefix = "" ,
134136) => languages . registerCompletionItemProvider ( languageSelector , {
135137 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 [ ] ;
254142 }
255143
256144 const wordRangeAtPosition = document . getWordRangeAtPosition ( position , / [ - \w , @ \\ : \[ \] ] + / ) ;
@@ -284,28 +172,12 @@ const registerCompletionProvider = (
284172 } ,
285173} , ...completionTriggerChars ) ;
286174
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 , {
298176 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 ;
309181 }
310182
311183 const range : Range | undefined = document . getWordRangeAtPosition ( position , / [ - \w , @ \\ : \[ \] ] + / ) ;
@@ -347,8 +219,8 @@ const registerJavaScriptProviders = (disposables: Disposable[]) =>
347219 workspace . getConfiguration ( )
348220 . get < string [ ] > ( Configuration . JavaScriptLanguages )
349221 ?. 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" } ) ) ;
352224 } ) ;
353225
354226function registerEmmetProviders ( disposables : Disposable [ ] ) {
0 commit comments