Merge VS Code 1.23.1 (#1520)

This commit is contained in:
Matt Irvine
2018-06-05 11:24:51 -07:00
committed by GitHub
parent e3baf5c443
commit 0c58f09e59
3651 changed files with 74249 additions and 48599 deletions

View File

@@ -0,0 +1,494 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as network from 'vs/base/common/network';
import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import URI from 'vs/base/common/uri';
import * as labels from 'vs/base/common/labels';
import * as strings from 'vs/base/common/strings';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { EditorInput } from 'vs/workbench/common/editor';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { Position as EditorPosition, IEditor, IEditorOptions } from 'vs/platform/editor/common/editor';
import { ITextModel } from 'vs/editor/common/model';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IPreferencesService, IPreferencesEditorModel, ISetting, getSettingsTargetName, FOLDER_SETTINGS_PATH, DEFAULT_SETTINGS_EDITOR_SETTING } from 'vs/workbench/services/preferences/common/preferences';
import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel, defaultKeybindingsContents, DefaultSettings, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { DefaultPreferencesEditorInput, PreferencesEditorInput, KeybindingsEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { getCodeEditor } from 'vs/editor/browser/services/codeEditorService';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position, IPosition } from 'vs/editor/common/core/position';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IModeService } from 'vs/editor/common/services/modeService';
import { parse } from 'vs/base/common/json';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { INotificationService } from 'vs/platform/notification/common/notification';
const emptyEditableSettingsContent = '{\n}';
export class PreferencesService extends Disposable implements IPreferencesService {
_serviceBrand: any;
private lastOpenedSettingsInput: PreferencesEditorInput = null;
private readonly _onDispose: Emitter<void> = new Emitter<void>();
private _defaultUserSettingsUriCounter = 0;
private _defaultUserSettingsContentModel: DefaultSettings;
private _defaultWorkspaceSettingsUriCounter = 0;
private _defaultWorkspaceSettingsContentModel: DefaultSettings;
private _defaultFolderSettingsUriCounter = 0;
private _defaultFolderSettingsContentModel: DefaultSettings;
constructor(
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IEditorGroupService private editorGroupService: IEditorGroupService,
@IFileService private fileService: IFileService,
@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService,
@INotificationService private notificationService: INotificationService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IInstantiationService private instantiationService: IInstantiationService,
@IEnvironmentService private environmentService: IEnvironmentService,
@ITelemetryService private telemetryService: ITelemetryService,
@ITextModelService private textModelResolverService: ITextModelService,
@IKeybindingService keybindingService: IKeybindingService,
@IModelService private modelService: IModelService,
@IJSONEditingService private jsonEditingService: IJSONEditingService,
@IModeService private modeService: IModeService
) {
super();
// The default keybindings.json updates based on keyboard layouts, so here we make sure
// if a model has been given out we update it accordingly.
keybindingService.onDidUpdateKeybindings(() => {
const model = modelService.getModel(this.defaultKeybindingsResource);
if (!model) {
// model has not been given out => nothing to do
return;
}
modelService.updateModel(model, defaultKeybindingsContents(keybindingService));
});
}
readonly defaultKeybindingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/keybindings.json' });
private readonly defaultSettingsRawResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/defaultSettings.json' });
get userSettingsResource(): URI {
return this.getEditableSettingsURI(ConfigurationTarget.USER);
}
get workspaceSettingsResource(): URI {
return this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE);
}
getFolderSettingsResource(resource: URI): URI {
return this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, resource);
}
resolveModel(uri: URI): TPromise<ITextModel> {
if (this.isDefaultSettingsResource(uri)) {
const target = this.getConfigurationTargetFromDefaultSettingsResource(uri);
const mode = this.modeService.getOrCreateMode('jsonc');
const model = this._register(this.modelService.createModel('', mode, uri));
let defaultSettings: DefaultSettings;
this.configurationService.onDidChangeConfiguration(e => {
if (e.source === ConfigurationTarget.DEFAULT) {
const model = this.modelService.getModel(uri);
if (!model) {
// model has not been given out => nothing to do
return;
}
defaultSettings = this.getDefaultSettings(target);
this.modelService.updateModel(model, defaultSettings.parse());
defaultSettings._onDidChange.fire();
}
});
// Check if Default settings is already created and updated in above promise
if (!defaultSettings) {
defaultSettings = this.getDefaultSettings(target);
this.modelService.updateModel(model, defaultSettings.parse());
}
return TPromise.as(model);
}
if (this.defaultSettingsRawResource.toString() === uri.toString()) {
let defaultSettings: DefaultSettings = this.getDefaultSettings(ConfigurationTarget.USER);
const mode = this.modeService.getOrCreateMode('jsonc');
const model = this._register(this.modelService.createModel(defaultSettings.raw, mode, uri));
return TPromise.as(model);
}
if (this.defaultKeybindingsResource.toString() === uri.toString()) {
const defaultKeybindingsEditorModel = this.instantiationService.createInstance(DefaultKeybindingsEditorModel, uri);
const mode = this.modeService.getOrCreateMode('jsonc');
const model = this._register(this.modelService.createModel(defaultKeybindingsEditorModel.content, mode, uri));
return TPromise.as(model);
}
return TPromise.as(null);
}
createPreferencesEditorModel(uri: URI): TPromise<IPreferencesEditorModel<any>> {
if (this.isDefaultSettingsResource(uri)) {
return this.createDefaultSettingsEditorModel(uri);
}
if (this.getEditableSettingsURI(ConfigurationTarget.USER).toString() === uri.toString()) {
return this.createEditableSettingsEditorModel(ConfigurationTarget.USER, uri);
}
const workspaceSettingsUri = this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE);
if (workspaceSettingsUri && workspaceSettingsUri.toString() === uri.toString()) {
return this.createEditableSettingsEditorModel(ConfigurationTarget.WORKSPACE, workspaceSettingsUri);
}
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
return this.createEditableSettingsEditorModel(ConfigurationTarget.WORKSPACE_FOLDER, uri);
}
return TPromise.wrap<IPreferencesEditorModel<any>>(null);
}
openRawDefaultSettings(): TPromise<void> {
return this.editorService.openEditor({ resource: this.defaultSettingsRawResource }, EditorPosition.ONE) as TPromise<any>;
}
openSettings(): TPromise<IEditor> {
const editorInput = this.getActiveSettingsEditorInput() || this.lastOpenedSettingsInput;
const resource = editorInput ? editorInput.master.getResource() : this.userSettingsResource;
const target = this.getConfigurationTargetFromSettingsResource(resource);
return this.openOrSwitchSettings(target, resource);
}
openGlobalSettings(options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
return this.openOrSwitchSettings(ConfigurationTarget.USER, this.userSettingsResource, options, position);
}
openWorkspaceSettings(options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
this.notificationService.info(nls.localize('openFolderFirst', "Open a folder first to create workspace settings"));
return TPromise.as(null);
}
return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE, this.workspaceSettingsResource, options, position);
}
openFolderSettings(folder: URI, options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE_FOLDER, this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, folder), options, position);
}
switchSettings(target: ConfigurationTarget, resource: URI): TPromise<void> {
const activeEditor = this.editorService.getActiveEditor();
if (activeEditor && activeEditor.input instanceof PreferencesEditorInput) {
return this.doSwitchSettings(target, resource, activeEditor.input, activeEditor.position).then(() => null);
} else {
return this.doOpenSettings(target, resource).then(() => null);
}
}
openGlobalKeybindingSettings(textual: boolean): TPromise<void> {
/* __GDPR__
"openKeybindings" : {
"textual" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
this.telemetryService.publicLog('openKeybindings', { textual });
if (textual) {
const emptyContents = '// ' + nls.localize('emptyKeybindingsHeader', "Place your key bindings in this file to overwrite the defaults") + '\n[\n]';
const editableKeybindings = URI.file(this.environmentService.appKeybindingsPath);
// Create as needed and open in editor
return this.createIfNotExists(editableKeybindings, emptyContents).then(() => {
return this.editorService.openEditors([
{ input: { resource: this.defaultKeybindingsResource, options: { pinned: true }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }, position: EditorPosition.ONE },
{ input: { resource: editableKeybindings, options: { pinned: true } }, position: EditorPosition.TWO },
]).then(() => {
this.editorGroupService.focusGroup(EditorPosition.TWO);
});
});
}
return this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { pinned: true }).then(() => null);
}
configureSettingsForLanguage(language: string): void {
this.openGlobalSettings()
.then(editor => {
const codeEditor = getCodeEditor(editor);
this.getPosition(language, codeEditor)
.then(position => {
codeEditor.setPosition(position);
codeEditor.focus();
});
});
}
private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
const activeGroup = this.editorGroupService.getStacksModel().activeGroup;
const positionToReplace = position !== void 0 ? position : activeGroup ? this.editorGroupService.getStacksModel().positionOfGroup(activeGroup) : EditorPosition.ONE;
const editorInput = this.getActiveSettingsEditorInput(positionToReplace);
if (editorInput && editorInput.master.getResource().fsPath !== resource.fsPath) {
return this.doSwitchSettings(configurationTarget, resource, editorInput, positionToReplace);
}
return this.doOpenSettings(configurationTarget, resource, options, position);
}
private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
const openDefaultSettings = !!this.configurationService.getValue(DEFAULT_SETTINGS_EDITOR_SETTING);
return this.getOrCreateEditableSettingsEditorInput(configurationTarget, resource)
.then(editableSettingsEditorInput => {
if (!options) {
options = { pinned: true };
} else {
options.pinned = true;
}
if (openDefaultSettings) {
const defaultPreferencesEditorInput = this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(configurationTarget));
const preferencesEditorInput = new PreferencesEditorInput(this.getPreferencesEditorInputName(configurationTarget, resource), editableSettingsEditorInput.getDescription(), defaultPreferencesEditorInput, <EditorInput>editableSettingsEditorInput);
this.lastOpenedSettingsInput = preferencesEditorInput;
return this.editorService.openEditor(preferencesEditorInput, options, position);
}
return this.editorService.openEditor(editableSettingsEditorInput, options, position);
});
}
private doSwitchSettings(target: ConfigurationTarget, resource: URI, input: PreferencesEditorInput, position?: EditorPosition): TPromise<IEditor> {
return this.getOrCreateEditableSettingsEditorInput(target, this.getEditableSettingsURI(target, resource))
.then(toInput => {
const replaceWith = new PreferencesEditorInput(this.getPreferencesEditorInputName(target, resource), toInput.getDescription(), this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(target)), toInput);
return this.editorService.replaceEditors([{
toReplace: input,
replaceWith
}], position).then(editors => {
this.lastOpenedSettingsInput = replaceWith;
return editors[0];
});
});
}
private getActiveSettingsEditorInput(position?: EditorPosition): PreferencesEditorInput {
const stacksModel = this.editorGroupService.getStacksModel();
const group = position !== void 0 ? stacksModel.groupAt(position) : stacksModel.activeGroup;
return group && <PreferencesEditorInput>group.getEditors().filter(e => e instanceof PreferencesEditorInput)[0];
}
private getConfigurationTargetFromSettingsResource(resource: URI): ConfigurationTarget {
if (this.userSettingsResource.toString() === resource.toString()) {
return ConfigurationTarget.USER;
}
const workspaceSettingsResource = this.workspaceSettingsResource;
if (workspaceSettingsResource && workspaceSettingsResource.toString() === resource.toString()) {
return ConfigurationTarget.WORKSPACE;
}
const folder = this.contextService.getWorkspaceFolder(resource);
if (folder) {
return ConfigurationTarget.WORKSPACE_FOLDER;
}
return ConfigurationTarget.USER;
}
private getConfigurationTargetFromDefaultSettingsResource(uri: URI) {
return this.isDefaultWorkspaceSettingsResource(uri) ? ConfigurationTarget.WORKSPACE : this.isDefaultFolderSettingsResource(uri) ? ConfigurationTarget.WORKSPACE_FOLDER : ConfigurationTarget.USER;
}
private isDefaultSettingsResource(uri: URI): boolean {
return this.isDefaultUserSettingsResource(uri) || this.isDefaultWorkspaceSettingsResource(uri) || this.isDefaultFolderSettingsResource(uri);
}
private isDefaultUserSettingsResource(uri: URI): boolean {
return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/(\d+\/)?settings\.json$/);
}
private isDefaultWorkspaceSettingsResource(uri: URI): boolean {
return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/(\d+\/)?workspaceSettings\.json$/);
}
private isDefaultFolderSettingsResource(uri: URI): boolean {
return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/(\d+\/)?resourceSettings\.json$/);
}
private getDefaultSettingsResource(configurationTarget: ConfigurationTarget): URI {
switch (configurationTarget) {
case ConfigurationTarget.WORKSPACE:
return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultWorkspaceSettingsUriCounter++}/workspaceSettings.json` });
case ConfigurationTarget.WORKSPACE_FOLDER:
return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultFolderSettingsUriCounter++}/resourceSettings.json` });
}
return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultUserSettingsUriCounter++}/settings.json` });
}
private getPreferencesEditorInputName(target: ConfigurationTarget, resource: URI): string {
const name = getSettingsTargetName(target, resource, this.contextService);
return target === ConfigurationTarget.WORKSPACE_FOLDER ? nls.localize('folderSettingsName', "{0} (Folder Settings)", name) : name;
}
private getOrCreateEditableSettingsEditorInput(target: ConfigurationTarget, resource: URI): TPromise<EditorInput> {
return this.createSettingsIfNotExists(target, resource)
.then(() => <EditorInput>this.editorService.createInput({ resource }));
}
private createEditableSettingsEditorModel(configurationTarget: ConfigurationTarget, resource: URI): TPromise<SettingsEditorModel> {
const settingsUri = this.getEditableSettingsURI(configurationTarget, resource);
if (settingsUri) {
const workspace = this.contextService.getWorkspace();
if (workspace.configuration && workspace.configuration.toString() === settingsUri.toString()) {
return this.textModelResolverService.createModelReference(settingsUri)
.then(reference => this.instantiationService.createInstance(WorkspaceConfigurationEditorModel, reference, configurationTarget));
}
return this.textModelResolverService.createModelReference(settingsUri)
.then(reference => this.instantiationService.createInstance(SettingsEditorModel, reference, configurationTarget));
}
return TPromise.wrap<SettingsEditorModel>(null);
}
private createDefaultSettingsEditorModel(defaultSettingsUri: URI): TPromise<DefaultSettingsEditorModel> {
return this.textModelResolverService.createModelReference(defaultSettingsUri)
.then(reference => {
const target = this.getConfigurationTargetFromDefaultSettingsResource(defaultSettingsUri);
return this.instantiationService.createInstance(DefaultSettingsEditorModel, defaultSettingsUri, reference, this.getDefaultSettings(target));
});
}
private getDefaultSettings(target: ConfigurationTarget): DefaultSettings {
if (target === ConfigurationTarget.WORKSPACE) {
if (!this._defaultWorkspaceSettingsContentModel) {
this._defaultWorkspaceSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), target);
}
return this._defaultWorkspaceSettingsContentModel;
}
if (target === ConfigurationTarget.WORKSPACE_FOLDER) {
if (!this._defaultFolderSettingsContentModel) {
this._defaultFolderSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), target);
}
return this._defaultFolderSettingsContentModel;
}
if (!this._defaultUserSettingsContentModel) {
this._defaultUserSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), target);
}
return this._defaultUserSettingsContentModel;
}
private getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): URI {
switch (configurationTarget) {
case ConfigurationTarget.USER:
return URI.file(this.environmentService.appSettingsPath);
case ConfigurationTarget.WORKSPACE:
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
return null;
}
const workspace = this.contextService.getWorkspace();
return workspace.configuration || workspace.folders[0].toResource(FOLDER_SETTINGS_PATH);
case ConfigurationTarget.WORKSPACE_FOLDER:
const folder = this.contextService.getWorkspaceFolder(resource);
return folder ? folder.toResource(FOLDER_SETTINGS_PATH) : null;
}
return null;
}
private createSettingsIfNotExists(target: ConfigurationTarget, resource: URI): TPromise<void> {
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && target === ConfigurationTarget.WORKSPACE) {
return this.fileService.resolveContent(this.contextService.getWorkspace().configuration)
.then(content => {
if (Object.keys(parse(content.value)).indexOf('settings') === -1) {
return this.jsonEditingService.write(resource, { key: 'settings', value: {} }, true).then(null, () => { });
}
return null;
});
}
return this.createIfNotExists(resource, emptyEditableSettingsContent).then(() => { });
}
private createIfNotExists(resource: URI, contents: string): TPromise<any> {
return this.fileService.resolveContent(resource, { acceptTextOnly: true }).then(null, error => {
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
return this.fileService.updateContent(resource, contents).then(null, error => {
return TPromise.wrapError(new Error(nls.localize('fail.createSettings', "Unable to create '{0}' ({1}).", labels.getPathLabel(resource, this.contextService, this.environmentService), error)));
});
}
return TPromise.wrapError(error);
});
}
private getMostCommonlyUsedSettings(): string[] {
return [
'files.autoSave',
'editor.fontSize',
'editor.fontFamily',
'editor.tabSize',
'editor.renderWhitespace',
'editor.cursorStyle',
'editor.multiCursorModifier',
'editor.insertSpaces',
'editor.wordWrap',
'files.exclude',
'files.associations'
];
}
private getPosition(language: string, codeEditor: ICodeEditor): TPromise<IPosition> {
return this.createPreferencesEditorModel(this.userSettingsResource)
.then((settingsModel: IPreferencesEditorModel<ISetting>) => {
const languageKey = `[${language}]`;
let setting = settingsModel.getPreference(languageKey);
const model = codeEditor.getModel();
const configuration = this.configurationService.getValue<{ editor: { tabSize: number; insertSpaces: boolean }, files: { eol: string } }>();
const eol = configuration.files && configuration.files.eol;
if (setting) {
if (setting.overrides.length) {
const lastSetting = setting.overrides[setting.overrides.length - 1];
let content;
if (lastSetting.valueRange.endLineNumber === setting.range.endLineNumber) {
content = ',' + eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor);
} else {
content = ',' + eol + this.spaces(2, configuration.editor);
}
const editOperation = EditOperation.insert(new Position(lastSetting.valueRange.endLineNumber, lastSetting.valueRange.endColumn), content);
model.pushEditOperations([], [editOperation], () => []);
return { lineNumber: lastSetting.valueRange.endLineNumber + 1, column: model.getLineMaxColumn(lastSetting.valueRange.endLineNumber + 1) };
}
return { lineNumber: setting.valueRange.startLineNumber, column: setting.valueRange.startColumn + 1 };
}
return this.configurationService.updateValue(languageKey, {}, ConfigurationTarget.USER)
.then(() => {
setting = settingsModel.getPreference(languageKey);
let content = eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor);
let editOperation = EditOperation.insert(new Position(setting.valueRange.endLineNumber, setting.valueRange.endColumn - 1), content);
model.pushEditOperations([], [editOperation], () => []);
let lineNumber = setting.valueRange.endLineNumber + 1;
settingsModel.dispose();
return { lineNumber, column: model.getLineMaxColumn(lineNumber) };
});
});
}
private spaces(count: number, { tabSize, insertSpaces }: { tabSize: number; insertSpaces: boolean }): string {
return insertSpaces ? strings.repeat(' ', tabSize * count) : strings.repeat('\t', count);
}
public dispose(): void {
this._onDispose.fire();
super.dispose();
}
}

View File

@@ -0,0 +1,571 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { distinct } from 'vs/base/common/arrays';
import * as strings from 'vs/base/common/strings';
import { OperatingSystem, language, LANGUAGE_DEFAULT } from 'vs/base/common/platform';
import { IMatch, IFilter, or, matchesContiguousSubString, matchesPrefix, matchesCamelCase, matchesWords } from 'vs/base/common/filters';
import { Registry } from 'vs/platform/registry/common/platform';
import { ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keyCodes';
import { AriaLabelProvider, UserSettingsLabelProvider, UILabelProvider, ModifierLabels as ModLabels } from 'vs/base/common/keybindingLabels';
import { MenuRegistry, ILocalizedString, ICommandAction } from 'vs/platform/actions/common/actions';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { EditorModel } from 'vs/workbench/common/editor';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
export const KEYBINDING_ENTRY_TEMPLATE_ID = 'keybinding.entry.template';
export const KEYBINDING_HEADER_TEMPLATE_ID = 'keybinding.header.template';
const SOURCE_DEFAULT = localize('default', "Default");
const SOURCE_USER = localize('user', "User");
export interface KeybindingMatch {
ctrlKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
metaKey?: boolean;
keyCode?: boolean;
}
export interface KeybindingMatches {
firstPart: KeybindingMatch;
chordPart: KeybindingMatch;
}
export interface IListEntry {
id: string;
templateId: string;
}
export interface IKeybindingItemEntry extends IListEntry {
keybindingItem: IKeybindingItem;
commandIdMatches?: IMatch[];
commandLabelMatches?: IMatch[];
commandDefaultLabelMatches?: IMatch[];
sourceMatches?: IMatch[];
whenMatches?: IMatch[];
keybindingMatches?: KeybindingMatches;
}
export interface IKeybindingItem {
keybinding: ResolvedKeybinding;
keybindingItem: ResolvedKeybindingItem;
commandLabel: string;
commandDefaultLabel: string;
command: string;
source: string;
when: string;
}
interface ModifierLabels {
ui: ModLabels;
aria: ModLabels;
user: ModLabels;
}
const wordFilter = or(matchesPrefix, matchesWords, matchesContiguousSubString);
export class KeybindingsEditorModel extends EditorModel {
private _keybindingItems: IKeybindingItem[];
private _keybindingItemsSortedByPrecedence: IKeybindingItem[];
private modifierLabels: ModifierLabels;
constructor(
os: OperatingSystem,
@IKeybindingService private keybindingsService: IKeybindingService,
@IExtensionService private extensionService: IExtensionService
) {
super();
this.modifierLabels = {
ui: UILabelProvider.modifierLabels[os],
aria: AriaLabelProvider.modifierLabels[os],
user: UserSettingsLabelProvider.modifierLabels[os]
};
}
public fetch(searchValue: string, sortByPrecedence: boolean = false): IKeybindingItemEntry[] {
let keybindingItems = sortByPrecedence ? this._keybindingItemsSortedByPrecedence : this._keybindingItems;
if (/@source:\s*(user|default)/i.test(searchValue)) {
keybindingItems = this.filterBySource(keybindingItems, searchValue);
searchValue = searchValue.replace(/@source:\s*(user|default)/i, '');
}
searchValue = searchValue.trim();
if (!searchValue) {
return keybindingItems.map(keybindingItem => ({ id: KeybindingsEditorModel.getId(keybindingItem), keybindingItem, templateId: KEYBINDING_ENTRY_TEMPLATE_ID }));
}
return this.filterByText(keybindingItems, searchValue);
}
private filterBySource(keybindingItems: IKeybindingItem[], searchValue: string): IKeybindingItem[] {
if (/@source:\s*default/i.test(searchValue)) {
return keybindingItems.filter(k => k.source === SOURCE_DEFAULT);
}
if (/@source:\s*user/i.test(searchValue)) {
return keybindingItems.filter(k => k.source === SOURCE_USER);
}
return keybindingItems;
}
private filterByText(keybindingItems: IKeybindingItem[], searchValue: string): IKeybindingItemEntry[] {
const quoteAtFirstChar = searchValue.charAt(0) === '"';
const quoteAtLastChar = searchValue.charAt(searchValue.length - 1) === '"';
const completeMatch = quoteAtFirstChar && quoteAtLastChar;
if (quoteAtFirstChar) {
searchValue = searchValue.substring(1);
}
if (quoteAtLastChar) {
searchValue = searchValue.substring(0, searchValue.length - 1);
}
searchValue = searchValue.trim();
const result: IKeybindingItemEntry[] = [];
const words = searchValue.split(' ');
const keybindingWords = this.splitKeybindingWords(words);
for (const keybindingItem of keybindingItems) {
let keybindingMatches = new KeybindingItemMatches(this.modifierLabels, keybindingItem, searchValue, words, keybindingWords, completeMatch);
if (keybindingMatches.commandIdMatches
|| keybindingMatches.commandLabelMatches
|| keybindingMatches.commandDefaultLabelMatches
|| keybindingMatches.sourceMatches
|| keybindingMatches.whenMatches
|| keybindingMatches.keybindingMatches) {
result.push({
id: KeybindingsEditorModel.getId(keybindingItem),
templateId: KEYBINDING_ENTRY_TEMPLATE_ID,
commandLabelMatches: keybindingMatches.commandLabelMatches,
commandDefaultLabelMatches: keybindingMatches.commandDefaultLabelMatches,
keybindingItem,
keybindingMatches: keybindingMatches.keybindingMatches,
commandIdMatches: keybindingMatches.commandIdMatches,
sourceMatches: keybindingMatches.sourceMatches,
whenMatches: keybindingMatches.whenMatches
});
}
}
return result;
}
private splitKeybindingWords(wordsSeparatedBySpaces: string[]): string[] {
const result = [];
for (const word of wordsSeparatedBySpaces) {
result.push(...word.split('+').filter(w => !!w));
}
return result;
}
public resolve(editorActionsLabels: { [id: string]: string; }): TPromise<EditorModel> {
return this.extensionService.whenInstalledExtensionsRegistered()
.then(() => {
const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
this._keybindingItemsSortedByPrecedence = [];
const boundCommands: Map<string, boolean> = new Map<string, boolean>();
for (const keybinding of this.keybindingsService.getKeybindings()) {
if (keybinding.command) { // Skip keybindings without commands
this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(keybinding.command, keybinding, workbenchActionsRegistry, editorActionsLabels));
boundCommands.set(keybinding.command, true);
}
}
const commandsWithDefaultKeybindings = this.keybindingsService.getDefaultKeybindings().map(keybinding => keybinding.command);
for (const command of KeybindingResolver.getAllUnboundCommands(boundCommands)) {
const keybindingItem = new ResolvedKeybindingItem(null, command, null, null, commandsWithDefaultKeybindings.indexOf(command) === -1);
this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(command, keybindingItem, workbenchActionsRegistry, editorActionsLabels));
}
this._keybindingItems = this._keybindingItemsSortedByPrecedence.slice(0).sort((a, b) => KeybindingsEditorModel.compareKeybindingData(a, b));
return this;
});
}
private static getId(keybindingItem: IKeybindingItem): string {
return keybindingItem.command + (keybindingItem.keybinding ? keybindingItem.keybinding.getAriaLabel() : '') + keybindingItem.source + keybindingItem.when;
}
private static compareKeybindingData(a: IKeybindingItem, b: IKeybindingItem): number {
if (a.keybinding && !b.keybinding) {
return -1;
}
if (b.keybinding && !a.keybinding) {
return 1;
}
if (a.commandLabel && !b.commandLabel) {
return -1;
}
if (b.commandLabel && !a.commandLabel) {
return 1;
}
if (a.commandLabel && b.commandLabel) {
if (a.commandLabel !== b.commandLabel) {
return a.commandLabel.localeCompare(b.commandLabel);
}
}
if (a.command === b.command) {
return a.keybindingItem.isDefault ? 1 : -1;
}
return a.command.localeCompare(b.command);
}
private static toKeybindingEntry(command: string, keybindingItem: ResolvedKeybindingItem, workbenchActionsRegistry: IWorkbenchActionRegistry, editorActions: { [id: string]: string; }): IKeybindingItem {
const menuCommand = MenuRegistry.getCommand(command);
const editorActionLabel = editorActions[command];
return <IKeybindingItem>{
keybinding: keybindingItem.resolvedKeybinding,
keybindingItem,
command,
commandLabel: KeybindingsEditorModel.getCommandLabel(menuCommand, editorActionLabel),
commandDefaultLabel: KeybindingsEditorModel.getCommandDefaultLabel(menuCommand, workbenchActionsRegistry),
when: keybindingItem.when ? keybindingItem.when.serialize() : '',
source: keybindingItem.isDefault ? SOURCE_DEFAULT : SOURCE_USER
};
}
private static getCommandDefaultLabel(menuCommand: ICommandAction, workbenchActionsRegistry: IWorkbenchActionRegistry): string {
if (language !== LANGUAGE_DEFAULT) {
if (menuCommand && menuCommand.title && (<ILocalizedString>menuCommand.title).original) {
return (<ILocalizedString>menuCommand.title).original;
}
}
return null;
}
private static getCommandLabel(menuCommand: ICommandAction, editorActionLabel: string): string {
if (menuCommand) {
return typeof menuCommand.title === 'string' ? menuCommand.title : menuCommand.title.value;
}
if (editorActionLabel) {
return editorActionLabel;
}
return '';
}
}
class KeybindingItemMatches {
public readonly commandIdMatches: IMatch[] = null;
public readonly commandLabelMatches: IMatch[] = null;
public readonly commandDefaultLabelMatches: IMatch[] = null;
public readonly sourceMatches: IMatch[] = null;
public readonly whenMatches: IMatch[] = null;
public readonly keybindingMatches: KeybindingMatches = null;
constructor(private modifierLabels: ModifierLabels, keybindingItem: IKeybindingItem, searchValue: string, words: string[], keybindingWords: string[], private completeMatch: boolean) {
this.commandIdMatches = this.matches(searchValue, keybindingItem.command, or(matchesWords, matchesCamelCase), words);
this.commandLabelMatches = keybindingItem.commandLabel ? this.matches(searchValue, keybindingItem.commandLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandLabel, true), words) : null;
this.commandDefaultLabelMatches = keybindingItem.commandDefaultLabel ? this.matches(searchValue, keybindingItem.commandDefaultLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandDefaultLabel, true), words) : null;
this.sourceMatches = this.matches(searchValue, keybindingItem.source, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.source, true), words);
this.whenMatches = keybindingItem.when ? this.matches(searchValue, keybindingItem.when, or(matchesWords, matchesCamelCase), words) : null;
this.keybindingMatches = keybindingItem.keybinding ? this.matchesKeybinding(keybindingItem.keybinding, searchValue, keybindingWords) : null;
}
private matches(searchValue: string, wordToMatchAgainst: string, wordMatchesFilter: IFilter, words: string[]): IMatch[] {
let matches = wordFilter(searchValue, wordToMatchAgainst);
if (!matches) {
matches = this.matchesWords(words, wordToMatchAgainst, wordMatchesFilter);
}
if (matches) {
matches = this.filterAndSort(matches);
}
return matches;
}
private matchesWords(words: string[], wordToMatchAgainst: string, wordMatchesFilter: IFilter): IMatch[] {
let matches: IMatch[] = [];
for (const word of words) {
const wordMatches = wordMatchesFilter(word, wordToMatchAgainst);
if (wordMatches) {
matches = [...(matches || []), ...wordMatches];
} else {
matches = null;
break;
}
}
return matches;
}
private filterAndSort(matches: IMatch[]): IMatch[] {
return distinct(matches, (a => a.start + '.' + a.end)).filter(match => !matches.some(m => !(m.start === match.start && m.end === match.end) && (m.start <= match.start && m.end >= match.end))).sort((a, b) => a.start - b.start);
}
private matchesKeybinding(keybinding: ResolvedKeybinding, searchValue: string, words: string[]): KeybindingMatches {
const [firstPart, chordPart] = keybinding.getParts();
if (strings.compareIgnoreCase(searchValue, keybinding.getAriaLabel()) === 0 || strings.compareIgnoreCase(searchValue, keybinding.getLabel()) === 0) {
return {
firstPart: this.createCompleteMatch(firstPart),
chordPart: this.createCompleteMatch(chordPart)
};
}
let firstPartMatch: KeybindingMatch = {};
let chordPartMatch: KeybindingMatch = {};
const matchedWords = [];
let firstPartMatchedWords = [];
let chordPartMatchedWords = [];
let matchFirstPart = true;
for (let index = 0; index < words.length; index++) {
const word = words[index];
let firstPartMatched = false;
let chordPartMatched = false;
matchFirstPart = matchFirstPart && !firstPartMatch.keyCode;
let matchChordPart = !chordPartMatch.keyCode;
if (matchFirstPart) {
firstPartMatched = this.matchPart(firstPart, firstPartMatch, word);
if (firstPartMatch.keyCode) {
for (const cordPartMatchedWordIndex of chordPartMatchedWords) {
if (firstPartMatchedWords.indexOf(cordPartMatchedWordIndex) === -1) {
matchedWords.splice(matchedWords.indexOf(cordPartMatchedWordIndex), 1);
}
}
chordPartMatch = {};
chordPartMatchedWords = [];
matchChordPart = false;
}
}
if (matchChordPart) {
chordPartMatched = this.matchPart(chordPart, chordPartMatch, word);
}
if (firstPartMatched) {
firstPartMatchedWords.push(index);
}
if (chordPartMatched) {
chordPartMatchedWords.push(index);
}
if (firstPartMatched || chordPartMatched) {
matchedWords.push(index);
}
matchFirstPart = matchFirstPart && this.isModifier(word);
}
if (matchedWords.length !== words.length) {
return null;
}
if (this.completeMatch && (!this.isCompleteMatch(firstPart, firstPartMatch) || !this.isCompleteMatch(chordPart, chordPartMatch))) {
return null;
}
return this.hasAnyMatch(firstPartMatch) || this.hasAnyMatch(chordPartMatch) ? { firstPart: firstPartMatch, chordPart: chordPartMatch } : null;
}
private matchPart(part: ResolvedKeybindingPart, match: KeybindingMatch, word: string): boolean {
let matched = false;
if (this.matchesMetaModifier(part, word)) {
matched = true;
match.metaKey = true;
}
if (this.matchesCtrlModifier(part, word)) {
matched = true;
match.ctrlKey = true;
}
if (this.matchesShiftModifier(part, word)) {
matched = true;
match.shiftKey = true;
}
if (this.matchesAltModifier(part, word)) {
matched = true;
match.altKey = true;
}
if (this.matchesKeyCode(part, word)) {
match.keyCode = true;
matched = true;
}
return matched;
}
private matchesKeyCode(keybinding: ResolvedKeybindingPart, word: string): boolean {
if (!keybinding) {
return false;
}
const ariaLabel = keybinding.keyAriaLabel;
if (this.completeMatch || ariaLabel.length === 1 || word.length === 1) {
if (strings.compareIgnoreCase(ariaLabel, word) === 0) {
return true;
}
} else {
if (matchesContiguousSubString(word, ariaLabel)) {
return true;
}
}
return false;
}
private matchesMetaModifier(keybinding: ResolvedKeybindingPart, word: string): boolean {
if (!keybinding) {
return false;
}
if (!keybinding.metaKey) {
return false;
}
return this.wordMatchesMetaModifier(word);
}
private wordMatchesMetaModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.metaKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.metaKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.metaKey, word)) {
return true;
}
if (matchesPrefix(localize('meta', "meta"), word)) {
return true;
}
return false;
}
private matchesCtrlModifier(keybinding: ResolvedKeybindingPart, word: string): boolean {
if (!keybinding) {
return false;
}
if (!keybinding.ctrlKey) {
return false;
}
return this.wordMatchesCtrlModifier(word);
}
private wordMatchesCtrlModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.ctrlKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.ctrlKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.ctrlKey, word)) {
return true;
}
return false;
}
private matchesShiftModifier(keybinding: ResolvedKeybindingPart, word: string): boolean {
if (!keybinding) {
return false;
}
if (!keybinding.shiftKey) {
return false;
}
return this.wordMatchesShiftModifier(word);
}
private wordMatchesShiftModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.shiftKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.shiftKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.shiftKey, word)) {
return true;
}
return false;
}
private matchesAltModifier(keybinding: ResolvedKeybindingPart, word: string): boolean {
if (!keybinding) {
return false;
}
if (!keybinding.altKey) {
return false;
}
return this.wordMatchesAltModifier(word);
}
private wordMatchesAltModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.altKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.altKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.altKey, word)) {
return true;
}
if (matchesPrefix(localize('option', "option"), word)) {
return true;
}
return false;
}
private hasAnyMatch(keybindingMatch: KeybindingMatch): boolean {
return keybindingMatch.altKey ||
keybindingMatch.ctrlKey ||
keybindingMatch.metaKey ||
keybindingMatch.shiftKey ||
keybindingMatch.keyCode;
}
private isCompleteMatch(part: ResolvedKeybindingPart, match: KeybindingMatch): boolean {
if (!part) {
return true;
}
if (!match.keyCode) {
return false;
}
if (part.metaKey && !match.metaKey) {
return false;
}
if (part.altKey && !match.altKey) {
return false;
}
if (part.ctrlKey && !match.ctrlKey) {
return false;
}
if (part.shiftKey && !match.shiftKey) {
return false;
}
return true;
}
private createCompleteMatch(part: ResolvedKeybindingPart): KeybindingMatch {
let match: KeybindingMatch = {};
if (part) {
match.keyCode = true;
if (part.metaKey) {
match.metaKey = true;
}
if (part.altKey) {
match.altKey = true;
}
if (part.ctrlKey) {
match.ctrlKey = true;
}
if (part.shiftKey) {
match.shiftKey = true;
}
}
return match;
}
private isModifier(word: string): boolean {
if (this.wordMatchesAltModifier(word)) {
return true;
}
if (this.wordMatchesCtrlModifier(word)) {
return true;
}
if (this.wordMatchesMetaModifier(word)) {
return true;
}
if (this.wordMatchesShiftModifier(word)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,164 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IEditor, Position, IEditorOptions } from 'vs/platform/editor/common/editor';
import { ITextModel } from 'vs/editor/common/model';
import { IRange } from 'vs/editor/common/core/range';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { join } from 'vs/base/common/paths';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { Event } from 'vs/base/common/event';
import { IStringDictionary } from 'vs/base/common/collections';
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { localize } from 'vs/nls';
export interface ISettingsGroup {
id: string;
range: IRange;
title: string;
titleRange: IRange;
sections: ISettingsSection[];
}
export interface ISettingsSection {
titleRange?: IRange;
title?: string;
settings: ISetting[];
}
export interface ISetting {
range: IRange;
key: string;
keyRange: IRange;
value: any;
valueRange: IRange;
description: string[];
descriptionRanges: IRange[];
overrides?: ISetting[];
overrideOf?: ISetting;
}
export interface IExtensionSetting extends ISetting {
extensionName: string;
extensionPublisher: string;
}
export interface ISearchResult {
filterMatches: ISettingMatch[];
metadata?: IFilterMetadata;
}
export interface ISearchResultGroup {
id: string;
label: string;
result: ISearchResult;
order: number;
}
export interface IFilterResult {
query?: string;
filteredGroups: ISettingsGroup[];
allGroups: ISettingsGroup[];
matches: IRange[];
metadata?: IStringDictionary<IFilterMetadata>;
}
export interface ISettingMatch {
setting: ISetting;
matches: IRange[];
score: number;
}
export interface IScoredResults {
[key: string]: IRemoteSetting;
}
export interface IRemoteSetting {
score: number;
key: string;
id: string;
defaultValue: string;
description: string;
packageId: string;
extensionName?: string;
extensionPublisher?: string;
}
export interface IFilterMetadata {
requestUrl: string;
requestBody: string;
timestamp: number;
duration: number;
scoredResults: IScoredResults;
extensions?: ILocalExtension[];
/** The number of requests made, since requests are split by number of filters */
requestCount?: number;
/** The name of the server that actually served the request */
context: string;
}
export interface IPreferencesEditorModel<T> {
uri: URI;
getPreference(key: string): T;
dispose(): void;
}
export type IGroupFilter = (group: ISettingsGroup) => boolean;
export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[], score: number };
export interface ISettingsEditorModel extends IPreferencesEditorModel<ISetting> {
readonly onDidChangeGroups: Event<void>;
settingsGroups: ISettingsGroup[];
filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[];
findValueMatches(filter: string, setting: ISetting): IRange[];
updateResultGroup(id: string, resultGroup: ISearchResultGroup): IFilterResult;
}
export interface IKeybindingsEditorModel<T> extends IPreferencesEditorModel<T> {
}
export const IPreferencesService = createDecorator<IPreferencesService>('preferencesService');
export interface IPreferencesService {
_serviceBrand: any;
userSettingsResource: URI;
workspaceSettingsResource: URI;
getFolderSettingsResource(resource: URI): URI;
resolveModel(uri: URI): TPromise<ITextModel>;
createPreferencesEditorModel<T>(uri: URI): TPromise<IPreferencesEditorModel<T>>;
openRawDefaultSettings(): TPromise<void>;
openSettings(): TPromise<IEditor>;
openGlobalSettings(options?: IEditorOptions, position?: Position): TPromise<IEditor>;
openWorkspaceSettings(options?: IEditorOptions, position?: Position): TPromise<IEditor>;
openFolderSettings(folder: URI, options?: IEditorOptions, position?: Position): TPromise<IEditor>;
switchSettings(target: ConfigurationTarget, resource: URI): TPromise<void>;
openGlobalKeybindingSettings(textual: boolean): TPromise<void>;
configureSettingsForLanguage(language: string): void;
}
export function getSettingsTargetName(target: ConfigurationTarget, resource: URI, workspaceContextService: IWorkspaceContextService): string {
switch (target) {
case ConfigurationTarget.USER:
return localize('userSettingsTarget', "User Settings");
case ConfigurationTarget.WORKSPACE:
return localize('workspaceSettingsTarget', "Workspace Settings");
case ConfigurationTarget.WORKSPACE_FOLDER:
const folder = workspaceContextService.getWorkspaceFolder(resource);
return folder ? folder.name : '';
}
return '';
}
export const FOLDER_SETTINGS_PATH = join('.vscode', 'settings.json');
export const DEFAULT_SETTINGS_EDITOR_SETTING = 'workbench.settings.openDefaultSettings';

View File

@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { SideBySideEditorInput, EditorInput } from 'vs/workbench/common/editor';
import { Verbosity } from 'vs/platform/editor/common/editor';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import URI from 'vs/base/common/uri';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { IHashService } from 'vs/workbench/services/hash/common/hashService';
import { KeybindingsEditorModel } from 'vs/workbench/services/preferences/common/keybindingsEditorModel';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { OS } from 'vs/base/common/platform';
import { TPromise } from 'vs/base/common/winjs.base';
export class PreferencesEditorInput extends SideBySideEditorInput {
public static readonly ID: string = 'workbench.editorinputs.preferencesEditorInput';
getTypeId(): string {
return PreferencesEditorInput.ID;
}
public supportsSplitEditor(): boolean {
return true;
}
public getTitle(verbosity: Verbosity): string {
return this.master.getTitle(verbosity);
}
}
export class DefaultPreferencesEditorInput extends ResourceEditorInput {
public static readonly ID = 'workbench.editorinputs.defaultpreferences';
constructor(defaultSettingsResource: URI,
@ITextModelService textModelResolverService: ITextModelService,
@IHashService hashService: IHashService
) {
super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, textModelResolverService, hashService);
}
getTypeId(): string {
return DefaultPreferencesEditorInput.ID;
}
matches(other: any): boolean {
if (other instanceof DefaultPreferencesEditorInput) {
return true;
}
if (!super.matches(other)) {
return false;
}
return true;
}
}
export class KeybindingsEditorInput extends EditorInput {
public static readonly ID: string = 'workbench.input.keybindings';
public readonly keybindingsModel: KeybindingsEditorModel;
constructor(@IInstantiationService instantiationService: IInstantiationService) {
super();
this.keybindingsModel = instantiationService.createInstance(KeybindingsEditorModel, OS);
}
getTypeId(): string {
return KeybindingsEditorInput.ID;
}
getName(): string {
return nls.localize('keybindingsInputName', "Keyboard Shortcuts");
}
resolve(refresh?: boolean): TPromise<KeybindingsEditorModel> {
return TPromise.as(this.keybindingsModel);
}
matches(otherInput: any): boolean {
return otherInput instanceof KeybindingsEditorInput;
}
}

View File

@@ -0,0 +1,926 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { assign } from 'vs/base/common/objects';
import * as map from 'vs/base/common/map';
import { tail, flatten } from 'vs/base/common/arrays';
import URI from 'vs/base/common/uri';
import { IReference, Disposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { Registry } from 'vs/platform/registry/common/platform';
import { visit, JSONVisitor } from 'vs/base/common/json';
import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
import { EditorModel } from 'vs/workbench/common/editor';
import { IConfigurationNode, IConfigurationRegistry, Extensions, OVERRIDE_PROPERTY_PATTERN, IConfigurationPropertySchema, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { ISettingsEditorModel, IKeybindingsEditorModel, ISettingsGroup, ISetting, IFilterResult, IGroupFilter, ISettingMatcher, ISettingMatch, ISearchResultGroup, IFilterMetadata } from 'vs/workbench/services/preferences/common/preferences';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { Selection } from 'vs/editor/common/core/selection';
import { IStringDictionary } from 'vs/base/common/collections';
export abstract class AbstractSettingsModel extends EditorModel {
protected _currentResultGroups = new Map<string, ISearchResultGroup>();
public updateResultGroup(id: string, resultGroup: ISearchResultGroup): IFilterResult {
if (resultGroup) {
this._currentResultGroups.set(id, resultGroup);
} else {
this._currentResultGroups.delete(id);
}
this.removeDuplicateResults();
return this.update();
}
/**
* Remove duplicates between result groups, preferring results in earlier groups
*/
private removeDuplicateResults(): void {
const settingKeys = new Set<string>();
map.keys(this._currentResultGroups)
.sort((a, b) => this._currentResultGroups.get(a).order - this._currentResultGroups.get(b).order)
.forEach(groupId => {
const group = this._currentResultGroups.get(groupId);
group.result.filterMatches = group.result.filterMatches.filter(s => !settingKeys.has(s.setting.key));
group.result.filterMatches.forEach(s => settingKeys.add(s.setting.key));
});
}
public filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[] {
const allGroups = this.filterGroups;
const filterMatches: ISettingMatch[] = [];
for (const group of allGroups) {
const groupMatched = groupFilter(group);
for (const section of group.sections) {
for (const setting of section.settings) {
const settingMatchResult = settingMatcher(setting, group);
if (groupMatched || settingMatchResult) {
filterMatches.push({
setting,
matches: settingMatchResult && settingMatchResult.matches,
score: settingMatchResult ? settingMatchResult.score : 0
});
}
}
}
}
return filterMatches.sort((a, b) => b.score - a.score);
}
public getPreference(key: string): ISetting {
for (const group of this.settingsGroups) {
for (const section of group.sections) {
for (const setting of section.settings) {
if (key === setting.key) {
return setting;
}
}
}
}
return null;
}
protected collectMetadata(groups: ISearchResultGroup[]): IStringDictionary<IFilterMetadata> {
const metadata = Object.create(null);
let hasMetadata = false;
groups.forEach(g => {
if (g.result.metadata) {
metadata[g.id] = g.result.metadata;
hasMetadata = true;
}
});
return hasMetadata ? metadata : null;
}
protected get filterGroups(): ISettingsGroup[] {
return this.settingsGroups;
}
public abstract settingsGroups: ISettingsGroup[];
public abstract findValueMatches(filter: string, setting: ISetting): IRange[];
protected abstract update(): IFilterResult;
}
export class SettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel {
private _settingsGroups: ISettingsGroup[];
protected settingsModel: ITextModel;
private readonly _onDidChangeGroups: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeGroups: Event<void> = this._onDidChangeGroups.event;
constructor(reference: IReference<ITextEditorModel>, private _configurationTarget: ConfigurationTarget) {
super();
this.settingsModel = reference.object.textEditorModel;
this._register(this.onDispose(() => reference.dispose()));
this._register(this.settingsModel.onDidChangeContent(() => {
this._settingsGroups = null;
this._onDidChangeGroups.fire();
}));
}
public get uri(): URI {
return this.settingsModel.uri;
}
public get configurationTarget(): ConfigurationTarget {
return this._configurationTarget;
}
public get settingsGroups(): ISettingsGroup[] {
if (!this._settingsGroups) {
this.parse();
}
return this._settingsGroups;
}
public get content(): string {
return this.settingsModel.getValue();
}
public findValueMatches(filter: string, setting: ISetting): IRange[] {
return this.settingsModel.findMatches(filter, setting.valueRange, false, false, null, false).map(match => match.range);
}
protected isSettingsProperty(property: string, previousParents: string[]): boolean {
return previousParents.length === 0; // Settings is root
}
protected parse(): void {
this._settingsGroups = parse(this.settingsModel, (property: string, previousParents: string[]): boolean => this.isSettingsProperty(property, previousParents));
}
protected update(): IFilterResult {
const resultGroups = map.values(this._currentResultGroups);
if (!resultGroups.length) {
return null;
}
// Transform resultGroups into IFilterResult - ISetting ranges are already correct here
const filteredSettings: ISetting[] = [];
const matches: IRange[] = [];
resultGroups.forEach(group => {
group.result.filterMatches.forEach(filterMatch => {
filteredSettings.push(filterMatch.setting);
matches.push(...filterMatch.matches);
});
});
let filteredGroup: ISettingsGroup;
const modelGroup = this.settingsGroups[0]; // Editable model has one or zero groups
if (modelGroup) {
filteredGroup = {
id: modelGroup.id,
range: modelGroup.range,
sections: [{
settings: filteredSettings
}],
title: modelGroup.title,
titleRange: modelGroup.titleRange
};
}
const metadata = this.collectMetadata(resultGroups);
return <IFilterResult>{
allGroups: this.settingsGroups,
filteredGroups: filteredGroup ? [filteredGroup] : [],
matches,
metadata
};
}
}
function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, previousParents: string[]) => boolean): ISettingsGroup[] {
const settings: ISetting[] = [];
let overrideSetting: ISetting = null;
let currentProperty: string = null;
let currentParent: any = [];
let previousParents: any[] = [];
let settingsPropertyIndex: number = -1;
let range = {
startLineNumber: 0,
startColumn: 0,
endLineNumber: 0,
endColumn: 0
};
function onValue(value: any, offset: number, length: number) {
if (Array.isArray(currentParent)) {
(<any[]>currentParent).push(value);
} else if (currentProperty) {
currentParent[currentProperty] = value;
}
if (previousParents.length === settingsPropertyIndex + 1 || (previousParents.length === settingsPropertyIndex + 2 && overrideSetting !== null)) {
// settings value started
const setting = previousParents.length === settingsPropertyIndex + 1 ? settings[settings.length - 1] : overrideSetting.overrides[overrideSetting.overrides.length - 1];
if (setting) {
let valueStartPosition = model.getPositionAt(offset);
let valueEndPosition = model.getPositionAt(offset + length);
setting.value = value;
setting.valueRange = {
startLineNumber: valueStartPosition.lineNumber,
startColumn: valueStartPosition.column,
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
};
setting.range = assign(setting.range, {
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
});
}
}
}
let visitor: JSONVisitor = {
onObjectBegin: (offset: number, length: number) => {
if (isSettingsProperty(currentProperty, previousParents)) {
// Settings started
settingsPropertyIndex = previousParents.length;
let position = model.getPositionAt(offset);
range.startLineNumber = position.lineNumber;
range.startColumn = position.column;
}
let object = {};
onValue(object, offset, length);
currentParent = object;
currentProperty = null;
previousParents.push(currentParent);
},
onObjectProperty: (name: string, offset: number, length: number) => {
currentProperty = name;
if (previousParents.length === settingsPropertyIndex + 1 || (previousParents.length === settingsPropertyIndex + 2 && overrideSetting !== null)) {
// setting started
let settingStartPosition = model.getPositionAt(offset);
const setting: ISetting = {
description: [],
key: name,
keyRange: {
startLineNumber: settingStartPosition.lineNumber,
startColumn: settingStartPosition.column + 1,
endLineNumber: settingStartPosition.lineNumber,
endColumn: settingStartPosition.column + length
},
range: {
startLineNumber: settingStartPosition.lineNumber,
startColumn: settingStartPosition.column,
endLineNumber: 0,
endColumn: 0
},
value: null,
valueRange: null,
descriptionRanges: null,
overrides: [],
overrideOf: overrideSetting
};
if (previousParents.length === settingsPropertyIndex + 1) {
settings.push(setting);
if (OVERRIDE_PROPERTY_PATTERN.test(name)) {
overrideSetting = setting;
}
} else {
overrideSetting.overrides.push(setting);
}
}
},
onObjectEnd: (offset: number, length: number) => {
currentParent = previousParents.pop();
if (previousParents.length === settingsPropertyIndex + 1 || (previousParents.length === settingsPropertyIndex + 2 && overrideSetting !== null)) {
// setting ended
const setting = previousParents.length === settingsPropertyIndex + 1 ? settings[settings.length - 1] : overrideSetting.overrides[overrideSetting.overrides.length - 1];
if (setting) {
let valueEndPosition = model.getPositionAt(offset + length);
setting.valueRange = assign(setting.valueRange, {
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
});
setting.range = assign(setting.range, {
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
});
}
if (previousParents.length === settingsPropertyIndex + 1) {
overrideSetting = null;
}
}
if (previousParents.length === settingsPropertyIndex) {
// settings ended
let position = model.getPositionAt(offset);
range.endLineNumber = position.lineNumber;
range.endColumn = position.column;
}
},
onArrayBegin: (offset: number, length: number) => {
let array: any[] = [];
onValue(array, offset, length);
previousParents.push(currentParent);
currentParent = array;
currentProperty = null;
},
onArrayEnd: (offset: number, length: number) => {
currentParent = previousParents.pop();
if (previousParents.length === settingsPropertyIndex + 1 || (previousParents.length === settingsPropertyIndex + 2 && overrideSetting !== null)) {
// setting value ended
const setting = previousParents.length === settingsPropertyIndex + 1 ? settings[settings.length - 1] : overrideSetting.overrides[overrideSetting.overrides.length - 1];
if (setting) {
let valueEndPosition = model.getPositionAt(offset + length);
setting.valueRange = assign(setting.valueRange, {
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
});
setting.range = assign(setting.range, {
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
});
}
}
},
onLiteralValue: onValue,
onError: (error) => {
const setting = settings[settings.length - 1];
if (setting && (!setting.range || !setting.keyRange || !setting.valueRange)) {
settings.pop();
}
}
};
if (!model.isDisposed()) {
visit(model.getValue(), visitor);
}
return settings.length > 0 ? [<ISettingsGroup>{
sections: [
{
settings
}
],
title: null,
titleRange: null,
range
}] : [];
}
export class WorkspaceConfigurationEditorModel extends SettingsEditorModel {
private _configurationGroups: ISettingsGroup[];
get configurationGroups(): ISettingsGroup[] {
return this._configurationGroups;
}
protected parse(): void {
super.parse();
this._configurationGroups = parse(this.settingsModel, (property: string, previousParents: string[]): boolean => previousParents.length === 0);
}
protected isSettingsProperty(property: string, previousParents: string[]): boolean {
return property === 'settings' && previousParents.length === 1;
}
}
export class DefaultSettings extends Disposable {
private static _RAW: string;
private _allSettingsGroups: ISettingsGroup[];
private _content: string;
private _settingsByName: Map<string, ISetting>;
readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
constructor(
private _mostCommonlyUsedSettingsKeys: string[],
readonly target: ConfigurationTarget,
) {
super();
}
get content(): string {
if (!this._content) {
this.parse();
}
return this._content;
}
get settingsGroups(): ISettingsGroup[] {
if (!this._allSettingsGroups) {
this.parse();
}
return this._allSettingsGroups;
}
parse(): string {
const settingsGroups = this.getRegisteredGroups();
this.initAllSettingsMap(settingsGroups);
const mostCommonlyUsed = this.getMostCommonlyUsedSettings(settingsGroups);
this._allSettingsGroups = [mostCommonlyUsed, ...settingsGroups];
this._content = this.toContent(true, this._allSettingsGroups);
return this._content;
}
get raw(): string {
if (!DefaultSettings._RAW) {
DefaultSettings._RAW = this.toContent(false, this.getRegisteredGroups());
}
return DefaultSettings._RAW;
}
getSettingByName(name: string): ISetting {
return this._settingsByName && this._settingsByName.get(name);
}
private getRegisteredGroups(): ISettingsGroup[] {
const configurations = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurations().slice();
return this.removeEmptySettingsGroups(configurations.sort(this.compareConfigurationNodes)
.reduce((result, config, index, array) => this.parseConfig(config, result, array), []));
}
private initAllSettingsMap(allSettingsGroups: ISettingsGroup[]): void {
this._settingsByName = new Map<string, ISetting>();
for (const group of allSettingsGroups) {
for (const section of group.sections) {
for (const setting of section.settings) {
this._settingsByName.set(setting.key, setting);
}
}
}
}
private getMostCommonlyUsedSettings(allSettingsGroups: ISettingsGroup[]): ISettingsGroup {
const settings = this._mostCommonlyUsedSettingsKeys.map(key => {
const setting = this._settingsByName.get(key);
if (setting) {
return <ISetting>{
description: setting.description,
key: setting.key,
value: setting.value,
range: null,
valueRange: null,
overrides: []
};
}
return null;
}).filter(setting => !!setting);
return <ISettingsGroup>{
id: 'mostCommonlyUsed',
range: null,
title: nls.localize('commonlyUsed', "Commonly Used"),
titleRange: null,
sections: [
{
settings
}
]
};
}
private parseConfig(config: IConfigurationNode, result: ISettingsGroup[], configurations: IConfigurationNode[], settingsGroup?: ISettingsGroup): ISettingsGroup[] {
let title = config.title;
if (!title) {
const configWithTitleAndSameId = configurations.filter(c => c.id === config.id && c.title)[0];
if (configWithTitleAndSameId) {
title = configWithTitleAndSameId.title;
}
}
if (title) {
if (!settingsGroup) {
settingsGroup = result.filter(g => g.title === title)[0];
if (!settingsGroup) {
settingsGroup = { sections: [{ settings: [] }], id: config.id, title: title, titleRange: null, range: null };
result.push(settingsGroup);
}
} else {
settingsGroup.sections[settingsGroup.sections.length - 1].title = title;
}
}
if (config.properties) {
if (!settingsGroup) {
settingsGroup = { sections: [{ settings: [] }], id: config.id, title: config.id, titleRange: null, range: null };
result.push(settingsGroup);
}
const configurationSettings: ISetting[] = [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties)];
if (configurationSettings.length) {
configurationSettings.sort((a, b) => a.key.localeCompare(b.key));
settingsGroup.sections[settingsGroup.sections.length - 1].settings = configurationSettings;
}
}
if (config.allOf) {
config.allOf.forEach(c => this.parseConfig(c, result, configurations, settingsGroup));
}
return result;
}
private removeEmptySettingsGroups(settingsGroups: ISettingsGroup[]): ISettingsGroup[] {
const result = [];
for (const settingsGroup of settingsGroups) {
settingsGroup.sections = settingsGroup.sections.filter(section => section.settings.length > 0);
if (settingsGroup.sections.length) {
result.push(settingsGroup);
}
}
return result;
}
private parseSettings(settingsObject: { [path: string]: IConfigurationPropertySchema; }): ISetting[] {
let result = [];
for (let key in settingsObject) {
const prop = settingsObject[key];
if (!prop.deprecationMessage && this.matchesScope(prop)) {
const value = prop.default;
const description = (prop.description || '').split('\n');
const overrides = OVERRIDE_PROPERTY_PATTERN.test(key) ? this.parseOverrideSettings(prop.default) : [];
result.push({ key, value, description, range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides });
}
}
return result;
}
private parseOverrideSettings(overrideSettings: any): ISetting[] {
return Object.keys(overrideSettings).map((key) => ({ key, value: overrideSettings[key], description: [], range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides: [] }));
}
private matchesScope(property: IConfigurationNode): boolean {
if (this.target === ConfigurationTarget.WORKSPACE_FOLDER) {
return property.scope === ConfigurationScope.RESOURCE;
}
if (this.target === ConfigurationTarget.WORKSPACE) {
return property.scope === ConfigurationScope.WINDOW || property.scope === ConfigurationScope.RESOURCE;
}
return true;
}
private compareConfigurationNodes(c1: IConfigurationNode, c2: IConfigurationNode): number {
if (typeof c1.order !== 'number') {
return 1;
}
if (typeof c2.order !== 'number') {
return -1;
}
if (c1.order === c2.order) {
const title1 = c1.title || '';
const title2 = c2.title || '';
return title1.localeCompare(title2);
}
return c1.order - c2.order;
}
private toContent(asArray: boolean, settingsGroups: ISettingsGroup[]): string {
const builder = new SettingsContentBuilder();
if (asArray) {
builder.pushLine('[');
}
settingsGroups.forEach((settingsGroup, i) => {
builder.pushGroup(settingsGroup);
builder.pushLine(',');
});
if (asArray) {
builder.pushLine(']');
}
return builder.getContent();
}
}
export class DefaultSettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel {
private _model: ITextModel;
private readonly _onDidChangeGroups: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeGroups: Event<void> = this._onDidChangeGroups.event;
constructor(
private _uri: URI,
reference: IReference<ITextEditorModel>,
private readonly defaultSettings: DefaultSettings
) {
super();
this._register(defaultSettings.onDidChange(() => this._onDidChangeGroups.fire()));
this._model = reference.object.textEditorModel;
this._register(this.onDispose(() => reference.dispose()));
}
public get uri(): URI {
return this._uri;
}
public get target(): ConfigurationTarget {
return this.defaultSettings.target;
}
public get settingsGroups(): ISettingsGroup[] {
return this.defaultSettings.settingsGroups;
}
protected get filterGroups(): ISettingsGroup[] {
// Don't look at "commonly used" for filter
return this.settingsGroups.slice(1);
}
protected update(): IFilterResult {
// Grab current result groups, only render non-empty groups
const resultGroups = map
.values(this._currentResultGroups)
.sort((a, b) => a.order - b.order);
const nonEmptyResultGroups = resultGroups.filter(group => group.result.filterMatches.length);
const startLine = tail(this.settingsGroups).range.endLineNumber + 2;
const { settingsGroups: filteredGroups, matches } = this.writeResultGroups(nonEmptyResultGroups, startLine);
const metadata = this.collectMetadata(resultGroups);
return resultGroups.length ?
<IFilterResult>{
allGroups: this.settingsGroups,
filteredGroups,
matches,
metadata
} :
null;
}
/**
* Translate the ISearchResultGroups to text, and write it to the editor model
*/
private writeResultGroups(groups: ISearchResultGroup[], startLine: number): { matches: IRange[], settingsGroups: ISettingsGroup[] } {
const contentBuilderOffset = startLine - 1;
const builder = new SettingsContentBuilder(contentBuilderOffset);
const settingsGroups: ISettingsGroup[] = [];
const matches: IRange[] = [];
builder.pushLine(',');
groups.forEach(resultGroup => {
const settingsGroup = this.getGroup(resultGroup);
settingsGroups.push(settingsGroup);
matches.push(...this.writeSettingsGroupToBuilder(builder, settingsGroup, resultGroup.result.filterMatches));
});
// note: 1-indexed line numbers here
const groupContent = builder.getContent() + '\n';
const groupEndLine = this._model.getLineCount();
const cursorPosition = new Selection(startLine, 1, startLine, 1);
const edit: IIdentifiedSingleEditOperation = {
text: groupContent,
forceMoveMarkers: true,
range: new Range(startLine, 1, groupEndLine, 1),
identifier: { major: 1, minor: 0 }
};
this._model.pushEditOperations([cursorPosition], [edit], () => [cursorPosition]);
// Force tokenization now - otherwise it may be slightly delayed, causing a flash of white text
const tokenizeTo = Math.min(startLine + 60, this._model.getLineCount());
this._model.forceTokenization(tokenizeTo);
return { matches, settingsGroups };
}
private writeSettingsGroupToBuilder(builder: SettingsContentBuilder, settingsGroup: ISettingsGroup, filterMatches: ISettingMatch[]): IRange[] {
filterMatches = filterMatches
.map(filteredMatch => {
// Fix match ranges to offset from setting start line
return <ISettingMatch>{
setting: filteredMatch.setting,
score: filteredMatch.score,
matches: filteredMatch.matches && filteredMatch.matches.map(match => {
return new Range(
match.startLineNumber - filteredMatch.setting.range.startLineNumber,
match.startColumn,
match.endLineNumber - filteredMatch.setting.range.startLineNumber,
match.endColumn);
})
};
});
builder.pushGroup(settingsGroup);
builder.pushLine(',');
// builder has rewritten settings ranges, fix match ranges
const fixedMatches = flatten(
filterMatches
.map(m => m.matches || [])
.map((settingMatches, i) => {
const setting = settingsGroup.sections[0].settings[i];
return settingMatches.map(range => {
return new Range(
range.startLineNumber + setting.range.startLineNumber,
range.startColumn,
range.endLineNumber + setting.range.startLineNumber,
range.endColumn);
});
}));
return fixedMatches;
}
private copySetting(setting: ISetting): ISetting {
return <ISetting>{
description: setting.description,
key: setting.key,
value: setting.value,
range: setting.range,
overrides: [],
overrideOf: setting.overrideOf
};
}
public findValueMatches(filter: string, setting: ISetting): IRange[] {
return [];
}
public getPreference(key: string): ISetting {
for (const group of this.settingsGroups) {
for (const section of group.sections) {
for (const setting of section.settings) {
if (setting.key === key) {
return setting;
}
}
}
}
return null;
}
private getGroup(resultGroup: ISearchResultGroup): ISettingsGroup {
return <ISettingsGroup>{
id: resultGroup.id,
range: null,
title: resultGroup.label,
titleRange: null,
sections: [
{
settings: resultGroup.result.filterMatches.map(m => this.copySetting(m.setting))
}
]
};
}
}
class SettingsContentBuilder {
private _contentByLines: string[];
private get lineCountWithOffset(): number {
return this._contentByLines.length + this._rangeOffset;
}
private get lastLine(): string {
return this._contentByLines[this._contentByLines.length - 1] || '';
}
constructor(private _rangeOffset = 0) {
this._contentByLines = [];
}
private offsetIndexToIndex(offsetIdx: number): number {
return offsetIdx - this._rangeOffset;
}
pushLine(...lineText: string[]): void {
this._contentByLines.push(...lineText);
}
pushGroup(settingsGroups: ISettingsGroup): void {
this._contentByLines.push('{');
this._contentByLines.push('');
this._contentByLines.push('');
const lastSetting = this._pushGroup(settingsGroups);
if (lastSetting) {
// Strip the comma from the last setting
const lineIdx = this.offsetIndexToIndex(lastSetting.range.endLineNumber);
const content = this._contentByLines[lineIdx - 2];
this._contentByLines[lineIdx - 2] = content.substring(0, content.length - 1);
}
this._contentByLines.push('}');
}
private _pushGroup(group: ISettingsGroup): ISetting {
const indent = ' ';
let lastSetting: ISetting = null;
let groupStart = this.lineCountWithOffset + 1;
for (const section of group.sections) {
if (section.title) {
let sectionTitleStart = this.lineCountWithOffset + 1;
this.addDescription([section.title], indent, this._contentByLines);
section.titleRange = { startLineNumber: sectionTitleStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length };
}
if (section.settings.length) {
for (const setting of section.settings) {
this.pushSetting(setting, indent);
lastSetting = setting;
}
}
}
group.range = { startLineNumber: groupStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length };
return lastSetting;
}
getContent(): string {
return this._contentByLines.join('\n');
}
private pushSetting(setting: ISetting, indent: string): void {
const settingStart = this.lineCountWithOffset + 1;
setting.descriptionRanges = [];
const descriptionPreValue = indent + '// ';
for (const line of setting.description) {
this._contentByLines.push(descriptionPreValue + line);
setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length });
}
let preValueConent = indent;
const keyString = JSON.stringify(setting.key);
preValueConent += keyString;
setting.keyRange = { startLineNumber: this.lineCountWithOffset + 1, startColumn: preValueConent.indexOf(setting.key) + 1, endLineNumber: this.lineCountWithOffset + 1, endColumn: setting.key.length };
preValueConent += ': ';
const valueStart = this.lineCountWithOffset + 1;
this.pushValue(setting, preValueConent, indent);
setting.valueRange = { startLineNumber: valueStart, startColumn: preValueConent.length + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length + 1 };
this._contentByLines[this._contentByLines.length - 1] += ',';
this._contentByLines.push('');
setting.range = { startLineNumber: settingStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length };
}
private pushValue(setting: ISetting, preValueConent: string, indent: string): void {
let valueString = JSON.stringify(setting.value, null, indent);
if (valueString && (typeof setting.value === 'object')) {
if (setting.overrides.length) {
this._contentByLines.push(preValueConent + ' {');
for (const subSetting of setting.overrides) {
this.pushSetting(subSetting, indent + indent);
this._contentByLines.pop();
}
const lastSetting = setting.overrides[setting.overrides.length - 1];
const content = this._contentByLines[lastSetting.range.endLineNumber - 2];
this._contentByLines[lastSetting.range.endLineNumber - 2] = content.substring(0, content.length - 1);
this._contentByLines.push(indent + '}');
} else {
const mulitLineValue = valueString.split('\n');
this._contentByLines.push(preValueConent + mulitLineValue[0]);
for (let i = 1; i < mulitLineValue.length; i++) {
this._contentByLines.push(indent + mulitLineValue[i]);
}
}
} else {
this._contentByLines.push(preValueConent + valueString);
}
}
private addDescription(description: string[], indent: string, result: string[]) {
for (const line of description) {
result.push(indent + '// ' + line);
}
}
}
export function defaultKeybindingsContents(keybindingService: IKeybindingService): string {
const defaultsHeader = '// ' + nls.localize('defaultKeybindingsHeader', "Overwrite key bindings by placing them into your key bindings file.");
return defaultsHeader + '\n' + keybindingService.getDefaultKeybindingsContent();
}
export class DefaultKeybindingsEditorModel implements IKeybindingsEditorModel<any> {
private _content: string;
constructor(private _uri: URI,
@IKeybindingService private keybindingService: IKeybindingService) {
}
public get uri(): URI {
return this._uri;
}
public get content(): string {
if (!this._content) {
this._content = defaultKeybindingsContents(this.keybindingService);
}
return this._content;
}
public getPreference(): any {
return null;
}
public dispose(): void {
// Not disposable
}
}

View File

@@ -0,0 +1,655 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as uuid from 'vs/base/common/uuid';
import { TPromise } from 'vs/base/common/winjs.base';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { Registry } from 'vs/platform/registry/common/platform';
import { Action } from 'vs/base/common/actions';
import { KeyCode, SimpleKeybinding, ChordKeybinding } from 'vs/base/common/keyCodes';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingsEditorModel, IKeybindingItemEntry } from 'vs/workbench/services/preferences/common/keybindingsEditorModel';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
interface Modifiers {
metaKey?: boolean;
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
}
class AnAction extends Action {
constructor(id: string) {
super(id);
}
}
suite('Keybindings Editor Model test', () => {
let instantiationService: TestInstantiationService;
let testObject: KeybindingsEditorModel;
setup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(IKeybindingService, {});
instantiationService.stub(IExtensionService, {}, 'whenInstalledExtensionsRegistered', () => TPromise.as(null));
testObject = instantiationService.createInstance(KeybindingsEditorModel, OS);
CommandsRegistry.registerCommand('command_without_keybinding', () => { });
});
test('fetch returns default keybindings', () => {
const expected = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'b' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } })
);
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch(''));
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns default keybindings at the top', () => {
const expected = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'b' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } })
);
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch('').slice(0, 2), true);
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns default keybindings sorted by command id', () => {
const keybindings = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'b' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'c' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Backspace } })
);
const expected = [keybindings[2], keybindings[0], keybindings[1]];
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch(''));
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns user keybinding first if default and user has same id', () => {
const sameId = 'b' + uuid.generateUuid();
const keybindings = prepareKeybindingService(
aResolvedKeybindingItem({ command: sameId, firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: sameId, firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape }, isDefault: false })
);
const expected = [keybindings[1], keybindings[0]];
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch(''));
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns keybinding with titles first', () => {
const keybindings = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'b' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'c' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'd' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } })
);
registerCommandWithTitle(keybindings[1].command, 'B Title');
registerCommandWithTitle(keybindings[3].command, 'A Title');
const expected = [keybindings[3], keybindings[1], keybindings[0], keybindings[2]];
instantiationService.stub(IKeybindingService, 'getKeybindings', () => keybindings);
instantiationService.stub(IKeybindingService, 'getDefaultKeybindings', () => keybindings);
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch(''));
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns keybinding with user first if title and id matches', () => {
const sameId = 'b' + uuid.generateUuid();
const keybindings = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: sameId, firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'c' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: sameId, firstPart: { keyCode: KeyCode.Escape }, isDefault: false })
);
registerCommandWithTitle(keybindings[1].command, 'Same Title');
registerCommandWithTitle(keybindings[3].command, 'Same Title');
const expected = [keybindings[3], keybindings[1], keybindings[0], keybindings[2]];
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch(''));
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns default keybindings sorted by precedence', () => {
const expected = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'b' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'c' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Backspace } })
);
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch('', true));
assertKeybindingItems(actuals, expected);
});
});
test('convert keybinding without title to entry', () => {
const expected = aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2' });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('')[0];
assert.equal(actual.keybindingItem.command, expected.command);
assert.equal(actual.keybindingItem.commandLabel, '');
assert.equal(actual.keybindingItem.commandDefaultLabel, null);
assert.equal(actual.keybindingItem.keybinding.getAriaLabel(), expected.resolvedKeybinding.getAriaLabel());
assert.equal(actual.keybindingItem.when, expected.when.serialize());
});
});
test('convert keybinding with title to entry', () => {
const expected = aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2' });
prepareKeybindingService(expected);
registerCommandWithTitle(expected.command, 'Some Title');
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('')[0];
assert.equal(actual.keybindingItem.command, expected.command);
assert.equal(actual.keybindingItem.commandLabel, 'Some Title');
assert.equal(actual.keybindingItem.commandDefaultLabel, null);
assert.equal(actual.keybindingItem.keybinding.getAriaLabel(), expected.resolvedKeybinding.getAriaLabel());
assert.equal(actual.keybindingItem.when, expected.when.serialize());
});
});
test('convert without title and binding to entry', () => {
CommandsRegistry.registerCommand('command_without_keybinding', () => { });
prepareKeybindingService();
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('').filter(element => element.keybindingItem.command === 'command_without_keybinding')[0];
assert.equal(actual.keybindingItem.command, 'command_without_keybinding');
assert.equal(actual.keybindingItem.commandLabel, '');
assert.equal(actual.keybindingItem.commandDefaultLabel, null);
assert.equal(actual.keybindingItem.keybinding, null);
assert.equal(actual.keybindingItem.when, '');
});
});
test('convert with title and wihtout binding to entry', () => {
const id = 'a' + uuid.generateUuid();
registerCommandWithTitle(id, 'some title');
prepareKeybindingService();
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('').filter(element => element.keybindingItem.command === id)[0];
assert.equal(actual.keybindingItem.command, id);
assert.equal(actual.keybindingItem.commandLabel, 'some title');
assert.equal(actual.keybindingItem.commandDefaultLabel, null);
assert.equal(actual.keybindingItem.keybinding, null);
assert.equal(actual.keybindingItem.when, '');
});
});
test('filter by command id', () => {
const id = 'workbench.action.increaseViewSize';
registerCommandWithTitle(id, 'some title');
prepareKeybindingService();
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('workbench action view size').filter(element => element.keybindingItem.command === id)[0];
assert.ok(actual);
});
});
test('filter by command title', () => {
const id = 'a' + uuid.generateUuid();
registerCommandWithTitle(id, 'Increase view size');
prepareKeybindingService();
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('increase size').filter(element => element.keybindingItem.command === id)[0];
assert.ok(actual);
});
});
test('filter by default source', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2' });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('default').filter(element => element.keybindingItem.command === command)[0];
assert.ok(actual);
});
});
test('filter by user source', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2', isDefault: false });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('user').filter(element => element.keybindingItem.command === command)[0];
assert.ok(actual);
});
});
test('filter by default source with "@source: " prefix', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2', isDefault: true });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('@source: default').filter(element => element.keybindingItem.command === command)[0];
assert.ok(actual);
});
});
test('filter by user source with "@source: " prefix', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2', isDefault: false });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('@source: user').filter(element => element.keybindingItem.command === command)[0];
assert.ok(actual);
});
});
test('filter by when context', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('when context').filter(element => element.keybindingItem.command === command)[0];
assert.ok(actual);
});
});
test('filter by cmd key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('cmd').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by meta key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('meta').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by command key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('command').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by windows key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Windows);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('windows').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by alt key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('alt').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { altKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by option key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('option').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { altKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by ctrl key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('ctrl').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { ctrlKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by control key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('control').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { ctrlKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by shift key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('shift').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { shiftKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by arrow', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.RightArrow, modifiers: { shiftKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('arrow').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by modifier and key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.RightArrow, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.RightArrow, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('alt right').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { altKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by key and modifier', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.RightArrow, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.RightArrow, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('right alt').filter(element => element.keybindingItem.command === command);
assert.equal(0, actual.length);
});
});
test('filter by modifiers and key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true, metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('alt cmd esc').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { altKey: true, metaKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by modifiers in random order and key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('cmd shift esc').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true, shiftKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by first part', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.Delete }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('cmd shift esc').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true, shiftKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter matches in chord part', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.Delete }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('cmd del').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, { keyCode: true });
});
});
test('filter matches first part and in chord part', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.Delete }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.UpArrow }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('cmd shift esc del').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { shiftKey: true, metaKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, { keyCode: true });
});
});
test('filter exact matches', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"ctrl c"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { ctrlKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter exact matches with first and chord part', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"shift meta escape ctrl c"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { shiftKey: true, metaKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, { ctrlKey: true, keyCode: true });
});
});
test('filter exact matches with first and chord part no results', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.Delete, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.UpArrow }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"cmd shift esc del"').filter(element => element.keybindingItem.command === command);
assert.equal(0, actual.length);
});
});
test('filter matches with + separator', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"control+c"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { ctrlKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter matches with + separator in first and chord parts', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"shift+meta+escape ctrl+c"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { shiftKey: true, metaKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, { keyCode: true, ctrlKey: true });
});
});
test('filter exact matches with space #32993', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Space, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Backspace, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"ctrl+space"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
});
});
function prepareKeybindingService(...keybindingItems: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] {
instantiationService.stub(IKeybindingService, 'getKeybindings', () => keybindingItems);
instantiationService.stub(IKeybindingService, 'getDefaultKeybindings', () => keybindingItems);
return keybindingItems;
}
function registerCommandWithTitle(command: string, title: string): void {
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(AnAction, command, title, { primary: null }), '');
}
function assertKeybindingItems(actual: ResolvedKeybindingItem[], expected: ResolvedKeybindingItem[]) {
assert.equal(actual.length, expected.length);
for (let i = 0; i < actual.length; i++) {
assertKeybindingItem(actual[i], expected[i]);
}
}
function assertKeybindingItem(actual: ResolvedKeybindingItem, expected: ResolvedKeybindingItem): void {
assert.equal(actual.command, expected.command);
if (actual.when) {
assert.ok(!!expected.when);
assert.equal(actual.when.serialize(), expected.when.serialize());
} else {
assert.ok(!expected.when);
}
assert.equal(actual.isDefault, expected.isDefault);
if (actual.resolvedKeybinding) {
assert.ok(!!expected.resolvedKeybinding);
assert.equal(actual.resolvedKeybinding.getLabel(), expected.resolvedKeybinding.getLabel());
} else {
assert.ok(!expected.resolvedKeybinding);
}
}
function aResolvedKeybindingItem({ command, when, isDefault, firstPart, chordPart }: { command?: string, when?: string, isDefault?: boolean, firstPart?: { keyCode: KeyCode, modifiers?: Modifiers }, chordPart?: { keyCode: KeyCode, modifiers?: Modifiers } }): ResolvedKeybindingItem {
const aSimpleKeybinding = function (part: { keyCode: KeyCode, modifiers?: Modifiers }): SimpleKeybinding {
const { ctrlKey, shiftKey, altKey, metaKey } = part.modifiers || { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false };
return new SimpleKeybinding(ctrlKey, shiftKey, altKey, metaKey, part.keyCode);
};
const keybinding = firstPart ? chordPart ? new ChordKeybinding(aSimpleKeybinding(firstPart), aSimpleKeybinding(chordPart)) : aSimpleKeybinding(firstPart) : null;
return new ResolvedKeybindingItem(keybinding ? new USLayoutResolvedKeybinding(keybinding, OS) : null, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : null, isDefault === void 0 ? true : isDefault);
}
function asResolvedKeybindingItems(keybindingEntries: IKeybindingItemEntry[], keepUnassigned: boolean = false): ResolvedKeybindingItem[] {
if (!keepUnassigned) {
keybindingEntries = keybindingEntries.filter(keybindingEntry => !!keybindingEntry.keybindingItem.keybinding);
}
return keybindingEntries.map(entry => entry.keybindingItem.keybindingItem);
}
});