diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 463b363..04da927 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -25,6 +25,7 @@ import { HoverRequest, DidChangeWatchedFilesNotification, FileChangeType, + Disposable, } from 'vscode-languageserver/node' import { TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' @@ -73,6 +74,7 @@ import { debounce } from 'debounce' import { getModuleDependencies } from './util/getModuleDependencies' import assert from 'assert' // import postcssLoadConfig from 'postcss-load-config' +import * as parcel from './watcher/index.js' const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}' const TRIGGER_CHARACTERS = [ @@ -151,6 +153,7 @@ function first(...options: Array<() => T>): T { interface ProjectService { state: State tryInit: () => Promise + dispose: () => void onUpdateSettings: (settings: any) => void onHover(params: TextDocumentPositionParams): Promise onCompletion(params: CompletionParams): Promise @@ -167,6 +170,7 @@ async function createProjectService( params: InitializeParams, documentService: DocumentService ): Promise { + const disposables: Disposable[] = [] const state: State = { enabled: false, editor: { @@ -208,7 +212,13 @@ async function createProjectService( const documentSettingsCache: Map = new Map() let registrations: Promise - let watcher: FSWatcher + let chokidarWatcher: FSWatcher + let ignore = [ + '**/.git/objects/**', + '**/.git/subtree-cache/**', + '**/node_modules/**', + '**/.hg/store/**', + ] function onFileEvents(changes: Array<{ file: string; type: FileChangeType }>): void { let needsInit = false @@ -217,22 +227,30 @@ async function createProjectService( for (let change of changes) { let file = normalizePath(change.file) + for (let ignorePattern of ignore) { + if (minimatch(file, ignorePattern)) { + continue + } + } + + let isConfigFile = minimatch(file, `**/${CONFIG_FILE_GLOB}`) + let isPackageFile = minimatch(file, '**/package.json') + let isDependency = state.dependencies && state.dependencies.includes(change.file) + + if (!isConfigFile && !isPackageFile && !isDependency) continue + if (change.type === FileChangeType.Created) { needsInit = true break } else if (change.type === FileChangeType.Changed) { - if (!state.enabled || minimatch(file, '**/package.json')) { + if (!state.enabled || isPackageFile) { needsInit = true break } else { needsRebuild = true } } else if (change.type === FileChangeType.Deleted) { - if ( - !state.enabled || - minimatch(file, '**/package.json') || - minimatch(file, `**/${CONFIG_FILE_GLOB}`) - ) { + if (!state.enabled || isPackageFile || isConfigFile) { needsInit = true break } else { @@ -261,34 +279,59 @@ async function createProjectService( connection.client.register(DidChangeWatchedFilesNotification.type, { watchers: [{ globPattern: `**/${CONFIG_FILE_GLOB}` }, { globPattern: '**/package.json' }], }) - } else { - watcher = chokidar.watch( - [ - normalizePath(`${folder}/**/${CONFIG_FILE_GLOB}`), - normalizePath(`${folder}/**/package.json`), - ], + } 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] }))) + }, { - ignorePermissionErrors: true, - ignoreInitial: true, - ignored: ['**/node_modules/**'], - awaitWriteFinish: { - stabilityThreshold: 100, - pollInterval: 20, - }, + ignore: ignore.map((ignorePattern) => + path.resolve(folder, ignorePattern.replace(/^[*/]+/, '').replace(/[*/]+$/, '')) + ), } ) - await new Promise((resolve) => { - watcher.on('ready', () => resolve()) + disposables.push({ + dispose() { + subscription.unsubscribe() + }, + }) + } else { + chokidarWatcher = chokidar.watch([`**/${CONFIG_FILE_GLOB}`, '**/package.json'], { + cwd: folder, + ignorePermissionErrors: true, + ignoreInitial: true, + ignored: ignore, + awaitWriteFinish: { + stabilityThreshold: 100, + pollInterval: 20, + }, }) - watcher + await new Promise((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 registerCapabilities(watchFiles?: string[]): void { + function registerCapabilities(watchFiles: string[] = []): void { if (supportsDynamicRegistration(connection, params)) { if (registrations) { registrations.then((r) => r.dispose()) @@ -310,7 +353,7 @@ async function createProjectService( resolveProvider: true, triggerCharacters: [...TRIGGER_CHARACTERS, state.separator], }) - if (watchFiles) { + if (watchFiles.length > 0) { capabilities.add(DidChangeWatchedFilesNotification.type, { watchers: watchFiles.map((file) => ({ globPattern: file })), }) @@ -323,13 +366,13 @@ async function createProjectService( function resetState(): void { clearAllDiagnostics(state) Object.keys(state).forEach((key) => { - if (key !== 'editor') { + // Keep `dependencies` to ensure that they are still watched + if (key !== 'editor' && key !== 'dependencies') { delete state[key] } }) state.enabled = false - registerCapabilities() - // TODO reset watcher (remove config dependencies) + registerCapabilities(state.dependencies) } async function tryInit() { @@ -813,10 +856,10 @@ async function createProjectService( } if (state.dependencies) { - watcher?.unwatch(state.dependencies) + chokidarWatcher?.unwatch(state.dependencies) } state.dependencies = getModuleDependencies(state.configPath) - watcher?.add(state.dependencies) + chokidarWatcher?.add(state.dependencies) state.configId = getConfigId(state.configPath, state.dependencies) @@ -837,6 +880,11 @@ async function createProjectService( return { state, tryInit, + dispose() { + for (let { dispose } of disposables) { + dispose() + } + }, onUpdateSettings(settings: any): void { documentSettingsCache.clear() if (state.enabled) { @@ -1279,7 +1327,9 @@ class TW { } dispose(): void { - // + for (let [, project] of this.projects) { + project.dispose() + } } } diff --git a/packages/tailwindcss-language-server/src/watcher/index.js b/packages/tailwindcss-language-server/src/watcher/index.js new file mode 100644 index 0000000..fcaf7ff --- /dev/null +++ b/packages/tailwindcss-language-server/src/watcher/index.js @@ -0,0 +1,159 @@ +const os = require('os') +const path = require('path') +const fs = require('fs') + +const vars = (process.config && process.config.variables) || {} +const arch = os.arch() +const platform = os.platform() +const abi = process.versions.modules +const runtime = isElectron() ? 'electron' : 'node' +const libc = process.env.LIBC || (isAlpine(platform) ? 'musl' : 'glibc') +const armv = process.env.ARM_VERSION || (arch === 'arm64' ? '8' : vars.arm_version) || '' +const uv = (process.versions.uv || '').split('.')[0] + +const prebuilds = { + 'darwin-x64': { + 'node.napi.glibc.node': () => require('./prebuilds/darwin-x64.node.napi.glibc.node'), + }, + 'linux-x64': { + 'node.napi.glibc.node': () => require('./prebuilds/linux-x64.node.napi.glibc.node'), + 'node.napi.musl.node': () => require('./prebuilds/linux-x64.node.napi.musl.node'), + }, + 'win32-x64': { + 'node.napi.glibc.node': () => require('./prebuilds/win32-x64.node.napi.glibc.node'), + }, +} + +let getBinding = () => { + let resolved = resolve() + getBinding = () => resolved + return resolved +} + +exports.getBinding = getBinding + +exports.writeSnapshot = (dir, snapshot, opts) => { + return getBinding().writeSnapshot( + path.resolve(dir), + path.resolve(snapshot), + normalizeOptions(dir, opts) + ) +} + +exports.getEventsSince = (dir, snapshot, opts) => { + return getBinding().getEventsSince( + path.resolve(dir), + path.resolve(snapshot), + normalizeOptions(dir, opts) + ) +} + +exports.subscribe = async (dir, fn, opts) => { + dir = path.resolve(dir) + opts = normalizeOptions(dir, opts) + await getBinding().subscribe(dir, fn, opts) + + return { + unsubscribe() { + return getBinding().unsubscribe(dir, fn, opts) + }, + } +} + +exports.unsubscribe = (dir, fn, opts) => { + return getBinding().unsubscribe(path.resolve(dir), fn, normalizeOptions(dir, opts)) +} + +function resolve() { + // Find most specific flavor first + var list = prebuilds[platform + '-' + arch] + var builds = Object.keys(list) + var parsed = builds.map(parseTags) + var candidates = parsed.filter(matchTags(runtime, abi)) + var winner = candidates.sort(compareTags(runtime))[0] + if (winner) return list[winner.file]() +} + +function parseTags(file) { + var arr = file.split('.') + var extension = arr.pop() + var tags = { file: file, specificity: 0 } + + if (extension !== 'node') return + + for (var i = 0; i < arr.length; i++) { + var tag = arr[i] + + if (tag === 'node' || tag === 'electron' || tag === 'node-webkit') { + tags.runtime = tag + } else if (tag === 'napi') { + tags.napi = true + } else if (tag.slice(0, 3) === 'abi') { + tags.abi = tag.slice(3) + } else if (tag.slice(0, 2) === 'uv') { + tags.uv = tag.slice(2) + } else if (tag.slice(0, 4) === 'armv') { + tags.armv = tag.slice(4) + } else if (tag === 'glibc' || tag === 'musl') { + tags.libc = tag + } else { + continue + } + + tags.specificity++ + } + + return tags +} + +function matchTags(runtime, abi) { + return function (tags) { + if (tags == null) return false + if (tags.runtime !== runtime && !runtimeAgnostic(tags)) return false + if (tags.abi !== abi && !tags.napi) return false + if (tags.uv && tags.uv !== uv) return false + if (tags.armv && tags.armv !== armv) return false + if (tags.libc && tags.libc !== libc) return false + + return true + } +} + +function runtimeAgnostic(tags) { + return tags.runtime === 'node' && tags.napi +} + +function compareTags(runtime) { + // Precedence: non-agnostic runtime, abi over napi, then by specificity. + return function (a, b) { + if (a.runtime !== b.runtime) { + return a.runtime === runtime ? -1 : 1 + } else if (a.abi !== b.abi) { + return a.abi ? -1 : 1 + } else if (a.specificity !== b.specificity) { + return a.specificity > b.specificity ? -1 : 1 + } else { + return 0 + } + } +} + +function normalizeOptions(dir, opts = {}) { + if (Array.isArray(opts.ignore)) { + opts = Object.assign({}, opts, { + ignore: opts.ignore.map((ignore) => path.resolve(dir, ignore)), + }) + } + + return opts +} + +function isElectron() { + if (process.versions && process.versions.electron) return true + if (process.env.ELECTRON_RUN_AS_NODE) return true + return typeof window !== 'undefined' && window.process && window.process.type === 'renderer' +} + +function isAlpine(platform) { + return platform === 'linux' && fs.existsSync('/etc/alpine-release') +} diff --git a/packages/tailwindcss-language-server/src/watcher/licenses/@parcel/watcher b/packages/tailwindcss-language-server/src/watcher/licenses/@parcel/watcher new file mode 100644 index 0000000..7fb9bc9 --- /dev/null +++ b/packages/tailwindcss-language-server/src/watcher/licenses/@parcel/watcher @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-present Devon Govett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/tailwindcss-language-server/src/watcher/licenses/node-gyp-build b/packages/tailwindcss-language-server/src/watcher/licenses/node-gyp-build new file mode 100644 index 0000000..56fce08 --- /dev/null +++ b/packages/tailwindcss-language-server/src/watcher/licenses/node-gyp-build @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/tailwindcss-language-server/src/watcher/prebuilds/darwin-x64.node.napi.glibc.node b/packages/tailwindcss-language-server/src/watcher/prebuilds/darwin-x64.node.napi.glibc.node new file mode 100644 index 0000000..b96f68c Binary files /dev/null and b/packages/tailwindcss-language-server/src/watcher/prebuilds/darwin-x64.node.napi.glibc.node differ diff --git a/packages/tailwindcss-language-server/src/watcher/prebuilds/linux-x64.node.napi.glibc.node b/packages/tailwindcss-language-server/src/watcher/prebuilds/linux-x64.node.napi.glibc.node new file mode 100644 index 0000000..af791d0 Binary files /dev/null and b/packages/tailwindcss-language-server/src/watcher/prebuilds/linux-x64.node.napi.glibc.node differ diff --git a/packages/tailwindcss-language-server/src/watcher/prebuilds/linux-x64.node.napi.musl.node b/packages/tailwindcss-language-server/src/watcher/prebuilds/linux-x64.node.napi.musl.node new file mode 100644 index 0000000..7b62ac3 Binary files /dev/null and b/packages/tailwindcss-language-server/src/watcher/prebuilds/linux-x64.node.napi.musl.node differ diff --git a/packages/tailwindcss-language-server/src/watcher/prebuilds/win32-x64.node.napi.glibc.node b/packages/tailwindcss-language-server/src/watcher/prebuilds/win32-x64.node.napi.glibc.node new file mode 100644 index 0000000..4bc689f Binary files /dev/null and b/packages/tailwindcss-language-server/src/watcher/prebuilds/win32-x64.node.napi.glibc.node differ diff --git a/packages/vscode-tailwindcss/.vscodeignore b/packages/vscode-tailwindcss/.vscodeignore index b3e111a..23a3765 100755 --- a/packages/vscode-tailwindcss/.vscodeignore +++ b/packages/vscode-tailwindcss/.vscodeignore @@ -8,3 +8,4 @@ tests/** **/*.map .gitignore **/tsconfig.json +**/*.node