From 3836cbf2a56469510e1066b56ed7f03df34fb475 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Sun, 3 May 2020 18:11:45 +0100 Subject: [PATCH] add includeLanguages setting and remove default language client --- package.json | 4 + src/extension.ts | 171 ++++++++++++++---------- src/lib/languages.ts | 2 +- src/lsp/providers/completionProvider.ts | 20 +-- src/lsp/providers/hoverProvider.ts | 10 +- src/lsp/server.ts | 10 +- src/lsp/util/css.ts | 17 ++- src/lsp/util/html.ts | 19 ++- src/lsp/util/js.ts | 19 ++- src/lsp/util/state.ts | 2 + 10 files changed, 176 insertions(+), 98 deletions(-) diff --git a/package.json b/package.json index 1e563a3..916bead 100755 --- a/package.json +++ b/package.json @@ -55,6 +55,10 @@ "type": "boolean", "default": false, "description": "" + }, + "tailwindCSS.includeLanguages": { + "type": "object", + "default": {} } } } diff --git a/src/extension.ts b/src/extension.ts index 0adfcd1..2497372 100755 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,10 +18,15 @@ import { TransportKind, } from 'vscode-languageclient' import { registerConfigErrorHandler } from './lib/registerConfigErrorHandler' -import { LANGUAGES } from './lib/languages' +import { DEFAULT_LANGUAGES } from './lib/languages' +import isObject from './util/isObject' +import { dedupe, equal } from './util/array' + +const CLIENT_ID = 'tailwindcss-intellisense' +const CLIENT_NAME = 'Tailwind CSS IntelliSense' -let defaultClient: LanguageClient let clients: Map = new Map() +let languages: Map = new Map() let _sortedWorkspaceFolders: string[] | undefined function sortedWorkspaceFolders(): string[] { @@ -60,48 +65,102 @@ function getOuterMostWorkspaceFolder(folder: WorkspaceFolder): WorkspaceFolder { 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( - 'lsp-multi-server-example' - ) + 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(() => { + registerConfigErrorHandler(client) + }) + + client.start() + clients.set(folder.uri.toString(), client) + } function didOpenTextDocument(document: TextDocument): void { // We are only interested in language mode text - if ( - LANGUAGES.indexOf(document.languageId) === -1 || - (document.uri.scheme !== 'file' && document.uri.scheme !== 'untitled') - ) { + if (document.uri.scheme !== 'file') { return } let uri = document.uri - // Untitled files go to a default client. - if (uri.scheme === 'untitled' && !defaultClient) { - let debugOptions = { execArgv: ['--nolazy', '--inspect=6010'] } - let serverOptions = { - run: { module, transport: TransportKind.ipc }, - debug: { module, transport: TransportKind.ipc, options: debugOptions }, - } - let clientOptions: LanguageClientOptions = { - documentSelector: LANGUAGES.map((language) => ({ - scheme: 'untitled', - language, - })), - diagnosticCollectionName: 'lsp-multi-server-example', - outputChannel: outputChannel, - } - defaultClient = new LanguageClient( - 'lsp-multi-server-example', - 'LSP Multi Server Example', - serverOptions, - clientOptions - ) - defaultClient.start() - return - } 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. @@ -111,39 +170,14 @@ export function activate(context: ExtensionContext) { // If we have nested workspace folders we only start a server on the outer most workspace folder. folder = getOuterMostWorkspaceFolder(folder) - if (!clients.has(folder.uri.toString())) { - 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.map((language) => ({ - scheme: 'file', - language, - pattern: `${folder.uri.fsPath}/**/*`, - })), - diagnosticCollectionName: 'lsp-multi-server-example', - workspaceFolder: folder, - outputChannel: outputChannel, - middleware: {}, - } - let client = new LanguageClient( - 'lsp-multi-server-example', - 'LSP Multi Server Example', - serverOptions, - clientOptions + if (!languages.has(folder.uri.toString())) { + languages.set( + folder.uri.toString(), + dedupe([...DEFAULT_LANGUAGES, ...Object.keys(getUserLanguages())]) ) - - client.onReady().then(() => { - registerConfigErrorHandler(client) - }) - - client.start() - clients.set(folder.uri.toString(), client) } + + bootWorkspaceClient(folder) } Workspace.onDidOpenTextDocument(didOpenTextDocument) @@ -161,9 +195,6 @@ export function activate(context: ExtensionContext) { export function deactivate(): Thenable { let promises: Thenable[] = [] - if (defaultClient) { - promises.push(defaultClient.stop()) - } for (let client of clients.values()) { promises.push(client.stop()) } diff --git a/src/lib/languages.ts b/src/lib/languages.ts index 8e1b859..b9238ed 100644 --- a/src/lib/languages.ts +++ b/src/lib/languages.ts @@ -1,4 +1,4 @@ -export const LANGUAGES = [ +export const DEFAULT_LANGUAGES = [ // html 'aspnetcorerazor', 'blade', diff --git a/src/lsp/providers/completionProvider.ts b/src/lsp/providers/completionProvider.ts index f1e9130..4dc3745 100644 --- a/src/lsp/providers/completionProvider.ts +++ b/src/lsp/providers/completionProvider.ts @@ -191,13 +191,13 @@ function provideClassNameCompletions( let doc = state.editor.documents.get(params.textDocument.uri) if ( - isHtmlContext(doc, params.position) || - isJsContext(doc, params.position) + isHtmlContext(state, doc, params.position) || + isJsContext(state, doc, params.position) ) { return provideClassAttributeCompletions(state, params) } - if (isCssContext(doc, params.position)) { + if (isCssContext(state, doc, params.position)) { return provideAtApplyCompletions(state, params) } @@ -210,7 +210,7 @@ function provideCssHelperCompletions( ): CompletionList { let doc = state.editor.documents.get(textDocument.uri) - if (!isCssContext(doc, position)) { + if (!isCssContext(state, doc, position)) { return null } @@ -318,7 +318,7 @@ function provideTailwindDirectiveCompletions( ): CompletionList { let doc = state.editor.documents.get(textDocument.uri) - if (!isCssContext(doc, position)) { + if (!isCssContext(state, doc, position)) { return null } @@ -409,7 +409,7 @@ function provideVariantsDirectiveCompletions( ): CompletionList { let doc = state.editor.documents.get(textDocument.uri) - if (!isCssContext(doc, position)) { + if (!isCssContext(state, doc, position)) { return null } @@ -457,7 +457,7 @@ function provideScreenDirectiveCompletions( ): CompletionList { let doc = state.editor.documents.get(textDocument.uri) - if (!isCssContext(doc, position)) { + if (!isCssContext(state, doc, position)) { return null } @@ -505,7 +505,7 @@ function provideCssDirectiveCompletions( ): CompletionList { let doc = state.editor.documents.get(textDocument.uri) - if (!isCssContext(doc, position)) { + if (!isCssContext(state, doc, position)) { return null } @@ -600,9 +600,9 @@ async function provideEmmetCompletions( let doc = state.editor.documents.get(textDocument.uri) - const syntax = isHtmlContext(doc, position) + const syntax = isHtmlContext(state, doc, position) ? 'html' - : isJsContext(doc, position) + : isJsContext(state, doc, position) ? 'jsx' : null diff --git a/src/lsp/providers/hoverProvider.ts b/src/lsp/providers/hoverProvider.ts index ce21375..7e27d9a 100644 --- a/src/lsp/providers/hoverProvider.ts +++ b/src/lsp/providers/hoverProvider.ts @@ -27,7 +27,7 @@ function provideCssHelperHover( ): Hover { let doc = state.editor.documents.get(textDocument.uri) - if (!isCssContext(doc, position)) return null + if (!isCssContext(state, doc, position)) return null const line = doc.getText({ start: { line: position.line, character: 0 }, @@ -81,7 +81,11 @@ function provideClassAttributeHover( ): Hover { let doc = state.editor.documents.get(textDocument.uri) - if (!isHtmlContext(doc, position) && !isJsContext(doc, position)) return null + if ( + !isHtmlContext(state, doc, position) && + !isJsContext(state, doc, position) + ) + return null let hovered = getClassNameAtPosition(doc, position) if (!hovered) return null @@ -111,7 +115,7 @@ function provideAtApplyHover( ): Hover { let doc = state.editor.documents.get(textDocument.uri) - if (!isCssContext(doc, position)) return null + if (!isCssContext(state, doc, position)) return null const classNames = findClassNamesInRange(doc, { start: { line: Math.max(position.line - 10, 0), character: 0 }, diff --git a/src/lsp/server.ts b/src/lsp/server.ts index 3315853..b28b1ba 100644 --- a/src/lsp/server.ts +++ b/src/lsp/server.ts @@ -32,7 +32,10 @@ let connection = createConnection(ProposedFeatures.all) let documents = new TextDocuments() let workspaceFolder: string | null -const defaultSettings: Settings = { emmetCompletions: false } +const defaultSettings: Settings = { + emmetCompletions: false, + includeLanguages: {}, +} let globalSettings: Settings = defaultSettings let documentSettings: Map = new Map() @@ -53,6 +56,11 @@ connection.onInitialize( documents, documentSettings, globalSettings, + userLanguages: + params.initializationOptions && + params.initializationOptions.userLanguages + ? params.initializationOptions.userLanguages + : {}, capabilities: { configuration: capabilities.workspace && !!capabilities.workspace.configuration, diff --git a/src/lsp/util/css.ts b/src/lsp/util/css.ts index fb4f90a..84b7888 100644 --- a/src/lsp/util/css.ts +++ b/src/lsp/util/css.ts @@ -1,5 +1,6 @@ import { TextDocument, Position } from 'vscode-languageserver' import { isInsideTag, isVueDoc, isSvelteDoc } from './html' +import { State } from './state' export const CSS_LANGUAGES = [ 'css', @@ -10,12 +11,20 @@ export const CSS_LANGUAGES = [ 'stylus', ] -function isCssDoc(doc: TextDocument): boolean { - return CSS_LANGUAGES.indexOf(doc.languageId) !== -1 +function isCssDoc(state: State, doc: TextDocument): boolean { + const userCssLanguages = Object.keys( + state.editor.userLanguages + ).filter((lang) => CSS_LANGUAGES.includes(state.editor.userLanguages[lang])) + + return [...CSS_LANGUAGES, ...userCssLanguages].indexOf(doc.languageId) !== -1 } -export function isCssContext(doc: TextDocument, position: Position): boolean { - if (isCssDoc(doc)) { +export function isCssContext( + state: State, + doc: TextDocument, + position: Position +): boolean { + if (isCssDoc(state, doc)) { return true } diff --git a/src/lsp/util/html.ts b/src/lsp/util/html.ts index d0f5743..8808141 100644 --- a/src/lsp/util/html.ts +++ b/src/lsp/util/html.ts @@ -1,4 +1,5 @@ import { TextDocument, Position } from 'vscode-languageserver' +import { State } from './state' export const HTML_LANGUAGES = [ 'aspnetcorerazor', @@ -27,8 +28,14 @@ export const HTML_LANGUAGES = [ 'twig', ] -export function isHtmlDoc(doc: TextDocument): boolean { - return HTML_LANGUAGES.indexOf(doc.languageId) !== -1 +export function isHtmlDoc(state: State, doc: TextDocument): boolean { + const userHtmlLanguages = Object.keys( + state.editor.userLanguages + ).filter((lang) => HTML_LANGUAGES.includes(state.editor.userLanguages[lang])) + + return ( + [...HTML_LANGUAGES, ...userHtmlLanguages].indexOf(doc.languageId) !== -1 + ) } export function isVueDoc(doc: TextDocument): boolean { @@ -39,13 +46,17 @@ export function isSvelteDoc(doc: TextDocument): boolean { return doc.languageId === 'svelte' } -export function isHtmlContext(doc: TextDocument, position: Position): boolean { +export function isHtmlContext( + state: State, + doc: TextDocument, + position: Position +): boolean { let str = doc.getText({ start: { line: 0, character: 0 }, end: position, }) - if (isHtmlDoc(doc) && !isInsideTag(str, ['script', 'style'])) { + if (isHtmlDoc(state, doc) && !isInsideTag(str, ['script', 'style'])) { return true } diff --git a/src/lsp/util/js.ts b/src/lsp/util/js.ts index 48107f0..8a62a5f 100644 --- a/src/lsp/util/js.ts +++ b/src/lsp/util/js.ts @@ -1,5 +1,6 @@ import { TextDocument, Position } from 'vscode-languageserver' import { isHtmlDoc, isInsideTag, isVueDoc, isSvelteDoc } from './html' +import { State } from './state' export const JS_LANGUAGES = [ 'javascript', @@ -8,12 +9,20 @@ export const JS_LANGUAGES = [ 'typescriptreact', ] -export function isJsDoc(doc: TextDocument): boolean { - return JS_LANGUAGES.indexOf(doc.languageId) !== -1 +export function isJsDoc(state: State, doc: TextDocument): boolean { + const userJsLanguages = Object.keys( + state.editor.userLanguages + ).filter((lang) => JS_LANGUAGES.includes(state.editor.userLanguages[lang])) + + return [...JS_LANGUAGES, ...userJsLanguages].indexOf(doc.languageId) !== -1 } -export function isJsContext(doc: TextDocument, position: Position): boolean { - if (isJsDoc(doc)) { +export function isJsContext( + state: State, + doc: TextDocument, + position: Position +): boolean { + if (isJsDoc(state, doc)) { return true } @@ -22,7 +31,7 @@ export function isJsContext(doc: TextDocument, position: Position): boolean { end: position, }) - if (isHtmlDoc(doc) && isInsideTag(str, ['script'])) { + if (isHtmlDoc(state, doc) && isInsideTag(str, ['script'])) { return true } diff --git a/src/lsp/util/state.ts b/src/lsp/util/state.ts index 401cedf..daaa050 100644 --- a/src/lsp/util/state.ts +++ b/src/lsp/util/state.ts @@ -18,6 +18,7 @@ export type EditorState = { documents: TextDocuments documentSettings: Map globalSettings: Settings + userLanguages: Record capabilities: { configuration: boolean } @@ -25,6 +26,7 @@ export type EditorState = { export type Settings = { emmetCompletions: boolean + includeLanguages: Record } export type State = null | {