Merge branch 'next' into diagnostics
commit
8bfb6d9b67
|
@ -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",
|
||||
|
|
|
@ -44,7 +44,8 @@
|
|||
"source.css.less",
|
||||
"source.css.postcss",
|
||||
"source.vue",
|
||||
"source.svelte"
|
||||
"source.svelte",
|
||||
"text.html"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
@ -75,6 +76,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 +95,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",
|
||||
|
|
|
@ -31,10 +31,12 @@ export const DEFAULT_LANGUAGES = [
|
|||
'sass',
|
||||
'scss',
|
||||
'stylus',
|
||||
'sugarss',
|
||||
// js
|
||||
'javascript',
|
||||
'javascriptreact',
|
||||
'reason',
|
||||
'typescript',
|
||||
'typescriptreact',
|
||||
// mixed
|
||||
'vue',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -10,7 +10,7 @@ import { getClassNameParts } from '../util/getClassNameAtPosition'
|
|||
const dlv = require('dlv')
|
||||
|
||||
function provideCssDiagnostics(state: State, document: TextDocument): void {
|
||||
const classNames = findClassNamesInRange(document)
|
||||
const classNames = findClassNamesInRange(document, undefined, 'css')
|
||||
|
||||
let diagnostics: Diagnostic[] = classNames
|
||||
.map(({ className, range }) => {
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
import { State, DocumentClassName } from '../util/state'
|
||||
import { State } from '../util/state'
|
||||
import { Hover, TextDocumentPositionParams } from 'vscode-languageserver'
|
||||
import {
|
||||
getClassNameAtPosition,
|
||||
getClassNameParts,
|
||||
} from '../util/getClassNameAtPosition'
|
||||
import { getClassNameParts } from '../util/getClassNameAtPosition'
|
||||
import { stringifyCss, stringifyConfigValue } from '../util/stringify'
|
||||
const dlv = require('dlv')
|
||||
import { isHtmlContext } from '../util/html'
|
||||
import { isCssContext } from '../util/css'
|
||||
import { isJsContext } from '../util/js'
|
||||
import { isWithinRange } from '../util/isWithinRange'
|
||||
import { findClassNamesInRange } from '../util/find'
|
||||
import { findClassNameAtPosition } from '../util/find'
|
||||
|
||||
export function provideHover(
|
||||
state: State,
|
||||
|
@ -75,68 +69,26 @@ function provideCssHelperHover(
|
|||
}
|
||||
}
|
||||
|
||||
function provideClassAttributeHover(
|
||||
function provideClassNameHover(
|
||||
state: State,
|
||||
{ textDocument, position }: TextDocumentPositionParams
|
||||
): Hover {
|
||||
let doc = state.editor.documents.get(textDocument.uri)
|
||||
|
||||
if (
|
||||
!isHtmlContext(state, doc, position) &&
|
||||
!isJsContext(state, doc, position)
|
||||
)
|
||||
return null
|
||||
let className = findClassNameAtPosition(state, doc, position)
|
||||
if (className === null) return null
|
||||
|
||||
let hovered = getClassNameAtPosition(doc, position)
|
||||
if (!hovered) return null
|
||||
|
||||
return classNameToHover(state, hovered)
|
||||
}
|
||||
|
||||
function classNameToHover(
|
||||
state: State,
|
||||
{ className, range }: DocumentClassName
|
||||
): Hover {
|
||||
const parts = getClassNameParts(state, className)
|
||||
const parts = getClassNameParts(state, className.className)
|
||||
if (!parts) return null
|
||||
|
||||
return {
|
||||
contents: {
|
||||
language: 'css',
|
||||
value: stringifyCss(className, dlv(state.classNames.classNames, parts)),
|
||||
value: stringifyCss(
|
||||
className.className,
|
||||
dlv(state.classNames.classNames, parts)
|
||||
),
|
||||
},
|
||||
range,
|
||||
range: className.range,
|
||||
}
|
||||
}
|
||||
|
||||
function provideAtApplyHover(
|
||||
state: State,
|
||||
{ textDocument, position }: TextDocumentPositionParams
|
||||
): Hover {
|
||||
let doc = state.editor.documents.get(textDocument.uri)
|
||||
|
||||
if (!isCssContext(state, doc, position)) return null
|
||||
|
||||
const classNames = findClassNamesInRange(doc, {
|
||||
start: { line: Math.max(position.line - 10, 0), character: 0 },
|
||||
end: { line: position.line + 10, character: 0 },
|
||||
})
|
||||
|
||||
const className = classNames.find(({ range }) =>
|
||||
isWithinRange(position, range)
|
||||
)
|
||||
|
||||
if (!className) return null
|
||||
|
||||
return classNameToHover(state, className)
|
||||
}
|
||||
|
||||
function provideClassNameHover(
|
||||
state: State,
|
||||
params: TextDocumentPositionParams
|
||||
): Hover {
|
||||
return (
|
||||
provideClassAttributeHover(state, params) ||
|
||||
provideAtApplyHover(state, params)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { TextDocument, Position } from 'vscode-languageserver'
|
||||
import { isInsideTag, isVueDoc, isSvelteDoc } from './html'
|
||||
import { isInsideTag, isVueDoc, isSvelteDoc, isHtmlDoc } from './html'
|
||||
import { State } from './state'
|
||||
|
||||
export const CSS_LANGUAGES = [
|
||||
|
@ -9,6 +9,7 @@ export const CSS_LANGUAGES = [
|
|||
'sass',
|
||||
'scss',
|
||||
'stylus',
|
||||
'sugarss',
|
||||
]
|
||||
|
||||
export function isCssDoc(state: State, doc: TextDocument): boolean {
|
||||
|
@ -28,7 +29,7 @@ export function isCssContext(
|
|||
return true
|
||||
}
|
||||
|
||||
if (isVueDoc(doc) || isSvelteDoc(doc)) {
|
||||
if (isHtmlDoc(state, doc) || isVueDoc(doc) || isSvelteDoc(doc)) {
|
||||
let str = doc.getText({
|
||||
start: { line: 0, character: 0 },
|
||||
end: position,
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { TextDocument, Range, Position } from 'vscode-languageserver'
|
||||
import { DocumentClassName, DocumentClassList } from './state'
|
||||
import { DocumentClassName, DocumentClassList, State } from './state'
|
||||
import lineColumn from 'line-column'
|
||||
import { isCssContext } from './css'
|
||||
import { isHtmlContext } from './html'
|
||||
import { isWithinRange } from './isWithinRange'
|
||||
import { isJsContext } from './js'
|
||||
import { getClassAttributeLexer } from './lexers'
|
||||
|
||||
export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
|
||||
let match: RegExpMatchArray
|
||||
|
@ -19,62 +24,12 @@ 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
|
||||
range?: Range,
|
||||
mode?: 'html' | 'css'
|
||||
): DocumentClassName[] {
|
||||
const classLists = findClassListsInRange(doc, range)
|
||||
const classLists = findClassListsInRange(doc, range, mode)
|
||||
return [].concat.apply(
|
||||
[],
|
||||
classLists.map(({ classList, range }) => {
|
||||
|
@ -109,7 +64,7 @@ export function findClassNamesInRange(
|
|||
)
|
||||
}
|
||||
|
||||
export function findClassListsInRange(
|
||||
export function findClassListsInCssRange(
|
||||
doc: TextDocument,
|
||||
range?: Range
|
||||
): DocumentClassList[] {
|
||||
|
@ -139,7 +94,146 @@ export function findClassListsInRange(
|
|||
})
|
||||
}
|
||||
|
||||
export function findClassListsInHtmlRange(
|
||||
doc: TextDocument,
|
||||
range: Range
|
||||
): DocumentClassList[] {
|
||||
const text = doc.getText(range)
|
||||
const matches = findAll(/[\s:]class(?:Name)?=['"`{]/g, text)
|
||||
const result: DocumentClassList[] = []
|
||||
|
||||
matches.forEach((match) => {
|
||||
const subtext = text.substr(match.index + match[0].length - 1, 200)
|
||||
|
||||
let lexer = getClassAttributeLexer()
|
||||
lexer.reset(subtext)
|
||||
|
||||
let classLists: { value: string; offset: number }[] = []
|
||||
let token: moo.Token
|
||||
let currentClassList: { value: string; offset: number }
|
||||
|
||||
try {
|
||||
for (let token of lexer) {
|
||||
if (token.type === 'classlist') {
|
||||
if (currentClassList) {
|
||||
currentClassList.value += token.value
|
||||
} else {
|
||||
currentClassList = {
|
||||
value: token.value,
|
||||
offset: token.offset,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (currentClassList) {
|
||||
classLists.push({
|
||||
value: currentClassList.value,
|
||||
offset: currentClassList.offset,
|
||||
})
|
||||
}
|
||||
currentClassList = undefined
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (currentClassList) {
|
||||
classLists.push({
|
||||
value: currentClassList.value,
|
||||
offset: currentClassList.offset,
|
||||
})
|
||||
}
|
||||
|
||||
result.push(
|
||||
...classLists
|
||||
.map(({ value, offset }) => {
|
||||
if (value.trim() === '') {
|
||||
return null
|
||||
}
|
||||
|
||||
const before = value.match(/^\s*/)
|
||||
const beforeOffset = before === null ? 0 : before[0].length
|
||||
const after = value.match(/\s*$/)
|
||||
const afterOffset = after === null ? 0 : -after[0].length
|
||||
|
||||
const start = indexToPosition(
|
||||
text,
|
||||
match.index + match[0].length - 1 + offset + beforeOffset
|
||||
)
|
||||
const end = indexToPosition(
|
||||
text,
|
||||
match.index +
|
||||
match[0].length -
|
||||
1 +
|
||||
offset +
|
||||
value.length +
|
||||
afterOffset
|
||||
)
|
||||
|
||||
return {
|
||||
classList: value,
|
||||
range: {
|
||||
start: {
|
||||
line: range.start.line + start.line,
|
||||
character: range.start.character + start.character,
|
||||
},
|
||||
end: {
|
||||
line: range.start.line + end.line,
|
||||
character: range.start.character + end.character,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
.filter((x) => x !== null)
|
||||
)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function findClassListsInRange(
|
||||
doc: TextDocument,
|
||||
range: Range,
|
||||
mode: 'html' | 'css'
|
||||
): DocumentClassList[] {
|
||||
if (mode === 'css') {
|
||||
return findClassListsInCssRange(doc, range)
|
||||
}
|
||||
return findClassListsInHtmlRange(doc, range)
|
||||
}
|
||||
|
||||
function indexToPosition(str: string, index: number): Position {
|
||||
const { line, col } = lineColumn(str + '\n', index)
|
||||
return { line: line - 1, character: col - 1 }
|
||||
}
|
||||
|
||||
export function findClassNameAtPosition(
|
||||
state: State,
|
||||
doc: TextDocument,
|
||||
position: Position
|
||||
): DocumentClassName {
|
||||
let classNames = []
|
||||
const searchRange = {
|
||||
start: { line: Math.max(position.line - 10, 0), character: 0 },
|
||||
end: { line: position.line + 10, character: 0 },
|
||||
}
|
||||
|
||||
if (isCssContext(state, doc, position)) {
|
||||
classNames = findClassNamesInRange(doc, searchRange, 'css')
|
||||
} else if (
|
||||
isHtmlContext(state, doc, position) ||
|
||||
isJsContext(state, doc, position)
|
||||
) {
|
||||
classNames = findClassNamesInRange(doc, searchRange, 'html')
|
||||
}
|
||||
|
||||
if (classNames.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const className = classNames.find(({ range }) =>
|
||||
isWithinRange(position, range)
|
||||
)
|
||||
|
||||
if (!className) return null
|
||||
|
||||
return className
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ export const JS_LANGUAGES = [
|
|||
'javascript',
|
||||
'javascriptreact',
|
||||
'reason',
|
||||
'typescript',
|
||||
'typescriptreact',
|
||||
]
|
||||
|
||||
|
|
|
@ -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