Path: blob/main/src/vs/workbench/services/keybinding/common/keybindingEditing.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize } from '../../../../nls.js';6import { Queue } from '../../../../base/common/async.js';7import * as json from '../../../../base/common/json.js';8import * as objects from '../../../../base/common/objects.js';9import { setProperty } from '../../../../base/common/jsonEdit.js';10import { Edit } from '../../../../base/common/jsonFormatter.js';11import { Disposable, IReference } from '../../../../base/common/lifecycle.js';12import { EditOperation } from '../../../../editor/common/core/editOperation.js';13import { Range } from '../../../../editor/common/core/range.js';14import { Selection } from '../../../../editor/common/core/selection.js';15import { ITextModel } from '../../../../editor/common/model.js';16import { ITextModelService, IResolvedTextEditorModel } from '../../../../editor/common/services/resolverService.js';17import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';18import { IFileService } from '../../../../platform/files/common/files.js';19import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';20import { IUserFriendlyKeybinding } from '../../../../platform/keybinding/common/keybinding.js';21import { ResolvedKeybindingItem } from '../../../../platform/keybinding/common/resolvedKeybindingItem.js';22import { ITextFileService } from '../../textfile/common/textfiles.js';23import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';24import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js';2526export const IKeybindingEditingService = createDecorator<IKeybindingEditingService>('keybindingEditingService');2728export interface IKeybindingEditingService {2930readonly _serviceBrand: undefined;3132addKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void>;3334editKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void>;3536removeKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void>;3738resetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void>;39}4041export class KeybindingsEditingService extends Disposable implements IKeybindingEditingService {4243public _serviceBrand: undefined;44private queue: Queue<void>;4546constructor(47@ITextModelService private readonly textModelResolverService: ITextModelService,48@ITextFileService private readonly textFileService: ITextFileService,49@IFileService private readonly fileService: IFileService,50@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,51) {52super();53this.queue = new Queue<void>();54}5556addKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void> {57return this.queue.queue(() => this.doEditKeybinding(keybindingItem, key, when, true)); // queue up writes to prevent race conditions58}5960editKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void> {61return this.queue.queue(() => this.doEditKeybinding(keybindingItem, key, when, false)); // queue up writes to prevent race conditions62}6364resetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {65return this.queue.queue(() => this.doResetKeybinding(keybindingItem)); // queue up writes to prevent race conditions66}6768removeKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {69return this.queue.queue(() => this.doRemoveKeybinding(keybindingItem)); // queue up writes to prevent race conditions70}7172private async doEditKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined, add: boolean): Promise<void> {73const reference = await this.resolveAndValidate();74const model = reference.object.textEditorModel;75if (add) {76this.updateKeybinding(keybindingItem, key, when, model, -1);77} else {78const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());79const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);80this.updateKeybinding(keybindingItem, key, when, model, userKeybindingEntryIndex);81if (keybindingItem.isDefault && keybindingItem.resolvedKeybinding) {82this.removeDefaultKeybinding(keybindingItem, model);83}84}85try {86await this.save();87} finally {88reference.dispose();89}90}9192private async doRemoveKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {93const reference = await this.resolveAndValidate();94const model = reference.object.textEditorModel;95if (keybindingItem.isDefault) {96this.removeDefaultKeybinding(keybindingItem, model);97} else {98this.removeUserKeybinding(keybindingItem, model);99}100try {101return await this.save();102} finally {103reference.dispose();104}105}106107private async doResetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {108const reference = await this.resolveAndValidate();109const model = reference.object.textEditorModel;110if (!keybindingItem.isDefault) {111this.removeUserKeybinding(keybindingItem, model);112this.removeUnassignedDefaultKeybinding(keybindingItem, model);113}114try {115return await this.save();116} finally {117reference.dispose();118}119}120121private save(): Promise<any> {122return this.textFileService.save(this.userDataProfileService.currentProfile.keybindingsResource);123}124125private updateKeybinding(keybindingItem: ResolvedKeybindingItem, newKey: string, when: string | undefined, model: ITextModel, userKeybindingEntryIndex: number): void {126const { tabSize, insertSpaces } = model.getOptions();127const eol = model.getEOL();128if (userKeybindingEntryIndex !== -1) {129// Update the keybinding with new key130this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model);131const edits = setProperty(model.getValue(), [userKeybindingEntryIndex, 'when'], when, { tabSize, insertSpaces, eol });132if (edits.length > 0) {133this.applyEditsToBuffer(edits[0], model);134}135} else {136// Add the new keybinding with new key137this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(newKey, keybindingItem.command, when, false), { tabSize, insertSpaces, eol })[0], model);138}139}140141private removeUserKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {142const { tabSize, insertSpaces } = model.getOptions();143const eol = model.getEOL();144const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());145const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);146if (userKeybindingEntryIndex !== -1) {147this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex], undefined, { tabSize, insertSpaces, eol })[0], model);148}149}150151private removeDefaultKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {152const { tabSize, insertSpaces } = model.getOptions();153const eol = model.getEOL();154const key = keybindingItem.resolvedKeybinding ? keybindingItem.resolvedKeybinding.getUserSettingsLabel() : null;155if (key) {156const entry: IUserFriendlyKeybinding = this.asObject(key, keybindingItem.command, keybindingItem.when ? keybindingItem.when.serialize() : undefined, true);157const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());158if (userKeybindingEntries.every(e => !this.areSame(e, entry))) {159this.applyEditsToBuffer(setProperty(model.getValue(), [-1], entry, { tabSize, insertSpaces, eol })[0], model);160}161}162}163164private removeUnassignedDefaultKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {165const { tabSize, insertSpaces } = model.getOptions();166const eol = model.getEOL();167const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());168const indices = this.findUnassignedDefaultKeybindingEntryIndex(keybindingItem, userKeybindingEntries).reverse();169for (const index of indices) {170this.applyEditsToBuffer(setProperty(model.getValue(), [index], undefined, { tabSize, insertSpaces, eol })[0], model);171}172}173174private findUserKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number {175for (let index = 0; index < userKeybindingEntries.length; index++) {176const keybinding = userKeybindingEntries[index];177if (keybinding.command === keybindingItem.command) {178if (!keybinding.when && !keybindingItem.when) {179return index;180}181if (keybinding.when && keybindingItem.when) {182const contextKeyExpr = ContextKeyExpr.deserialize(keybinding.when);183if (contextKeyExpr && contextKeyExpr.serialize() === keybindingItem.when.serialize()) {184return index;185}186}187}188}189return -1;190}191192private findUnassignedDefaultKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number[] {193const indices: number[] = [];194for (let index = 0; index < userKeybindingEntries.length; index++) {195if (userKeybindingEntries[index].command === `-${keybindingItem.command}`) {196indices.push(index);197}198}199return indices;200}201202private asObject(key: string, command: string | null, when: string | undefined, negate: boolean): any {203const object: any = { key };204if (command) {205object['command'] = negate ? `-${command}` : command;206}207if (when) {208object['when'] = when;209}210return object;211}212213private areSame(a: IUserFriendlyKeybinding, b: IUserFriendlyKeybinding): boolean {214if (a.command !== b.command) {215return false;216}217if (a.key !== b.key) {218return false;219}220const whenA = ContextKeyExpr.deserialize(a.when);221const whenB = ContextKeyExpr.deserialize(b.when);222if ((whenA && !whenB) || (!whenA && whenB)) {223return false;224}225if (whenA && whenB && !whenA.equals(whenB)) {226return false;227}228if (!objects.equals(a.args, b.args)) {229return false;230}231return true;232}233234private applyEditsToBuffer(edit: Edit, model: ITextModel): void {235const startPosition = model.getPositionAt(edit.offset);236const endPosition = model.getPositionAt(edit.offset + edit.length);237const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);238const currentText = model.getValueInRange(range);239const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);240model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);241}242243private async resolveModelReference(): Promise<IReference<IResolvedTextEditorModel>> {244const exists = await this.fileService.exists(this.userDataProfileService.currentProfile.keybindingsResource);245if (!exists) {246await this.textFileService.write(this.userDataProfileService.currentProfile.keybindingsResource, this.getEmptyContent(), { encoding: 'utf8' });247}248return this.textModelResolverService.createModelReference(this.userDataProfileService.currentProfile.keybindingsResource);249}250251private async resolveAndValidate(): Promise<IReference<IResolvedTextEditorModel>> {252253// Target cannot be dirty if not writing into buffer254if (this.textFileService.isDirty(this.userDataProfileService.currentProfile.keybindingsResource)) {255throw new Error(localize('errorKeybindingsFileDirty', "Unable to write because the keybindings configuration file has unsaved changes. Please save it first and then try again."));256}257258const reference = await this.resolveModelReference();259const model = reference.object.textEditorModel;260const EOL = model.getEOL();261if (model.getValue()) {262const parsed = this.parse(model);263if (parsed.parseErrors.length) {264reference.dispose();265throw 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."));266}267if (parsed.result) {268if (!Array.isArray(parsed.result)) {269reference.dispose();270throw 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."));271}272} else {273const content = EOL + '[]';274this.applyEditsToBuffer({ content, length: content.length, offset: model.getValue().length }, model);275}276} else {277const content = this.getEmptyContent();278this.applyEditsToBuffer({ content, length: content.length, offset: 0 }, model);279}280return reference;281}282283private parse(model: ITextModel): { result: IUserFriendlyKeybinding[]; parseErrors: json.ParseError[] } {284const parseErrors: json.ParseError[] = [];285const result = json.parse(model.getValue(), parseErrors, { allowTrailingComma: true, allowEmptyContent: true });286return { result, parseErrors };287}288289private getEmptyContent(): string {290return '// ' + localize('emptyKeybindingsHeader', "Place your key bindings in this file to override the defaults") + '\n[\n]';291}292}293294registerSingleton(IKeybindingEditingService, KeybindingsEditingService, InstantiationType.Delayed);295296297