Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Binary file removed .github/content/definition.gif
Binary file not shown.
Binary file added .github/content/documentation.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ Hover over a variable to see its value. Or values in the case of colors across d

![Hover preview](https://raw.githubusercontent.com/primer/primitives-vscode-extension/refs/heads/main/.github/content/hover.gif)

## Go to definition in Docs
## Open documentation for variable

Right click -> `Go to definition` to open documentation in a preview within VSCode.
Right click -> `Open Primer documentation for variable` to open documentation in a preview within VSCode.

![Open definition in docs](https://raw.githubusercontent.com/primer/primitives-vscode-extension/refs/heads/main/.github/content/definition.gif)
![Open documentation for variable](https://raw.githubusercontent.com/primer/primitives-vscode-extension/refs/heads/main/.github/content/documentation.gif)

 

Expand Down
17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@
"activationEvents": [
"onStartupFinished"
],
"contributes": {
"menus": {
"editor/context": [
{
"command": "primer-primitives-autocomplete.openDocs",
"group": "navigation@100",
"when": "editorTextFocus && editorHasHoverProvider"
}
]
},
"commands": [
{
"command": "primer-primitives-autocomplete.openDocs",
"title": "Open Primer documentation for variable"
}
]
},
"repository": {
"type": "git",
"url": "https://github.com/primer/primitives-vscode-extension.git"
Expand Down
42 changes: 32 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {join} from 'node:path'
import {workspace, ExtensionContext, window, ViewColumn, WebviewPanel} from 'vscode'
import {workspace, ExtensionContext, window, ViewColumn, WebviewPanel, commands} from 'vscode'
import {LanguageClient, LanguageClientOptions, ServerOptions, TransportKind} from 'vscode-languageclient/node'
import {getCssVariable} from './utils/get-css-variable'
import {getVariableInfo} from './utils/get-variable-info'
import {getCurrentWord} from './utils/get-current-word'

let client: LanguageClient

Expand Down Expand Up @@ -57,19 +60,39 @@ export function activate(context: ExtensionContext) {

let panel: WebviewPanel

client.onRequest('open-docs', ({variable, openPanelIfClosed = true}) => {
if (!panel && openPanelIfClosed) {
commands.registerCommand('primer-primitives-autocomplete.openDocs', async () => {
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The command registration is not being added to context.subscriptions, which could lead to a resource leak. The command should be disposed when the extension is deactivated.

Add the registration to subscriptions:

context.subscriptions.push(
  commands.registerCommand('primer-primitives-autocomplete.openDocs', async () => {
    // ... command implementation
  })
)

Copilot uses AI. Check for mistakes.
const editor = window.activeTextEditor
if (!editor) return

const document = editor.document
if (!document) return null

const position = editor.selection.active
const offset = document.offsetAt(position)
const variableName = getCssVariable(document, offset)
if (!variableName) {
const currentWord = getCurrentWord(document, offset)
window.showInformationMessage(`Unrecognized variable: ${currentWord}. Cannot open Primer documentation.`)
return null
}

const variableInfo = getVariableInfo(variableName)
if (!variableInfo) {
const currentWord = getCurrentWord(document, offset)
Comment on lines +72 to +81
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The error message duplicates the call to getCurrentWord() when both getCssVariable() and getVariableInfo() fail. This is inefficient since getCurrentWord() was already called on line 72 as part of getCssVariable().

Consider storing the result once and reusing it:

const variableName = getCssVariable(document, offset)
if (!variableName) {
  const currentWord = getCurrentWord(document, offset)
  window.showInformationMessage(`Unrecognized variable: ${currentWord}. Cannot open Primer documentation.`)
  return null
}

const variableInfo = getVariableInfo(variableName)
if (!variableInfo) {
  window.showInformationMessage(`Unrecognized variable: ${variableName}. Cannot open Primer documentation.`)
  return null
}
Suggested change
const variableName = getCssVariable(document, offset)
if (!variableName) {
const currentWord = getCurrentWord(document, offset)
window.showInformationMessage(`Unrecognized variable: ${currentWord}. Cannot open Primer documentation.`)
return null
}
const variableInfo = getVariableInfo(variableName)
if (!variableInfo) {
const currentWord = getCurrentWord(document, offset)
const currentWord = getCurrentWord(document, offset)
const variableName = getCssVariable(document, offset)
if (!variableName) {
window.showInformationMessage(`Unrecognized variable: ${currentWord}. Cannot open Primer documentation.`)
return null
}
const variableInfo = getVariableInfo(variableName)
if (!variableInfo) {

Copilot uses AI. Check for mistakes.
window.showInformationMessage(`Unrecognized variable: ${currentWord}. Cannot open Primer documentation.`)
return null
}

if (!panel) {
panel = window.createWebviewPanel('custom view type', 'Primer Primitives', ViewColumn.Beside, {
enableScripts: true,
enableFindWidget: true,
})
panel.onDidDispose(() => (panel = null))
}

if (panel) {
panel.title = variable.name

panel.webview.html = `
panel.title = variableInfo.name
panel.webview.html = `
<style>
body {
padding: 0;
Expand All @@ -88,17 +111,16 @@ export function activate(context: ExtensionContext) {

</style>

<a href=${variable.docsUrl}>
<a href=${variableInfo.docsUrl}>
Open in Browser
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path d="M3.75 2h3.5a.75.75 0 0 1 0 1.5h-3.5a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-3.5a.75.75 0 0 1 1.5 0v3.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-8.5C2 2.784 2.784 2 3.75 2Zm6.854-1h4.146a.25.25 0 0 1 .25.25v4.146a.25.25 0 0 1-.427.177L13.03 4.03 9.28 7.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.75-3.75-1.543-1.543A.25.25 0 0 1 10.604 1Z"></path>
</svg>
</a>

<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms" src="${variable.docsUrl}"></iframe>
<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms" src="${variableInfo.docsUrl}"></iframe>
Comment on lines +114 to +121
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The HTML template has missing quotes around attribute values, which could cause XSS vulnerabilities or incorrect rendering if the URLs contain special characters.

Add quotes around the href and src attributes:

panel.webview.html = `
  <style>
    body {
      padding: 0;
    }
    a {
      padding: 16px;
      text-decoration: none;
      display: flex;
      gap: 4px;
    }
    iframe {
      width: calc(100% + 20px);
      height: 100vh;
      border: none;
    }
    
  </style>

  <a href="${variableInfo.docsUrl}">
    Open in Browser
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
      <path d="M3.75 2h3.5a.75.75 0 0 1 0 1.5h-3.5a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-3.5a.75.75 0 0 1 1.5 0v3.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-8.5C2 2.784 2.784 2 3.75 2Zm6.854-1h4.146a.25.25 0 0 1 .25.25v4.146a.25.25 0 0 1-.427.177L13.03 4.03 9.28 7.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.75-3.75-1.543-1.543A.25.25 0 0 1 10.604 1Z"></path>
    </svg>
  </a>

  <iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms" src="${variableInfo.docsUrl}"></iframe>
`

Copilot uses AI. Check for mistakes.

`
}
})
}

Expand Down
17 changes: 0 additions & 17 deletions src/language-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import camelCase from 'lodash.camelcase'
import {isColor} from './utils/is-color'
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'

Expand All @@ -34,7 +33,6 @@ connection.onInitialize(async () => {
completionProvider: {resolveProvider: true, triggerCharacters: [':']},
hoverProvider: true,
textDocumentSync: TextDocumentSyncKind.Incremental,
definitionProvider: true,
},
}

Expand Down Expand Up @@ -163,21 +161,6 @@ connection.onHover(params => {
return {contents: {kind: 'markdown', value: documentation}} as Hover
})

connection.onDefinition(params => {
const doc = documents.get(params.textDocument.uri)
if (!doc) return null

const offset = doc.offsetAt(params.position)
const variableName = getCssVariable(doc, offset)
if (!variableName) return null

const variableInfo = getVariableInfo(variableName)
if (!variableInfo) return null

connection.sendRequest('open-docs', {variable: variableInfo})
return null
})

// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection)
Expand Down
8 changes: 6 additions & 2 deletions src/utils/get-css-variable.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {TextDocument} from 'vscode-languageserver-textdocument'
import {TextDocument as LanguageServerTextDocument} from 'vscode-languageserver-textdocument'
import {TextDocument as VSCodeTextDocument} from 'vscode'
import {getCurrentWord} from './get-current-word'

/**
* Extracts a CSS variable name from the current cursor position.
* @returns The CSS variable name (e.g., '--color-red') or null if not found
*/
export function getCssVariable(document: TextDocument, offset: number): `--${string}` | null {
export function getCssVariable(
document: LanguageServerTextDocument | VSCodeTextDocument,
offset: number,
): `--${string}` | null {
const currentWord = getCurrentWord(document, offset)

const match = currentWord.match(/--[a-zA-Z0-9-]+/)
Expand Down
5 changes: 3 additions & 2 deletions src/utils/get-current-word.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {TextDocument} from 'vscode-languageserver-textdocument'
import {TextDocument as LanguageServerTextDocument} from 'vscode-languageserver-textdocument'
import {TextDocument as VSCodeTextDocument} from 'vscode'

export function getCurrentWord(document: TextDocument, offset: number): string {
export function getCurrentWord(document: LanguageServerTextDocument | VSCodeTextDocument, offset: number): string {
const text = document.getText()
const delimiters = ' \t\n\r":{[()]},*>+'

Expand Down