/* -------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ import * as path from 'path' import { workspace as Workspace, window as Window, ExtensionContext, TextDocument, OutputChannel, WorkspaceFolder, Uri, ConfigurationScope, } from 'vscode' import { LanguageClient, LanguageClientOptions, TransportKind, } from 'vscode-languageclient' import { registerConfigErrorHandler } from './lib/registerConfigErrorHandler' import { DEFAULT_LANGUAGES } from './lib/languages' import isObject from './util/isObject' import { dedupe, equal } from './util/array' import { createEmitter } from './lib/emitter' import { onMessage } from './lsp/notifications' import { registerColorDecorator } from './lib/registerColorDecorator' const CLIENT_ID = 'tailwindcss-intellisense' const CLIENT_NAME = 'Tailwind CSS IntelliSense' let clients: Map = new Map() let languages: Map = new Map() let _sortedWorkspaceFolders: string[] | undefined function sortedWorkspaceFolders(): string[] { if (_sortedWorkspaceFolders === void 0) { _sortedWorkspaceFolders = Workspace.workspaceFolders ? Workspace.workspaceFolders .map((folder) => { let result = folder.uri.toString() if (result.charAt(result.length - 1) !== '/') { result = result + '/' } return result }) .sort((a, b) => { return a.length - b.length }) : [] } return _sortedWorkspaceFolders } Workspace.onDidChangeWorkspaceFolders( () => (_sortedWorkspaceFolders = undefined) ) function getOuterMostWorkspaceFolder(folder: WorkspaceFolder): WorkspaceFolder { let sorted = sortedWorkspaceFolders() for (let element of sorted) { let uri = folder.uri.toString() if (uri.charAt(uri.length - 1) !== '/') { uri = uri + '/' } if (uri.startsWith(element)) { return Workspace.getWorkspaceFolder(Uri.parse(element))! } } return folder } function getUserLanguages(folder?: WorkspaceFolder): Record { const langs = Workspace.getConfiguration('tailwindCSS', folder) .includeLanguages return isObject(langs) ? langs : {} } export function activate(context: ExtensionContext) { let module = context.asAbsolutePath(path.join('dist', 'server', 'index.js')) let outputChannel: OutputChannel = Window.createOutputChannel(CLIENT_ID) // TODO: check if the actual language MAPPING changed // not just the language IDs // e.g. "plaintext" already exists but you change it from "html" to "css" Workspace.onDidChangeConfiguration((event) => { clients.forEach((client, key) => { const folder = Workspace.getWorkspaceFolder(Uri.parse(key)) if (event.affectsConfiguration('tailwindCSS', folder)) { const userLanguages = getUserLanguages(folder) if (userLanguages) { const userLanguageIds = Object.keys(userLanguages) const newLanguages = dedupe([ ...DEFAULT_LANGUAGES, ...userLanguageIds, ]) if (!equal(newLanguages, languages.get(folder.uri.toString()))) { languages.set(folder.uri.toString(), newLanguages) if (client) { clients.delete(folder.uri.toString()) client.stop() bootWorkspaceClient(folder) } } } } }) }) function bootWorkspaceClient(folder: WorkspaceFolder) { if (clients.has(folder.uri.toString())) { return } // placeholder so we don't boot another server before this one is ready clients.set(folder.uri.toString(), null) let debugOptions = { execArgv: ['--nolazy', `--inspect=${6011 + clients.size}`], } let serverOptions = { run: { module, transport: TransportKind.ipc }, debug: { module, transport: TransportKind.ipc, options: debugOptions, }, } let clientOptions: LanguageClientOptions = { documentSelector: languages .get(folder.uri.toString()) .map((language) => ({ scheme: 'file', language, pattern: `${folder.uri.fsPath}/**/*`, })), diagnosticCollectionName: CLIENT_ID, workspaceFolder: folder, outputChannel: outputChannel, middleware: {}, initializationOptions: { userLanguages: getUserLanguages(folder), }, } let client = new LanguageClient( CLIENT_ID, CLIENT_NAME, serverOptions, clientOptions ) client.onReady().then(() => { let emitter = createEmitter(client) registerConfigErrorHandler(emitter) registerColorDecorator(client, context, emitter) onMessage(client, 'getConfiguration', async (scope) => { return { tabSize: Workspace.getConfiguration('editor', scope).get('tabSize') || 2, ...Workspace.getConfiguration('tailwindCSS', scope), } }) }) client.start() clients.set(folder.uri.toString(), client) } function didOpenTextDocument(document: TextDocument): void { // We are only interested in language mode text if (document.uri.scheme !== 'file') { return } let uri = document.uri let folder = Workspace.getWorkspaceFolder(uri) // Files outside a folder can't be handled. This might depend on the language. // Single file languages like JSON might handle files outside the workspace folders. if (!folder) { return } // If we have nested workspace folders we only start a server on the outer most workspace folder. folder = getOuterMostWorkspaceFolder(folder) if (!languages.has(folder.uri.toString())) { languages.set( folder.uri.toString(), dedupe([...DEFAULT_LANGUAGES, ...Object.keys(getUserLanguages())]) ) } bootWorkspaceClient(folder) } Workspace.onDidOpenTextDocument(didOpenTextDocument) Workspace.textDocuments.forEach(didOpenTextDocument) Workspace.onDidChangeWorkspaceFolders((event) => { for (let folder of event.removed) { let client = clients.get(folder.uri.toString()) if (client) { clients.delete(folder.uri.toString()) client.stop() } } }) } export function deactivate(): Thenable { let promises: Thenable[] = [] for (let client of clients.values()) { promises.push(client.stop()) } return Promise.all(promises).then(() => undefined) }