Skip to content

Commit a9a0983

Browse files
author
Brad Cornes
committed
add initial hover provider
1 parent adadf06 commit a9a0983

File tree

7 files changed

+1504
-680
lines changed

7 files changed

+1504
-680
lines changed

packages/tailwindcss-language-server/package-lock.json

Lines changed: 1306 additions & 659 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/tailwindcss-language-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@babel/register": "^7.9.0",
2020
"@types/node": "^13.9.3",
2121
"@zeit/ncc": "^0.22.0",
22+
"css.escape": "^1.5.1",
2223
"dlv": "^1.1.3",
2324
"glob-exec": "^0.1.1",
2425
"tailwindcss-class-names": "0.0.1",

packages/tailwindcss-language-server/src/providers/completionProvider.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { getColor, getColorFromString } from '../util/color'
1313
import { isHtmlContext } from '../util/html'
1414
import { isCssContext } from '../util/css'
1515
import { findLast, findJsxStrings, arrFindLast } from '../util/find'
16-
import { stringifyConfigValue } from '../util/stringify'
16+
import { stringifyConfigValue, stringifyCss } from '../util/stringify'
1717
import isObject from '../util/isObject'
1818

1919
function completionsFromClassList(
@@ -343,25 +343,6 @@ function stringifyDecls(obj: any): string {
343343
.join(' ')
344344
}
345345

346-
function stringifyCss(obj: any, indent: number = 0): string {
347-
let indentStr = ' '.repeat(indent)
348-
if (obj.__decls === true) {
349-
return Object.keys(removeMeta(obj))
350-
.reduce((acc, curr, i) => {
351-
return `${acc}${i === 0 ? '' : '\n'}${indentStr}${curr}: ${obj[curr]};`
352-
}, '')
353-
.trim()
354-
}
355-
return Object.keys(removeMeta(obj))
356-
.reduce((acc, curr, i) => {
357-
return `${acc}${i === 0 ? '' : '\n'}${indentStr}${curr} {\n${stringifyCss(
358-
obj[curr],
359-
indent + 2
360-
)}\n${indentStr}}`
361-
}, '')
362-
.trim()
363-
}
364-
365346
function getCssDetail(state: State, className: any): string {
366347
if (Array.isArray(className)) {
367348
return `${className.length} rules`
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { State } from '../util/state'
2+
import { Hover, TextDocumentPositionParams } from 'vscode-languageserver'
3+
import {
4+
getClassNameAtPosition,
5+
getClassNameParts,
6+
} from '../util/getClassNameAtPosition'
7+
import { stringifyCss } from '../util/stringify'
8+
const dlv = require('dlv')
9+
import escapeClassName from 'css.escape'
10+
import { isHtmlContext } from '../util/html'
11+
12+
export function provideHover(
13+
state: State,
14+
params: TextDocumentPositionParams
15+
): Hover {
16+
let doc = state.editor.documents.get(params.textDocument.uri)
17+
18+
if (isHtmlContext(doc, params.position)) {
19+
return provideClassNameHover(state, params)
20+
}
21+
22+
return null
23+
}
24+
25+
function provideClassNameHover(
26+
state: State,
27+
{ textDocument, position }: TextDocumentPositionParams
28+
): Hover {
29+
let doc = state.editor.documents.get(textDocument.uri)
30+
let hovered = getClassNameAtPosition(doc, position)
31+
if (!hovered) return null
32+
33+
const parts = getClassNameParts(state, hovered.className)
34+
if (parts === null) return null
35+
36+
return {
37+
contents: {
38+
language: 'css',
39+
value: stringifyCss(dlv(state.classNames.classNames, parts), {
40+
selector: augmentClassName(parts, state),
41+
}),
42+
},
43+
range: hovered.range,
44+
}
45+
}
46+
47+
// TODO
48+
function augmentClassName(className: string | string[], state: State): string {
49+
const parts = Array.isArray(className)
50+
? className
51+
: getClassNameParts(state, className)
52+
const obj = dlv(state.classNames.classNames, parts)
53+
const pseudo = obj.__pseudo ? obj.__pseudo.join('') : ''
54+
const scope = obj.__scope ? `${obj.__scope} ` : ''
55+
return `${scope}.${escapeClassName(parts.join(state.separator))}${pseudo}`
56+
}

packages/tailwindcss-language-server/src/server.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ import {
1313
InitializeResult,
1414
CompletionParams,
1515
CompletionList,
16+
Hover,
17+
TextDocumentPositionParams,
1618
} from 'vscode-languageserver'
1719
import getTailwindState from 'tailwindcss-class-names'
1820
import { State } from './util/state'
1921
import {
2022
provideCompletions,
2123
resolveCompletionItem,
2224
} from './providers/completionProvider'
25+
import { provideHover } from './providers/hoverProvider'
2326
import { URI } from 'vscode-uri'
2427

2528
let state: State = null
@@ -62,6 +65,7 @@ connection.onInitialize(
6265
resolveProvider: true,
6366
triggerCharacters: ['"', "'", '`', ' ', '.', '[', state.separator],
6467
},
68+
hoverProvider: true,
6569
},
6670
}
6771
}
@@ -88,4 +92,10 @@ connection.onCompletionResolve(
8892
}
8993
)
9094

95+
connection.onHover(
96+
(params: TextDocumentPositionParams): Hover => {
97+
return provideHover(state, params)
98+
}
99+
)
100+
91101
connection.listen()
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { TextDocument, Range, Position } from 'vscode-languageserver'
2+
import { State } from './state'
3+
const dlv = require('dlv')
4+
5+
export function getClassNameAtPosition(
6+
document: TextDocument,
7+
position: Position
8+
): { className: string; range: Range } {
9+
const range1: Range = {
10+
start: { line: Math.max(position.line - 5, 0), character: 0 },
11+
end: position,
12+
}
13+
const text1: string = document.getText(range1)
14+
15+
if (!/\bclass(Name)?=['"][^'"]*$/.test(text1)) return null
16+
17+
const range2: Range = {
18+
start: { line: Math.max(position.line - 5, 0), character: 0 },
19+
end: { line: position.line + 1, character: position.character },
20+
}
21+
const text2: string = document.getText(range2)
22+
23+
let str: string = text1 + text2.substr(text1.length).match(/^([^"' ]*)/)[0]
24+
let matches: RegExpMatchArray = str.match(/\bclass(Name)?=["']([^"']+)$/)
25+
26+
if (!matches) return null
27+
28+
let className: string = matches[2].split(' ').pop()
29+
if (!className) return null
30+
31+
let range: Range = {
32+
start: {
33+
line: position.line,
34+
character:
35+
position.character + str.length - text1.length - className.length,
36+
},
37+
end: {
38+
line: position.line,
39+
character: position.character + str.length - text1.length,
40+
},
41+
}
42+
43+
return { className, range }
44+
}
45+
46+
export function getClassNameParts(state: State, className: string): string[] {
47+
let separator = state.separator
48+
className = className.replace(/^\./, '')
49+
let parts: string[] = className.split(separator)
50+
51+
if (parts.length === 1) {
52+
return dlv(state.classNames.classNames, [className, '__rule']) === true
53+
? [className]
54+
: null
55+
}
56+
57+
let points = combinations('123456789'.substr(0, parts.length - 1)).map((x) =>
58+
x.split('').map((x) => parseInt(x, 10))
59+
)
60+
61+
let possibilities: string[][] = [
62+
[className],
63+
...points.map((p) => {
64+
let result = []
65+
let i = 0
66+
p.forEach((x) => {
67+
result.push(parts.slice(i, x).join('-'))
68+
i = x
69+
})
70+
result.push(parts.slice(i).join('-'))
71+
return result
72+
}),
73+
]
74+
75+
return possibilities.find((key) => {
76+
if (dlv(state.classNames.classNames, [...key, '__rule']) === true) {
77+
return true
78+
}
79+
return false
80+
})
81+
}
82+
83+
function combinations(str: string): string[] {
84+
let fn = function (active: string, rest: string, a: string[]) {
85+
if (!active && !rest) return
86+
if (!rest) {
87+
a.push(active)
88+
} else {
89+
fn(active + rest[0], rest.slice(1), a)
90+
fn(active, rest.slice(1), a)
91+
}
92+
return a
93+
}
94+
return fn('', str, [])
95+
}
Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,45 @@
1+
import removeMeta from './removeMeta'
2+
13
export function stringifyConfigValue(x: any): string {
24
if (typeof x === 'string') return x
35
if (typeof x === 'number') return x.toString()
46
if (Array.isArray(x)) {
57
return x
6-
.filter(y => typeof y === 'string')
8+
.filter((y) => typeof y === 'string')
79
.filter(Boolean)
810
.join(', ')
911
}
1012
return ''
1113
}
14+
15+
export function stringifyCss(
16+
obj: any,
17+
{ indent = 0, selector }: { indent?: number; selector?: string } = {}
18+
): string {
19+
let indentStr = '\t'.repeat(indent)
20+
if (obj.__decls === true) {
21+
let before = ''
22+
let after = ''
23+
if (selector) {
24+
before = `${indentStr}${selector} {\n`
25+
after = `\n${indentStr}}`
26+
indentStr += '\t'
27+
}
28+
return (
29+
before +
30+
Object.keys(removeMeta(obj)).reduce((acc, curr, i) => {
31+
return `${acc}${i === 0 ? '' : '\n'}${indentStr}${curr}: ${obj[curr]};`
32+
}, '') +
33+
after
34+
)
35+
}
36+
return Object.keys(removeMeta(obj)).reduce((acc, curr, i) => {
37+
return `${acc}${i === 0 ? '' : '\n'}${indentStr}${curr} {\n${stringifyCss(
38+
obj[curr],
39+
{
40+
indent: indent + 1,
41+
selector,
42+
}
43+
)}\n${indentStr}}`
44+
}, '')
45+
}

0 commit comments

Comments
 (0)