use lexer for class attribute completions
parent
c78faee425
commit
b5daeb43c3
|
@ -1033,6 +1033,12 @@
|
|||
"integrity": "sha512-dOrgprHnkDaj1pmrwdcMAf0QRNQzqTB5rxJph+iIQshSmIvtgRqJ0nim8u1vvXU8iOXZrH96+M46JDFTPLingA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/moo": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/moo/-/moo-0.5.3.tgz",
|
||||
"integrity": "sha512-PJJ/jvb5Gor8DWvXN3e75njfQyYNRz0PaFSZ3br9GfHM9N2FxvuJ/E/ytcQePJOLzHlvgFSsIJIvfUMUxWTbnA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "13.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
|
||||
|
@ -4943,6 +4949,12 @@
|
|||
"integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==",
|
||||
"dev": true
|
||||
},
|
||||
"moo": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz",
|
||||
"integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==",
|
||||
"dev": true
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"devDependencies": {
|
||||
"@ctrl/tinycolor": "^3.1.0",
|
||||
"@types/mocha": "^5.2.0",
|
||||
"@types/moo": "^0.5.3",
|
||||
"@types/node": "^13.9.3",
|
||||
"@types/vscode": "^1.32.0",
|
||||
"@zeit/ncc": "^0.22.0",
|
||||
|
@ -93,6 +94,7 @@
|
|||
"line-column": "^1.0.2",
|
||||
"mitt": "^1.2.0",
|
||||
"mkdirp": "^1.0.3",
|
||||
"moo": "^0.5.1",
|
||||
"pkg-up": "^3.1.0",
|
||||
"postcss": "^7.0.27",
|
||||
"postcss-selector-parser": "^6.0.2",
|
||||
|
|
|
@ -12,7 +12,7 @@ import removeMeta from '../util/removeMeta'
|
|||
import { getColor, getColorFromValue } from '../util/color'
|
||||
import { isHtmlContext } from '../util/html'
|
||||
import { isCssContext } from '../util/css'
|
||||
import { findLast, findJsxStrings, arrFindLast } from '../util/find'
|
||||
import { findLast } from '../util/find'
|
||||
import { stringifyConfigValue, stringifyCss } from '../util/stringify'
|
||||
import { stringifyScreen, Screen } from '../util/screens'
|
||||
import isObject from '../../util/isObject'
|
||||
|
@ -24,6 +24,10 @@ import { naturalExpand } from '../util/naturalExpand'
|
|||
import semver from 'semver'
|
||||
import { docsUrl } from '../util/docsUrl'
|
||||
import { ensureArray } from '../../util/array'
|
||||
import {
|
||||
getClassAttributeLexer,
|
||||
getComputedClassAttributeLexer,
|
||||
} from '../util/lexers'
|
||||
|
||||
function completionsFromClassList(
|
||||
state: State,
|
||||
|
@ -122,24 +126,31 @@ function provideClassAttributeCompletions(
|
|||
end: position,
|
||||
})
|
||||
|
||||
const match = findLast(/\bclass(?:Name)?=(?<initial>['"`{])/gi, str)
|
||||
const match = findLast(/[\s:]class(?:Name)?=['"`{]/gi, str)
|
||||
|
||||
if (match === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rest = str.substr(match.index + match[0].length)
|
||||
const lexer =
|
||||
match[0][0] === ':'
|
||||
? getComputedClassAttributeLexer()
|
||||
: getClassAttributeLexer()
|
||||
lexer.reset(str.substr(match.index + match[0].length - 1))
|
||||
|
||||
try {
|
||||
let tokens = Array.from(lexer)
|
||||
let last = tokens[tokens.length - 1]
|
||||
if (last.type.startsWith('start') || last.type === 'classlist') {
|
||||
let classList = ''
|
||||
for (let i = tokens.length - 1; i >= 0; i--) {
|
||||
if (tokens[i].type === 'classlist') {
|
||||
classList = tokens[i].value + classList
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (match.groups.initial === '{') {
|
||||
const strings = findJsxStrings('{' + rest)
|
||||
const lastOpenString = arrFindLast(
|
||||
strings,
|
||||
(string) => typeof string.end === 'undefined'
|
||||
)
|
||||
if (lastOpenString) {
|
||||
const classList = str.substr(
|
||||
str.length - rest.length + lastOpenString.start - 1
|
||||
)
|
||||
return completionsFromClassList(state, classList, {
|
||||
start: {
|
||||
line: position.line,
|
||||
|
@ -148,20 +159,9 @@ function provideClassAttributeCompletions(
|
|||
end: position,
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (rest.indexOf(match.groups.initial) !== -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return completionsFromClassList(state, rest, {
|
||||
start: {
|
||||
line: position.line,
|
||||
character: position.character - rest.length,
|
||||
},
|
||||
end: position,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
function provideAtApplyCompletions(
|
||||
|
|
|
@ -19,57 +19,6 @@ export function findLast(re: RegExp, str: string): RegExpMatchArray {
|
|||
return matches[matches.length - 1]
|
||||
}
|
||||
|
||||
export function arrFindLast<T>(arr: T[], predicate: (item: T) => boolean): T {
|
||||
for (let i = arr.length - 1; i >= 0; --i) {
|
||||
const x = arr[i]
|
||||
if (predicate(x)) {
|
||||
return x
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
enum Quote {
|
||||
SINGLE = "'",
|
||||
DOUBLE = '"',
|
||||
TICK = '`',
|
||||
}
|
||||
type StringInfo = {
|
||||
start: number
|
||||
end?: number
|
||||
char: Quote
|
||||
}
|
||||
|
||||
export function findJsxStrings(str: string): StringInfo[] {
|
||||
const chars = str.split('')
|
||||
const strings: StringInfo[] = []
|
||||
let bracketCount = 0
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const char = chars[i]
|
||||
if (char === '{') {
|
||||
bracketCount += 1
|
||||
} else if (char === '}') {
|
||||
bracketCount -= 1
|
||||
} else if (
|
||||
char === Quote.SINGLE ||
|
||||
char === Quote.DOUBLE ||
|
||||
char === Quote.TICK
|
||||
) {
|
||||
let open = arrFindLast(strings, (string) => string.char === char)
|
||||
if (strings.length === 0 || !open || (open && open.end)) {
|
||||
strings.push({ start: i + 1, char })
|
||||
} else {
|
||||
open.end = i
|
||||
}
|
||||
}
|
||||
if (i !== 0 && bracketCount === 0) {
|
||||
// end
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings
|
||||
}
|
||||
|
||||
export function findClassNamesInRange(
|
||||
doc: TextDocument,
|
||||
range: Range
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// https://www.codementor.io/@agustinchiappeberrini/lazy-evaluation-and-javascript-a5m7g8gs3
|
||||
|
||||
export interface Lazy<T> {
|
||||
(): T
|
||||
isLazy: boolean
|
||||
}
|
||||
|
||||
export const lazy = <T>(getter: () => T): Lazy<T> => {
|
||||
let evaluated: boolean = false
|
||||
let _res: T = null
|
||||
const res = <Lazy<T>>function (): T {
|
||||
if (evaluated) return _res
|
||||
_res = getter.apply(this, arguments)
|
||||
evaluated = true
|
||||
return _res
|
||||
}
|
||||
res.isLazy = true
|
||||
return res
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import moo from 'moo'
|
||||
import { lazy } from './lazy'
|
||||
|
||||
const classAttributeStates: { [x: string]: moo.Rules } = {
|
||||
doubleClassList: {
|
||||
lbrace: { match: /(?<!\\)\{/, push: 'interp' },
|
||||
rbrace: { match: /(?<!\\)\}/, pop: 1 },
|
||||
end: { match: /(?<!\\)"/, pop: 1 },
|
||||
classlist: { match: /[\s\S]/, lineBreaks: true },
|
||||
},
|
||||
singleClassList: {
|
||||
lbrace: { match: /(?<!\\)\{/, push: 'interp' },
|
||||
rbrace: { match: /(?<!\\)\}/, pop: 1 },
|
||||
end: { match: /(?<!\\)'/, pop: 1 },
|
||||
classlist: { match: /[\s\S]/, lineBreaks: true },
|
||||
},
|
||||
tickClassList: {
|
||||
lbrace: { match: /(?<=(?<!\\)\$)\{/, push: 'interp' },
|
||||
rbrace: { match: /(?<!\\)\}/, pop: 1 },
|
||||
end: { match: /(?<!\\)`/, pop: 1 },
|
||||
classlist: { match: /[\s\S]/, lineBreaks: true },
|
||||
},
|
||||
interp: {
|
||||
startSingle: { match: /(?<!\\)'/, push: 'singleClassList' },
|
||||
startDouble: { match: /(?<!\\)"/, push: 'doubleClassList' },
|
||||
startTick: { match: /(?<!\\)`/, push: 'tickClassList' },
|
||||
lbrace: { match: /(?<!\\)\{/, push: 'interp' },
|
||||
rbrace: { match: /(?<!\\)\}/, pop: 1 },
|
||||
text: { match: /[\s\S]/, lineBreaks: true },
|
||||
},
|
||||
}
|
||||
|
||||
export const getClassAttributeLexer = lazy(() =>
|
||||
moo.states({
|
||||
main: {
|
||||
start1: { match: '"', push: 'doubleClassList' },
|
||||
start2: { match: "'", push: 'singleClassList' },
|
||||
start3: { match: '{', push: 'interp' },
|
||||
},
|
||||
...classAttributeStates,
|
||||
})
|
||||
)
|
||||
|
||||
export const getComputedClassAttributeLexer = lazy(() =>
|
||||
moo.states({
|
||||
main: {
|
||||
quote: { match: /['"{]/, push: 'interp' },
|
||||
},
|
||||
// TODO: really this should use a different interp definition that is
|
||||
// terminated correctly based on the initial quote type
|
||||
...classAttributeStates,
|
||||
})
|
||||
)
|
Loading…
Reference in New Issue