Home

tailwind-ctp-intellisense @master - refs - log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
tree log patch
Add automatic support for multi-config workspaces, including `@config` resolution (#633) * wip * wip * Boot client if a CSS file contains `@config` * wip * Check document exists * wip * Fix duplicate document selector * wip * Use enum for document selector priorities * Delete unused functions * Remove unused state type * Share glob patterns * Update config file glob * fix logs * Fix filename checks on Windows * Don't show error popups * wip * handle negated content paths * Handle non-tailwind dependency installs * add package root to document selectors * tidy * wip * dedupe document selectors * Fix `@config` regex * Fix document selectors when using `experimental.configFile` * Remove log
Signature
-----BEGIN PGP SIGNATURE----- wsBcBAABCAAQBQJjTv/mCRBK7hj4Ov3rIwAA6noIAJYeTqKvQkFnX2Dn05L6iPVy oy/uBjCUJlPeZc8XO3GfzGofsrXWRd29iUQLlN9jxxyX2MU+tQLqQnJrifXYP6Px p+hwo0TSs+nXVSAD/+v4frt7Qoe7REuktFDVO/UWm2PMgFMy3XcjmSjG3WP7q3yY jqcvgbrLJ2/9VHpOwvS3j5tf0GtD9ysLudvNVOFF3p+Cf4+4sK29FeTUr0ZTJVYr 46rw1JARPRpQE+XNoWvYJIjWjBl+Z474toxtMocrV7DJalwskZNru2vjoYwhdsYP UHR/539+7H+DBgJucpYbItzLhMPDeCoGSD/ydXbDQcgbbh0iYyC0r3zVxZLGSag= =V/Bw -----END PGP SIGNATURE-----
Brad Cornes <hello@bradley.dev>
2 years ago
9 changed files, 914 additions(+), 351 deletions(-)
I packages/tailwindcss-language-server/src/lib/constants.ts
diff --git a/packages/tailwindcss-language-server/src/lib/constants.ts b/packages/tailwindcss-language-server/src/lib/constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7324327c59283b13931a342a761e3242e62b4c35
--- /dev/null
+++ b/packages/tailwindcss-language-server/src/lib/constants.ts
@@ -0,0 +1,3 @@
+export const CONFIG_GLOB = '{tailwind,tailwind.config,tailwind.*.config,tailwind.config.*}.{js,cjs}'
+export const PACKAGE_LOCK_GLOB = '{package-lock.json,yarn.lock,pnpm-lock.yaml}'
+export const CSS_GLOB = '*.{css,scss,sass,less,pcss}'
M packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts -> packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts
diff --git a/packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts b/packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts
index e5f01a8998bd771b9bd02cd78332e2dda1033527..fe08b247f564cd57d80d9ece27c2481456e23eaf 100644
--- a/packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts
+++ b/packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts
@@ -20,15 +20,3 @@     uri: document.uri,
     diagnostics: [],
   })
 }
-
-export function clearAllDiagnostics(state: State): void {
-  state.editor?.documents.all().forEach((document) => {
-    clearDiagnostics(state, document)
-  })
-}
-
-export function updateAllDiagnostics(state: State): void {
-  state.editor?.documents.all().forEach((document) => {
-    provideDiagnostics(state, document)
-  })
-}
M packages/tailwindcss-language-server/src/server.ts -> packages/tailwindcss-language-server/src/server.ts
diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts
index 008c221a7744fb579ca9a8887c75c87a9916755c..0b9b4f576833ca4784c611e280d7422d6253f4e8 100644
--- a/packages/tailwindcss-language-server/src/server.ts
+++ b/packages/tailwindcss-language-server/src/server.ts
@@ -65,11 +65,7 @@   Settings,
   ClassNames,
   Variant,
 } from 'tailwindcss-language-service/src/util/state'
-import {
-  provideDiagnostics,
-  updateAllDiagnostics,
-  clearAllDiagnostics,
-} from './lsp/diagnosticsProvider'
+import { provideDiagnostics } from './lsp/diagnosticsProvider'
 import { doCodeActions } from 'tailwindcss-language-service/src/codeActions/codeActionProvider'
 import { getDocumentColors } from 'tailwindcss-language-service/src/documentColorProvider'
 import { getDocumentLinks } from 'tailwindcss-language-service/src/documentLinksProvider'
@@ -88,6 +84,8 @@ import { getFileFsPath, normalizeFileNameToFsPath } from './util/uri'
 import { equal } from 'tailwindcss-language-service/src/util/array'
 import preflight from 'tailwindcss/lib/css/preflight.css'
 import merge from 'deepmerge'
+import { getTextWithoutComments } from 'tailwindcss-language-service/src/util/doc'
+import { CONFIG_GLOB, CSS_GLOB, PACKAGE_LOCK_GLOB } from './lib/constants'
 
 // @ts-ignore
 global.__preflight = preflight
@@ -105,8 +103,6 @@     }
   `
 )(require, __dirname)
 
-const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}'
-const PACKAGE_GLOB = '{package-lock.json,yarn.lock,pnpm-lock.yaml}'
 const TRIGGER_CHARACTERS = [
   // class attributes
   '"',
@@ -181,22 +177,42 @@   }
 }
 
 interface ProjectService {
+  enabled: () => boolean
+  enable: () => void
+  documentSelector: () => Array<DocumentSelector>
   state: State
   tryInit: () => Promise<void>
-  dispose: () => void
+  dispose: () => Promise<void>
   onUpdateSettings: (settings: any) => void
   onFileEvents: (changes: Array<{ file: string; type: FileChangeType }>) => void
   onHover(params: TextDocumentPositionParams): Promise<Hover>
   onCompletion(params: CompletionParams): Promise<CompletionList>
   onCompletionResolve(item: CompletionItem): Promise<CompletionItem>
   provideDiagnostics(document: TextDocument): void
+  provideDiagnosticsForce(document: TextDocument): void
   onDocumentColor(params: DocumentColorParams): Promise<ColorInformation[]>
   onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]>
   onCodeAction(params: CodeActionParams): Promise<CodeAction[]>
   onDocumentLinks(params: DocumentLinkParams): DocumentLink[]
 }
 
-type ProjectConfig = { folder: string; configPath?: string; documentSelector?: string[] }
+type ProjectConfig = {
+  folder: string
+  configPath?: string
+  documentSelector?: Array<DocumentSelector>
+  isUserConfigured: boolean
+}
+
+enum DocumentSelectorPriority {
+  USER_CONFIGURED = 0,
+  CONFIG_FILE = 0,
+  CSS_FILE = 0,
+  CONTENT_FILE = 1,
+  CSS_DIRECTORY = 2,
+  CONFIG_DIRECTORY = 3,
+  ROOT_DIRECTORY = 4,
+}
+type DocumentSelector = { pattern: string; priority: DocumentSelectorPriority }
 
 function getMode(config: any): unknown {
   if (typeof config.mode !== 'undefined') {
@@ -221,77 +237,150 @@     }
   }
 }
 
+const documentSettingsCache: Map<string, Settings> = new Map()
+async function getConfiguration(uri?: string) {
+  if (documentSettingsCache.has(uri)) {
+    return documentSettingsCache.get(uri)
+  }
+  let [editor, tailwindCSS] = await Promise.all([
+    connection.workspace.getConfiguration({
+      section: 'editor',
+      scopeUri: uri,
+    }),
+    connection.workspace.getConfiguration({
+      section: 'tailwindCSS',
+      scopeUri: uri,
+    }),
+  ])
+  editor = isObject(editor) ? editor : {}
+  tailwindCSS = isObject(tailwindCSS) ? tailwindCSS : {}
+
+  let config: Settings = merge<Settings>(
+    {
+      editor: { tabSize: 2 },
+      tailwindCSS: {
+        emmetCompletions: false,
+        classAttributes: ['class', 'className', 'ngClass'],
+        codeActions: true,
+        hovers: true,
+        suggestions: true,
+        validate: true,
+        colorDecorators: true,
+        rootFontSize: 16,
+        lint: {
+          cssConflict: 'warning',
+          invalidApply: 'error',
+          invalidScreen: 'error',
+          invalidVariant: 'error',
+          invalidConfigPath: 'error',
+          invalidTailwindDirective: 'error',
+          recommendedVariantOrder: 'warning',
+        },
+        showPixelEquivalents: true,
+        includeLanguages: {},
+        files: { exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'] },
+        experimental: {
+          classRegex: [],
+          configFile: null,
+        },
+      },
+    },
+    { editor, tailwindCSS },
+    { arrayMerge: (_destinationArray, sourceArray, _options) => sourceArray }
+  )
+  documentSettingsCache.set(uri, config)
+  return config
+}
+
+function clearRequireCache(): void {
+  Object.keys(require.cache).forEach((key) => {
+    if (!key.endsWith('.node')) {
+      delete require.cache[key]
+    }
+  })
+  Object.keys((Module as any)._pathCache).forEach((key) => {
+    delete (Module as any)._pathCache[key]
+  })
+}
+
+function withoutLogs<T>(getter: () => T): T {
+  let fns = {
+    log: console.log,
+    warn: console.warn,
+    error: console.error,
+  }
+  for (let key in fns) {
+    console[key] = () => {}
+  }
+  try {
+    return getter()
+  } finally {
+    for (let key in fns) {
+      console[key] = fns[key]
+    }
+  }
+}
+
+function withFallback<T>(getter: () => T, fallback: T): T {
+  try {
+    return getter()
+  } catch (e) {
+    return fallback
+  }
+}
+
+function dirContains(dir: string, file: string): boolean {
+  let relative = path.relative(dir, file)
+  return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative)
+}
+
+function changeAffectsFile(change: string, files: string[]): boolean {
+  for (let file of files) {
+    if (change === file || dirContains(change, file)) {
+      return true
+    }
+  }
+  return false
+}
+
+// We need to add parent directories to the watcher:
+// https://github.com/microsoft/vscode/issues/60813
+function getWatchPatternsForFile(file: string): string[] {
+  let tmp: string
+  let dir = path.dirname(file)
+  let patterns: string[] = [file, dir]
+  while (true) {
+    dir = path.dirname((tmp = dir))
+    if (tmp === dir) {
+      break
+    } else {
+      patterns.push(dir)
+    }
+  }
+  return patterns
+}
+
 async function createProjectService(
   projectConfig: ProjectConfig,
   connection: Connection,
   params: InitializeParams,
   documentService: DocumentService,
-  updateCapabilities: () => void
+  updateCapabilities: () => void,
+  checkOpenDocuments: () => void,
+  refreshDiagnostics: () => void,
+  watchPatterns: (patterns: string[]) => void,
+  initialTailwindVersion: string
 ): Promise<ProjectService> {
+  let enabled = false
   const folder = projectConfig.folder
-  const disposables: Disposable[] = []
-  const documentSettingsCache: Map<string, Settings> = new Map()
-
-  async function getConfiguration(uri?: string) {
-    if (documentSettingsCache.has(uri)) {
-      return documentSettingsCache.get(uri)
-    }
-    let [editor, tailwindCSS] = await Promise.all([
-      connection.workspace.getConfiguration({
-        section: 'editor',
-        scopeUri: uri,
-      }),
-      connection.workspace.getConfiguration({
-        section: 'tailwindCSS',
-        scopeUri: uri,
-      }),
-    ])
-    editor = isObject(editor) ? editor : {}
-    tailwindCSS = isObject(tailwindCSS) ? tailwindCSS : {}
-
-    let config: Settings = merge<Settings>(
-      {
-        editor: { tabSize: 2 },
-        tailwindCSS: {
-          emmetCompletions: false,
-          classAttributes: ['class', 'className', 'ngClass'],
-          codeActions: true,
-          hovers: true,
-          suggestions: true,
-          validate: true,
-          colorDecorators: true,
-          rootFontSize: 16,
-          lint: {
-            cssConflict: 'warning',
-            invalidApply: 'error',
-            invalidScreen: 'error',
-            invalidVariant: 'error',
-            invalidConfigPath: 'error',
-            invalidTailwindDirective: 'error',
-            recommendedVariantOrder: 'warning',
-          },
-          showPixelEquivalents: true,
-          includeLanguages: {},
-          files: { exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'] },
-          experimental: {
-            classRegex: [],
-            configFile: null,
-          },
-        },
-      },
-      { editor, tailwindCSS },
-      { arrayMerge: (_destinationArray, sourceArray, _options) => sourceArray }
-    )
-    documentSettingsCache.set(uri, config)
-    return config
-  }
+  const disposables: Array<Disposable | Promise<Disposable>> = []
+  let documentSelector = projectConfig.documentSelector
 
-  const state: State = {
+  let state: State = {
     enabled: false,
     editor: {
       connection,
       folder,
-      globalSettings: await getConfiguration(),
       userLanguages: params.initializationOptions.userLanguages
         ? params.initializationOptions.userLanguages
         : {},
@@ -329,8 +418,22 @@       },
     },
   }
 
-  let chokidarWatcher: chokidar.FSWatcher
-  let ignore = state.editor.globalSettings.tailwindCSS.files.exclude
+  if (projectConfig.configPath) {
+    let deps = []
+    try {
+      deps = getModuleDependencies(projectConfig.configPath)
+    } catch {}
+    watchPatterns([
+      ...getWatchPatternsForFile(projectConfig.configPath),
+      ...deps.flatMap((dep) => getWatchPatternsForFile(dep)),
+    ])
+  }
+
+  function log(...args: string[]): void {
+    console.log(
+      `[${path.relative(projectConfig.folder, projectConfig.configPath)}] ${args.join(' ')}`
+    )
+  }
 
   function onFileEvents(changes: Array<{ file: string; type: FileChangeType }>): void {
     let needsInit = false
@@ -339,22 +442,40 @@
     for (let change of changes) {
       let file = normalizePath(change.file)
 
-      for (let ignorePattern of ignore) {
-        if (minimatch(file, ignorePattern, { dot: true })) {
-          continue
-        }
-      }
+      let isConfigFile = changeAffectsFile(file, [projectConfig.configPath])
+      let isDependency = changeAffectsFile(change.file, state.dependencies ?? [])
+      let isPackageFile = minimatch(file, `**/${PACKAGE_LOCK_GLOB}`, { dot: true })
 
-      let isConfigFile = minimatch(file, `**/${CONFIG_FILE_GLOB}`, { dot: true })
-      let isPackageFile = minimatch(file, `**/${PACKAGE_GLOB}`, { dot: true })
-      let isDependency = state.dependencies && state.dependencies.includes(change.file)
+      if (!isConfigFile && !isDependency && !isPackageFile) continue
 
-      if (!isConfigFile && !isPackageFile && !isDependency) continue
+      if (!enabled) {
+        if (
+          !projectConfig.isUserConfigured &&
+          projectConfig.configPath &&
+          (isConfigFile || isDependency)
+        ) {
+          documentSelector = [
+            ...documentSelector.filter(
+              ({ priority }) => priority !== DocumentSelectorPriority.CONTENT_FILE
+            ),
+            ...getContentDocumentSelectorFromConfigFile(
+              projectConfig.configPath,
+              initialTailwindVersion,
+              projectConfig.folder
+            ),
+          ]
+
+          checkOpenDocuments()
+        }
+        continue
+      }
 
       if (change.type === FileChangeType.Created) {
+        log('File created:', change.file)
         needsInit = true
         break
       } else if (change.type === FileChangeType.Changed) {
+        log('File changed:', change.file)
         if (!state.enabled || isPackageFile) {
           needsInit = true
           break
@@ -362,7 +483,8 @@         } else {
           needsRebuild = true
         }
       } else if (change.type === FileChangeType.Deleted) {
-        if (!state.enabled || isPackageFile || isConfigFile) {
+        log('File deleted:', change.file)
+        if (!state.enabled || isConfigFile || isPackageFile) {
           needsInit = true
           break
         } else {
@@ -378,65 +500,8 @@       tryRebuild()
     }
   }
 
-  if (params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) {
-    connection.client.register(DidChangeWatchedFilesNotification.type, {
-      watchers: [{ globPattern: `**/${CONFIG_FILE_GLOB}` }, { globPattern: `**/${PACKAGE_GLOB}` }],
-    })
-  } else if (parcel.getBinding()) {
-    let typeMap = {
-      create: FileChangeType.Created,
-      update: FileChangeType.Changed,
-      delete: FileChangeType.Deleted,
-    }
-
-    let subscription = await parcel.subscribe(
-      folder,
-      (err, events) => {
-        onFileEvents(events.map((event) => ({ file: event.path, type: typeMap[event.type] })))
-      },
-      {
-        ignore: ignore.map((ignorePattern) =>
-          path.resolve(folder, ignorePattern.replace(/^[*/]+/, '').replace(/[*/]+$/, ''))
-        ),
-      }
-    )
-
-    disposables.push({
-      dispose() {
-        subscription.unsubscribe()
-      },
-    })
-  } else {
-    let watch: typeof chokidar.watch = require('chokidar').watch
-    chokidarWatcher = watch([`**/${CONFIG_FILE_GLOB}`, `**/${PACKAGE_GLOB}`], {
-      cwd: folder,
-      ignorePermissionErrors: true,
-      ignoreInitial: true,
-      ignored: ignore,
-      awaitWriteFinish: {
-        stabilityThreshold: 100,
-        pollInterval: 20,
-      },
-    })
-
-    await new Promise<void>((resolve) => {
-      chokidarWatcher.on('ready', () => resolve())
-    })
-
-    chokidarWatcher
-      .on('add', (file) => onFileEvents([{ file, type: FileChangeType.Created }]))
-      .on('change', (file) => onFileEvents([{ file, type: FileChangeType.Changed }]))
-      .on('unlink', (file) => onFileEvents([{ file, type: FileChangeType.Deleted }]))
-
-    disposables.push({
-      dispose() {
-        chokidarWatcher.close()
-      },
-    })
-  }
-
   function resetState(): void {
-    clearAllDiagnostics(state)
+    // clearAllDiagnostics(state)
     Object.keys(state).forEach((key) => {
       // Keep `dependencies` to ensure that they are still watched
       if (key !== 'editor' && key !== 'dependencies') {
@@ -444,10 +509,14 @@         delete state[key]
       }
     })
     state.enabled = false
+    refreshDiagnostics()
     updateCapabilities()
   }
 
   async function tryInit() {
+    if (!enabled) {
+      return
+    }
     try {
       await init()
     } catch (error) {
@@ -457,6 +526,9 @@     }
   }
 
   async function tryRebuild() {
+    if (!enabled) {
+      return
+    }
     try {
       await rebuild()
     } catch (error) {
@@ -465,41 +537,18 @@       showError(connection, error)
     }
   }
 
-  function clearRequireCache(): void {
-    Object.keys(require.cache).forEach((key) => {
-      if (!key.endsWith('.node')) {
-        delete require.cache[key]
-      }
-    })
-    Object.keys((Module as any)._pathCache).forEach((key) => {
-      delete (Module as any)._pathCache[key]
-    })
-  }
+  async function init() {
+    log('Initializing...')
 
-  async function init() {
     clearRequireCache()
 
     let configPath = projectConfig.configPath
 
     if (!configPath) {
-      configPath = (
-        await glob([`**/${CONFIG_FILE_GLOB}`], {
-          cwd: folder,
-          ignore: state.editor.globalSettings.tailwindCSS.files.exclude,
-          onlyFiles: true,
-          absolute: true,
-          suppressErrors: true,
-          dot: true,
-          concurrency: Math.max(os.cpus().length, 1),
-        })
-      )
-        .sort((a: string, b: string) => a.split('/').length - b.split('/').length)
-        .map(path.normalize)[0]
+      throw new SilentError('No config file found.')
     }
 
-    if (!configPath) {
-      throw new SilentError('No config file found.')
-    }
+    watchPatterns(getWatchPatternsForFile(configPath))
 
     const pnpPath = findUp.sync(
       (dir) => {
@@ -573,14 +622,14 @@       ) {
         return
       }
 
-      console.log(`Found Tailwind CSS config file: ${configPath}`)
+      log(`Loaded Tailwind CSS config file: ${configPath}`)
 
       postcss = require(postcssPath)
       postcssSelectorParser = require(postcssSelectorParserPath)
-      console.log(`Loaded postcss v${postcssVersion}: ${postcssDir}`)
+      log(`Loaded postcss v${postcssVersion}: ${postcssDir}`)
 
       tailwindcss = require(tailwindcssPath)
-      console.log(`Loaded tailwindcss v${tailwindcssVersion}: ${tailwindDir}`)
+      log(`Loaded tailwindcss v${tailwindcssVersion}: ${tailwindDir}`)
 
       try {
         resolveConfigFn = require(resolveFrom(tailwindDir, './resolveConfig.js'))
@@ -727,9 +776,9 @@         expandApplyAtRules: {
           module: require('tailwindcss/lib/lib/expandApplyAtRules').default,
         },
       }
-      console.log('Failed to load workspace modules.')
-      console.log(`Using bundled version of \`tailwindcss\`: v${tailwindcssVersion}`)
-      console.log(`Using bundled version of \`postcss\`: v${postcssVersion}`)
+      log('Failed to load workspace modules.')
+      log(`Using bundled version of \`tailwindcss\`: v${tailwindcssVersion}`)
+      log(`Using bundled version of \`postcss\`: v${postcssVersion}`)
     }
 
     state.configPath = configPath
@@ -817,6 +866,8 @@     await tryRebuild()
   }
 
   async function rebuild() {
+    log('Building...')
+
     clearRequireCache()
 
     const { tailwindcss, postcss, resolveConfig } = state.modules
@@ -912,6 +963,22 @@     if (!originalConfig) {
       throw new SilentError(`Failed to load config file: ${state.configPath}`)
     }
 
+    /////////////////////
+    if (!projectConfig.isUserConfigured) {
+      documentSelector = [
+        ...documentSelector.filter(
+          ({ priority }) => priority !== DocumentSelectorPriority.CONTENT_FILE
+        ),
+        ...getContentDocumentSelectorFromConfigFile(
+          state.configPath,
+          tailwindcss.version,
+          projectConfig.folder,
+          originalConfig
+        ),
+      ]
+    }
+    //////////////////////
+
     try {
       state.config = resolveConfig.module(originalConfig)
       state.separator = state.config.separator
@@ -967,11 +1034,12 @@         hook.unhook()
       }
     }
 
-    if (state.dependencies) {
-      chokidarWatcher?.unwatch(state.dependencies)
-    }
+    // if (state.dependencies) {
+    //   chokidarWatcher?.unwatch(state.dependencies)
+    // }
     state.dependencies = getModuleDependencies(state.configPath)
-    chokidarWatcher?.add(state.dependencies)
+    // chokidarWatcher?.add(state.dependencies)
+    watchPatterns((state.dependencies ?? []).flatMap((dep) => getWatchPatternsForFile(dep)))
 
     state.configId = getConfigId(state.configPath, state.dependencies)
 
@@ -986,74 +1054,86 @@     state.screens = isObject(screens) ? Object.keys(screens) : []
 
     state.enabled = true
 
-    updateAllDiagnostics(state)
+    // updateAllDiagnostics(state)
+    refreshDiagnostics()
 
     updateCapabilities()
   }
 
   return {
+    enabled() {
+      return enabled
+    },
+    enable() {
+      enabled = true
+    },
     state,
+    documentSelector() {
+      return documentSelector
+    },
     tryInit,
-    dispose() {
-      for (let { dispose } of disposables) {
-        dispose()
+    async dispose() {
+      state = { enabled: false }
+      for (let disposable of disposables) {
+        ;(await disposable).dispose()
       }
     },
     async onUpdateSettings(settings: any): Promise<void> {
-      documentSettingsCache.clear()
-      let previousExclude = state.editor.globalSettings.tailwindCSS.files.exclude
-      state.editor.globalSettings = await state.editor.getConfiguration()
-      if (!equal(previousExclude, settings.tailwindCSS.files.exclude)) {
-        tryInit()
+      if (state.enabled) {
+        refreshDiagnostics()
+      }
+      if (settings.editor.colorDecorators) {
+        updateCapabilities()
       } else {
-        if (state.enabled) {
-          updateAllDiagnostics(state)
-        }
-        if (settings.editor.colorDecorators) {
-          updateCapabilities()
-        } else {
-          connection.sendNotification('@/tailwindCSS/clearColors')
-        }
+        connection.sendNotification('@/tailwindCSS/clearColors')
       }
     },
     onFileEvents,
     async onHover(params: TextDocumentPositionParams): Promise<Hover> {
-      if (!state.enabled) return null
-      let document = documentService.getDocument(params.textDocument.uri)
-      if (!document) return null
-      let settings = await state.editor.getConfiguration(document.uri)
-      if (!settings.tailwindCSS.hovers) return null
-      if (await isExcluded(state, document)) return null
-      return doHover(state, document, params.position)
+      return withFallback(async () => {
+        if (!state.enabled) return null
+        let document = documentService.getDocument(params.textDocument.uri)
+        if (!document) return null
+        let settings = await state.editor.getConfiguration(document.uri)
+        if (!settings.tailwindCSS.hovers) return null
+        if (await isExcluded(state, document)) return null
+        return doHover(state, document, params.position)
+      }, null)
     },
     async onCompletion(params: CompletionParams): Promise<CompletionList> {
-      if (!state.enabled) return null
-      let document = documentService.getDocument(params.textDocument.uri)
-      if (!document) return null
-      let settings = await state.editor.getConfiguration(document.uri)
-      if (!settings.tailwindCSS.suggestions) return null
-      if (await isExcluded(state, document)) return null
-      let result = await doComplete(state, document, params.position, params.context)
-      if (!result) return result
-      return {
-        isIncomplete: result.isIncomplete,
-        items: result.items.map((item) => ({
-          ...item,
-          data: { projectKey: JSON.stringify(projectConfig), originalData: item.data },
-        })),
-      }
+      return withFallback(async () => {
+        if (!state.enabled) return null
+        let document = documentService.getDocument(params.textDocument.uri)
+        if (!document) return null
+        let settings = await state.editor.getConfiguration(document.uri)
+        if (!settings.tailwindCSS.suggestions) return null
+        if (await isExcluded(state, document)) return null
+        let result = await doComplete(state, document, params.position, params.context)
+        if (!result) return result
+        return {
+          isIncomplete: result.isIncomplete,
+          items: result.items.map((item) => ({
+            ...item,
+            data: { projectKey: JSON.stringify(projectConfig), originalData: item.data },
+          })),
+        }
+      }, null)
     },
     onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
-      if (!state.enabled) return null
-      return resolveCompletionItem(state, { ...item, data: item.data?.originalData })
+      return withFallback(() => {
+        if (!state.enabled) return null
+        return resolveCompletionItem(state, { ...item, data: item.data?.originalData })
+      }, null)
     },
     async onCodeAction(params: CodeActionParams): Promise<CodeAction[]> {
-      if (!state.enabled) return null
-      let document = documentService.getDocument(params.textDocument.uri)
-      if (!document) return null
-      let settings = await state.editor.getConfiguration(document.uri)
-      if (!settings.tailwindCSS.codeActions) return null
-      return doCodeActions(state, params)
+      return withFallback(async () => {
+        if (!state.enabled) return null
+        let document = documentService.getDocument(params.textDocument.uri)
+        if (!document) return null
+        let settings = await state.editor.getConfiguration(document.uri)
+        if (!settings.tailwindCSS.codeActions) return null
+        return doCodeActions(state, params)
+      }, null)
     },
     onDocumentLinks(params: DocumentLinkParams): DocumentLink[] {
       if (!state.enabled) return null
@@ -1067,15 +1147,22 @@     provideDiagnostics: debounce((document: TextDocument) => {
       if (!state.enabled) return
       provideDiagnostics(state, document)
     }, 500),
+    provideDiagnosticsForce: (document: TextDocument) => {
+      if (!state.enabled) return
+      provideDiagnostics(state, document)
+    },
     async onDocumentColor(params: DocumentColorParams): Promise<ColorInformation[]> {
-      if (!state.enabled) return []
-      let document = documentService.getDocument(params.textDocument.uri)
-      if (!document) return []
-      if (await isExcluded(state, document)) return null
-      return getDocumentColors(state, document)
+      return withFallback(async () => {
+        if (!state.enabled) return []
+        let document = documentService.getDocument(params.textDocument.uri)
+        if (!document) return []
+        if (await isExcluded(state, document)) return null
+        return getDocumentColors(state, document)
+      }, null)
     },
     async onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]> {
       let document = documentService.getDocument(params.textDocument.uri)
+      if (!document) return []
       let className = document.getText(params.range)
       let match = className.match(
         new RegExp(`-\\[(${colorNames.join('|')}|(?:(?:#|rgba?\\(|hsla?\\())[^\\]]+)\\]$`, 'i')
@@ -1251,19 +1338,21 @@
             for (let fn of fns) {
               let definition: string
               let container = root.clone()
-              let returnValue = fn({
-                container,
-                separator: state.separator,
-                modifySelectors,
-                format: (def: string) => {
-                  definition = def.replace(/:merge\(([^)]+)\)/g, '$1')
-                },
-                wrap: (rule: Container) => {
-                  if (isAtRule(rule)) {
-                    definition = `@${rule.name} ${rule.params}`
-                  }
-                },
-              })
+              let returnValue = withoutLogs(() =>
+                fn({
+                  container,
+                  separator: state.separator,
+                  modifySelectors,
+                  format: (def: string) => {
+                    definition = def.replace(/:merge\(([^)]+)\)/g, '$1')
+                  },
+                  wrap: (rule: Container) => {
+                    if (isAtRule(rule)) {
+                      definition = `@${rule.name} ${rule.params}`
+                    }
+                  },
+                })
+              )
 
               if (!definition) {
                 definition = returnValue
@@ -1404,13 +1493,77 @@   //   return []
   // }
 }
 
+async function getConfigFileFromCssFile(cssFile: string): Promise<string | null> {
+  let css = getTextWithoutComments(await fs.promises.readFile(cssFile, 'utf8'), 'css')
+  let match = css.match(/@config\s*(?<config>'[^']+'|"[^"]+")/)
+  if (!match) {
+    return null
+  }
+  return path.resolve(path.dirname(cssFile), match.groups.config.slice(1, -1))
+}
+
+function getPackageRoot(cwd: string, rootDir: string) {
+  try {
+    let pkgJsonPath = findUp.sync(
+      (dir) => {
+        let pkgJson = path.join(dir, 'package.json')
+        if (findUp.sync.exists(pkgJson)) {
+          return pkgJson
+        }
+        if (dir === rootDir) {
+          return findUp.stop
+        }
+      },
+      { cwd }
+    )
+    return pkgJsonPath ? path.dirname(pkgJsonPath) : rootDir
+  } catch {
+    return rootDir
+  }
+}
+
+function getContentDocumentSelectorFromConfigFile(
+  configPath: string,
+  tailwindVersion: string,
+  rootDir: string,
+  actualConfig?: any
+): DocumentSelector[] {
+  let config = actualConfig ?? require(configPath)
+  let contentConfig: unknown = config.content?.files ?? config.content
+  let content = Array.isArray(contentConfig) ? contentConfig : []
+  let relativeEnabled = semver.gte(tailwindVersion, '3.2.0')
+    ? config.future?.relativeContentPathsByDefault || config.content?.relative
+    : false
+  let contentBase: string
+  if (relativeEnabled) {
+    contentBase = path.dirname(configPath)
+  } else {
+    contentBase = getPackageRoot(path.dirname(configPath), rootDir)
+  }
+  return content
+    .filter((item): item is string => typeof item === 'string')
+    .map((item) =>
+      item.startsWith('!')
+        ? `!${path.resolve(contentBase, item.slice(1))}`
+        : path.resolve(contentBase, item)
+    )
+    .map((item) => ({
+      pattern: normalizePath(item),
+      priority: DocumentSelectorPriority.CONTENT_FILE,
+    }))
+}
+
 class TW {
   private initialized = false
+  private lspHandlersAdded = false
   private workspaces: Map<string, { name: string; workspaceFsPath: string }>
   private projects: Map<string, ProjectService>
   private documentService: DocumentService
   public initializeParams: InitializeParams
   private registrations: Promise<BulkUnregistration>
+  private disposables: Disposable[] = []
+  private watchPatterns: (patterns: string[]) => void
+  private watched: string[] = []
 
   constructor(private connection: Connection) {
     this.documentService = new DocumentService(this.connection)
@@ -1421,34 +1574,25 @@
   async init(): Promise<void> {
     if (this.initialized) return
 
-    this.initialized = true
+    clearRequireCache()
 
-    // TODO
-    let workspaceFolders: Array<ProjectConfig> =
-      false &&
-      Array.isArray(this.initializeParams.workspaceFolders) &&
-      this.initializeParams.capabilities.workspace?.workspaceFolders
-        ? this.initializeParams.workspaceFolders.map((el) => ({
-            folder: getFileFsPath(el.uri),
-          }))
-        : this.initializeParams.rootPath
-        ? [{ folder: normalizeFileNameToFsPath(this.initializeParams.rootPath) }]
-        : []
+    this.initialized = true
 
-    if (workspaceFolders.length === 0) {
+    if (!this.initializeParams.rootPath) {
       console.error('No workspace folders found, not initializing.')
       return
     }
 
-    let configFileOrFiles = dlv(
-      await connection.workspace.getConfiguration('tailwindCSS'),
-      'experimental.configFile',
-      null
-    ) as Settings['tailwindCSS']['experimental']['configFile']
+    let workspaceFolders: Array<ProjectConfig> = []
+    let globalSettings = await getConfiguration()
+    let ignore = globalSettings.tailwindCSS.files.exclude
+    let configFileOrFiles = globalSettings.tailwindCSS.experimental.configFile
+
+    let base = normalizeFileNameToFsPath(this.initializeParams.rootPath)
+    let cssFileConfigMap: Map<string, string> = new Map()
+    let configTailwindVersionMap: Map<string, string> = new Map()
 
     if (configFileOrFiles) {
-      let base = workspaceFolders[0].folder
-
       if (
         typeof configFileOrFiles !== 'string' &&
         (!isObject(configFileOrFiles) ||
@@ -1472,66 +1616,441 @@         ([relativeConfigPath, relativeDocumentSelectorOrSelectors]) => {
           return {
             folder: base,
             configPath: path.resolve(base, relativeConfigPath),
-            documentSelector: []
-              .concat(relativeDocumentSelectorOrSelectors)
-              .map((selector) => path.resolve(base, selector)),
+            documentSelector: [].concat(relativeDocumentSelectorOrSelectors).map((selector) => ({
+              priority: DocumentSelectorPriority.USER_CONFIGURED,
+              pattern: path.resolve(base, selector),
+            })),
+            isUserConfigured: true,
           }
         }
       )
+    } else {
+      let projects: Record<string, Array<DocumentSelector>> = {}
+
+      let files = await glob([`**/${CONFIG_GLOB}`, `**/${CSS_GLOB}`], {
+        cwd: base,
+        ignore: (await getConfiguration()).tailwindCSS.files.exclude,
+        onlyFiles: true,
+        absolute: true,
+        suppressErrors: true,
+        dot: true,
+        concurrency: Math.max(os.cpus().length, 1),
+      })
+
+      for (let filename of files) {
+        let normalizedFilename = normalizePath(filename)
+        let isCssFile = minimatch(normalizedFilename, `**/${CSS_GLOB}`, { dot: true })
+        let configPath = isCssFile ? await getConfigFileFromCssFile(filename) : filename
+        if (!configPath) {
+          continue
+        }
+
+        let twVersion = require('tailwindcss/package.json').version
+        let isDefaultVersion = true
+        try {
+          let v = require(resolveFrom(path.dirname(configPath), 'tailwindcss/package.json')).version
+          if (typeof v === 'string') {
+            twVersion = v
+            isDefaultVersion = false
+          }
+        } catch {}
+
+        if (isCssFile && (!semver.gte(twVersion, '3.2.0') || isDefaultVersion)) {
+          continue
+        }
+
+        configTailwindVersionMap.set(configPath, twVersion)
+
+        let contentSelector: Array<DocumentSelector> = []
+        try {
+          contentSelector = getContentDocumentSelectorFromConfigFile(configPath, twVersion, base)
+        } catch {}
+
+        let documentSelector: DocumentSelector[] = [
+          {
+            pattern: normalizePath(filename),
+            priority: isCssFile
+              ? DocumentSelectorPriority.CSS_FILE
+              : DocumentSelectorPriority.CONFIG_FILE,
+          },
+          ...(isCssFile
+            ? [
+                {
+                  pattern: normalizePath(configPath),
+                  priority: DocumentSelectorPriority.CONFIG_FILE,
+                },
+              ]
+            : []),
+          ...contentSelector,
+          {
+            pattern: normalizePath(path.join(path.dirname(filename), '**')),
+            priority: isCssFile
+              ? DocumentSelectorPriority.CSS_DIRECTORY
+              : DocumentSelectorPriority.CONFIG_DIRECTORY,
+          },
+          ...(isCssFile
+            ? [
+                {
+                  pattern: normalizePath(path.join(path.dirname(configPath), '**')),
+                  priority: DocumentSelectorPriority.CONFIG_DIRECTORY,
+                },
+              ]
+            : []),
+          {
+            pattern: normalizePath(path.join(getPackageRoot(path.dirname(configPath), base), '**')),
+            priority: DocumentSelectorPriority.ROOT_DIRECTORY,
+          },
+        ]
+
+        projects[configPath] = [...(projects[configPath] ?? []), ...documentSelector]
+
+        if (isCssFile) {
+          cssFileConfigMap.set(normalizedFilename, configPath)
+        }
+      }
+
+      if (Object.keys(projects).length > 0) {
+        workspaceFolders = Object.entries(projects).map(([configPath, documentSelector]) => {
+          return {
+            folder: base,
+            configPath,
+            isUserConfigured: false,
+            documentSelector: documentSelector
+              .sort((a, z) => a.priority - z.priority)
+              .filter(
+                ({ pattern }, index, documentSelectors) =>
+                  documentSelectors.findIndex(({ pattern: p }) => p === pattern) === index
+              ),
+          }
+        })
+      }
     }
 
-    await Promise.all(
-      workspaceFolders.map((projectConfig) => this.addProject(projectConfig, this.initializeParams))
-    )
+    console.log(`[Global] Creating projects: ${JSON.stringify(workspaceFolders)}`)
 
-    this.setupLSPHandlers()
+    const onDidChangeWatchedFiles = async (
+      changes: Array<{ file: string; type: FileChangeType }>
+    ): Promise<void> => {
+      let needsRestart = false
+
+      changeLoop: for (let change of changes) {
+        let normalizedFilename = normalizePath(change.file)
+
+        for (let ignorePattern of ignore) {
+          if (minimatch(normalizedFilename, ignorePattern, { dot: true })) {
+            continue changeLoop
+          }
+        }
+
+        let isPackageFile = minimatch(normalizedFilename, `**/${PACKAGE_LOCK_GLOB}`, { dot: true })
+        if (isPackageFile) {
+          for (let [key] of this.projects) {
+            let projectConfig = JSON.parse(key) as ProjectConfig
+            let twVersion = require('tailwindcss/package.json').version
+            try {
+              let v = require(resolveFrom(
+                path.dirname(projectConfig.configPath),
+                'tailwindcss/package.json'
+              )).version
+              if (typeof v === 'string') {
+                twVersion = v
+              }
+            } catch {}
+            if (configTailwindVersionMap.get(projectConfig.configPath) !== twVersion) {
+              needsRestart = true
+              break changeLoop
+            }
+          }
+        }
+
+        let isCssFile = minimatch(normalizedFilename, `**/${CSS_GLOB}`, {
+          dot: true,
+        })
+        if (isCssFile) {
+          let configPath = await getConfigFileFromCssFile(change.file)
+          if (
+            cssFileConfigMap.has(normalizedFilename) &&
+            cssFileConfigMap.get(normalizedFilename) !== configPath
+          ) {
+            needsRestart = true
+            break
+          } else if (!cssFileConfigMap.has(normalizedFilename) && configPath) {
+            needsRestart = true
+            break
+          }
+        }
+
+        let isConfigFile = minimatch(normalizedFilename, `**/${CONFIG_GLOB}`, {
+          dot: true,
+        })
+        if (isConfigFile && change.type === FileChangeType.Created) {
+          needsRestart = true
+          break
+        }
+
+        for (let [key] of this.projects) {
+          let projectConfig = JSON.parse(key) as ProjectConfig
+          if (
+            change.type === FileChangeType.Deleted &&
+            changeAffectsFile(normalizedFilename, [projectConfig.configPath])
+          ) {
+            needsRestart = true
+            break changeLoop
+          }
+        }
+      }
+
+      if (needsRestart) {
+        this.restart()
+        return
+      }
+
+      for (let [, project] of this.projects) {
+        project.onFileEvents(changes)
+      }
+    }
 
     if (this.initializeParams.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) {
-      this.connection.onDidChangeWatchedFiles(({ changes }) => {
-        for (let [, project] of this.projects) {
-          project.onFileEvents(
-            changes.map(({ uri, type }) => ({
+      this.disposables.push(
+        this.connection.onDidChangeWatchedFiles(async ({ changes }) => {
+          let normalizedChanges = changes
+            .map(({ uri, type }) => ({
               file: URI.parse(uri).fsPath,
               type,
             }))
+            .filter(
+              (change, changeIndex, changes) =>
+                changes.findIndex((c) => c.file === change.file && c.type === change.type) ===
+                changeIndex
+            )
+
+          await onDidChangeWatchedFiles(normalizedChanges)
+        })
+      )
+
+      let disposable = await this.connection.client.register(
+        DidChangeWatchedFilesNotification.type,
+        {
+          watchers: [
+            { globPattern: `**/${CONFIG_GLOB}` },
+            { globPattern: `**/${PACKAGE_LOCK_GLOB}` },
+            { globPattern: `**/${CSS_GLOB}` },
+          ],
+        }
+      )
+
+      this.disposables.push(disposable)
+
+      this.watchPatterns = (patterns) => {
+        let newPatterns = this.filterNewWatchPatterns(patterns)
+        if (newPatterns.length) {
+          console.log(`[Global] Adding watch patterns: ${newPatterns.join(', ')}`)
+          this.connection.client
+            .register(DidChangeWatchedFilesNotification.type, {
+              watchers: newPatterns.map((pattern) => ({ globPattern: pattern })),
+            })
+            .then((disposable) => {
+              this.disposables.push(disposable)
+            })
+        }
+      }
+    } else if (parcel.getBinding()) {
+      let typeMap = {
+        create: FileChangeType.Created,
+        update: FileChangeType.Changed,
+        delete: FileChangeType.Deleted,
+      }
+
+      let subscription = await parcel.subscribe(
+        base,
+        (err, events) => {
+          onDidChangeWatchedFiles(
+            events.map((event) => ({ file: event.path, type: typeMap[event.type] }))
           )
+        },
+        {
+          ignore: ignore.map((ignorePattern) =>
+            path.resolve(base, ignorePattern.replace(/^[*/]+/, '').replace(/[*/]+$/, ''))
+          ),
         }
+      )
+
+      this.disposables.push({
+        dispose() {
+          subscription.unsubscribe()
+        },
       })
+    } else {
+      let watch: typeof chokidar.watch = require('chokidar').watch
+      let chokidarWatcher = watch(
+        [`**/${CONFIG_GLOB}`, `**/${PACKAGE_LOCK_GLOB}`, `**/${CSS_GLOB}`],
+        {
+          cwd: base,
+          ignorePermissionErrors: true,
+          ignoreInitial: true,
+          ignored: ignore,
+          awaitWriteFinish: {
+            stabilityThreshold: 100,
+            pollInterval: 20,
+          },
+        }
+      )
+
+      await new Promise<void>((resolve) => {
+        chokidarWatcher.on('ready', () => resolve())
+      })
+
+      chokidarWatcher
+        .on('add', (file) =>
+          onDidChangeWatchedFiles([
+            { file: path.resolve(base, file), type: FileChangeType.Created },
+          ])
+        )
+        .on('change', (file) =>
+          onDidChangeWatchedFiles([
+            { file: path.resolve(base, file), type: FileChangeType.Changed },
+          ])
+        )
+        .on('unlink', (file) =>
+          onDidChangeWatchedFiles([
+            { file: path.resolve(base, file), type: FileChangeType.Deleted },
+          ])
+        )
+
+      this.disposables.push({
+        dispose() {
+          chokidarWatcher.close()
+        },
+      })
+
+      this.watchPatterns = (patterns) => {
+        let newPatterns = this.filterNewWatchPatterns(patterns)
+        if (newPatterns.length) {
+          console.log(`[Global] Adding watch patterns: ${newPatterns.join(', ')}`)
+          chokidarWatcher.add(newPatterns)
+        }
+      }
     }
 
-    this.connection.onDidChangeConfiguration(async ({ settings }) => {
-      for (let [, project] of this.projects) {
-        project.onUpdateSettings(settings)
+    await Promise.all(
+      workspaceFolders.map((projectConfig) =>
+        this.addProject(
+          projectConfig,
+          this.initializeParams,
+          this.watchPatterns,
+          configTailwindVersionMap.get(projectConfig.configPath)
+        )
+      )
+    )
+
+    // init projects for documents that are _already_ open
+    for (let document of this.documentService.getAllDocuments()) {
+      let project = this.getProject(document)
+      if (project && !project.enabled()) {
+        project.enable()
+        await project.tryInit()
       }
-    })
+    }
 
-    this.connection.onShutdown(() => {
-      this.dispose()
-    })
+    this.setupLSPHandlers()
 
-    this.documentService.onDidChangeContent((change) => {
-      this.getProject(change.document)?.provideDiagnostics(change.document)
-    })
+    this.disposables.push(
+      this.connection.onDidChangeConfiguration(async ({ settings }) => {
+        let previousExclude = globalSettings.tailwindCSS.files.exclude
+
+        documentSettingsCache.clear()
+        globalSettings = await getConfiguration()
+
+        if (!equal(previousExclude, globalSettings.tailwindCSS.files.exclude)) {
+          this.restart()
+          return
+        }
+
+        for (let [, project] of this.projects) {
+          project.onUpdateSettings(settings)
+        }
+      })
+    )
+
+    this.disposables.push(
+      this.connection.onShutdown(() => {
+        this.dispose()
+      })
+    )
+
+    this.disposables.push(
+      this.documentService.onDidChangeContent((change) => {
+        this.getProject(change.document)?.provideDiagnostics(change.document)
+      })
+    )
+
+    this.disposables.push(
+      this.documentService.onDidOpen((event) => {
+        let project = this.getProject(event.document)
+        if (project && !project.enabled()) {
+          project.enable()
+          project.tryInit()
+        }
+      })
+    )
   }
 
-  private async addProject(projectConfig: ProjectConfig, params: InitializeParams): Promise<void> {
+  private filterNewWatchPatterns(patterns: string[]) {
+    let newWatchPatterns = patterns.filter((pattern) => !this.watched.includes(pattern))
+    this.watched.push(...newWatchPatterns)
+    return newWatchPatterns
+  }
+
+  private async addProject(
+    projectConfig: ProjectConfig,
+    params: InitializeParams,
+    watchPatterns: (patterns: string[]) => void,
+    tailwindVersion: string
+  ): Promise<void> {
     let key = JSON.stringify(projectConfig)
-    if (this.projects.has(key)) {
-      await this.projects.get(key).tryInit()
-    } else {
+
+    if (!this.projects.has(key)) {
       const project = await createProjectService(
         projectConfig,
         this.connection,
         params,
         this.documentService,
-        () => this.updateCapabilities()
+        () => this.updateCapabilities(),
+        () => {
+          for (let document of this.documentService.getAllDocuments()) {
+            let project = this.getProject(document)
+            if (project && !project.enabled()) {
+              project.enable()
+              project.tryInit()
+              break
+            }
+          }
+        },
+        () => this.refreshDiagnostics(),
+        (patterns: string[]) => watchPatterns(patterns),
+        tailwindVersion
       )
       this.projects.set(key, project)
-      await project.tryInit()
+    }
+  }
+
+  private refreshDiagnostics() {
+    for (let doc of this.documentService.getAllDocuments()) {
+      let project = this.getProject(doc)
+      if (project) {
+        project.provideDiagnosticsForce(doc)
+      } else {
+        this.connection.sendDiagnostics({ uri: doc.uri, diagnostics: [] })
+      }
     }
   }
 
   private setupLSPHandlers() {
+    if (this.lspHandlersAdded) {
+      return
+    }
+    this.lspHandlersAdded = true
+
     this.connection.onHover(this.onHover.bind(this))
     this.connection.onCompletion(this.onCompletion.bind(this))
     this.connection.onCompletionResolve(this.onCompletionResolve.bind(this))
@@ -1567,23 +2086,38 @@           .map((sep) => sep.slice(-1)),
       ].filter(Boolean),
     })
 
-    capabilities.add(DidChangeWatchedFilesNotification.type, {
-      watchers: projects.flatMap(
-        (project) => project.state.dependencies?.map((file) => ({ globPattern: file })) ?? []
-      ),
-    })
-
     this.registrations = this.connection.client.register(capabilities)
   }
 
   private getProject(document: TextDocumentIdentifier): ProjectService {
     let fallbackProject: ProjectService
+    let matchedProject: ProjectService
+    let matchedPriority: number = Infinity
+
     for (let [key, project] of this.projects) {
       let projectConfig = JSON.parse(key) as ProjectConfig
       if (projectConfig.configPath) {
-        for (let selector of projectConfig.documentSelector) {
-          if (minimatch(URI.parse(document.uri).fsPath, selector)) {
-            return project
+        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 { pattern, priority } of documentSelector) {
+          let fsPath = URI.parse(document.uri).fsPath
+          if (pattern.startsWith('!') && minimatch(fsPath, pattern.slice(1), { dot: true })) {
+            break
+          }
+          if (minimatch(fsPath, pattern, { dot: true }) && priority < matchedPriority) {
+            matchedProject = project
+            matchedPriority = priority
           }
         }
       } else {
@@ -1592,6 +2126,11 @@           fallbackProject = project
         }
       }
     }
+
+    if (matchedProject) {
+      return matchedProject
+    }
+
     return fallbackProject
   }
 
@@ -1631,6 +2170,26 @@   dispose(): void {
     for (let [, project] of this.projects) {
       project.dispose()
     }
+    this.projects = new Map()
+
+    this.refreshDiagnostics()
+
+    if (this.registrations) {
+      this.registrations.then((r) => r.dispose())
+      this.registrations = undefined
+    }
+
+    this.disposables.forEach((d) => d.dispose())
+    this.disposables.length = 0
+
+    this.watched.length = 0
+  }
+
+  restart(): void {
+    console.log('----------\nRESTARTING\n----------')
+    this.dispose()
+    this.initialized = false
+    this.init()
   }
 }
 
@@ -1655,6 +2214,9 @@     return this.documents.onDidChangeContent
   }
   get onDidClose() {
     return this.documents.onDidClose
+  }
+  get onDidOpen() {
+    return this.documents.onDidOpen
   }
 }
 
M packages/tailwindcss-language-server/src/util/error.ts -> packages/tailwindcss-language-server/src/util/error.ts
diff --git a/packages/tailwindcss-language-server/src/util/error.ts b/packages/tailwindcss-language-server/src/util/error.ts
index 620412e0cecc14f2aeca93055bc0825d52278ef7..00fdac2592cf946d8c5b454db8a03cfe70ce671a 100644
--- a/packages/tailwindcss-language-server/src/util/error.ts
+++ b/packages/tailwindcss-language-server/src/util/error.ts
@@ -25,11 +25,11 @@   err: any,
   message: string = 'Tailwind CSS'
 ): void {
   console.error(formatError(message, err))
-  if (!(err instanceof SilentError)) {
-    connection.sendNotification('@/tailwindCSS/error', {
-      message: formatError(message, err, false),
-    })
-  }
+  // if (!(err instanceof SilentError)) {
+  //   connection.sendNotification('@/tailwindCSS/error', {
+  //     message: formatError(message, err, false),
+  //   })
+  // }
 }
 
 export function SilentError(message: string) {
M packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts -> packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts
diff --git a/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts b/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts
index cc3fbc63c752f51d817fdcea685b50a3d9049f69..df30e6610a098d3409e49592af8cdbf9b8fd4a28 100644
--- a/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts
+++ b/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts
@@ -24,6 +24,7 @@   params: CodeActionParams,
   only?: DiagnosticKind[]
 ): Promise<AugmentedDiagnostic[]> {
   let document = state.editor.documents.get(params.textDocument.uri)
+  if (!document) return []
   let diagnostics = await doValidate(state, document, only)
 
   return params.context.diagnostics
@@ -40,6 +41,10 @@     .filter(Boolean)
 }
 
 export async function doCodeActions(state: State, params: CodeActionParams): Promise<CodeAction[]> {
+  if (!state.enabled) {
+    return []
+  }
+
   let diagnostics = await getDiagnosticsFromCodeActionParams(
     state,
     params,
M packages/tailwindcss-language-service/src/codeActions/provideInvalidApplyCodeActions.ts -> packages/tailwindcss-language-service/src/codeActions/provideInvalidApplyCodeActions.ts
diff --git a/packages/tailwindcss-language-service/src/codeActions/provideInvalidApplyCodeActions.ts b/packages/tailwindcss-language-service/src/codeActions/provideInvalidApplyCodeActions.ts
index 3730492a688a967d2b088d953e3ab4ae10323f6f..c212eead15bf139f3f373bf56d5ce4808a0ef71e 100644
--- a/packages/tailwindcss-language-service/src/codeActions/provideInvalidApplyCodeActions.ts
+++ b/packages/tailwindcss-language-service/src/codeActions/provideInvalidApplyCodeActions.ts
@@ -25,6 +25,7 @@   params: CodeActionParams,
   diagnostic: InvalidApplyDiagnostic
 ): Promise<CodeAction[]> {
   let document = state.editor.documents.get(params.textDocument.uri)
+  if (!document) return []
   let documentText = getTextWithoutComments(document, 'css')
   let cssRange: Range
   let cssText = documentText
M packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts -> packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts
diff --git a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts
index 5ef686a605e75066a8c177eff5d969e790942ebc..a8d7f7dcec481c422d802a3d0b1bd8ccdbdf8bed 100644
--- a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts
+++ b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts
@@ -50,29 +50,3 @@           : []),
       ]
     : []
 }
-
-export async function provideDiagnostics(state: State, document: TextDocument) {
-  // state.editor.connection.sendDiagnostics({
-  //   uri: document.uri,
-  //   diagnostics: await doValidate(state, document),
-  // })
-}
-
-export function clearDiagnostics(state: State, document: TextDocument): void {
-  // state.editor.connection.sendDiagnostics({
-  //   uri: document.uri,
-  //   diagnostics: [],
-  // })
-}
-
-export function clearAllDiagnostics(state: State): void {
-  state.editor.documents.all().forEach((document) => {
-    clearDiagnostics(state, document)
-  })
-}
-
-export function updateAllDiagnostics(state: State): void {
-  state.editor.documents.all().forEach((document) => {
-    provideDiagnostics(state, document)
-  })
-}
M packages/tailwindcss-language-service/src/util/state.ts -> packages/tailwindcss-language-service/src/util/state.ts
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index c958d2b1aeb517b609b701fb1524e202b2719882..5d5c8321727efd11d0426c3d3eca1678def9318f 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -21,7 +21,6 @@ export type EditorState = {
   connection: Connection
   folder: string
   documents: TextDocuments<TextDocument>
-  globalSettings: Settings
   userLanguages: Record<string, string>
   capabilities: {
     configuration: boolean
M packages/vscode-tailwindcss/src/extension.ts -> packages/vscode-tailwindcss/src/extension.ts
diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts
index d1cb232a15727263b29110a09d58a70b75da31fe..e618e06cf86e348ce942c0b94ba102df3307d186 100755
--- a/packages/vscode-tailwindcss/src/extension.ts
+++ b/packages/vscode-tailwindcss/src/extension.ts
@@ -42,13 +42,12 @@ import isObject from 'tailwindcss-language-service/src/util/isObject'
 import { dedupe, equal } from 'tailwindcss-language-service/src/util/array'
 import namedColors from 'color-name'
 import minimatch from 'minimatch'
+import { CONFIG_GLOB, CSS_GLOB } from 'tailwindcss-language-server/src/lib/constants'
 
 const colorNames = Object.keys(namedColors)
 
 const CLIENT_ID = 'tailwindcss-intellisense'
 const CLIENT_NAME = 'Tailwind CSS IntelliSense'
-
-const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}'
 
 let clients: Map<string, LanguageClient> = new Map()
 let languages: Map<string, string[]> = new Map()
@@ -131,6 +130,11 @@     },
   }
 }
 
+async function fileContainsAtConfig(uri: Uri) {
+  let contents = (await Workspace.fs.readFile(uri)).toString()
+  return /@config\s*['"]/.test(contents)
+}
+
 export async function activate(context: ExtensionContext) {
   let module = context.asAbsolutePath(path.join('dist', 'server.js'))
   let prod = path.join('dist', 'tailwindServer.js')
@@ -206,20 +210,36 @@   //     }
   //   )
   // )
 
-  let watcher = Workspace.createFileSystemWatcher(`**/${CONFIG_FILE_GLOB}`, false, true, true)
+  let configWatcher = Workspace.createFileSystemWatcher(`**/${CONFIG_GLOB}`, false, true, true)
 
-  watcher.onDidCreate((uri) => {
+  configWatcher.onDidCreate((uri) => {
     let folder = Workspace.getWorkspaceFolder(uri)
-    if (!folder) {
+    if (!folder || isExcluded(uri.fsPath, folder)) {
       return
     }
-    if (!isExcluded(uri.fsPath, folder)) {
+    folder = getOuterMostWorkspaceFolder(folder)
+    bootWorkspaceClient(folder)
+  })
+
+  context.subscriptions.push(configWatcher)
+
+  let cssWatcher = Workspace.createFileSystemWatcher(`**/${CSS_GLOB}`, false, false, true)
+
+  async function bootClientIfCssFileContainsAtConfig(uri: Uri) {
+    let folder = Workspace.getWorkspaceFolder(uri)
+    if (!folder || isExcluded(uri.fsPath, folder)) {
+      return
+    }
+    if (await fileContainsAtConfig(uri)) {
       folder = getOuterMostWorkspaceFolder(folder)
       bootWorkspaceClient(folder)
     }
-  })
+  }
+
+  cssWatcher.onDidCreate(bootClientIfCssFileContainsAtConfig)
+  cssWatcher.onDidChange(bootClientIfCssFileContainsAtConfig)
 
-  context.subscriptions.push(watcher)
+  context.subscriptions.push(cssWatcher)
 
   // TODO: check if the actual language MAPPING changed
   // not just the language IDs
@@ -607,16 +627,27 @@ 
     searchedFolders.add(folder.uri.toString())
 
     let [configFile] = await Workspace.findFiles(
-      new RelativePattern(folder, `**/${CONFIG_FILE_GLOB}`),
+      new RelativePattern(folder, `**/${CONFIG_GLOB}`),
       `{${getExcludePatterns(folder).join(',')}}`,
       1
     )
 
-    if (!configFile) {
+    if (configFile) {
+      bootWorkspaceClient(folder)
       return
     }
 
-    bootWorkspaceClient(folder)
+    let cssFiles = await Workspace.findFiles(
+      new RelativePattern(folder, `**/${CSS_GLOB}`),
+      `{${getExcludePatterns(folder).join(',')}}`
+    )
+
+    for (let cssFile of cssFiles) {
+      if (await fileContainsAtConfig(cssFile)) {
+        bootWorkspaceClient(folder)
+        return
+      }
+    }
   }
 
   context.subscriptions.push(Workspace.onDidOpenTextDocument(didOpenTextDocument))