1/* --------------------------------------------------------------------------------------------
2 * Copyright (c) Microsoft Corporation. All rights reserved.
3 * Licensed under the MIT License. See License.txt in the project root for license information.
4 * ------------------------------------------------------------------------------------------ */
5import {
6  createConnection,
7  TextDocuments,
8  Diagnostic,
9  DiagnosticSeverity,
10  ProposedFeatures,
11  InitializeParams,
12  DidChangeConfigurationNotification,
13  CompletionItem,
14  CompletionItemKind,
15  TextDocumentPositionParams,
16  TextDocumentSyncKind,
17  InitializeResult,
18  IConnection
19} from 'vscode-languageserver';
20
21import {
22  TextDocument
23} from 'vscode-languageserver-textdocument';
24
25import {
26  createProtocolConnection, createClientSocketTransport
27} from 'vscode-languageserver-protocol';
28
29function RunServer(connection: IConnection) {
30  // Create a simple text document manager.
31  let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
32
33  let hasConfigurationCapability: boolean = false;
34  let hasWorkspaceFolderCapability: boolean = false;
35  let hasDiagnosticRelatedInformationCapability: boolean = false;
36
37  connection.onInitialize((params: InitializeParams) => {
38    let capabilities = params.capabilities;
39
40    // Does the client support the `workspace/configuration` request?
41    // If not, we fall back using global settings.
42    hasConfigurationCapability = !!(
43      capabilities.workspace && !!capabilities.workspace.configuration
44    );
45    hasWorkspaceFolderCapability = !!(
46      capabilities.workspace && !!capabilities.workspace.workspaceFolders
47    );
48    hasDiagnosticRelatedInformationCapability = !!(
49      capabilities.textDocument &&
50      capabilities.textDocument.publishDiagnostics &&
51      capabilities.textDocument.publishDiagnostics.relatedInformation
52    );
53
54    const result: InitializeResult = {
55      capabilities: {
56        textDocumentSync: TextDocumentSyncKind.Incremental,
57        // Tell the client that this server supports code completion.
58        completionProvider: {
59          resolveProvider: true
60        }
61      }
62    };
63    if (hasWorkspaceFolderCapability) {
64      result.capabilities.workspace = {
65        workspaceFolders: {
66          supported: true
67        }
68      };
69    }
70    return result;
71  });
72
73  connection.onInitialized(() => {
74    if (hasConfigurationCapability) {
75      // Register for all configuration changes.
76      connection.client.register(DidChangeConfigurationNotification.type, undefined);
77    }
78    if (hasWorkspaceFolderCapability) {
79      connection.workspace.onDidChangeWorkspaceFolders(_event => {
80        connection.console.log('Workspace folder change event received.');
81      });
82    }
83  });
84
85  // The example settings
86  interface ExampleSettings {
87    maxNumberOfProblems: number;
88  }
89
90  // The global settings, used when the `workspace/configuration` request is not supported by the client.
91  // Please note that this is not the case when using this server with the client provided in this example
92  // but could happen with other clients.
93  const defaultSettings: ExampleSettings = { maxNumberOfProblems: 1000 };
94  let globalSettings: ExampleSettings = defaultSettings;
95
96  // Cache the settings of all open documents
97  let documentSettings: Map<string, Thenable<ExampleSettings>> = new Map();
98
99  connection.onDidChangeConfiguration(change => {
100    if (hasConfigurationCapability) {
101      // Reset all cached document settings
102      documentSettings.clear();
103    } else {
104      globalSettings = <ExampleSettings>(
105        (change.settings.languageServerExample || defaultSettings)
106      );
107    }
108
109    // Revalidate all open text documents
110    documents.all().forEach(validateTextDocument);
111  });
112
113  function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
114    if (!hasConfigurationCapability) {
115      return Promise.resolve(globalSettings);
116    }
117    let result = documentSettings.get(resource);
118    if (!result) {
119      result = connection.workspace.getConfiguration({
120        scopeUri: resource,
121        section: 'languageServerExample'
122      });
123      documentSettings.set(resource, result);
124    }
125    return result;
126  }
127
128  // Only keep settings for open documents
129  documents.onDidClose(e => {
130    documentSettings.delete(e.document.uri);
131  });
132
133  // The content of a text document has changed. This event is emitted
134  // when the text document first opened or when its content has changed.
135  documents.onDidChangeContent(change => {
136    validateTextDocument(change.document);
137  });
138
139  async function validateTextDocument(textDocument: TextDocument): Promise<void> {
140    // In this simple example we get the settings for every validate run.
141    let settings = await getDocumentSettings(textDocument.uri);
142
143    // The validator creates diagnostics for all uppercase words length 2 and more
144    let text = textDocument.getText();
145    let pattern = /\b[A-Z]{2,}\b/g;
146    let m: RegExpExecArray | null;
147
148    let problems = 0;
149    let diagnostics: Diagnostic[] = [];
150    while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
151      problems++;
152      let diagnostic: Diagnostic = {
153        severity: DiagnosticSeverity.Warning,
154        range: {
155          start: textDocument.positionAt(m.index),
156          end: textDocument.positionAt(m.index + m[0].length)
157        },
158        message: `${m[0]} is all uppercase.`,
159        source: 'ex'
160      };
161      if (hasDiagnosticRelatedInformationCapability) {
162        diagnostic.relatedInformation = [
163          {
164            location: {
165              uri: textDocument.uri,
166              range: Object.assign({}, diagnostic.range)
167            },
168            message: 'Spelling matters'
169          },
170          {
171            location: {
172              uri: textDocument.uri,
173              range: Object.assign({}, diagnostic.range)
174            },
175            message: 'Particularly for names'
176          }
177        ];
178      }
179      diagnostics.push(diagnostic);
180    }
181
182    // Send the computed diagnostics to VSCode.
183    connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
184  }
185
186  connection.onDidChangeWatchedFiles(_change => {
187    // Monitored files have change in VSCode
188    connection.console.log('We received an file change event');
189  });
190
191  // This handler provides the initial list of the completion items.
192  connection.onCompletion(
193    (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
194      // The pass parameter contains the position of the text document in
195      // which code complete got requested. For the example we ignore this
196      // info and always provide the same completion items.
197      return [
198        {
199          label: 'TypeScript',
200          kind: CompletionItemKind.Text,
201          data: 1
202        },
203        {
204          label: 'JavaScript',
205          kind: CompletionItemKind.Text,
206          data: 2
207        }
208      ];
209    }
210  );
211
212  // This handler resolves additional information for the item selected in
213  // the completion list.
214  connection.onCompletionResolve(
215    (item: CompletionItem): CompletionItem => {
216      if (item.data === 1) {
217        item.detail = 'TypeScript details';
218        item.documentation = 'TypeScript documentation';
219      } else if (item.data === 2) {
220        item.detail = 'JavaScript details';
221        item.documentation = 'JavaScript documentation';
222      }
223      return item;
224    }
225  );
226
227  // Make the text document manager listen on the connection
228  // for open, change and close text document events
229  documents.listen(connection);
230
231  // Listen on the connection
232  connection.listen();
233}
234
235// Create a connection for the server, using Node's IPC as a transport.
236// Also include all preview / proposed LSP features.
237const idx = process.argv.indexOf('--listen')
238if (idx >= 0 && idx + 1 < process.argv.length) {
239  const port = parseInt(process.argv[idx + 1]);
240  createClientSocketTransport(port).then((transport) => {
241    return transport.onConnected().then((protocol) => {
242      let connection = createConnection(protocol[0], protocol[1])
243      RunServer(connection);
244    });
245  });
246
247} else {
248  let connection = createConnection(ProposedFeatures.all);
249  RunServer(connection);
250}
251