Skip to content

Commit 51a1050

Browse files
author
Brad Cornes
committed
update hover provider to use lexer for html class attribute hovers
1 parent 5348119 commit 51a1050

File tree

2 files changed

+161
-64
lines changed

2 files changed

+161
-64
lines changed

src/lsp/providers/hoverProvider.ts

+12-60
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
1-
import { State, DocumentClassName } from '../util/state'
1+
import { State } from '../util/state'
22
import { Hover, TextDocumentPositionParams } from 'vscode-languageserver'
3-
import {
4-
getClassNameAtPosition,
5-
getClassNameParts,
6-
} from '../util/getClassNameAtPosition'
3+
import { getClassNameParts } from '../util/getClassNameAtPosition'
74
import { stringifyCss, stringifyConfigValue } from '../util/stringify'
85
const dlv = require('dlv')
9-
import { isHtmlContext } from '../util/html'
106
import { isCssContext } from '../util/css'
11-
import { isJsContext } from '../util/js'
12-
import { isWithinRange } from '../util/isWithinRange'
13-
import { findClassNamesInRange } from '../util/find'
7+
import { findClassNameAtPosition } from '../util/find'
148

159
export function provideHover(
1610
state: State,
@@ -75,68 +69,26 @@ function provideCssHelperHover(
7569
}
7670
}
7771

78-
function provideClassAttributeHover(
72+
function provideClassNameHover(
7973
state: State,
8074
{ textDocument, position }: TextDocumentPositionParams
8175
): Hover {
8276
let doc = state.editor.documents.get(textDocument.uri)
8377

84-
if (
85-
!isHtmlContext(state, doc, position) &&
86-
!isJsContext(state, doc, position)
87-
)
88-
return null
89-
90-
let hovered = getClassNameAtPosition(doc, position)
91-
if (!hovered) return null
78+
let className = findClassNameAtPosition(state, doc, position)
79+
if (className === null) return null
9280

93-
return classNameToHover(state, hovered)
94-
}
95-
96-
function classNameToHover(
97-
state: State,
98-
{ className, range }: DocumentClassName
99-
): Hover {
100-
const parts = getClassNameParts(state, className)
81+
const parts = getClassNameParts(state, className.className)
10182
if (!parts) return null
10283

10384
return {
10485
contents: {
10586
language: 'css',
106-
value: stringifyCss(className, dlv(state.classNames.classNames, parts)),
87+
value: stringifyCss(
88+
className.className,
89+
dlv(state.classNames.classNames, parts)
90+
),
10791
},
108-
range,
92+
range: className.range,
10993
}
11094
}
111-
112-
function provideAtApplyHover(
113-
state: State,
114-
{ textDocument, position }: TextDocumentPositionParams
115-
): Hover {
116-
let doc = state.editor.documents.get(textDocument.uri)
117-
118-
if (!isCssContext(state, doc, position)) return null
119-
120-
const classNames = findClassNamesInRange(doc, {
121-
start: { line: Math.max(position.line - 10, 0), character: 0 },
122-
end: { line: position.line + 10, character: 0 },
123-
})
124-
125-
const className = classNames.find(({ range }) =>
126-
isWithinRange(position, range)
127-
)
128-
129-
if (!className) return null
130-
131-
return classNameToHover(state, className)
132-
}
133-
134-
function provideClassNameHover(
135-
state: State,
136-
params: TextDocumentPositionParams
137-
): Hover {
138-
return (
139-
provideClassAttributeHover(state, params) ||
140-
provideAtApplyHover(state, params)
141-
)
142-
}

src/lsp/util/find.ts

+149-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { TextDocument, Range, Position } from 'vscode-languageserver'
2-
import { DocumentClassName, DocumentClassList } from './state'
2+
import { DocumentClassName, DocumentClassList, State } from './state'
33
import lineColumn from 'line-column'
4+
import { isCssContext } from './css'
5+
import { isHtmlContext } from './html'
6+
import { isWithinRange } from './isWithinRange'
7+
import { isJsContext } from './js'
8+
import { getClassAttributeLexer } from './lexers'
49

510
export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
611
let match: RegExpMatchArray
@@ -21,9 +26,10 @@ export function findLast(re: RegExp, str: string): RegExpMatchArray {
2126

2227
export function findClassNamesInRange(
2328
doc: TextDocument,
24-
range: Range
29+
range: Range,
30+
mode: 'html' | 'css'
2531
): DocumentClassName[] {
26-
const classLists = findClassListsInRange(doc, range)
32+
const classLists = findClassListsInRange(doc, range, mode)
2733
return [].concat.apply(
2834
[],
2935
classLists.map(({ classList, range }) => {
@@ -58,7 +64,7 @@ export function findClassNamesInRange(
5864
)
5965
}
6066

61-
export function findClassListsInRange(
67+
export function findClassListsInCssRange(
6268
doc: TextDocument,
6369
range: Range
6470
): DocumentClassList[] {
@@ -87,7 +93,146 @@ export function findClassListsInRange(
8793
})
8894
}
8995

96+
export function findClassListsInHtmlRange(
97+
doc: TextDocument,
98+
range: Range
99+
): DocumentClassList[] {
100+
const text = doc.getText(range)
101+
const matches = findAll(/[\s:]class(?:Name)?=['"`{]/g, text)
102+
const result: DocumentClassList[] = []
103+
104+
matches.forEach((match) => {
105+
const subtext = text.substr(match.index + match[0].length - 1, 200)
106+
107+
let lexer = getClassAttributeLexer()
108+
lexer.reset(subtext)
109+
110+
let classLists: { value: string; offset: number }[] = []
111+
let token: moo.Token
112+
let currentClassList: { value: string; offset: number }
113+
114+
try {
115+
for (let token of lexer) {
116+
if (token.type === 'classlist') {
117+
if (currentClassList) {
118+
currentClassList.value += token.value
119+
} else {
120+
currentClassList = {
121+
value: token.value,
122+
offset: token.offset,
123+
}
124+
}
125+
} else {
126+
if (currentClassList) {
127+
classLists.push({
128+
value: currentClassList.value,
129+
offset: currentClassList.offset,
130+
})
131+
}
132+
currentClassList = undefined
133+
}
134+
}
135+
} catch (_) {}
136+
137+
if (currentClassList) {
138+
classLists.push({
139+
value: currentClassList.value,
140+
offset: currentClassList.offset,
141+
})
142+
}
143+
144+
result.push(
145+
...classLists
146+
.map(({ value, offset }) => {
147+
if (value.trim() === '') {
148+
return null
149+
}
150+
151+
const before = value.match(/^\s*/)
152+
const beforeOffset = before === null ? 0 : before[0].length
153+
const after = value.match(/\s*$/)
154+
const afterOffset = after === null ? 0 : -after[0].length
155+
156+
const start = indexToPosition(
157+
text,
158+
match.index + match[0].length - 1 + offset + beforeOffset
159+
)
160+
const end = indexToPosition(
161+
text,
162+
match.index +
163+
match[0].length -
164+
1 +
165+
offset +
166+
value.length +
167+
afterOffset
168+
)
169+
170+
return {
171+
classList: value,
172+
range: {
173+
start: {
174+
line: range.start.line + start.line,
175+
character: range.start.character + start.character,
176+
},
177+
end: {
178+
line: range.start.line + end.line,
179+
character: range.start.character + end.character,
180+
},
181+
},
182+
}
183+
})
184+
.filter((x) => x !== null)
185+
)
186+
})
187+
188+
return result
189+
}
190+
191+
export function findClassListsInRange(
192+
doc: TextDocument,
193+
range: Range,
194+
mode: 'html' | 'css'
195+
): DocumentClassList[] {
196+
if (mode === 'css') {
197+
return findClassListsInCssRange(doc, range)
198+
}
199+
return findClassListsInHtmlRange(doc, range)
200+
}
201+
90202
function indexToPosition(str: string, index: number): Position {
91203
const { line, col } = lineColumn(str + '\n', index)
92204
return { line: line - 1, character: col - 1 }
93205
}
206+
207+
export function findClassNameAtPosition(
208+
state: State,
209+
doc: TextDocument,
210+
position: Position
211+
): DocumentClassName {
212+
let classNames = []
213+
const searchRange = {
214+
start: { line: Math.max(position.line - 10, 0), character: 0 },
215+
end: { line: position.line + 10, character: 0 },
216+
}
217+
218+
if (isCssContext(state, doc, position)) {
219+
classNames = findClassNamesInRange(doc, searchRange, 'css')
220+
} else if (
221+
isHtmlContext(state, doc, position) ||
222+
isJsContext(state, doc, position)
223+
) {
224+
classNames = findClassNamesInRange(doc, searchRange, 'html')
225+
}
226+
227+
if (classNames.length === 0) {
228+
return null
229+
}
230+
231+
const className = classNames.find(({ range }) =>
232+
isWithinRange(position, range)
233+
)
234+
235+
if (!className) return null
236+
237+
return className
238+
}

0 commit comments

Comments
 (0)