Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions src/language-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
// import {properties, stories} from '@primer/primitives/dist/js/intellisense'
import camelCase from 'lodash.camelcase'
import {isColor} from './utils/is-color'
import {getSuggestions} from './suggestions'
import {getSuggestions, getSuggestionsLikeVariable, type SuggestionWithSortText} from './suggestions'
import {getCssVariable} from './utils/get-css-variable'
import {getVariableInfo} from './utils/get-variable-info'
import {getDocumentation} from './documentation'
import {getCurrentWord} from './utils/get-current-word'

// Create a connection for the server, using Node's IPC as a transport.
// Also include all preview / proposed LSP features.
Expand Down Expand Up @@ -67,12 +68,25 @@
let property: string = ast.nodes[0].type === 'decl' ? camelCase(ast.nodes[0].prop) : undefined
if (!property) return []

const value: string =
ast.nodes[0].type === 'decl' ? ast.nodes[0].value.replace(');', '').replace('\n', '').trim() : undefined
Comment thread
siddharthkp marked this conversation as resolved.
Comment thread
siddharthkp marked this conversation as resolved.

const suggestedVariablesWithSortText: SuggestionWithSortText[] = []

// if the cursor is at the css variable, get getSuggestionsLikeVariable instead of getSuggestions(property)
const document = documents.get(params.textDocument.uri)
const offset = document.offsetAt(params.position)
const currentWord = getCurrentWord(document, offset)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant variable assignment. The doc variable is already retrieved at line 50. Consider reusing doc instead of fetching the document again with const document = documents.get(params.textDocument.uri).

Suggested change
const document = documents.get(params.textDocument.uri)
const offset = document.offsetAt(params.position)
const currentWord = getCurrentWord(document, offset)
const offset = doc.offsetAt(params.position)
const currentWord = getCurrentWord(doc, offset)

Copilot uses AI. Check for mistakes.
if (currentWord.includes('--') && currentWord.length > 2 /* checking it's not just -- */) {
const variableSuggestions = getSuggestionsLikeVariable(currentWord.replace('--', ''))
Comment thread
siddharthkp marked this conversation as resolved.
suggestedVariablesWithSortText.push(...variableSuggestions)
}

// TODO: for shorthands, property might be the second property like borderColor or paddingInline
// we can be smarter about this
if (property === 'padding') {
try {
// padding: block inline
const value = currentLine.split(':')[1].trim()
const [blockValue] = value.split(' ')
const blockValuePositionEnd = currentLine.indexOf(blockValue) + blockValue.length

Expand All @@ -87,7 +101,6 @@
} else if (property === 'border') {
try {
// border: width style color
const value = currentLine.split(':')[1].trim()
const [borderWidth] = value.split(' ')
const borderWidthPositionEnd = currentLine.indexOf(borderWidth) + borderWidth.length

Expand All @@ -101,7 +114,7 @@
}
}

const suggestedVariablesWithSortText = getSuggestions(property)
suggestedVariablesWithSortText.push(...getSuggestions(property))

const items = suggestedVariablesWithSortText.map(variable => {
const documentation = getDocumentation(variable.name)
Expand All @@ -114,8 +127,8 @@
typeof variable.value === 'string' && isColor(variable.value)
? CompletionItemKind.Color
: variable.kind === 'functional'
? CompletionItemKind.Field
: CompletionItemKind.Constructor,
? CompletionItemKind.Field
: CompletionItemKind.Constructor,
sortText: variable.sortText,
// this is slightly silly because what about multiple variables in one line
// like shorthands or fallbacks
Expand Down
12 changes: 11 additions & 1 deletion src/suggestions.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import {describe, it, expect} from 'vitest'
import {getSuggestions} from './suggestions'
import {getSuggestions, getSuggestionsLikeVariable} from './suggestions'

describe('Suggestions', () => {
it('returns suggestions for paddingBottom', () => {
const suggestions = getSuggestions('paddingBottom')
expect(suggestions).toMatchSnapshot()
})

it('returns suggestions for variable part: --fgColor', () => {
const suggestions = getSuggestionsLikeVariable('--fgColor')
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent API usage in test. The test passes '--fgColor' (with dashes) to getSuggestionsLikeVariable, but in the actual implementation at language-server.ts:81, the '--' prefix is stripped before calling this function (currentWord.replace('--', '')). The test should either pass 'fgColor' without dashes to match the actual usage, or the function should be documented to accept both formats.

Suggested change
const suggestions = getSuggestionsLikeVariable('--fgColor')
const suggestions = getSuggestionsLikeVariable('fgColor')

Copilot uses AI. Check for mistakes.
expect(suggestions[0].name).toBe('--fgColor-accent')
})

it.todo('returns suggestions for property: outline', () => {
const suggestions = getSuggestions('outline')
expect(suggestions[0].name).toBe('--focus-outline')
})
})
24 changes: 23 additions & 1 deletion src/suggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {propertiesMap, aliases, type Suggestion} from './data'
// eslint-disable-next-line import/no-namespace
import type * as CSS from 'csstype'

export const getSuggestions = (property: keyof CSS.Properties) => {
export type SuggestionWithSortText = Suggestion & {sortText: string}

export const getSuggestions = (property: keyof CSS.Properties): SuggestionWithSortText[] => {
// TODO: for shorthands, property might be the second property like borderColor or paddingInline
// we can be smarter about this
let suggestedVariables: Suggestion[]
Expand All @@ -24,6 +26,22 @@ export const getSuggestions = (property: keyof CSS.Properties) => {

if (!suggestedVariables) return []

const suggestedVariablesWithSortText = sortSuggestions(suggestedVariables)
return suggestedVariablesWithSortText
}

// slower than getSuggestions, use judiciously
export const getSuggestionsLikeVariable = (variableQuery: string): SuggestionWithSortText[] => {
const allVariables = flatten(Object.values(propertiesMap))
const suggestedVariables: Suggestion[] = allVariables.filter(variable => variable.name.includes(variableQuery))
Comment on lines +34 to +36
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance concern: flatten(Object.values(propertiesMap)) is executed on every call to getSuggestionsLikeVariable. Since propertiesMap is static data, consider memoizing or caching the flattened array to avoid redundant computation on each autocomplete request.

Suggested change
export const getSuggestionsLikeVariable = (variableQuery: string): SuggestionWithSortText[] => {
const allVariables = flatten(Object.values(propertiesMap))
const suggestedVariables: Suggestion[] = allVariables.filter(variable => variable.name.includes(variableQuery))
const ALL_VARIABLES: Suggestion[] = flatten(Object.values(propertiesMap))
export const getSuggestionsLikeVariable = (variableQuery: string): SuggestionWithSortText[] => {
const suggestedVariables: Suggestion[] = ALL_VARIABLES.filter(variable => variable.name.includes(variableQuery))

Copilot uses AI. Check for mistakes.
Comment thread
siddharthkp marked this conversation as resolved.

if (!suggestedVariables) return []
Comment thread
siddharthkp marked this conversation as resolved.

const suggestedVariablesWithSortText = sortSuggestions(suggestedVariables)
return suggestedVariablesWithSortText
}

const sortSuggestions = (suggestedVariables: Suggestion[]): SuggestionWithSortText[] => {
const functionalVariables = suggestedVariables.filter(variable => variable.kind === 'functional')
const baseVariables = suggestedVariables.filter(variable => variable.kind === 'base')

Expand All @@ -35,6 +53,10 @@ export const getSuggestions = (property: keyof CSS.Properties) => {
// TODO 2: contextual repetition
// if there are other variables in the same block/document
// we should take hints from them like hover state or control-small or button-primary

// TODO 3: for shorthands like outline and font, the shorthand token should come before
// the tokens for subproperties

const suggestedVariablesWithSortText = [
...functionalVariables.map((variable, index) => {
// we have to use alphabet instead of numbers to sort because it uses text
Expand Down
17 changes: 13 additions & 4 deletions src/utils/get-current-word.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import {TextDocument} from 'vscode-languageserver-textdocument'

export function getCurrentWord(document: TextDocument, offset: number): string {
let left = offset - 1
let right = offset + 1
const text = document.getText()
const delimiters = ' \t\n\r":{[()]},*>+'

// Ensure offset is within bounds
if (offset < 0 || offset > text.length) {
return ''
}

let left = offset - 1
let right = offset

while (left >= 0 && ' \t\n\r":{[()]},*>+'.indexOf(text.charAt(left)) === -1) {
// Move left to find start of word
while (left >= 0 && delimiters.indexOf(text.charAt(left)) === -1) {
left--
}

while (right <= text.length && ' \t\n\r":{[()]},*>+'.indexOf(text.charAt(right)) === -1) {
// Move right to find end of word
while (right < text.length && delimiters.indexOf(text.charAt(right)) === -1) {
right++
}

Expand Down