Path: blob/main/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.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 * as nls from '../../../../nls.js';6import { RunOnceScheduler } from '../../../../base/common/async.js';7import { MarkdownString } from '../../../../base/common/htmlContent.js';8import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';9import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';10import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';11import { Range } from '../../../../editor/common/core/range.js';12import { registerEditorContribution, EditorContributionInstantiation } from '../../../../editor/browser/editorExtensions.js';13import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';14import { SnippetController2 } from '../../../../editor/contrib/snippet/browser/snippetController2.js';15import { SmartSnippetInserter } from '../common/smartSnippetInserter.js';16import { DefineKeybindingOverlayWidget } from './keybindingWidgets.js';17import { parseTree, Node } from '../../../../base/common/json.js';18import { WindowsNativeResolvedKeybinding } from '../../../services/keybinding/common/windowsKeyboardMapper.js';19import { themeColorFromId } from '../../../../platform/theme/common/themeService.js';20import { ThemeColor } from '../../../../base/common/themables.js';21import { overviewRulerInfo, overviewRulerError } from '../../../../editor/common/core/editorColorRegistry.js';22import { IModelDeltaDecoration, ITextModel, TrackedRangeStickiness, OverviewRulerLane } from '../../../../editor/common/model.js';23import { KeybindingParser } from '../../../../base/common/keybindingParser.js';24import { assertReturnsDefined } from '../../../../base/common/types.js';25import { isEqual } from '../../../../base/common/resources.js';26import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';27import { DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, IDefineKeybindingEditorContribution } from '../../../services/preferences/common/preferences.js';28import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js';2930const NLS_KB_LAYOUT_ERROR_MESSAGE = nls.localize('defineKeybinding.kbLayoutErrorMessage', "You won't be able to produce this key combination under your current keyboard layout.");3132class DefineKeybindingEditorContribution extends Disposable implements IDefineKeybindingEditorContribution {3334private readonly _keybindingDecorationRenderer = this._register(new MutableDisposable<KeybindingEditorDecorationsRenderer>());3536private readonly _defineWidget: DefineKeybindingOverlayWidget;3738constructor(39private _editor: ICodeEditor,40@IInstantiationService private readonly _instantiationService: IInstantiationService,41@IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService42) {43super();4445this._defineWidget = this._register(this._instantiationService.createInstance(DefineKeybindingOverlayWidget, this._editor));46this._register(this._editor.onDidChangeModel(e => this._update()));47this._update();48}4950private _update(): void {51this._keybindingDecorationRenderer.value = isInterestingEditorModel(this._editor, this._userDataProfileService)52// Decorations are shown for the default keybindings.json **and** for the user keybindings.json53? this._instantiationService.createInstance(KeybindingEditorDecorationsRenderer, this._editor)54: undefined;55}5657showDefineKeybindingWidget(): void {58if (isInterestingEditorModel(this._editor, this._userDataProfileService)) {59this._defineWidget.start().then(keybinding => this._onAccepted(keybinding));60}61}6263private _onAccepted(keybinding: string | null): void {64this._editor.focus();65if (keybinding && this._editor.hasModel()) {66const regexp = new RegExp(/\\/g);67const backslash = regexp.test(keybinding);68if (backslash) {69keybinding = keybinding.slice(0, -1) + '\\\\';70}71let snippetText = [72'{',73'\t"key": ' + JSON.stringify(keybinding) + ',',74'\t"command": "${1:commandId}",',75'\t"when": "${2:editorTextFocus}"',76'}$0'77].join('\n');7879const smartInsertInfo = SmartSnippetInserter.insertSnippet(this._editor.getModel(), this._editor.getPosition());80snippetText = smartInsertInfo.prepend + snippetText + smartInsertInfo.append;81this._editor.setPosition(smartInsertInfo.position);8283SnippetController2.get(this._editor)?.insert(snippetText, { overwriteBefore: 0, overwriteAfter: 0 });84}85}86}8788export class KeybindingEditorDecorationsRenderer extends Disposable {8990private _updateDecorations: RunOnceScheduler;91private readonly _dec: IEditorDecorationsCollection;9293constructor(94private _editor: ICodeEditor,95@IKeybindingService private readonly _keybindingService: IKeybindingService,96) {97super();98this._dec = this._editor.createDecorationsCollection();99100this._updateDecorations = this._register(new RunOnceScheduler(() => this._updateDecorationsNow(), 500));101102const model = assertReturnsDefined(this._editor.getModel());103this._register(model.onDidChangeContent(() => this._updateDecorations.schedule()));104this._register(this._keybindingService.onDidUpdateKeybindings(() => this._updateDecorations.schedule()));105this._register({106dispose: () => {107this._dec.clear();108this._updateDecorations.cancel();109}110});111this._updateDecorations.schedule();112}113114private _updateDecorationsNow(): void {115const model = assertReturnsDefined(this._editor.getModel());116117const newDecorations: IModelDeltaDecoration[] = [];118119const root = parseTree(model.getValue());120if (root && Array.isArray(root.children)) {121for (let i = 0, len = root.children.length; i < len; i++) {122const entry = root.children[i];123const dec = this._getDecorationForEntry(model, entry);124if (dec !== null) {125newDecorations.push(dec);126}127}128}129130this._dec.set(newDecorations);131}132133private _getDecorationForEntry(model: ITextModel, entry: Node): IModelDeltaDecoration | null {134if (!Array.isArray(entry.children)) {135return null;136}137for (let i = 0, len = entry.children.length; i < len; i++) {138const prop = entry.children[i];139if (prop.type !== 'property') {140continue;141}142if (!Array.isArray(prop.children) || prop.children.length !== 2) {143continue;144}145const key = prop.children[0];146if (key.value !== 'key') {147continue;148}149const value = prop.children[1];150if (value.type !== 'string') {151continue;152}153154const resolvedKeybindings = this._keybindingService.resolveUserBinding(value.value);155if (resolvedKeybindings.length === 0) {156return this._createDecoration(true, null, null, model, value);157}158const resolvedKeybinding = resolvedKeybindings[0];159let usLabel: string | null = null;160if (resolvedKeybinding instanceof WindowsNativeResolvedKeybinding) {161usLabel = resolvedKeybinding.getUSLabel();162}163if (!resolvedKeybinding.isWYSIWYG()) {164const uiLabel = resolvedKeybinding.getLabel();165if (typeof uiLabel === 'string' && value.value.toLowerCase() === uiLabel.toLowerCase()) {166// coincidentally, this is actually WYSIWYG167return null;168}169return this._createDecoration(false, resolvedKeybinding.getLabel(), usLabel, model, value);170}171if (/abnt_|oem_/.test(value.value)) {172return this._createDecoration(false, resolvedKeybinding.getLabel(), usLabel, model, value);173}174const expectedUserSettingsLabel = resolvedKeybinding.getUserSettingsLabel();175if (typeof expectedUserSettingsLabel === 'string' && !KeybindingEditorDecorationsRenderer._userSettingsFuzzyEquals(value.value, expectedUserSettingsLabel)) {176return this._createDecoration(false, resolvedKeybinding.getLabel(), usLabel, model, value);177}178return null;179}180return null;181}182183static _userSettingsFuzzyEquals(a: string, b: string): boolean {184a = a.trim().toLowerCase();185b = b.trim().toLowerCase();186187if (a === b) {188return true;189}190191const aKeybinding = KeybindingParser.parseKeybinding(a);192const bKeybinding = KeybindingParser.parseKeybinding(b);193if (aKeybinding === null && bKeybinding === null) {194return true;195}196if (!aKeybinding || !bKeybinding) {197return false;198}199return aKeybinding.equals(bKeybinding);200}201202private _createDecoration(isError: boolean, uiLabel: string | null, usLabel: string | null, model: ITextModel, keyNode: Node): IModelDeltaDecoration {203let msg: MarkdownString;204let className: string;205let overviewRulerColor: ThemeColor;206207if (isError) {208// this is the error case209msg = new MarkdownString().appendText(NLS_KB_LAYOUT_ERROR_MESSAGE);210className = 'keybindingError';211overviewRulerColor = themeColorFromId(overviewRulerError);212} else {213// this is the info case214if (usLabel && uiLabel !== usLabel) {215msg = new MarkdownString(216nls.localize({217key: 'defineKeybinding.kbLayoutLocalAndUSMessage',218comment: [219'Please translate maintaining the stars (*) around the placeholders such that they will be rendered in bold.',220'The placeholders will contain a keyboard combination e.g. Ctrl+Shift+/'221]222}, "**{0}** for your current keyboard layout (**{1}** for US standard).", uiLabel, usLabel)223);224} else {225msg = new MarkdownString(226nls.localize({227key: 'defineKeybinding.kbLayoutLocalMessage',228comment: [229'Please translate maintaining the stars (*) around the placeholder such that it will be rendered in bold.',230'The placeholder will contain a keyboard combination e.g. Ctrl+Shift+/'231]232}, "**{0}** for your current keyboard layout.", uiLabel)233);234}235className = 'keybindingInfo';236overviewRulerColor = themeColorFromId(overviewRulerInfo);237}238239const startPosition = model.getPositionAt(keyNode.offset);240const endPosition = model.getPositionAt(keyNode.offset + keyNode.length);241const range = new Range(242startPosition.lineNumber, startPosition.column,243endPosition.lineNumber, endPosition.column244);245246// icon + highlight + message decoration247return {248range: range,249options: {250description: 'keybindings-widget',251stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,252className: className,253hoverMessage: msg,254overviewRuler: {255color: overviewRulerColor,256position: OverviewRulerLane.Right257}258}259};260}261262}263264function isInterestingEditorModel(editor: ICodeEditor, userDataProfileService: IUserDataProfileService): boolean {265const model = editor.getModel();266if (!model) {267return false;268}269return isEqual(model.uri, userDataProfileService.currentProfile.keybindingsResource);270}271272registerEditorContribution(DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, DefineKeybindingEditorContribution, EditorContributionInstantiation.AfterFirstRender);273274275