Skip to content

Commit b5daeb4

Browse files
author
Brad Cornes
committed
use lexer for class attribute completions
1 parent c78faee commit b5daeb4

File tree

6 files changed

+112
-77
lines changed

6 files changed

+112
-77
lines changed

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"devDependencies": {
7676
"@ctrl/tinycolor": "^3.1.0",
7777
"@types/mocha": "^5.2.0",
78+
"@types/moo": "^0.5.3",
7879
"@types/node": "^13.9.3",
7980
"@types/vscode": "^1.32.0",
8081
"@zeit/ncc": "^0.22.0",
@@ -93,6 +94,7 @@
9394
"line-column": "^1.0.2",
9495
"mitt": "^1.2.0",
9596
"mkdirp": "^1.0.3",
97+
"moo": "^0.5.1",
9698
"pkg-up": "^3.1.0",
9799
"postcss": "^7.0.27",
98100
"postcss-selector-parser": "^6.0.2",

src/lsp/providers/completionProvider.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import removeMeta from '../util/removeMeta'
1212
import { getColor, getColorFromValue } from '../util/color'
1313
import { isHtmlContext } from '../util/html'
1414
import { isCssContext } from '../util/css'
15-
import { findLast, findJsxStrings, arrFindLast } from '../util/find'
15+
import { findLast } from '../util/find'
1616
import { stringifyConfigValue, stringifyCss } from '../util/stringify'
1717
import { stringifyScreen, Screen } from '../util/screens'
1818
import isObject from '../../util/isObject'
@@ -24,6 +24,10 @@ import { naturalExpand } from '../util/naturalExpand'
2424
import semver from 'semver'
2525
import { docsUrl } from '../util/docsUrl'
2626
import { ensureArray } from '../../util/array'
27+
import {
28+
getClassAttributeLexer,
29+
getComputedClassAttributeLexer,
30+
} from '../util/lexers'
2731

2832
function completionsFromClassList(
2933
state: State,
@@ -122,24 +126,31 @@ function provideClassAttributeCompletions(
122126
end: position,
123127
})
124128

125-
const match = findLast(/\bclass(?:Name)?=(?<initial>['"`{])/gi, str)
129+
const match = findLast(/[\s:]class(?:Name)?=['"`{]/gi, str)
126130

127131
if (match === null) {
128132
return null
129133
}
130134

131-
const rest = str.substr(match.index + match[0].length)
135+
const lexer =
136+
match[0][0] === ':'
137+
? getComputedClassAttributeLexer()
138+
: getClassAttributeLexer()
139+
lexer.reset(str.substr(match.index + match[0].length - 1))
140+
141+
try {
142+
let tokens = Array.from(lexer)
143+
let last = tokens[tokens.length - 1]
144+
if (last.type.startsWith('start') || last.type === 'classlist') {
145+
let classList = ''
146+
for (let i = tokens.length - 1; i >= 0; i--) {
147+
if (tokens[i].type === 'classlist') {
148+
classList = tokens[i].value + classList
149+
} else {
150+
break
151+
}
152+
}
132153

133-
if (match.groups.initial === '{') {
134-
const strings = findJsxStrings('{' + rest)
135-
const lastOpenString = arrFindLast(
136-
strings,
137-
(string) => typeof string.end === 'undefined'
138-
)
139-
if (lastOpenString) {
140-
const classList = str.substr(
141-
str.length - rest.length + lastOpenString.start - 1
142-
)
143154
return completionsFromClassList(state, classList, {
144155
start: {
145156
line: position.line,
@@ -148,20 +159,9 @@ function provideClassAttributeCompletions(
148159
end: position,
149160
})
150161
}
151-
return null
152-
}
162+
} catch (_) {}
153163

154-
if (rest.indexOf(match.groups.initial) !== -1) {
155-
return null
156-
}
157-
158-
return completionsFromClassList(state, rest, {
159-
start: {
160-
line: position.line,
161-
character: position.character - rest.length,
162-
},
163-
end: position,
164-
})
164+
return null
165165
}
166166

167167
function provideAtApplyCompletions(

src/lsp/util/find.ts

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,57 +19,6 @@ export function findLast(re: RegExp, str: string): RegExpMatchArray {
1919
return matches[matches.length - 1]
2020
}
2121

22-
export function arrFindLast<T>(arr: T[], predicate: (item: T) => boolean): T {
23-
for (let i = arr.length - 1; i >= 0; --i) {
24-
const x = arr[i]
25-
if (predicate(x)) {
26-
return x
27-
}
28-
}
29-
return null
30-
}
31-
32-
enum Quote {
33-
SINGLE = "'",
34-
DOUBLE = '"',
35-
TICK = '`',
36-
}
37-
type StringInfo = {
38-
start: number
39-
end?: number
40-
char: Quote
41-
}
42-
43-
export function findJsxStrings(str: string): StringInfo[] {
44-
const chars = str.split('')
45-
const strings: StringInfo[] = []
46-
let bracketCount = 0
47-
for (let i = 0; i < chars.length; i++) {
48-
const char = chars[i]
49-
if (char === '{') {
50-
bracketCount += 1
51-
} else if (char === '}') {
52-
bracketCount -= 1
53-
} else if (
54-
char === Quote.SINGLE ||
55-
char === Quote.DOUBLE ||
56-
char === Quote.TICK
57-
) {
58-
let open = arrFindLast(strings, (string) => string.char === char)
59-
if (strings.length === 0 || !open || (open && open.end)) {
60-
strings.push({ start: i + 1, char })
61-
} else {
62-
open.end = i
63-
}
64-
}
65-
if (i !== 0 && bracketCount === 0) {
66-
// end
67-
break
68-
}
69-
}
70-
return strings
71-
}
72-
7322
export function findClassNamesInRange(
7423
doc: TextDocument,
7524
range: Range

src/lsp/util/lazy.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// https://www.codementor.io/@agustinchiappeberrini/lazy-evaluation-and-javascript-a5m7g8gs3
2+
3+
export interface Lazy<T> {
4+
(): T
5+
isLazy: boolean
6+
}
7+
8+
export const lazy = <T>(getter: () => T): Lazy<T> => {
9+
let evaluated: boolean = false
10+
let _res: T = null
11+
const res = <Lazy<T>>function (): T {
12+
if (evaluated) return _res
13+
_res = getter.apply(this, arguments)
14+
evaluated = true
15+
return _res
16+
}
17+
res.isLazy = true
18+
return res
19+
}

src/lsp/util/lexers.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import moo from 'moo'
2+
import { lazy } from './lazy'
3+
4+
const classAttributeStates: { [x: string]: moo.Rules } = {
5+
doubleClassList: {
6+
lbrace: { match: /(?<!\\)\{/, push: 'interp' },
7+
rbrace: { match: /(?<!\\)\}/, pop: 1 },
8+
end: { match: /(?<!\\)"/, pop: 1 },
9+
classlist: { match: /[\s\S]/, lineBreaks: true },
10+
},
11+
singleClassList: {
12+
lbrace: { match: /(?<!\\)\{/, push: 'interp' },
13+
rbrace: { match: /(?<!\\)\}/, pop: 1 },
14+
end: { match: /(?<!\\)'/, pop: 1 },
15+
classlist: { match: /[\s\S]/, lineBreaks: true },
16+
},
17+
tickClassList: {
18+
lbrace: { match: /(?<=(?<!\\)\$)\{/, push: 'interp' },
19+
rbrace: { match: /(?<!\\)\}/, pop: 1 },
20+
end: { match: /(?<!\\)`/, pop: 1 },
21+
classlist: { match: /[\s\S]/, lineBreaks: true },
22+
},
23+
interp: {
24+
startSingle: { match: /(?<!\\)'/, push: 'singleClassList' },
25+
startDouble: { match: /(?<!\\)"/, push: 'doubleClassList' },
26+
startTick: { match: /(?<!\\)`/, push: 'tickClassList' },
27+
lbrace: { match: /(?<!\\)\{/, push: 'interp' },
28+
rbrace: { match: /(?<!\\)\}/, pop: 1 },
29+
text: { match: /[\s\S]/, lineBreaks: true },
30+
},
31+
}
32+
33+
export const getClassAttributeLexer = lazy(() =>
34+
moo.states({
35+
main: {
36+
start1: { match: '"', push: 'doubleClassList' },
37+
start2: { match: "'", push: 'singleClassList' },
38+
start3: { match: '{', push: 'interp' },
39+
},
40+
...classAttributeStates,
41+
})
42+
)
43+
44+
export const getComputedClassAttributeLexer = lazy(() =>
45+
moo.states({
46+
main: {
47+
quote: { match: /['"{]/, push: 'interp' },
48+
},
49+
// TODO: really this should use a different interp definition that is
50+
// terminated correctly based on the initial quote type
51+
...classAttributeStates,
52+
})
53+
)

0 commit comments

Comments
 (0)