/* -------------------------------------------------------------------------------------------- * 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, commands, SymbolInformation, Position, Range, TextEditorDecorationType, } from 'vscode' import { LanguageClient, LanguageClientOptions, TransportKind } from 'vscode-languageclient/node' import { DEFAULT_LANGUAGES } from './lib/languages' import isObject from './util/isObject' import { dedupe, equal } from 'tailwindcss-language-service/src/util/array' import { names as namedColors } from '@ctrl/tinycolor' const colorNames = Object.keys(namedColors) 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 : {} } let colorDecorationType: TextEditorDecorationType export function activate(context: ExtensionContext) { let module = context.asAbsolutePath(path.join('dist', 'server', 'index.js')) let outputChannel: OutputChannel = Window.createOutputChannel(CLIENT_NAME) context.subscriptions.push( commands.registerCommand('tailwindCSS.showOutput', () => { outputChannel.show() }) ) // 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: { async resolveCompletionItem(item, token, next) { let result = await next(item, token) let selections = Window.activeTextEditor.selections if (selections.length > 1 && result.additionalTextEdits?.length > 0) { let length = selections[0].start.character - result.additionalTextEdits[0].range.start.character let prefixLength = result.additionalTextEdits[0].range.end.character - result.additionalTextEdits[0].range.start.character let ranges = selections.map((selection) => { return new Range( new Position(selection.start.line, selection.start.character - length), new Position( selection.start.line, selection.start.character - length + prefixLength ) ) }) if ( ranges .map((range) => Window.activeTextEditor.document.getText(range)) .every((text, _index, arr) => arr.indexOf(text) === 0) ) { // all the same result.additionalTextEdits = ranges.map((range) => { return { range, newText: result.additionalTextEdits[0].newText } }) } else { result.insertText = result.label result.additionalTextEdits = [] } } return result }, async provideDocumentColors(document, token, next) { let colors = await next(document, token) let editableColors = colors.filter((color) => { let text = Workspace.textDocuments.find((doc) => doc === document)?.getText(color.range) ?? '' return new RegExp( `-\\[(${colorNames.join('|')}|((?:#|rgba?\\(|hsla?\\())[^\\]]+)\\]$` ).test(text) }) let nonEditableColors = colors.filter((color) => !editableColors.includes(color)) if (!colorDecorationType) { colorDecorationType = Window.createTextEditorDecorationType({ before: { width: '0.8em', height: '0.8em', contentText: ' ', border: '0.1em solid', margin: '0.1em 0.2em 0', }, dark: { before: { borderColor: '#eeeeee', }, }, light: { before: { borderColor: '#000000', }, }, }) } Window.visibleTextEditors .find((editor) => editor.document === document) ?.setDecorations( colorDecorationType, nonEditableColors.map(({ range, color }) => ({ range, renderOptions: { before: { backgroundColor: `rgba(${color.red * 255}, ${color.green * 255}, ${ color.blue * 255 }, ${color.alpha})`, }, }, })) ) return editableColors }, workspace: { configuration: (params, token, next) => { try { return params.items.map(({ section, scopeUri }) => { if (section === 'tailwindCSS') { let scope = scopeUri ? { languageId: Workspace.textDocuments.find( (doc) => doc.uri.toString() === scopeUri ).languageId, } : folder let tabSize = Workspace.getConfiguration('editor', scope).get('tabSize') || 2 return { tabSize, ...Workspace.getConfiguration(section, scope) } } throw Error() }) } catch (_error) { return next(params, token) } }, }, }, initializationOptions: { userLanguages: getUserLanguages(folder), configuration: Workspace.getConfiguration('tailwindCSS', folder), }, synchronize: { configurationSection: ['editor', 'tailwindCSS'], }, } let client = new LanguageClient(CLIENT_ID, CLIENT_NAME, serverOptions, clientOptions) client.onReady().then(() => { client.onNotification('@/tailwindCSS/error', async ({ message }) => { let action = await Window.showErrorMessage(message, 'Go to output') if (action === 'Go to output') { commands.executeCommand('tailwindCSS.showOutput') } }) client.onNotification('@/tailwindCSS/clearColors', () => { if (colorDecorationType) { colorDecorationType.dispose() colorDecorationType = undefined } }) client.onRequest('@/tailwindCSS/getDocumentSymbols', async ({ uri }) => { return commands.executeCommand( 'vscode.executeDocumentSymbolProvider', Uri.parse(uri) ) }) }) 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) }