keybindingEditing.ts 11.9 KB
Newer Older
1 2 3 4 5 6 7 8 9
/*---------------------------------------------------------------------------------------------
 *  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 { Queue } from 'vs/base/common/async';
import * as json from 'vs/base/common/json';
import { setProperty } from 'vs/base/common/jsonEdit';
A
Alex Dima 已提交
10 11 12 13
import { Edit } from 'vs/base/common/jsonFormatter';
import { Disposable, IReference } from 'vs/base/common/lifecycle';
import { isArray } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
14 15 16
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
A
Alex Dima 已提交
17 18 19 20
import { ITextModel } from 'vs/editor/common/model';
import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
21 22
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
A
Alex Dima 已提交
23 24
import { ServiceIdentifier, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding';
25
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
A
Alex Dima 已提交
26
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
27
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
28 29 30 31 32 33 34

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

export interface IKeybindingEditingService {

	_serviceBrand: ServiceIdentifier<any>;

35
	editKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void>;
36

J
Johannes Rieken 已提交
37
	removeKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void>;
38

J
Johannes Rieken 已提交
39
	resetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void>;
40 41
}

42 43 44 45 46 47 48 49
export class KeybindingsEditingService extends Disposable implements IKeybindingEditingService {

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

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

	constructor(
50 51 52 53 54
		@ITextModelService private readonly textModelResolverService: ITextModelService,
		@ITextFileService private readonly textFileService: ITextFileService,
		@IFileService private readonly fileService: IFileService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IEnvironmentService private readonly environmentService: IEnvironmentService
55 56 57 58 59
	) {
		super();
		this.queue = new Queue<void>();
	}

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

J
Johannes Rieken 已提交
64
	resetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
65 66 67
		return this.queue.queue(() => this.doResetKeybinding(keybindingItem)); // queue up writes to prevent race conditions
	}

J
Johannes Rieken 已提交
68
	removeKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
69 70 71
		return this.queue.queue(() => this.doRemoveKeybinding(keybindingItem)); // queue up writes to prevent race conditions
	}

72
	private doEditKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void> {
73 74 75
		return this.resolveAndValidate()
			.then(reference => {
				const model = reference.object.textEditorModel;
S
Sandeep Somavarapu 已提交
76 77
				const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
				const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
78
				this.updateKeybinding(keybindingItem, key, when, model, userKeybindingEntryIndex);
S
Sandeep Somavarapu 已提交
79 80
				if (keybindingItem.isDefault && keybindingItem.resolvedKeybinding) {
					this.removeDefaultKeybinding(keybindingItem, model);
81 82 83 84 85
				}
				return this.save().then(() => reference.dispose());
			});
	}

J
Johannes Rieken 已提交
86
	private doRemoveKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
87 88 89
		return this.resolveAndValidate()
			.then(reference => {
				const model = reference.object.textEditorModel;
90
				if (keybindingItem.isDefault) {
91
					this.removeDefaultKeybinding(keybindingItem, model);
92 93
				} else {
					this.removeUserKeybinding(keybindingItem, model);
94 95 96 97 98
				}
				return this.save().then(() => reference.dispose());
			});
	}

J
Johannes Rieken 已提交
99
	private doResetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
100 101 102 103 104 105 106 107 108 109 110
		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());
			});
	}

J
Johannes Rieken 已提交
111
	private save(): Promise<any> {
112
		return this.textFileService.save(this.resource);
113 114
	}

115
	private updateKeybinding(keybindingItem: ResolvedKeybindingItem, newKey: string, when: string | undefined, model: ITextModel, userKeybindingEntryIndex: number): void {
A
Alex Dima 已提交
116
		const { tabSize, insertSpaces } = model.getOptions();
117
		const eol = model.getEOL();
118
		if (userKeybindingEntryIndex !== -1) {
119
			// Update the keybinding with new key
120
			this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model);
121 122 123 124
			const edits = setProperty(model.getValue(), [userKeybindingEntryIndex, 'when'], when, { tabSize, insertSpaces, eol });
			if (edits.length > 1) {
				this.applyEditsToBuffer(edits[0], model);
			}
125
		} else {
S
Sandeep Somavarapu 已提交
126
			// Add the new keybinding with new key
127
			this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(newKey, keybindingItem.command, when, false), { tabSize, insertSpaces, eol })[0], model);
128 129 130
		}
	}

A
Alex Dima 已提交
131
	private removeUserKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {
A
Alex Dima 已提交
132
		const { tabSize, insertSpaces } = model.getOptions();
133
		const eol = model.getEOL();
134
		const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
135 136
		const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
		if (userKeybindingEntryIndex !== -1) {
R
Rob Lourens 已提交
137
			this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex], undefined, { tabSize, insertSpaces, eol })[0], model);
138 139 140
		}
	}

A
Alex Dima 已提交
141
	private removeDefaultKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {
A
Alex Dima 已提交
142
		const { tabSize, insertSpaces } = model.getOptions();
143
		const eol = model.getEOL();
144
		this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(keybindingItem.resolvedKeybinding.getUserSettingsLabel(), keybindingItem.command, keybindingItem.when ? keybindingItem.when.serialize() : undefined, true), { tabSize, insertSpaces, eol })[0], model);
145 146
	}

A
Alex Dima 已提交
147
	private removeUnassignedDefaultKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {
A
Alex Dima 已提交
148
		const { tabSize, insertSpaces } = model.getOptions();
149 150
		const eol = model.getEOL();
		const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
S
Sandeep Somavarapu 已提交
151 152
		const indices = this.findUnassignedDefaultKeybindingEntryIndex(keybindingItem, userKeybindingEntries).reverse();
		for (const index of indices) {
R
Rob Lourens 已提交
153
			this.applyEditsToBuffer(setProperty(model.getValue(), [index], undefined, { tabSize, insertSpaces, eol })[0], model);
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
		}
	}

	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;
					}
				}
169
			}
170 171 172 173
		}
		return -1;
	}

S
Sandeep Somavarapu 已提交
174
	private findUnassignedDefaultKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number[] {
M
Matt Bierner 已提交
175
		const indices: number[] = [];
176 177
		for (let index = 0; index < userKeybindingEntries.length; index++) {
			if (userKeybindingEntries[index].command === `-${keybindingItem.command}`) {
S
Sandeep Somavarapu 已提交
178
				indices.push(index);
179
			}
180
		}
S
Sandeep Somavarapu 已提交
181
		return indices;
182 183
	}

184
	private asObject(key: string, command: string, when: string, negate: boolean): any {
185 186 187
		const object = { key };
		object['command'] = negate ? `-${command}` : command;
		if (when) {
188
			object['when'] = when;
189 190 191 192 193
		}
		return object;
	}


A
Alex Dima 已提交
194
	private applyEditsToBuffer(edit: Edit, model: ITextModel): void {
195 196 197 198 199 200 201 202 203
		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], () => []);
	}


J
Johannes Rieken 已提交
204
	private resolveModelReference(): Promise<IReference<ITextEditorModel>> {
205 206
		return this.fileService.existsFile(this.resource)
			.then(exists => {
207
				const EOL = this.configurationService.getValue('files', { overrideIdentifier: 'json' })['eol'];
J
Johannes Rieken 已提交
208
				const result: Promise<any> = exists ? Promise.resolve(null) : this.fileService.updateContent(this.resource, this.getEmptyContent(EOL), { encoding: 'utf8' });
209 210 211 212
				return result.then(() => this.textModelResolverService.createModelReference(this.resource));
			});
	}

J
Johannes Rieken 已提交
213
	private resolveAndValidate(): Promise<IReference<ITextEditorModel>> {
214 215 216

		// Target cannot be dirty if not writing into buffer
		if (this.textFileService.isDirty(this.resource)) {
217
			return Promise.reject(new Error(localize('errorKeybindingsFileDirty', "Unable to write because the keybindings configuration file is dirty. Please save it first and then try again.")));
218 219 220 221 222
		}

		return this.resolveModelReference()
			.then(reference => {
				const model = reference.object.textEditorModel;
S
Sandeep Somavarapu 已提交
223 224 225 226
				const EOL = model.getEOL();
				if (model.getValue()) {
					const parsed = this.parse(model);
					if (parsed.parseErrors.length) {
J
Johannes Rieken 已提交
227
						return Promise.reject<any>(new Error(localize('parseErrors', "Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again.")));
S
Sandeep Somavarapu 已提交
228 229 230
					}
					if (parsed.result) {
						if (!isArray(parsed.result)) {
J
Johannes Rieken 已提交
231
							return Promise.reject<any>(new Error(localize('errorInvalidConfiguration', "Unable to write to the keybindings configuration file. It has an object which is not of type Array. Please open the file to clean up and try again.")));
S
Sandeep Somavarapu 已提交
232 233 234 235 236 237
						}
					} else {
						const content = EOL + '[]';
						this.applyEditsToBuffer({ content, length: content.length, offset: model.getValue().length }, model);
					}
				} else {
238
					const content = this.getEmptyContent(EOL);
S
Sandeep Somavarapu 已提交
239
					this.applyEditsToBuffer({ content, length: content.length, offset: 0 }, model);
240 241 242 243 244
				}
				return reference;
			});
	}

A
Alex Dima 已提交
245
	private parse(model: ITextModel): { result: IUserFriendlyKeybinding[], parseErrors: json.ParseError[] } {
246
		const parseErrors: json.ParseError[] = [];
247
		const result = json.parse(model.getValue(), parseErrors);
S
Sandeep Somavarapu 已提交
248
		return { result, parseErrors };
249
	}
250 251

	private getEmptyContent(EOL: string): string {
252
		return '// ' + localize('emptyKeybindingsHeader', "Place your key bindings in this file to override the defaults") + EOL + '[]';
253
	}
254
}
255 256

registerSingleton(IKeybindingEditingService, KeybindingsEditingService, true);