diff --git a/lsp-multi-root-sample/README.md b/lsp-multi-root-sample/README.md index 40e3d281..24e11164 100644 --- a/lsp-multi-root-sample/README.md +++ b/lsp-multi-root-sample/README.md @@ -14,3 +14,5 @@ The example uses proposed Language Server protocol. So the code demoed here migh - Run the lauch config - If you want to debug the server as well use the launch configuration `Attach to Server` +- In the [Extension Development Host] instance of VSCOde, open a document in 'plain text' laguage mode and write some words. The extension will emit diagnostics for words in all-uppercase (e.g. 'TEST'). + diff --git a/lsp-multi-root-sample/client/src/extension.ts b/lsp-multi-root-sample/client/src/extension.ts index e83a5c70..1cd542ca 100644 --- a/lsp-multi-root-sample/client/src/extension.ts +++ b/lsp-multi-root-sample/client/src/extension.ts @@ -68,14 +68,15 @@ export function activate(context: ExtensionContext) { documentSelector: [{scheme: 'file', language: 'plaintext'}], synchronize: { // Notify the server about file changes to '.clientrc files contain in the workspace - fileEvents: workspace.createFileSystemWatcher('**/.clientrc') + fileEvents: workspace.createFileSystemWatcher('**/.clientrc'), + configurationSection: [ 'lspMultiRootSample' ] }, middleware: middleware as Middleware } // Create the language client and start the client. let client = new LanguageClient('languageServerExample', 'Language Server Example', serverOptions, clientOptions); - // Register new propose protocol if available. + // Register new proposed protocol if available. client.registerProposedFeatures(); // Start the client. This will also launch the server diff --git a/lsp-multi-root-sample/server/src/server.ts b/lsp-multi-root-sample/server/src/server.ts index 6f9d395d..4f4262c5 100644 --- a/lsp-multi-root-sample/server/src/server.ts +++ b/lsp-multi-root-sample/server/src/server.ts @@ -5,112 +5,112 @@ 'use strict'; import { - createConnection, TextDocuments, TextDocument, Diagnostic, DiagnosticSeverity, - InitializeResult, DidChangeConfigurationNotification, Proposed, ProposedFeatures, + createConnection, TextDocuments, TextDocument, Diagnostic, DiagnosticSeverity, + ProposedFeatures, InitializeParams, } from 'vscode-languageserver'; -// Create a connection for the server. The connection uses Node's IPC as a transport +// create a connection for the server. The connection uses Node's IPC as a transport let connection = createConnection(ProposedFeatures.all); -// Create a simple text document manager. The text document manager +// create a simple text document manager. The text document manager // supports full document sync only let documents: TextDocuments = new TextDocuments(); -connection.onInitialize((_params): InitializeResult => { +let hasConfigurationCapability = false; + +connection.onInitialize((params: InitializeParams) => { + function hasClientCapability(...keys: string[]) { + let c = params.capabilities; + for (let i = 0; c && i < keys.length; i++) { + c = c[keys[i]]; + } + return !!c; + } + // does the client support the `workspace/configuration` request? + // if not, we will fall back using global settings + hasConfigurationCapability = hasClientCapability('workspace', 'configuration'); return { capabilities: { - // Tell the client that the server works in FULL text document sync mode textDocumentSync: documents.syncKind } } }); -// The example settings +// the example settings interface MultiRootExampleSettings { maxNumberOfProblems: number; } -let settings: Map> = new Map(); +// the global settings, used when the `workspace/configuration` request is not supported by the client +let globalSettings: MultiRootExampleSettings = { maxNumberOfProblems: 1000 }; -function getConfiguration(resource: string): Thenable { - let result = settings.get(resource); +// cache the settings of all open documents +let documentSettings: Map> = new Map(); + +connection.onDidChangeConfiguration(change => { + globalSettings = (change.settings.lspMultiRootSample || {}); + + // reset all document settings + documentSettings.clear(); + + // revalidate all open text documents + documents.all().forEach(validateTextDocument); +}); + +function getDocumentSettings(resource: string): Thenable { + if (!hasConfigurationCapability) { + return Promise.resolve(globalSettings); + } + let result = documentSettings.get(resource); if (!result) { - result = connection.workspace.getConfiguration({ section: '', scopeUri: resource }); - settings.set(resource, result); + result = connection.workspace.getConfiguration({ scopeUri: resource }); + documentSettings.set(resource, result); } return result; } -connection.onInitialized(() => { - // Register to configuration change events. - connection.client.register(DidChangeConfigurationNotification.type); +// only keep settings for open documents +documents.onDidClose(e => { + documentSettings.delete(e.document.uri); }); - -connection.onNotification(DidChangeConfigurationNotification.type, () => { - let toRequest: Proposed.ConfigurationItem[] = []; - for (let resource of settings.keys()) { - toRequest.push({ section: '', scopeUri: resource}); - } - settings.clear(); - // Reread all cached configuration - connection.workspace.getConfiguration(toRequest).then((values: MultiRootExampleSettings[]) => { - let toRevalidate: string[] = []; - for (let i = 0; i < values.length; i++) { - let resource = toRequest[i].scopeUri; - let value = values[i]; - // If the value got already added to the settings cache then a change has - // occurred before the configuration request got return. Ignore the value - // in this case. - if (value && !settings.has(resource)) { - settings.set(resource, Promise.resolve(value)); - toRevalidate.push(resource); - } - } - for (let resource of toRevalidate) { - validateTextDocument(documents.get(resource)); - } - }); -}); - - -// The content of a text document has changed. This event is emitted +// the content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. documents.onDidChangeContent((change) => { validateTextDocument(change.document); }); +async function validateTextDocument(textDocument: TextDocument): Promise { + // in this simple example we get the settings for every validate run. + let settings = await getDocumentSettings(textDocument.uri); -function validateTextDocument(textDocument: TextDocument): void { - // In this simple example we get the settings for every validate run. - getConfiguration(textDocument.uri).then((settings: MultiRootExampleSettings) => { - let diagnostics: Diagnostic[] = []; - let lines = textDocument.getText().split(/\r?\n/g); - let problems = 0; - for (var i = 0; i < lines.length && problems < settings.maxNumberOfProblems; i++) { - let line = lines[i]; - let index = line.indexOf('typescript'); - if (index >= 0) { - problems++; - diagnostics.push({ - severity: DiagnosticSeverity.Warning, - range: { - start: { line: i, character: index}, - end: { line: i, character: index + 10 } - }, - message: `${line.substr(index, 10)} should be spelled TypeScript`, - source: 'ex' - }); - } - } - // Send the computed diagnostics to VSCode. - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); - }); + // the validator creates diagnostics for all uppercase words length 2 and more + let text = textDocument.getText(); + let pattern = /\b[A-Z]{2,}\b/g; + let m: RegExpExecArray; + + let problems = 0; + let diagnostics: Diagnostic[] = []; + while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) { + problems++; + diagnostics.push({ + severity: DiagnosticSeverity.Warning, + range: { + start: textDocument.positionAt(m.index), + end: textDocument.positionAt(m.index + m[0].length) + }, + message: `${m[0].length} is all uppercase.`, + source: 'ex' + }); + } + + // send the computed diagnostics to VSCode. + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); } -// Make the text document manager listen on the connection +// make the text document manager listen on the connection // for open, change and close text document events documents.listen(connection); -// Listen on the connection +// listen on the connection connection.listen(); \ No newline at end of file