keybindingEditing.ts 11.9 KB
Newer Older
1 2 3 4 5 6 7 8
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { localize } from 'vs/nls';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
S
Sandeep Somavarapu 已提交
9
import { isArray } from 'vs/base/common/types';
10 11 12 13 14 15 16 17 18 19
import { Queue } from 'vs/base/common/async';
import { IReference, Disposable } from 'vs/base/common/lifecycle';
import * as json from 'vs/base/common/json';
import { Edit } from 'vs/base/common/jsonFormatter';
import { setProperty } from 'vs/base/common/jsonEdit';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
20
import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding';
21 22 23 24 25
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IFileService } from 'vs/platform/files/common/files';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
26 27
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';

28 29 30 31 32 33 34

export const IKeybindingEditingService = createDecorator<IKeybindingEditingService>('keybindingEditingService');

export interface IKeybindingEditingService {

	_serviceBrand: ServiceIdentifier<any>;

35
	editKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): TPromise<void>;
36

37
	removeKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void>;
38 39

	resetKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void>;
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
}

export class KeybindingsEditingService extends Disposable implements IKeybindingEditingService {

	public _serviceBrand: any;
	private queue: Queue<void>;

	private resource: URI = URI.file(this.environmentService.appKeybindingsPath);

	constructor(
		@ITextModelResolverService private textModelResolverService: ITextModelResolverService,
		@ITextFileService private textFileService: ITextFileService,
		@IFileService private fileService: IFileService,
		@IEnvironmentService private environmentService: IEnvironmentService
	) {
		super();
		this.queue = new Queue<void>();
	}

59
	editKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): TPromise<void> {
60 61 62
		return this.queue.queue(() => this.doEditKeybinding(key, keybindingItem)); // queue up writes to prevent race conditions
	}

63 64 65 66
	resetKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void> {
		return this.queue.queue(() => this.doResetKeybinding(keybindingItem)); // queue up writes to prevent race conditions
	}

67
	removeKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void> {
68 69 70
		return this.queue.queue(() => this.doRemoveKeybinding(keybindingItem)); // queue up writes to prevent race conditions
	}

71
	private doEditKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): TPromise<void> {
72 73 74
		return this.resolveAndValidate()
			.then(reference => {
				const model = reference.object.textEditorModel;
75
				if (keybindingItem.isDefault) {
76
					this.updateDefaultKeybinding(key, keybindingItem, model);
77 78
				} else {
					this.updateUserKeybinding(key, keybindingItem, model);
79 80 81 82 83
				}
				return this.save().then(() => reference.dispose());
			});
	}

84
	private doRemoveKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void> {
85 86 87
		return this.resolveAndValidate()
			.then(reference => {
				const model = reference.object.textEditorModel;
88
				if (keybindingItem.isDefault) {
89
					this.removeDefaultKeybinding(keybindingItem, model);
90 91
				} else {
					this.removeUserKeybinding(keybindingItem, model);
92 93 94 95 96
				}
				return this.save().then(() => reference.dispose());
			});
	}

97 98 99 100 101 102 103 104 105 106 107 108
	private doResetKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void> {
		return this.resolveAndValidate()
			.then(reference => {
				const model = reference.object.textEditorModel;
				if (!keybindingItem.isDefault) {
					this.removeUserKeybinding(keybindingItem, model);
					this.removeUnassignedDefaultKeybinding(keybindingItem, model);
				}
				return this.save().then(() => reference.dispose());
			});
	}

109
	private save(): TPromise<any> {
110
		return this.textFileService.save(this.resource);
111 112
	}

113
	private updateUserKeybinding(newKey: string, keybindingItem: ResolvedKeybindingItem, model: editorCommon.IModel): void {
114 115 116
		const {tabSize, insertSpaces} = model.getOptions();
		const eol = model.getEOL();
		const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
117 118 119
		const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
		if (userKeybindingEntryIndex !== -1) {
			this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model);
120 121 122
		}
	}

123
	private updateDefaultKeybinding(newKey: string, keybindingItem: ResolvedKeybindingItem, model: editorCommon.IModel): void {
124 125 126
		const {tabSize, insertSpaces} = model.getOptions();
		const eol = model.getEOL();
		const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
127 128
		const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
		if (userKeybindingEntryIndex !== -1) {
129
			// Update the keybinding with new key
130
			this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model);
131 132 133 134
		} else {
			// Add the new keybinidng with new key
			this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(newKey, keybindingItem.command, keybindingItem.when, false), { tabSize, insertSpaces, eol })[0], model);
		}
135
		if (keybindingItem.resolvedKeybinding) {
136
			// Unassign the default keybinding
137
			this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(keybindingItem.resolvedKeybinding.getUserSettingsLabel(), keybindingItem.command, keybindingItem.when, true), { tabSize, insertSpaces, eol })[0], model);
138 139 140
		}
	}

141
	private removeUserKeybinding(keybindingItem: ResolvedKeybindingItem, model: editorCommon.IModel): void {
142
		const {tabSize, insertSpaces} = model.getOptions();
143
		const eol = model.getEOL();
144
		const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
145 146 147
		const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
		if (userKeybindingEntryIndex !== -1) {
			this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex], void 0, { tabSize, insertSpaces, eol })[0], model);
148 149 150
		}
	}

151
	private removeDefaultKeybinding(keybindingItem: ResolvedKeybindingItem, model: editorCommon.IModel): void {
152 153
		const {tabSize, insertSpaces} = model.getOptions();
		const eol = model.getEOL();
154
		this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(keybindingItem.resolvedKeybinding.getUserSettingsLabel(), keybindingItem.command, keybindingItem.when, true), { tabSize, insertSpaces, eol })[0], model);
155 156
	}

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
	private removeUnassignedDefaultKeybinding(keybindingItem: ResolvedKeybindingItem, model: editorCommon.IModel): void {
		const {tabSize, insertSpaces} = model.getOptions();
		const eol = model.getEOL();
		const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
		const index = this.findUnassignedDefaultKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
		if (index !== -1) {
			this.applyEditsToBuffer(setProperty(model.getValue(), [index], void 0, { tabSize, insertSpaces, eol })[0], model);
		}
	}

	private findUserKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number {
		for (let index = 0; index < userKeybindingEntries.length; index++) {
			const keybinding = userKeybindingEntries[index];
			if (keybinding.command === keybindingItem.command) {
				if (!keybinding.when && !keybindingItem.when) {
					return index;
				}
				if (keybinding.when && keybindingItem.when) {
					if (ContextKeyExpr.deserialize(keybinding.when).serialize() === keybindingItem.when.serialize()) {
						return index;
					}
				}
179
			}
180 181 182 183 184 185 186 187
		}
		return -1;
	}

	private findUnassignedDefaultKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number {
		for (let index = 0; index < userKeybindingEntries.length; index++) {
			if (userKeybindingEntries[index].command === `-${keybindingItem.command}`) {
				return index;
188
			}
189 190
		}
		return -1;
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
	}

	private asObject(key: string, command: string, when: ContextKeyExpr, negate: boolean): any {
		const object = { key };
		object['command'] = negate ? `-${command}` : command;
		if (when) {
			object['when'] = when.serialize();
		}
		return object;
	}


	private applyEditsToBuffer(edit: Edit, model: editorCommon.IModel): void {
		const startPosition = model.getPositionAt(edit.offset);
		const endPosition = model.getPositionAt(edit.offset + edit.length);
		const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
		let currentText = model.getValueInRange(range);
		const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
		model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
	}


	private resolveModelReference(): TPromise<IReference<ITextEditorModel>> {
		return this.fileService.existsFile(this.resource)
			.then(exists => {
S
Sandeep Somavarapu 已提交
216
				const result = exists ? TPromise.as(null) : this.fileService.updateContent(this.resource, '[]', { encoding: 'utf8' });
217 218 219 220 221 222 223 224 225 226 227 228 229 230
				return result.then(() => this.textModelResolverService.createModelReference(this.resource));
			});
	}

	private resolveAndValidate(): TPromise<IReference<ITextEditorModel>> {

		// Target cannot be dirty if not writing into buffer
		if (this.textFileService.isDirty(this.resource)) {
			return TPromise.wrapError(localize('errorKeybindingsFileDirty', "Unable to write because the file is dirty. Please save the **Keybindings** file and try again."));
		}

		return this.resolveModelReference()
			.then(reference => {
				const model = reference.object.textEditorModel;
S
Sandeep Somavarapu 已提交
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
				const EOL = model.getEOL();
				if (model.getValue()) {
					const parsed = this.parse(model);
					if (parsed.parseErrors.length) {
						return TPromise.wrapError(localize('parseErrors', "Unable to write keybindings. Please open **Keybindings file** to correct errors/warnings in the file and try again."));
					}
					if (parsed.result) {
						if (!isArray(parsed.result)) {
							return TPromise.wrapError(localize('errorInvalidConfiguration', "Unable to write keybindings. **Keybindings file** has an object which is not of type Array. Please open the file to clean up and try again."));
						}
					} else {
						const content = EOL + '[]';
						this.applyEditsToBuffer({ content, length: content.length, offset: model.getValue().length }, model);
					}
				} else {
					const content = '// ' + localize('emptyKeybindingsHeader', "Place your key bindings in this file to overwrite the defaults") + EOL + '[]';
					this.applyEditsToBuffer({ content, length: content.length, offset: 0 }, model);
248 249 250 251 252
				}
				return reference;
			});
	}

S
Sandeep Somavarapu 已提交
253
	private parse(model: editorCommon.IModel): { result: IUserFriendlyKeybinding[], parseErrors: json.ParseError[] } {
254
		const parseErrors: json.ParseError[] = [];
S
Sandeep Somavarapu 已提交
255
		const result = json.parse(model.getValue(), parseErrors, { allowTrailingComma: true });
S
Sandeep Somavarapu 已提交
256
		return { result, parseErrors };
257 258
	}
}