diff --git a/extensions/configuration-editing/src/extension.ts b/extensions/configuration-editing/src/extension.ts index 8983c3fcbfb..d7f078f38a0 100644 --- a/extensions/configuration-editing/src/extension.ts +++ b/extensions/configuration-editing/src/extension.ts @@ -44,7 +44,7 @@ function registerKeybindingsCompletions(): vscode.Disposable { if (location.path[1] === 'command') { const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); - return commands.then(ids => ids.map(id => newCompletionItem(JSON.stringify(id), range))); + return commands.then(ids => ids.map(id => newSimpleCompletionItem(JSON.stringify(id), range))); } } }); @@ -61,13 +61,107 @@ function registerSettingsCompletions(): vscode.Disposable { if (location.path[0] === 'window.title') { const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); - completions.push(newCompletionItem('${activeEditorName}', range, localize('activeEditorName', "e.g. myFile.txt"))); - completions.push(newCompletionItem('${activeFilePath}', range, localize('activeFilePath', "e.g. /Users/Development/myProject/myFile.txt"))); - completions.push(newCompletionItem('${rootName}', range, localize('rootName', "e.g. myProject"))); - completions.push(newCompletionItem('${rootPath}', range, localize('rootPath', "e.g. /Users/Development/myProject"))); - completions.push(newCompletionItem('${appName}', range, localize('appName', "e.g. VS Code"))); - completions.push(newCompletionItem('${dirty}', range, localize('dirty', "a dirty indicator if the active editor is dirty"))); - completions.push(newCompletionItem('${separator}', range, localize('separator', "a conditional separator (' - ') that only shows when surrounded by variables with values"))); + completions.push(newSimpleCompletionItem('${activeEditorName}', range, localize('activeEditorName', "e.g. myFile.txt"))); + completions.push(newSimpleCompletionItem('${activeFilePath}', range, localize('activeFilePath', "e.g. /Users/Development/myProject/myFile.txt"))); + completions.push(newSimpleCompletionItem('${rootName}', range, localize('rootName', "e.g. myProject"))); + completions.push(newSimpleCompletionItem('${rootPath}', range, localize('rootPath', "e.g. /Users/Development/myProject"))); + completions.push(newSimpleCompletionItem('${appName}', range, localize('appName', "e.g. VS Code"))); + completions.push(newSimpleCompletionItem('${dirty}', range, localize('dirty', "a dirty indicator if the active editor is dirty"))); + completions.push(newSimpleCompletionItem('${separator}', range, localize('separator', "a conditional separator (' - ') that only shows when surrounded by variables with values"))); + } + + // files.association + else if (location.path[0] === 'files.associations') { + const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); + + // Key + if (location.path.length === 1) { + completions.push(newSnippetCompletionItem({ + label: localize('assocLabelFile', "Files with Extension"), + documentation: localize('assocDescriptionFile', "Map all files matching the glob pattern in their filename to the language with the given identifier."), + snippet: '"*.${1:extension}": "${2:language}"', + range + })); + + completions.push(newSnippetCompletionItem({ + label: localize('assocLabelPath', "Files with Path"), + documentation: localize('assocDescriptionPath', "Map all files matching the absolute path glob pattern in their path to the language with the given identifier."), + snippet: '"/${1:path to file}/*.${2:extension}": "${3:language}"', + range + })); + } + + // Value + else { + return vscode.languages.getLanguages().then(languages => { + return Promise.resolve(languages.map(l => { + return newSimpleCompletionItem(l, range); + })); + }); + } + } + + // files.exclude, search.exclude + else if (location.path[0] === 'files.exclude' || location.path[0] === 'search.exclude') { + const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); + + // Key + if (location.path.length === 1) { + completions.push(newSnippetCompletionItem({ + label: localize('fileLabel', "Files by Extension"), + documentation: localize('fileDescription', "Match all files of a specific file extension."), + snippet: '"**/*.${1:extension}": true', + range + })); + + completions.push(newSnippetCompletionItem({ + label: localize('filesLabel', "Files with Multiple Extensions"), + documentation: localize('filesDescription', "Match all files with any of the file extensions."), + snippet: '"**/*.{ext1,ext2,ext3}": true', + range + })); + + completions.push(newSnippetCompletionItem({ + label: localize('derivedLabel', "Files with Siblings by Name"), + documentation: localize('derivedDescription', "Match files that have siblings with the same name but a different extension."), + snippet: '"**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" }', + range + })); + + completions.push(newSnippetCompletionItem({ + label: localize('topFolderLabel', "Folder by Name (Top Level)"), + documentation: localize('topFolderDescription', "Match a top level folder with a specific name."), + snippet: '"${1:name}": true', + range + })); + + completions.push(newSnippetCompletionItem({ + label: localize('topFoldersLabel', "Folders with Multiple Names (Top Level)"), + documentation: localize('topFoldersDescription', "Match multiple top level folders."), + snippet: '"{folder1,folder2,folder3}": true', + range + })); + + completions.push(newSnippetCompletionItem({ + label: localize('folderLabel', "Folder by Name (Any Location)"), + documentation: localize('folderDescription', "Match a folder with a specific name in any location."), + snippet: '"**/${1:name}": true', + range + })); + } + + // Value + else { + completions.push(newSimpleCompletionItem('false', range, localize('falseDescription', "Disable the pattern."))); + completions.push(newSimpleCompletionItem('true', range, localize('trueDescription', "Enable the pattern."))); + + completions.push(newSnippetCompletionItem({ + label: localize('derivedLabel', "Files with Siblings by Name"), + documentation: localize('siblingsDescription', "Match files that have siblings with the same name but a different extension."), + snippet: '{ "when": "$(basename).${1:extension}" }', + range + })); + } } return Promise.resolve(completions); @@ -75,7 +169,7 @@ function registerSettingsCompletions(): vscode.Disposable { }); } -function newCompletionItem(text: string, range: vscode.Range, description?: string): vscode.CompletionItem { +function newSimpleCompletionItem(text: string, range: vscode.Range, description?: string): vscode.CompletionItem { const item = new vscode.CompletionItem(text); item.kind = vscode.CompletionItemKind.Value; item.detail = description; @@ -87,6 +181,16 @@ function newCompletionItem(text: string, range: vscode.Range, description?: stri return item; } +function newSnippetCompletionItem(o: { label: string; documentation?: string; snippet: string; range: vscode.Range; }): vscode.CompletionItem { + const item = new vscode.CompletionItem(o.label); + item.kind = vscode.CompletionItemKind.Value; + item.documentation = o.documentation; + item.insertText = new vscode.SnippetString(o.snippet); + item.range = o.range; + + return item; +} + function updateLaunchJsonDecorations(editor: vscode.TextEditor | undefined): void { if (!editor || path.basename(editor.document.fileName) !== 'launch.json') { return; diff --git a/extensions/json/client/src/jsonMain.ts b/extensions/json/client/src/jsonMain.ts index 04b0d918bff..1c5ab6145ba 100644 --- a/extensions/json/client/src/jsonMain.ts +++ b/extensions/json/client/src/jsonMain.ts @@ -36,65 +36,58 @@ export function activate(context: ExtensionContext) { let packageInfo = getPackageInfo(context); let telemetryReporter: TelemetryReporter = packageInfo && new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); - // Resolve language ids to pass around as initialization data - languages.getLanguages().then(languageIds => { + // The server is implemented in node + let serverModule = context.asAbsolutePath(path.join('server', 'out', 'jsonServerMain.js')); + // The debug options for the server + let debugOptions = { execArgv: ['--nolazy', '--debug=6004'] }; - // The server is implemented in node - let serverModule = context.asAbsolutePath(path.join('server', 'out', 'jsonServerMain.js')); - // The debug options for the server - let debugOptions = { execArgv: ['--nolazy', '--debug=6004'] }; + // If the extension is launch in debug mode the debug server options are use + // Otherwise the run options are used + let serverOptions: ServerOptions = { + run: { module: serverModule, transport: TransportKind.ipc }, + debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } + }; - // If the extension is launch in debug mode the debug server options are use - // Otherwise the run options are used - let serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, - debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } - }; + // Options to control the language client + let clientOptions: LanguageClientOptions = { + // Register the server for json documents + documentSelector: ['json'], + synchronize: { + // Synchronize the setting section 'json' to the server + configurationSection: ['json', 'http.proxy', 'http.proxyStrictSSL'], + fileEvents: workspace.createFileSystemWatcher('**/*.json') + } + }; - // Options to control the language client - let clientOptions: LanguageClientOptions = { - // Register the server for json documents - documentSelector: ['json'], - synchronize: { - // Synchronize the setting section 'json' to the server - configurationSection: ['json', 'http.proxy', 'http.proxyStrictSSL'], - fileEvents: workspace.createFileSystemWatcher('**/*.json') - }, - initializationOptions: { - languageIds + // Create the language client and start the client. + let client = new LanguageClient('json', localize('jsonserver.name', 'JSON Language Server'), serverOptions, clientOptions); + let disposable = client.start(); + client.onReady().then(() => { + client.onTelemetry(e => { + if (telemetryReporter) { + telemetryReporter.sendTelemetryEvent(e.key, e.data); } - }; - - // Create the language client and start the client. - let client = new LanguageClient('json', localize('jsonserver.name', 'JSON Language Server'), serverOptions, clientOptions); - let disposable = client.start(); - client.onReady().then(() => { - client.onTelemetry(e => { - if (telemetryReporter) { - telemetryReporter.sendTelemetryEvent(e.key, e.data); - } - }); - - // handle content request - client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { - let uri = Uri.parse(uriPath); - return workspace.openTextDocument(uri).then(doc => { - return doc.getText(); - }, error => { - return Promise.reject(error); - }); - }); - - client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociation(context)); }); - // Push the disposable to the context's subscriptions so that the - // client can be deactivated on extension deactivation - context.subscriptions.push(disposable); - - languages.setLanguageConfiguration('json', { - wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/ + // handle content request + client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { + let uri = Uri.parse(uriPath); + return workspace.openTextDocument(uri).then(doc => { + return doc.getText(); + }, error => { + return Promise.reject(error); + }); }); + + client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociation(context)); + }); + + // Push the disposable to the context's subscriptions so that the + // client can be deactivated on extension deactivation + context.subscriptions.push(disposable); + + languages.setLanguageConfiguration('json', { + wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/ }); } diff --git a/extensions/json/server/src/jsonServerMain.ts b/extensions/json/server/src/jsonServerMain.ts index 3719674ec66..3b06405dcb4 100644 --- a/extensions/json/server/src/jsonServerMain.ts +++ b/extensions/json/server/src/jsonServerMain.ts @@ -18,8 +18,6 @@ import * as URL from 'url'; import Strings = require('./utils/strings'); import { JSONDocument, JSONSchema, LanguageSettings, getLanguageService } from 'vscode-json-languageservice'; import { ProjectJSONContribution } from './jsoncontributions/projectJSONContribution'; -import { GlobPatternContribution } from './jsoncontributions/globPatternContribution'; -import { FileAssociationContribution } from './jsoncontributions/fileAssociationContribution'; import { getLanguageModelCache } from './languageModelCache'; import * as nls from 'vscode-nls'; @@ -53,16 +51,12 @@ documents.listen(connection); let clientSnippetSupport = false; let clientDynamicRegisterSupport = false; -const filesAssociationContribution = new FileAssociationContribution(); - // After the server has started the client sends an initilize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilities. let workspaceRoot: URI; connection.onInitialize((params: InitializeParams): InitializeResult => { workspaceRoot = URI.parse(params.rootPath); - if (params.initializationOptions) { - filesAssociationContribution.setLanguageIds(params.initializationOptions.languageIds); - } + function hasClientCapability(...keys: string[]) { let c = params.capabilities; for (let i = 0; c && i < keys.length; i++) { @@ -126,9 +120,7 @@ let languageService = getLanguageService({ schemaRequestService, workspaceContext, contributions: [ - new ProjectJSONContribution(), - new GlobPatternContribution(), - filesAssociationContribution + new ProjectJSONContribution() ] }); diff --git a/extensions/json/server/src/jsoncontributions/fileAssociationContribution.ts b/extensions/json/server/src/jsoncontributions/fileAssociationContribution.ts deleted file mode 100644 index 1abf5517ee6..00000000000 --- a/extensions/json/server/src/jsoncontributions/fileAssociationContribution.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { MarkedString, CompletionItemKind, CompletionItem, InsertTextFormat } from 'vscode-languageserver'; -import Strings = require('../utils/strings'); -import { JSONWorkerContribution, JSONPath, CompletionsCollector } from 'vscode-json-languageservice'; - -import * as nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - -let globProperties: CompletionItem[] = [ - { kind: CompletionItemKind.Value, label: localize('assocLabelFile', "Files with Extension"), insertText: '"*.${1:extension}": "${2:language}"', insertTextFormat: InsertTextFormat.Snippet, documentation: localize('assocDescriptionFile', "Map all files matching the glob pattern in their filename to the language with the given identifier.") }, - { kind: CompletionItemKind.Value, label: localize('assocLabelPath', "Files with Path"), insertText: '"/${1:path to file}/*.${2:extension}": "${3:language}"', insertTextFormat: InsertTextFormat.Snippet, documentation: localize('assocDescriptionPath', "Map all files matching the absolute path glob pattern in their path to the language with the given identifier.") } -]; - -export class FileAssociationContribution implements JSONWorkerContribution { - private languageIds: string[]; - - constructor() { - } - - public setLanguageIds(ids: string[]): void { - this.languageIds = ids; - } - - private isSettingsFile(resource: string): boolean { - return Strings.endsWith(resource, '/settings.json'); - } - - public collectDefaultCompletions(resource: string, result: CompletionsCollector): Thenable { - return null; - } - - public collectPropertyCompletions(resource: string, location: JSONPath, currentWord: string, addValue: boolean, isLast: boolean, result: CompletionsCollector): Thenable { - if (this.isSettingsFile(resource) && location.length === 1 && location[0] === 'files.associations') { - globProperties.forEach(e => { - result.add(e); - }); - } - - return null; - } - - public collectValueCompletions(resource: string, location: JSONPath, currentKey: string, result: CompletionsCollector): Thenable { - if (this.isSettingsFile(resource) && location.length === 1 && location[0] === 'files.associations') { - this.languageIds.forEach(l => { - result.add({ - kind: CompletionItemKind.Value, - label: l, - insertText: JSON.stringify('${1:' + l + '}'), - insertTextFormat: InsertTextFormat.Snippet, - filterText: JSON.stringify(l) - }); - }); - } - - return null; - } - - public getInfoContribution(resource: string, location: JSONPath): Thenable { - return null; - } -} \ No newline at end of file diff --git a/extensions/json/server/src/jsoncontributions/globPatternContribution.ts b/extensions/json/server/src/jsoncontributions/globPatternContribution.ts deleted file mode 100644 index 04926db360c..00000000000 --- a/extensions/json/server/src/jsoncontributions/globPatternContribution.ts +++ /dev/null @@ -1,65 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { MarkedString, CompletionItemKind, CompletionItem, InsertTextFormat } from 'vscode-languageserver'; -import Strings = require('../utils/strings'); -import { JSONWorkerContribution, JSONPath, CompletionsCollector } from 'vscode-json-languageservice'; - -import * as nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - -let globProperties: CompletionItem[] = [ - { kind: CompletionItemKind.Value, label: localize('fileLabel', "Files by Extension"), insertText: '"**/*.${1:extension}": true', insertTextFormat: InsertTextFormat.Snippet, documentation: localize('fileDescription', "Match all files of a specific file extension.") }, - { kind: CompletionItemKind.Value, label: localize('filesLabel', "Files with Multiple Extensions"), insertText: '"**/*.{ext1,ext2,ext3}": true', documentation: localize('filesDescription', "Match all files with any of the file extensions.") }, - { kind: CompletionItemKind.Value, label: localize('derivedLabel', "Files with Siblings by Name"), insertText: '"**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" }', insertTextFormat: InsertTextFormat.Snippet, documentation: localize('derivedDescription', "Match files that have siblings with the same name but a different extension.") }, - { kind: CompletionItemKind.Value, label: localize('topFolderLabel', "Folder by Name (Top Level)"), insertText: '"${1:name}": true', insertTextFormat: InsertTextFormat.Snippet, documentation: localize('topFolderDescription', "Match a top level folder with a specific name.") }, - { kind: CompletionItemKind.Value, label: localize('topFoldersLabel', "Folders with Multiple Names (Top Level)"), insertText: '"{folder1,folder2,folder3}": true', documentation: localize('topFoldersDescription', "Match multiple top level folders.") }, - { kind: CompletionItemKind.Value, label: localize('folderLabel', "Folder by Name (Any Location)"), insertText: '"**/${1:name}": true', insertTextFormat: InsertTextFormat.Snippet, documentation: localize('folderDescription', "Match a folder with a specific name in any location.") }, -]; - -let globValues: CompletionItem[] = [ - { kind: CompletionItemKind.Value, label: localize('trueLabel', "true"), filterText: 'true', insertText: 'true', documentation: localize('trueDescription', "Enable the pattern.") }, - { kind: CompletionItemKind.Value, label: localize('falseLabel', "false"), filterText: 'false', insertText: 'false', documentation: localize('falseDescription', "Disable the pattern.") }, - { kind: CompletionItemKind.Value, label: localize('derivedLabel', "Files with Siblings by Name"), insertText: '{ "when": "$(basename).${1:extension}" }', insertTextFormat: InsertTextFormat.Snippet, documentation: localize('siblingsDescription', "Match files that have siblings with the same name but a different extension.") } -]; - -export class GlobPatternContribution implements JSONWorkerContribution { - - constructor() { - } - - private isSettingsFile(resource: string): boolean { - return Strings.endsWith(resource, '/settings.json'); - } - - public collectDefaultCompletions(resource: string, result: CompletionsCollector): Thenable { - return null; - } - - public collectPropertyCompletions(resource: string, location: JSONPath, currentWord: string, addValue: boolean, isLast: boolean, result: CompletionsCollector): Thenable { - if (this.isSettingsFile(resource) && location.length === 1 && ((location[0] === 'files.exclude') || (location[0] === 'search.exclude'))) { - globProperties.forEach(e => { - result.add(e); - }); - } - - return null; - } - - public collectValueCompletions(resource: string, location: JSONPath, currentKey: string, result: CompletionsCollector): Thenable { - if (this.isSettingsFile(resource) && location.length === 1 && ((location[0] === 'files.exclude') || (location[0] === 'search.exclude'))) { - globValues.forEach(e => { - result.add(e); - }); - } - - return null; - } - - public getInfoContribution(resource: string, location: JSONPath): Thenable { - return null; - } -} \ No newline at end of file