Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/keybinding/common/keybindingEditing.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize } from '../../../../nls.js';
7
import { Queue } from '../../../../base/common/async.js';
8
import * as json from '../../../../base/common/json.js';
9
import * as objects from '../../../../base/common/objects.js';
10
import { setProperty } from '../../../../base/common/jsonEdit.js';
11
import { Edit } from '../../../../base/common/jsonFormatter.js';
12
import { Disposable, IReference } from '../../../../base/common/lifecycle.js';
13
import { EditOperation } from '../../../../editor/common/core/editOperation.js';
14
import { Range } from '../../../../editor/common/core/range.js';
15
import { Selection } from '../../../../editor/common/core/selection.js';
16
import { ITextModel } from '../../../../editor/common/model.js';
17
import { ITextModelService, IResolvedTextEditorModel } from '../../../../editor/common/services/resolverService.js';
18
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
19
import { IFileService } from '../../../../platform/files/common/files.js';
20
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
21
import { IUserFriendlyKeybinding } from '../../../../platform/keybinding/common/keybinding.js';
22
import { ResolvedKeybindingItem } from '../../../../platform/keybinding/common/resolvedKeybindingItem.js';
23
import { ITextFileService } from '../../textfile/common/textfiles.js';
24
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
25
import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js';
26
27
export const IKeybindingEditingService = createDecorator<IKeybindingEditingService>('keybindingEditingService');
28
29
export interface IKeybindingEditingService {
30
31
readonly _serviceBrand: undefined;
32
33
addKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void>;
34
35
editKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void>;
36
37
removeKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void>;
38
39
resetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void>;
40
}
41
42
export class KeybindingsEditingService extends Disposable implements IKeybindingEditingService {
43
44
public _serviceBrand: undefined;
45
private queue: Queue<void>;
46
47
constructor(
48
@ITextModelService private readonly textModelResolverService: ITextModelService,
49
@ITextFileService private readonly textFileService: ITextFileService,
50
@IFileService private readonly fileService: IFileService,
51
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
52
) {
53
super();
54
this.queue = new Queue<void>();
55
}
56
57
addKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void> {
58
return this.queue.queue(() => this.doEditKeybinding(keybindingItem, key, when, true)); // queue up writes to prevent race conditions
59
}
60
61
editKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void> {
62
return this.queue.queue(() => this.doEditKeybinding(keybindingItem, key, when, false)); // queue up writes to prevent race conditions
63
}
64
65
resetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
66
return this.queue.queue(() => this.doResetKeybinding(keybindingItem)); // queue up writes to prevent race conditions
67
}
68
69
removeKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
70
return this.queue.queue(() => this.doRemoveKeybinding(keybindingItem)); // queue up writes to prevent race conditions
71
}
72
73
private async doEditKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined, add: boolean): Promise<void> {
74
const reference = await this.resolveAndValidate();
75
const model = reference.object.textEditorModel;
76
if (add) {
77
this.updateKeybinding(keybindingItem, key, when, model, -1);
78
} else {
79
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
80
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
81
this.updateKeybinding(keybindingItem, key, when, model, userKeybindingEntryIndex);
82
if (keybindingItem.isDefault && keybindingItem.resolvedKeybinding) {
83
this.removeDefaultKeybinding(keybindingItem, model);
84
}
85
}
86
try {
87
await this.save();
88
} finally {
89
reference.dispose();
90
}
91
}
92
93
private async doRemoveKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
94
const reference = await this.resolveAndValidate();
95
const model = reference.object.textEditorModel;
96
if (keybindingItem.isDefault) {
97
this.removeDefaultKeybinding(keybindingItem, model);
98
} else {
99
this.removeUserKeybinding(keybindingItem, model);
100
}
101
try {
102
return await this.save();
103
} finally {
104
reference.dispose();
105
}
106
}
107
108
private async doResetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
109
const reference = await this.resolveAndValidate();
110
const model = reference.object.textEditorModel;
111
if (!keybindingItem.isDefault) {
112
this.removeUserKeybinding(keybindingItem, model);
113
this.removeUnassignedDefaultKeybinding(keybindingItem, model);
114
}
115
try {
116
return await this.save();
117
} finally {
118
reference.dispose();
119
}
120
}
121
122
private save(): Promise<any> {
123
return this.textFileService.save(this.userDataProfileService.currentProfile.keybindingsResource);
124
}
125
126
private updateKeybinding(keybindingItem: ResolvedKeybindingItem, newKey: string, when: string | undefined, model: ITextModel, userKeybindingEntryIndex: number): void {
127
const { tabSize, insertSpaces } = model.getOptions();
128
const eol = model.getEOL();
129
if (userKeybindingEntryIndex !== -1) {
130
// Update the keybinding with new key
131
this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model);
132
const edits = setProperty(model.getValue(), [userKeybindingEntryIndex, 'when'], when, { tabSize, insertSpaces, eol });
133
if (edits.length > 0) {
134
this.applyEditsToBuffer(edits[0], model);
135
}
136
} else {
137
// Add the new keybinding with new key
138
this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(newKey, keybindingItem.command, when, false), { tabSize, insertSpaces, eol })[0], model);
139
}
140
}
141
142
private removeUserKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {
143
const { tabSize, insertSpaces } = model.getOptions();
144
const eol = model.getEOL();
145
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
146
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
147
if (userKeybindingEntryIndex !== -1) {
148
this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex], undefined, { tabSize, insertSpaces, eol })[0], model);
149
}
150
}
151
152
private removeDefaultKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {
153
const { tabSize, insertSpaces } = model.getOptions();
154
const eol = model.getEOL();
155
const key = keybindingItem.resolvedKeybinding ? keybindingItem.resolvedKeybinding.getUserSettingsLabel() : null;
156
if (key) {
157
const entry: IUserFriendlyKeybinding = this.asObject(key, keybindingItem.command, keybindingItem.when ? keybindingItem.when.serialize() : undefined, true);
158
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
159
if (userKeybindingEntries.every(e => !this.areSame(e, entry))) {
160
this.applyEditsToBuffer(setProperty(model.getValue(), [-1], entry, { tabSize, insertSpaces, eol })[0], model);
161
}
162
}
163
}
164
165
private removeUnassignedDefaultKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {
166
const { tabSize, insertSpaces } = model.getOptions();
167
const eol = model.getEOL();
168
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
169
const indices = this.findUnassignedDefaultKeybindingEntryIndex(keybindingItem, userKeybindingEntries).reverse();
170
for (const index of indices) {
171
this.applyEditsToBuffer(setProperty(model.getValue(), [index], undefined, { tabSize, insertSpaces, eol })[0], model);
172
}
173
}
174
175
private findUserKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number {
176
for (let index = 0; index < userKeybindingEntries.length; index++) {
177
const keybinding = userKeybindingEntries[index];
178
if (keybinding.command === keybindingItem.command) {
179
if (!keybinding.when && !keybindingItem.when) {
180
return index;
181
}
182
if (keybinding.when && keybindingItem.when) {
183
const contextKeyExpr = ContextKeyExpr.deserialize(keybinding.when);
184
if (contextKeyExpr && contextKeyExpr.serialize() === keybindingItem.when.serialize()) {
185
return index;
186
}
187
}
188
}
189
}
190
return -1;
191
}
192
193
private findUnassignedDefaultKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number[] {
194
const indices: number[] = [];
195
for (let index = 0; index < userKeybindingEntries.length; index++) {
196
if (userKeybindingEntries[index].command === `-${keybindingItem.command}`) {
197
indices.push(index);
198
}
199
}
200
return indices;
201
}
202
203
private asObject(key: string, command: string | null, when: string | undefined, negate: boolean): any {
204
const object: any = { key };
205
if (command) {
206
object['command'] = negate ? `-${command}` : command;
207
}
208
if (when) {
209
object['when'] = when;
210
}
211
return object;
212
}
213
214
private areSame(a: IUserFriendlyKeybinding, b: IUserFriendlyKeybinding): boolean {
215
if (a.command !== b.command) {
216
return false;
217
}
218
if (a.key !== b.key) {
219
return false;
220
}
221
const whenA = ContextKeyExpr.deserialize(a.when);
222
const whenB = ContextKeyExpr.deserialize(b.when);
223
if ((whenA && !whenB) || (!whenA && whenB)) {
224
return false;
225
}
226
if (whenA && whenB && !whenA.equals(whenB)) {
227
return false;
228
}
229
if (!objects.equals(a.args, b.args)) {
230
return false;
231
}
232
return true;
233
}
234
235
private applyEditsToBuffer(edit: Edit, model: ITextModel): void {
236
const startPosition = model.getPositionAt(edit.offset);
237
const endPosition = model.getPositionAt(edit.offset + edit.length);
238
const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
239
const currentText = model.getValueInRange(range);
240
const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
241
model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
242
}
243
244
private async resolveModelReference(): Promise<IReference<IResolvedTextEditorModel>> {
245
const exists = await this.fileService.exists(this.userDataProfileService.currentProfile.keybindingsResource);
246
if (!exists) {
247
await this.textFileService.write(this.userDataProfileService.currentProfile.keybindingsResource, this.getEmptyContent(), { encoding: 'utf8' });
248
}
249
return this.textModelResolverService.createModelReference(this.userDataProfileService.currentProfile.keybindingsResource);
250
}
251
252
private async resolveAndValidate(): Promise<IReference<IResolvedTextEditorModel>> {
253
254
// Target cannot be dirty if not writing into buffer
255
if (this.textFileService.isDirty(this.userDataProfileService.currentProfile.keybindingsResource)) {
256
throw new Error(localize('errorKeybindingsFileDirty', "Unable to write because the keybindings configuration file has unsaved changes. Please save it first and then try again."));
257
}
258
259
const reference = await this.resolveModelReference();
260
const model = reference.object.textEditorModel;
261
const EOL = model.getEOL();
262
if (model.getValue()) {
263
const parsed = this.parse(model);
264
if (parsed.parseErrors.length) {
265
reference.dispose();
266
throw 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."));
267
}
268
if (parsed.result) {
269
if (!Array.isArray(parsed.result)) {
270
reference.dispose();
271
throw 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."));
272
}
273
} else {
274
const content = EOL + '[]';
275
this.applyEditsToBuffer({ content, length: content.length, offset: model.getValue().length }, model);
276
}
277
} else {
278
const content = this.getEmptyContent();
279
this.applyEditsToBuffer({ content, length: content.length, offset: 0 }, model);
280
}
281
return reference;
282
}
283
284
private parse(model: ITextModel): { result: IUserFriendlyKeybinding[]; parseErrors: json.ParseError[] } {
285
const parseErrors: json.ParseError[] = [];
286
const result = json.parse(model.getValue(), parseErrors, { allowTrailingComma: true, allowEmptyContent: true });
287
return { result, parseErrors };
288
}
289
290
private getEmptyContent(): string {
291
return '// ' + localize('emptyKeybindingsHeader', "Place your key bindings in this file to override the defaults") + '\n[\n]';
292
}
293
}
294
295
registerSingleton(IKeybindingEditingService, KeybindingsEditingService, InstantiationType.Delayed);
296
297