update hover provider to use lexer for html class attribute hovers

master
Brad Cornes 2020-05-17 17:13:14 +01:00
parent 53481192eb
commit 51a1050a26
2 changed files with 161 additions and 64 deletions

View File

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

View File

@ -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
@ -21,9 +26,10 @@ export function findLast(re: RegExp, str: string): RegExpMatchArray {
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 }) => {
@ -58,7 +64,7 @@ export function findClassNamesInRange(
) )
} }
export function findClassListsInRange( export function findClassListsInCssRange(
doc: TextDocument, doc: TextDocument,
range: Range range: Range
): DocumentClassList[] { ): DocumentClassList[] {
@ -87,7 +93,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
}