use lexer for class attribute completions
parent
c78faee425
commit
b5daeb43c3
|
@ -1033,6 +1033,12 @@
|
||||||
"integrity": "sha512-dOrgprHnkDaj1pmrwdcMAf0QRNQzqTB5rxJph+iIQshSmIvtgRqJ0nim8u1vvXU8iOXZrH96+M46JDFTPLingA==",
|
"integrity": "sha512-dOrgprHnkDaj1pmrwdcMAf0QRNQzqTB5rxJph+iIQshSmIvtgRqJ0nim8u1vvXU8iOXZrH96+M46JDFTPLingA==",
|
||||||
"dev": true
|
"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": {
|
"@types/node": {
|
||||||
"version": "13.13.4",
|
"version": "13.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
|
||||||
|
@ -4943,6 +4949,12 @@
|
||||||
"integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==",
|
"integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==",
|
||||||
"dev": true
|
"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": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ctrl/tinycolor": "^3.1.0",
|
"@ctrl/tinycolor": "^3.1.0",
|
||||||
"@types/mocha": "^5.2.0",
|
"@types/mocha": "^5.2.0",
|
||||||
|
"@types/moo": "^0.5.3",
|
||||||
"@types/node": "^13.9.3",
|
"@types/node": "^13.9.3",
|
||||||
"@types/vscode": "^1.32.0",
|
"@types/vscode": "^1.32.0",
|
||||||
"@zeit/ncc": "^0.22.0",
|
"@zeit/ncc": "^0.22.0",
|
||||||
|
@ -93,6 +94,7 @@
|
||||||
"line-column": "^1.0.2",
|
"line-column": "^1.0.2",
|
||||||
"mitt": "^1.2.0",
|
"mitt": "^1.2.0",
|
||||||
"mkdirp": "^1.0.3",
|
"mkdirp": "^1.0.3",
|
||||||
|
"moo": "^0.5.1",
|
||||||
"pkg-up": "^3.1.0",
|
"pkg-up": "^3.1.0",
|
||||||
"postcss": "^7.0.27",
|
"postcss": "^7.0.27",
|
||||||
"postcss-selector-parser": "^6.0.2",
|
"postcss-selector-parser": "^6.0.2",
|
||||||
|
|
|
@ -12,7 +12,7 @@ import removeMeta from '../util/removeMeta'
|
||||||
import { getColor, getColorFromValue } from '../util/color'
|
import { getColor, getColorFromValue } from '../util/color'
|
||||||
import { isHtmlContext } from '../util/html'
|
import { isHtmlContext } from '../util/html'
|
||||||
import { isCssContext } from '../util/css'
|
import { isCssContext } from '../util/css'
|
||||||
import { findLast, findJsxStrings, arrFindLast } from '../util/find'
|
import { findLast } from '../util/find'
|
||||||
import { stringifyConfigValue, stringifyCss } from '../util/stringify'
|
import { stringifyConfigValue, stringifyCss } from '../util/stringify'
|
||||||
import { stringifyScreen, Screen } from '../util/screens'
|
import { stringifyScreen, Screen } from '../util/screens'
|
||||||
import isObject from '../../util/isObject'
|
import isObject from '../../util/isObject'
|
||||||
|
@ -24,6 +24,10 @@ import { naturalExpand } from '../util/naturalExpand'
|
||||||
import semver from 'semver'
|
import semver from 'semver'
|
||||||
import { docsUrl } from '../util/docsUrl'
|
import { docsUrl } from '../util/docsUrl'
|
||||||
import { ensureArray } from '../../util/array'
|
import { ensureArray } from '../../util/array'
|
||||||
|
import {
|
||||||
|
getClassAttributeLexer,
|
||||||
|
getComputedClassAttributeLexer,
|
||||||
|
} from '../util/lexers'
|
||||||
|
|
||||||
function completionsFromClassList(
|
function completionsFromClassList(
|
||||||
state: State,
|
state: State,
|
||||||
|
@ -122,24 +126,31 @@ function provideClassAttributeCompletions(
|
||||||
end: position,
|
end: position,
|
||||||
})
|
})
|
||||||
|
|
||||||
const match = findLast(/\bclass(?:Name)?=(?<initial>['"`{])/gi, str)
|
const match = findLast(/[\s:]class(?:Name)?=['"`{]/gi, str)
|
||||||
|
|
||||||
if (match === null) {
|
if (match === null) {
|
||||||
return 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, {
|
return completionsFromClassList(state, classList, {
|
||||||
start: {
|
start: {
|
||||||
line: position.line,
|
line: position.line,
|
||||||
|
@ -148,22 +159,11 @@ function provideClassAttributeCompletions(
|
||||||
end: position,
|
end: position,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return null
|
} catch (_) {}
|
||||||
}
|
|
||||||
|
|
||||||
if (rest.indexOf(match.groups.initial) !== -1) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return completionsFromClassList(state, rest, {
|
|
||||||
start: {
|
|
||||||
line: position.line,
|
|
||||||
character: position.character - rest.length,
|
|
||||||
},
|
|
||||||
end: position,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function provideAtApplyCompletions(
|
function provideAtApplyCompletions(
|
||||||
state: State,
|
state: State,
|
||||||
{ context, position, textDocument }: CompletionParams
|
{ context, position, textDocument }: CompletionParams
|
||||||
|
|
|
@ -19,57 +19,6 @@ export function findLast(re: RegExp, str: string): RegExpMatchArray {
|
||||||
return matches[matches.length - 1]
|
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(
|
export function findClassNamesInRange(
|
||||||
doc: TextDocument,
|
doc: TextDocument,
|
||||||
range: Range
|
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