use lexer for class attribute completions

master
Brad Cornes 2020-05-16 19:10:17 +01:00
parent c78faee425
commit b5daeb43c3
6 changed files with 112 additions and 77 deletions

12
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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,20 +159,9 @@ 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(

View File

@ -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

View File

@ -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
}

View File

@ -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,
})
)