diff --git a/client/src/extension.ts b/client/src/extension.ts index f6db496..42d1dcf 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -99,6 +99,10 @@ export function activate(context: ExtensionContext): void { const peekToExclude: Array = config.get( "peekToExclude" ) as Array; + const peekToLinkedOnly: boolean = config.get( + "peekToLinkedOnly", + false + ) as boolean; function didOpenTextDocument(document: TextDocument): void { try { @@ -159,6 +163,7 @@ export function activate(context: ExtensionContext): void { initializationOptions: { stylesheets: [], peekFromLanguages, + peekToLinkedOnly, }, diagnosticCollectionName: "css-peek", outputChannel, @@ -219,6 +224,7 @@ export function activate(context: ExtensionContext): void { fsPath: u.fsPath, })), peekFromLanguages, + peekToLinkedOnly, }, workspaceFolder: folder, outputChannel, diff --git a/package.json b/package.json index 1e49537..bf60742 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,12 @@ }, "description": "A list of file globs that filters out peekable files" }, + "cssPeek.peekToLinkedOnly": { + "scope": "window", + "type": "boolean", + "default": false, + "description": "When enabled, peek targets are restricted to stylesheets that are referenced from the current source file (e.g. via in HTML or `import './foo.css'` / `@import 'foo.css'` in JS/TS/SCSS). Useful in large monorepos to avoid matching unrelated stylesheets." + }, "cssPeek.trace.server": { "scope": "window", "type": "string", diff --git a/server/src/core/findDefinition.ts b/server/src/core/findDefinition.ts index b88c286..dae0bad 100644 --- a/server/src/core/findDefinition.ts +++ b/server/src/core/findDefinition.ts @@ -68,9 +68,14 @@ export function findSymbols( options: { peekVariables?: boolean; embeddedStylesheetMap?: StylesheetMap; + allowedUris?: Set; } = {} ): SymbolInformation[] { - const { peekVariables = true, embeddedStylesheetMap = {} } = options; + const { + peekVariables = true, + embeddedStylesheetMap = {}, + allowedUris, + } = options; const foundSymbols: SymbolInformation[] = []; // Merge the persistent stylesheet cache with any in-memory embedded @@ -126,6 +131,9 @@ export function findSymbols( // Test all the symbols against the RegExp Object.keys(combinedMap).forEach((uri) => { + if (allowedUris && !allowedUris.has(uri)) { + return; + } const styleSheet = combinedMap[uri]; try { let symbols: SymbolInformation[]; @@ -191,6 +199,7 @@ export function findDefinition( options: { peekVariables?: boolean; embeddedStylesheetMap?: StylesheetMap; + allowedUris?: Set; } = {} ): Location[] { return findSymbols(selector, stylesheetMap, options).map( diff --git a/server/src/server.ts b/server/src/server.ts index 93f962e..3129032 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -28,6 +28,7 @@ import { extractEmbeddedStylesheets, hasEmbeddedStyles, } from "./core/embeddedStyles"; +import { findLinkedStylesheets } from "./utils/linkedStylesheets"; import { create } from "./logger"; // Creates the LSP connection @@ -137,6 +138,7 @@ interface Settings { peekVariables: boolean; peekFromLanguages: string[]; peekToExclude: string[]; + peekToLinkedOnly: boolean; } connection.onInitialized(() => { if (hasConfigurationCapability) { @@ -158,6 +160,7 @@ const defaultSettings: Settings = { peekVariables: true, peekFromLanguages: ["html"], peekToExclude: ["**/node_modules/**", "**/bower_components/**"], + peekToLinkedOnly: false, }; let globalSettings: Settings = defaultSettings; @@ -241,9 +244,14 @@ connection.onDefinition( ? extractEmbeddedStylesheets(document) : {}; + const allowedUris = settings.peekToLinkedOnly + ? new Set(findLinkedStylesheets(document)) + : undefined; + return findDefinition(selector, styleSheets, { peekVariables: settings.peekVariables, embeddedStylesheetMap, + allowedUris, }); } ); diff --git a/server/src/utils/linkedStylesheets.ts b/server/src/utils/linkedStylesheets.ts new file mode 100644 index 0000000..7d3438c --- /dev/null +++ b/server/src/utils/linkedStylesheets.ts @@ -0,0 +1,98 @@ +import { URL } from "url"; +import { TextDocument } from "vscode-languageserver-textdocument"; + +/** + * Parse a source document for explicit stylesheet references and resolve them + * to absolute URIs (string form). + * + * Supported syntaxes (intentionally simple string matching, not a full parser): + * - HTML / templating: (any attribute order) + * - JS / TS / JSX / TSX / Svelte / Vue: import './foo.css' / import "foo.css" + * - CSS / SCSS / LESS: @import 'foo.css' / @import url('foo.css') + * + * Limitations: no CSS-in-JS, no Sass `@use` / `@forward`, no dynamic imports, + * no URL/module resolution through bundler aliases, no `