From 51a1050a2648d789282fc38b497551a9c17d3576 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Sun, 17 May 2020 17:13:14 +0100 Subject: [PATCH] update hover provider to use lexer for html class attribute hovers --- src/lsp/providers/hoverProvider.ts | 72 +++----------- src/lsp/util/find.ts | 153 ++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 64 deletions(-) diff --git a/src/lsp/providers/hoverProvider.ts b/src/lsp/providers/hoverProvider.ts index 7e27d9a..a9010a3 100644 --- a/src/lsp/providers/hoverProvider.ts +++ b/src/lsp/providers/hoverProvider.ts @@ -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) - ) -} diff --git a/src/lsp/util/find.ts b/src/lsp/util/find.ts index bd770aa..2fa3895 100644 --- a/src/lsp/util/find.ts +++ b/src/lsp/util/find.ts @@ -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 @@ -21,9 +26,10 @@ export function findLast(re: RegExp, str: string): RegExpMatchArray { 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 }) => { @@ -58,7 +64,7 @@ export function findClassNamesInRange( ) } -export function findClassListsInRange( +export function findClassListsInCssRange( doc: TextDocument, range: Range ): 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 { 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 +}