diff --git a/extensions/json/server/src/jsoncontributions/fileAssociationContribution.ts b/extensions/json/server/src/jsoncontributions/fileAssociationContribution.ts new file mode 100644 index 00000000000..852fb97ddf1 --- /dev/null +++ b/extensions/json/server/src/jsoncontributions/fileAssociationContribution.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * 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} from 'vscode-languageserver'; +import Strings = require('../utils/strings'); +import {IJSONWorkerContribution, ISuggestionsCollector} from '../jsonContributions'; +import {JSONLocation} from '../jsonLocation'; + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +let globProperties: CompletionItem[] = [ + { kind: CompletionItemKind.Value, label: localize('assocLabel', "Files with Extension"), insertText: '"*.{{extension}}": "{{language}}"', documentation: localize('assocDescription', "Map all files matching the given pattern to the language with the given id.") }, +]; + +let globValues: CompletionItem[] = [ + { kind: CompletionItemKind.Value, label: localize('languageId', "Language Identifier"), insertText: '"{{language}}"', documentation: localize('languageDescription', "The identifier of the language to associate with the file name pattern.") }, +]; + +export class FileAssociationContribution implements IJSONWorkerContribution { + + constructor() { + } + + private isSettingsFile(resource: string): boolean { + return Strings.endsWith(resource, '/settings.json'); + } + + public collectDefaultSuggestions(resource: string, result: ISuggestionsCollector): Thenable { + return null; + } + + public collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast: boolean, result: ISuggestionsCollector): Thenable { + if (this.isSettingsFile(resource) && location.matches(['files.association'])) { + globProperties.forEach((e) => result.add(e)); + } + + return null; + } + + public collectValueSuggestions(resource: string, location: JSONLocation, currentKey: string, result: ISuggestionsCollector): Thenable { + if (this.isSettingsFile(resource) && location.matches(['files.association'])) { + globValues.forEach((e) => result.add(e)); + } + + return null; + } + + public getInfoContribution(resource: string, location: JSONLocation): 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 index dacdb7b707f..47f1f61c07e 100644 --- a/extensions/json/server/src/jsoncontributions/globPatternContribution.ts +++ b/extensions/json/server/src/jsoncontributions/globPatternContribution.ts @@ -12,19 +12,19 @@ import {JSONLocation} from '../jsonLocation'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -let globProperties:CompletionItem[] = [ - { kind: CompletionItemKind.Value, label: localize('fileLabel', "Files by Extension"), insertText: '"**/*.{{extension}}": true', 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: '"**/*.{{source-extension}}": { "when": "$(basename).{{target-extension}}" }', 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: '"{{name}}": true', 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: '"**/{{name}}": true', documentation: localize('folderDescription', "Match a folder with a specific name in any location.")}, +let globProperties: CompletionItem[] = [ + { kind: CompletionItemKind.Value, label: localize('fileLabel', "Files by Extension"), insertText: '"**/*.{{extension}}": true', 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: '"**/*.{{source-extension}}": { "when": "$(basename).{{target-extension}}" }', 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: '"{{name}}": true', 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: '"**/{{name}}": true', documentation: localize('folderDescription', "Match a folder with a specific name in any location.") }, ]; -let globValues:CompletionItem[] = [ - { kind: CompletionItemKind.Value, label: localize('trueLabel', "True"), insertText: 'true', documentation: localize('trueDescription', "Enable the pattern.")}, - { kind: CompletionItemKind.Value, label: localize('falseLabel', "False"), insertText: 'false', documentation: localize('falseDescription', "Disable the pattern.")}, - { kind: CompletionItemKind.Value, label: localize('derivedLabel', "Files with Siblings by Name"), insertText: '{ "when": "$(basename).{{extension}}" }', documentation: localize('siblingsDescription', "Match files that have siblings with the same name but a different extension.")} +let globValues: CompletionItem[] = [ + { kind: CompletionItemKind.Value, label: localize('trueLabel', "True"), insertText: 'true', documentation: localize('trueDescription', "Enable the pattern.") }, + { kind: CompletionItemKind.Value, label: localize('falseLabel', "False"), insertText: 'false', documentation: localize('falseDescription', "Disable the pattern.") }, + { kind: CompletionItemKind.Value, label: localize('derivedLabel', "Files with Siblings by Name"), insertText: '{ "when": "$(basename).{{extension}}" }', documentation: localize('siblingsDescription', "Match files that have siblings with the same name but a different extension.") } ]; export class GlobPatternContribution implements IJSONWorkerContribution { @@ -40,9 +40,8 @@ export class GlobPatternContribution implements IJSONWorkerContribution { return null; } - public collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast:boolean, result: ISuggestionsCollector) : Thenable { + public collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast: boolean, result: ISuggestionsCollector): Thenable { if (this.isSettingsFile(resource) && (location.matches(['files.exclude']) || location.matches(['search.exclude']))) { - globProperties.forEach((e) => result.add(e)); } @@ -51,7 +50,6 @@ export class GlobPatternContribution implements IJSONWorkerContribution { public collectValueSuggestions(resource: string, location: JSONLocation, currentKey: string, result: ISuggestionsCollector): Thenable { if (this.isSettingsFile(resource) && (location.matches(['files.exclude']) || location.matches(['search.exclude']))) { - globValues.forEach((e) => result.add(e)); } diff --git a/extensions/json/server/src/server.ts b/extensions/json/server/src/server.ts index 1e5167f398c..adc1ca04843 100644 --- a/extensions/json/server/src/server.ts +++ b/extensions/json/server/src/server.ts @@ -30,6 +30,7 @@ import {BowerJSONContribution} from './jsoncontributions/bowerJSONContribution'; import {PackageJSONContribution} from './jsoncontributions/packageJSONContribution'; import {ProjectJSONContribution} from './jsoncontributions/projectJSONContribution'; import {GlobPatternContribution} from './jsoncontributions/globPatternContribution'; +import {FileAssociationContribution} from './jsoncontributions/fileAssociationContribution'; import * as nls from 'vscode-nls'; nls.config(process.env['VSCODE_NLS_CONFIG']); @@ -118,7 +119,8 @@ let contributions = [ new ProjectJSONContribution(request), new PackageJSONContribution(request), new BowerJSONContribution(request), - new GlobPatternContribution() + new GlobPatternContribution(), + new FileAssociationContribution() ]; let jsonSchemaService = new JSONSchemaService(request, workspaceContext, telemetry); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 5ff1a74d39a..89695427850 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -425,6 +425,7 @@ export const AutoSaveConfiguration = { export interface IFilesConfiguration { files: { + association: { [filePattern: string]: string }; exclude: glob.IExpression; encoding: string; trimTrailingWhitespace: boolean; diff --git a/src/vs/workbench/parts/files/browser/files.contribution.ts b/src/vs/workbench/parts/files/browser/files.contribution.ts index 34bf4b13d7a..cadfc4c05c6 100644 --- a/src/vs/workbench/parts/files/browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/browser/files.contribution.ts @@ -26,6 +26,7 @@ import {AutoSaveConfiguration} from 'vs/platform/files/common/files'; import {FILE_EDITOR_INPUT_ID, VIEWLET_ID} from 'vs/workbench/parts/files/common/files'; import {FileTracker} from 'vs/workbench/parts/files/browser/fileTracker'; import {SaveParticipant} from 'vs/workbench/parts/files/common/editors/saveParticipant'; +import {FileAssociations} from 'vs/workbench/parts/files/common/editors/fileAssociations'; import {FileEditorInput} from 'vs/workbench/parts/files/browser/editors/fileEditorInput'; import {TextFileEditor} from 'vs/workbench/parts/files/browser/editors/textFileEditor'; import {BinaryFileEditor} from 'vs/workbench/parts/files/browser/editors/binaryFileEditor'; @@ -161,6 +162,11 @@ class FileEditorInputFactory implements IEditorInputFactory { SaveParticipant ); +// Register File Associations +(Registry.as(WorkbenchExtensions.Workbench)).registerWorkbenchContribution( + FileAssociations +); + // Configuration let configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -171,7 +177,6 @@ configurationRegistry.registerConfiguration({ 'type': 'object', 'properties': { 'files.exclude': { - 'id': 'glob-pattern', 'type': 'object', 'description': nls.localize('exclude', "Configure glob patterns for excluding files and folders."), 'default': { '**/.git': true, '**/.DS_Store': true }, @@ -195,6 +200,10 @@ configurationRegistry.registerConfiguration({ ] } }, + 'files.association': { + 'type': 'object', + 'description': nls.localize('association', "Configure file associations to languages (e.g. \"*.extension\": \"html\"). These have precedence over the default associations of the languages installed."), + }, 'files.encoding': { 'type': 'string', 'enum': Object.keys(SUPPORTED_ENCODINGS), diff --git a/src/vs/workbench/parts/files/common/editors/fileAssociations.ts b/src/vs/workbench/parts/files/common/editors/fileAssociations.ts new file mode 100644 index 00000000000..09e9e6503a6 --- /dev/null +++ b/src/vs/workbench/parts/files/common/editors/fileAssociations.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {IWorkbenchContribution} from 'vs/workbench/common/contributions'; +import errors = require('vs/base/common/errors'); +import mime = require('vs/base/common/mime'); +import {IFilesConfiguration} from 'vs/platform/files/common/files'; +import {IConfigurationService, IConfigurationServiceEvent, ConfigurationServiceEventTypes} from 'vs/platform/configuration/common/configuration'; +import {IEventService} from 'vs/platform/event/common/event'; +import {IModeService} from 'vs/editor/common/services/modeService'; + +// Register and update configured file associations +export class FileAssociations implements IWorkbenchContribution { + private toUnbind: { (): void; }[]; + + constructor( + @IConfigurationService private configurationService: IConfigurationService, + @IEventService private eventService: IEventService, + @IModeService private modeService: IModeService + ) { + this.toUnbind = []; + + this.registerListeners(); + this.loadConfiguration(); + } + + private registerListeners(): void { + this.toUnbind.push(this.configurationService.addListener(ConfigurationServiceEventTypes.UPDATED, (e: IConfigurationServiceEvent) => this.onConfigurationChange(e.config))); + } + + private loadConfiguration(): void { + this.configurationService.loadConfiguration().done((configuration: IFilesConfiguration) => { + this.onConfigurationChange(configuration); + }, errors.onUnexpectedError); + } + + private onConfigurationChange(configuration: IFilesConfiguration): void { + if (configuration.files && configuration.files.association) { + Object.keys(configuration.files.association).forEach(pattern => { + mime.registerTextMimeByFilename(pattern, this.modeService.getMimeForMode(configuration.files.association[pattern])); + }); + } + } + + public getId(): string { + return 'vs.files.associations'; + } + + public dispose(): void { + while (this.toUnbind.length) { + this.toUnbind.pop()(); + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/search/browser/search.contribution.ts b/src/vs/workbench/parts/search/browser/search.contribution.ts index a041edf3906..c582ea5d44b 100644 --- a/src/vs/workbench/parts/search/browser/search.contribution.ts +++ b/src/vs/workbench/parts/search/browser/search.contribution.ts @@ -171,7 +171,6 @@ configurationRegistry.registerConfiguration({ 'type': 'object', 'properties': { 'search.exclude': { - 'id': 'glob-pattern', 'type': 'object', 'description': nls.localize('exclude', "Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the file.exclude setting."), 'default': { '**/node_modules': true, '**/bower_components': true },