Merge branch 'next' into diagnostics
commit
8bfb6d9b67
|
@ -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",
|
||||||
|
|
|
@ -44,7 +44,8 @@
|
||||||
"source.css.less",
|
"source.css.less",
|
||||||
"source.css.postcss",
|
"source.css.postcss",
|
||||||
"source.vue",
|
"source.vue",
|
||||||
"source.svelte"
|
"source.svelte",
|
||||||
|
"text.html"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -75,6 +76,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 +95,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",
|
||||||
|
|
|
@ -31,10 +31,12 @@ export const DEFAULT_LANGUAGES = [
|
||||||
'sass',
|
'sass',
|
||||||
'scss',
|
'scss',
|
||||||
'stylus',
|
'stylus',
|
||||||
|
'sugarss',
|
||||||
// js
|
// js
|
||||||
'javascript',
|
'javascript',
|
||||||
'javascriptreact',
|
'javascriptreact',
|
||||||
'reason',
|
'reason',
|
||||||
|
'typescript',
|
||||||
'typescriptreact',
|
'typescriptreact',
|
||||||
// mixed
|
// mixed
|
||||||
'vue',
|
'vue',
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { getClassNameParts } from '../util/getClassNameAtPosition'
|
||||||
const dlv = require('dlv')
|
const dlv = require('dlv')
|
||||||
|
|
||||||
function provideCssDiagnostics(state: State, document: TextDocument): void {
|
function provideCssDiagnostics(state: State, document: TextDocument): void {
|
||||||
const classNames = findClassNamesInRange(document)
|
const classNames = findClassNamesInRange(document, undefined, 'css')
|
||||||
|
|
||||||
let diagnostics: Diagnostic[] = classNames
|
let diagnostics: Diagnostic[] = classNames
|
||||||
.map(({ className, range }) => {
|
.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 { Hover, TextDocumentPositionParams } from 'vscode-languageserver'
|
||||||
import {
|
import { getClassNameParts } from '../util/getClassNameAtPosition'
|
||||||
getClassNameAtPosition,
|
|
||||||
getClassNameParts,
|
|
||||||
} from '../util/getClassNameAtPosition'
|
|
||||||
import { stringifyCss, stringifyConfigValue } from '../util/stringify'
|
import { stringifyCss, stringifyConfigValue } from '../util/stringify'
|
||||||
const dlv = require('dlv')
|
const dlv = require('dlv')
|
||||||
import { isHtmlContext } from '../util/html'
|
|
||||||
import { isCssContext } from '../util/css'
|
import { isCssContext } from '../util/css'
|
||||||
import { isJsContext } from '../util/js'
|
import { findClassNameAtPosition } from '../util/find'
|
||||||
import { isWithinRange } from '../util/isWithinRange'
|
|
||||||
import { findClassNamesInRange } from '../util/find'
|
|
||||||
|
|
||||||
export function provideHover(
|
export function provideHover(
|
||||||
state: State,
|
state: State,
|
||||||
|
@ -75,68 +69,26 @@ function provideCssHelperHover(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function provideClassAttributeHover(
|
function provideClassNameHover(
|
||||||
state: State,
|
state: State,
|
||||||
{ textDocument, position }: TextDocumentPositionParams
|
{ textDocument, position }: TextDocumentPositionParams
|
||||||
): Hover {
|
): Hover {
|
||||||
let doc = state.editor.documents.get(textDocument.uri)
|
let doc = state.editor.documents.get(textDocument.uri)
|
||||||
|
|
||||||
if (
|
let className = findClassNameAtPosition(state, doc, position)
|
||||||
!isHtmlContext(state, doc, position) &&
|
if (className === null) return null
|
||||||
!isJsContext(state, doc, position)
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
|
|
||||||
let hovered = getClassNameAtPosition(doc, position)
|
const parts = getClassNameParts(state, className.className)
|
||||||
if (!hovered) return null
|
|
||||||
|
|
||||||
return classNameToHover(state, hovered)
|
|
||||||
}
|
|
||||||
|
|
||||||
function classNameToHover(
|
|
||||||
state: State,
|
|
||||||
{ className, range }: DocumentClassName
|
|
||||||
): Hover {
|
|
||||||
const parts = getClassNameParts(state, className)
|
|
||||||
if (!parts) return null
|
if (!parts) return null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contents: {
|
contents: {
|
||||||
language: 'css',
|
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 { TextDocument, Position } from 'vscode-languageserver'
|
||||||
import { isInsideTag, isVueDoc, isSvelteDoc } from './html'
|
import { isInsideTag, isVueDoc, isSvelteDoc, isHtmlDoc } from './html'
|
||||||
import { State } from './state'
|
import { State } from './state'
|
||||||
|
|
||||||
export const CSS_LANGUAGES = [
|
export const CSS_LANGUAGES = [
|
||||||
|
@ -9,6 +9,7 @@ export const CSS_LANGUAGES = [
|
||||||
'sass',
|
'sass',
|
||||||
'scss',
|
'scss',
|
||||||
'stylus',
|
'stylus',
|
||||||
|
'sugarss',
|
||||||
]
|
]
|
||||||
|
|
||||||
export function isCssDoc(state: State, doc: TextDocument): boolean {
|
export function isCssDoc(state: State, doc: TextDocument): boolean {
|
||||||
|
@ -28,7 +29,7 @@ export function isCssContext(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVueDoc(doc) || isSvelteDoc(doc)) {
|
if (isHtmlDoc(state, doc) || isVueDoc(doc) || isSvelteDoc(doc)) {
|
||||||
let str = doc.getText({
|
let str = doc.getText({
|
||||||
start: { line: 0, character: 0 },
|
start: { line: 0, character: 0 },
|
||||||
end: position,
|
end: position,
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { TextDocument, Range, Position } from 'vscode-languageserver'
|
import { TextDocument, Range, Position } from 'vscode-languageserver'
|
||||||
import { DocumentClassName, DocumentClassList } from './state'
|
import { DocumentClassName, DocumentClassList, State } from './state'
|
||||||
import lineColumn from 'line-column'
|
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[] {
|
export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
|
||||||
let match: RegExpMatchArray
|
let match: RegExpMatchArray
|
||||||
|
@ -19,62 +24,12 @@ 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,
|
||||||
|
mode?: 'html' | 'css'
|
||||||
): DocumentClassName[] {
|
): DocumentClassName[] {
|
||||||
const classLists = findClassListsInRange(doc, range)
|
const classLists = findClassListsInRange(doc, range, mode)
|
||||||
return [].concat.apply(
|
return [].concat.apply(
|
||||||
[],
|
[],
|
||||||
classLists.map(({ classList, range }) => {
|
classLists.map(({ classList, range }) => {
|
||||||
|
@ -109,7 +64,7 @@ export function findClassNamesInRange(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findClassListsInRange(
|
export function findClassListsInCssRange(
|
||||||
doc: TextDocument,
|
doc: TextDocument,
|
||||||
range?: Range
|
range?: Range
|
||||||
): DocumentClassList[] {
|
): 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 {
|
function indexToPosition(str: string, index: number): Position {
|
||||||
const { line, col } = lineColumn(str + '\n', index)
|
const { line, col } = lineColumn(str + '\n', index)
|
||||||
return { line: line - 1, character: col - 1 }
|
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',
|
'javascript',
|
||||||
'javascriptreact',
|
'javascriptreact',
|
||||||
'reason',
|
'reason',
|
||||||
|
'typescript',
|
||||||
'typescriptreact',
|
'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