From 4a98c04399f417e7b6650ad84de8533d80fbc614 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 22 May 2025 16:37:28 -0400 Subject: [PATCH 1/4] Sort document selectors once --- .../src/project-locator.ts | 7 +++++++ packages/tailwindcss-language-server/src/tw.ts | 16 +--------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index 0f3751ed..99d68505 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -815,5 +815,12 @@ export async function calculateDocumentSelectors( documentSelectors.findIndex(({ pattern: p }) => p === pattern) === index, ) + // Move all the negated patterns to the front + selectors = selectors.sort((a, z) => { + if (a.pattern.startsWith('!') && !z.pattern.startsWith('!')) return -1 + if (!a.pattern.startsWith('!') && z.pattern.startsWith('!')) return 1 + return 0 + }) + return selectors } diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index bc1b3bcc..e5b50440 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -984,21 +984,7 @@ export class TW { continue } - let documentSelector = project - .documentSelector() - .concat() - // move all the negated patterns to the front - .sort((a, z) => { - if (a.pattern.startsWith('!') && !z.pattern.startsWith('!')) { - return -1 - } - if (!a.pattern.startsWith('!') && z.pattern.startsWith('!')) { - return 1 - } - return 0 - }) - - for (let selector of documentSelector) { + for (let selector of project.documentSelector()) { let pattern = selector.pattern.replace(/[\[\]{}()]/g, (m) => `\\${m}`) if (pattern.startsWith('!')) { From 2a6b2770a0ba997f73a7afd0851dbf6bf4f678a4 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 22 May 2025 16:39:07 -0400 Subject: [PATCH 2/4] Run positive matches less often --- .../tailwindcss-language-server/src/tw.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index e5b50440..20b7d9e2 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -997,18 +997,20 @@ export class TW { } } - if (picomatch(pattern, { dot: true })(fsPath) && selector.priority < matchedPriority) { - matchedProject = project - matchedPriority = selector.priority + if (selector.priority < matchedPriority) { + if (picomatch(pattern, { dot: true })(fsPath)) { + matchedProject = project + matchedPriority = selector.priority - continue - } + continue + } - if (picomatch(pattern, { dot: true })(normalPath) && selector.priority < matchedPriority) { - matchedProject = project - matchedPriority = selector.priority + if (picomatch(pattern, { dot: true })(normalPath)) { + matchedProject = project + matchedPriority = selector.priority - continue + continue + } } } } From 911a0ac1e1bc2099bc5420fdc8fde7df52491ab9 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 22 May 2025 16:44:06 -0400 Subject: [PATCH 3/4] Cache compiled path matchers --- .../src/matching.ts | 24 +++++++++++ .../tailwindcss-language-server/src/tw.ts | 40 ++++++++----------- 2 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 packages/tailwindcss-language-server/src/matching.ts diff --git a/packages/tailwindcss-language-server/src/matching.ts b/packages/tailwindcss-language-server/src/matching.ts new file mode 100644 index 00000000..a373b116 --- /dev/null +++ b/packages/tailwindcss-language-server/src/matching.ts @@ -0,0 +1,24 @@ +import picomatch from 'picomatch' +import { DefaultMap } from './util/default-map' + +export interface PathMatcher { + anyMatches(pattern: string, paths: string[]): boolean + clear(): void +} + +export function createPathMatcher(): PathMatcher { + let matchers = new DefaultMap((pattern) => { + // Escape picomatch special characters so they're matched literally + pattern = pattern.replace(/[\[\]{}()]/g, (m) => `\\${m}`) + + return picomatch(pattern, { dot: true }) + }) + + return { + anyMatches: (pattern, paths) => { + let check = matchers.get(pattern) + return paths.some((path) => check(path)) + }, + clear: () => matchers.clear(), + } +} diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 20b7d9e2..4c5ac53e 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -56,6 +56,7 @@ import { ProjectLocator, type ProjectConfig } from './project-locator' import type { TailwindCssSettings } from '@tailwindcss/language-service/src/util/state' import { createResolver, Resolver } from './resolver' import { analyzeStylesheet } from './version-guesser.js' +import { createPathMatcher, PathMatcher } from './matching.js' const TRIGGER_CHARACTERS = [ // class attributes @@ -104,12 +105,14 @@ export class TW { private watched: string[] = [] private settingsCache: SettingsCache + private pathMatcher: PathMatcher constructor(private connection: Connection) { this.documentService = new DocumentService(this.connection) this.projects = new Map() this.projectCounter = 0 this.settingsCache = createSettingsCache(connection) + this.pathMatcher = createPathMatcher() } async init(): Promise { @@ -151,6 +154,7 @@ export class TW { private async _init(): Promise { clearRequireCache() + this.pathMatcher.clear() let folders = this.getWorkspaceFolders().map((folder) => normalizePath(folder.uri)) if (folders.length === 0) { @@ -985,32 +989,20 @@ export class TW { } for (let selector of project.documentSelector()) { - let pattern = selector.pattern.replace(/[\[\]{}()]/g, (m) => `\\${m}`) - - if (pattern.startsWith('!')) { - if (picomatch(pattern.slice(1), { dot: true })(fsPath)) { - break - } - - if (picomatch(pattern.slice(1), { dot: true })(normalPath)) { - break - } + if ( + selector.pattern.startsWith('!') && + this.pathMatcher.anyMatches(selector.pattern.slice(1), [fsPath, normalPath]) + ) { + break } - if (selector.priority < matchedPriority) { - if (picomatch(pattern, { dot: true })(fsPath)) { - matchedProject = project - matchedPriority = selector.priority - - continue - } - - if (picomatch(pattern, { dot: true })(normalPath)) { - matchedProject = project - matchedPriority = selector.priority - - continue - } + if ( + selector.priority < matchedPriority && + this.pathMatcher.anyMatches(selector.pattern, [fsPath, normalPath]) + ) { + matchedProject = project + matchedPriority = selector.priority + continue } } } From 837dc11494670db9093aaec1675e6b714f03e428 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 22 May 2025 16:44:11 -0400 Subject: [PATCH 4/4] Add TODOs --- packages/tailwindcss-language-server/src/tw.ts | 2 ++ packages/tailwindcss-language-server/src/util/isExcluded.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 4c5ac53e..da8de46c 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -329,6 +329,7 @@ export class TW { let needsRestart = false let needsSoftRestart = false + // TODO: This should use the server-level path matcher let isPackageMatcher = picomatch(`**/${PACKAGE_LOCK_GLOB}`, { dot: true }) let isCssMatcher = picomatch(`**/${CSS_GLOB}`, { dot: true }) let isConfigMatcher = picomatch(`**/${CONFIG_GLOB}`, { dot: true }) @@ -343,6 +344,7 @@ export class TW { normalizedFilename = normalizeDriveLetter(normalizedFilename) for (let ignorePattern of ignore) { + // TODO: This should use the server-level path matcher let isIgnored = picomatch(ignorePattern, { dot: true }) if (isIgnored(normalizedFilename)) { diff --git a/packages/tailwindcss-language-server/src/util/isExcluded.ts b/packages/tailwindcss-language-server/src/util/isExcluded.ts index df998e7f..63547304 100644 --- a/packages/tailwindcss-language-server/src/util/isExcluded.ts +++ b/packages/tailwindcss-language-server/src/util/isExcluded.ts @@ -20,6 +20,7 @@ export default async function isExcluded( pattern = normalizePath(pattern) pattern = normalizeDriveLetter(pattern) + // TODO: This should use the server-level path matcher if (picomatch(pattern)(file)) { return true }