Skip to content

Commit e053a6d

Browse files
authored
Show primitive value on hover (#25)
* add @types/lodash.flatten, bump @types/node * add type to generated properties * format getCurrentWord * fix off-by-one error in getCurrentWord * add getCssVariable function * add getVariableInfo function * add get-documentation-link util * render markdown summary on hover * remove unnecessary variable * simplified documentation link
1 parent c60285f commit e053a6d

10 files changed

Lines changed: 328 additions & 124 deletions

package-lock.json

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

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
},
3939
"devDependencies": {
4040
"@github/prettier-config": "^0.0.6",
41-
"@types/node": "^18.7.13",
41+
"@types/lodash.flatten": "^4.4.9",
42+
"@types/node": "^24.0.0",
4243
"@types/vscode": "^1.63.0",
4344
"@typescript-eslint/eslint-plugin": "^5.35.1",
4445
"@typescript-eslint/parser": "^5.35.1",

src/data/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export {propertiesMap} from './properties-generated'
1+
export {propertiesMap, type Property} from './properties-generated'
22
export {aliases} from './aliases'
33
export {type Suggestion} from './rules'

src/data/precompile.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ fs.writeFileSync(
3030
filePath,
3131
`// Generated file, do not edit manually. Run 'npm run precompile' to regenerate.
3232
33-
export const propertiesMap = ${JSON.stringify(propertiesMapFromRules, null, 2)}
33+
export type Property = {
34+
name: string
35+
value?: string
36+
kind: 'base' | 'functional'
37+
type: string
38+
}
39+
40+
export const propertiesMap: Record<string, Property[]> = ${JSON.stringify(propertiesMapFromRules, null, 2)}
3441
`,
3542
)

src/data/properties-generated.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
// Generated file, do not edit manually. Run 'npm run precompile' to regenerate.
22

3-
export const propertiesMap = {
3+
export type Property = {
4+
name: string
5+
value?: string
6+
kind: 'base' | 'functional'
7+
type: string
8+
}
9+
10+
export const propertiesMap: Record<string, Property[]> = {
411
"padding": [
512
{
613
"name": "--base-size-112",

src/language-server.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import camelCase from 'lodash.camelcase'
1616
import {getCurrentWord} from './utils/get-current-word'
1717
import {isColor} from './utils/is-color'
1818
import {getSuggestions} from './suggestions'
19+
import {getCssVariable} from './utils/get-css-variable'
20+
import {getVariableInfo} from './utils/get-variable-info'
21+
import {getDocumentationLink} from './utils/get-documentation-link'
1922

2023
// Create a connection for the server, using Node's IPC as a transport.
2124
// Also include all preview / proposed LSP features.
@@ -152,19 +155,40 @@ connection.onHover(params => {
152155
if (!doc) return null
153156

154157
const offset = doc.offsetAt(params.position)
155-
const currentWord = getCurrentWord(doc, offset).slice(1)
156-
if (!currentWord) return null
158+
const variableName = getCssVariable(doc, offset)
157159

158-
const currentVariable = null
159-
// TODO: replace this with lookup from styleLint output
160-
// flatten(Object.values(properties)).find(variable => variable.name === currentWord)
160+
if (!variableName) return null
161+
162+
const variableInfo = getVariableInfo(variableName)
163+
164+
if (!variableInfo) return null
165+
166+
let markdown = `**\`${variableInfo.name}\`**\n\n`
167+
markdown += `\n---\n\n`
168+
169+
markdown += `- **Kind:** [${variableInfo.kind}](https://primer.style/product/primitives/token-names/#${variableInfo.kind})\n`
170+
markdown += `- **Type:** ${variableInfo.type}\n`
171+
markdown += `\n---\n\n`
161172

162-
if (currentVariable) {
163-
// TODO: would be nice to put docs link here as well
164-
return {contents: currentVariable.value} as Hover
173+
if (variableInfo.themeValues && Object.keys(variableInfo.themeValues).length > 0) {
174+
for (const [themeName, value] of Object.entries(variableInfo.themeValues)) {
175+
markdown += `- **${themeName}:** \`${value}\`\n`
176+
}
177+
} else {
178+
markdown += `**Value:** \`${variableInfo.value}\`\n\n`
165179
}
166180

167-
return null
181+
markdown += `\n---\n\n`
182+
183+
const docLink = getDocumentationLink(variableInfo.type)
184+
markdown += `[View documentation](${docLink})\n`
185+
186+
return {
187+
contents: {
188+
kind: 'markdown',
189+
value: markdown,
190+
},
191+
} as Hover
168192
})
169193

170194
connection.onDefinition(params => {

src/utils/get-css-variable.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {TextDocument} from 'vscode-languageserver-textdocument'
2+
import {getCurrentWord} from './get-current-word'
3+
4+
/**
5+
* Extracts a CSS variable name from the current cursor position.
6+
* @returns The CSS variable name (e.g., '--color-red') or null if not found
7+
*/
8+
export function getCssVariable(document: TextDocument, offset: number): `--${string}` | null {
9+
const currentWord = getCurrentWord(document, offset)
10+
11+
const match = currentWord.match(/--[a-zA-Z0-9-]+/)
12+
13+
if (!match) return null
14+
15+
return match[0] as `--${string}`
16+
}

src/utils/get-current-word.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { TextDocument } from 'vscode-languageserver-textdocument';
1+
import {TextDocument} from 'vscode-languageserver-textdocument'
22

33
export function getCurrentWord(document: TextDocument, offset: number): string {
4-
let left = offset - 1;
5-
let right = offset + 1;
6-
const text = document.getText();
4+
let left = offset - 1
5+
let right = offset + 1
6+
const text = document.getText()
77

88
while (left >= 0 && ' \t\n\r":{[()]},*>+'.indexOf(text.charAt(left)) === -1) {
9-
left--;
9+
left--
1010
}
1111

1212
while (right <= text.length && ' \t\n\r":{[()]},*>+'.indexOf(text.charAt(right)) === -1) {
13-
right++;
13+
right++
1414
}
1515

16-
return text.substring(left, right);
16+
return text.substring(left + 1, right)
1717
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const DOC_CONFIG = {
2+
color: 'https://primer.style/product/primitives/color/',
3+
dimension: 'https://primer.style/product/primitives/size/',
4+
typography: 'https://primer.style/product/primitives/typography/',
5+
}
6+
7+
export const getDocumentationLink = (variableName: string, type: string): string => {
8+
const typeKey = type === 'font' ? 'typography' : type
9+
return DOC_CONFIG[typeKey] ?? 'https://primer.style/product/primitives/'
10+
}

src/utils/get-variable-info.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import flatten from 'lodash.flatten'
2+
import {propertiesMap, type Property} from '../data'
3+
import lightTheme from '@primer/primitives/dist/styleLint/functional/themes/light.json'
4+
import darkTheme from '@primer/primitives/dist/styleLint/functional/themes/dark.json'
5+
import darkDimmedTheme from '@primer/primitives/dist/styleLint/functional/themes/dark-dimmed.json'
6+
import darkHighContrastTheme from '@primer/primitives/dist/styleLint/functional/themes/dark-high-contrast.json'
7+
import lightHighContrastTheme from '@primer/primitives/dist/styleLint/functional/themes/light-high-contrast.json'
8+
import {isColor} from './is-color'
9+
10+
const allThemes = {
11+
Light: lightTheme,
12+
Dark: darkTheme,
13+
'Dark Dimmed': darkDimmedTheme,
14+
'Dark High Contrast': darkHighContrastTheme,
15+
'Light High Contrast': lightHighContrastTheme,
16+
}
17+
18+
export type VariableInfo = Property & {
19+
themeValues?: Record<string, string>
20+
}
21+
22+
export function getVariableInfo(variableName: `--${string}`): VariableInfo | null {
23+
const cleanName = variableName.slice(2)
24+
25+
const allVariables = flatten(Object.values(propertiesMap))
26+
const variable = allVariables.find(v => v.name === variableName)
27+
28+
if (!variable) return null
29+
30+
const info: VariableInfo = {...variable}
31+
32+
if (info.type === 'color' || (info.value && isColor(info.value))) {
33+
info.themeValues = {}
34+
35+
for (const [themeName, themeData] of Object.entries(allThemes)) {
36+
const themeVariable = themeData[cleanName]
37+
if (themeVariable && themeVariable.$value) {
38+
info.themeValues[themeName] = themeVariable.$value
39+
}
40+
}
41+
42+
// If the value is just a placeholder (#), use the first theme value as display value
43+
if (info.value === '#' && info.themeValues && Object.keys(info.themeValues).length > 0) {
44+
const firstThemeValue = Object.values(info.themeValues)[0]
45+
info.value = firstThemeValue
46+
}
47+
}
48+
49+
return info
50+
}

0 commit comments

Comments
 (0)