Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.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 * as nls from '../../../../nls.js';
7
import { RunOnceScheduler } from '../../../../base/common/async.js';
8
import { MarkdownString } from '../../../../base/common/htmlContent.js';
9
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
10
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
11
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
12
import { Range } from '../../../../editor/common/core/range.js';
13
import { registerEditorContribution, EditorContributionInstantiation } from '../../../../editor/browser/editorExtensions.js';
14
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
15
import { SnippetController2 } from '../../../../editor/contrib/snippet/browser/snippetController2.js';
16
import { SmartSnippetInserter } from '../common/smartSnippetInserter.js';
17
import { DefineKeybindingOverlayWidget } from './keybindingWidgets.js';
18
import { parseTree, Node } from '../../../../base/common/json.js';
19
import { WindowsNativeResolvedKeybinding } from '../../../services/keybinding/common/windowsKeyboardMapper.js';
20
import { themeColorFromId } from '../../../../platform/theme/common/themeService.js';
21
import { ThemeColor } from '../../../../base/common/themables.js';
22
import { overviewRulerInfo, overviewRulerError } from '../../../../editor/common/core/editorColorRegistry.js';
23
import { IModelDeltaDecoration, ITextModel, TrackedRangeStickiness, OverviewRulerLane } from '../../../../editor/common/model.js';
24
import { KeybindingParser } from '../../../../base/common/keybindingParser.js';
25
import { assertReturnsDefined } from '../../../../base/common/types.js';
26
import { isEqual } from '../../../../base/common/resources.js';
27
import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
28
import { DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, IDefineKeybindingEditorContribution } from '../../../services/preferences/common/preferences.js';
29
import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js';
30
31
const NLS_KB_LAYOUT_ERROR_MESSAGE = nls.localize('defineKeybinding.kbLayoutErrorMessage', "You won't be able to produce this key combination under your current keyboard layout.");
32
33
class DefineKeybindingEditorContribution extends Disposable implements IDefineKeybindingEditorContribution {
34
35
private readonly _keybindingDecorationRenderer = this._register(new MutableDisposable<KeybindingEditorDecorationsRenderer>());
36
37
private readonly _defineWidget: DefineKeybindingOverlayWidget;
38
39
constructor(
40
private _editor: ICodeEditor,
41
@IInstantiationService private readonly _instantiationService: IInstantiationService,
42
@IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService
43
) {
44
super();
45
46
this._defineWidget = this._register(this._instantiationService.createInstance(DefineKeybindingOverlayWidget, this._editor));
47
this._register(this._editor.onDidChangeModel(e => this._update()));
48
this._update();
49
}
50
51
private _update(): void {
52
this._keybindingDecorationRenderer.value = isInterestingEditorModel(this._editor, this._userDataProfileService)
53
// Decorations are shown for the default keybindings.json **and** for the user keybindings.json
54
? this._instantiationService.createInstance(KeybindingEditorDecorationsRenderer, this._editor)
55
: undefined;
56
}
57
58
showDefineKeybindingWidget(): void {
59
if (isInterestingEditorModel(this._editor, this._userDataProfileService)) {
60
this._defineWidget.start().then(keybinding => this._onAccepted(keybinding));
61
}
62
}
63
64
private _onAccepted(keybinding: string | null): void {
65
this._editor.focus();
66
if (keybinding && this._editor.hasModel()) {
67
const regexp = new RegExp(/\\/g);
68
const backslash = regexp.test(keybinding);
69
if (backslash) {
70
keybinding = keybinding.slice(0, -1) + '\\\\';
71
}
72
let snippetText = [
73
'{',
74
'\t"key": ' + JSON.stringify(keybinding) + ',',
75
'\t"command": "${1:commandId}",',
76
'\t"when": "${2:editorTextFocus}"',
77
'}$0'
78
].join('\n');
79
80
const smartInsertInfo = SmartSnippetInserter.insertSnippet(this._editor.getModel(), this._editor.getPosition());
81
snippetText = smartInsertInfo.prepend + snippetText + smartInsertInfo.append;
82
this._editor.setPosition(smartInsertInfo.position);
83
84
SnippetController2.get(this._editor)?.insert(snippetText, { overwriteBefore: 0, overwriteAfter: 0 });
85
}
86
}
87
}
88
89
export class KeybindingEditorDecorationsRenderer extends Disposable {
90
91
private _updateDecorations: RunOnceScheduler;
92
private readonly _dec: IEditorDecorationsCollection;
93
94
constructor(
95
private _editor: ICodeEditor,
96
@IKeybindingService private readonly _keybindingService: IKeybindingService,
97
) {
98
super();
99
this._dec = this._editor.createDecorationsCollection();
100
101
this._updateDecorations = this._register(new RunOnceScheduler(() => this._updateDecorationsNow(), 500));
102
103
const model = assertReturnsDefined(this._editor.getModel());
104
this._register(model.onDidChangeContent(() => this._updateDecorations.schedule()));
105
this._register(this._keybindingService.onDidUpdateKeybindings(() => this._updateDecorations.schedule()));
106
this._register({
107
dispose: () => {
108
this._dec.clear();
109
this._updateDecorations.cancel();
110
}
111
});
112
this._updateDecorations.schedule();
113
}
114
115
private _updateDecorationsNow(): void {
116
const model = assertReturnsDefined(this._editor.getModel());
117
118
const newDecorations: IModelDeltaDecoration[] = [];
119
120
const root = parseTree(model.getValue());
121
if (root && Array.isArray(root.children)) {
122
for (let i = 0, len = root.children.length; i < len; i++) {
123
const entry = root.children[i];
124
const dec = this._getDecorationForEntry(model, entry);
125
if (dec !== null) {
126
newDecorations.push(dec);
127
}
128
}
129
}
130
131
this._dec.set(newDecorations);
132
}
133
134
private _getDecorationForEntry(model: ITextModel, entry: Node): IModelDeltaDecoration | null {
135
if (!Array.isArray(entry.children)) {
136
return null;
137
}
138
for (let i = 0, len = entry.children.length; i < len; i++) {
139
const prop = entry.children[i];
140
if (prop.type !== 'property') {
141
continue;
142
}
143
if (!Array.isArray(prop.children) || prop.children.length !== 2) {
144
continue;
145
}
146
const key = prop.children[0];
147
if (key.value !== 'key') {
148
continue;
149
}
150
const value = prop.children[1];
151
if (value.type !== 'string') {
152
continue;
153
}
154
155
const resolvedKeybindings = this._keybindingService.resolveUserBinding(value.value);
156
if (resolvedKeybindings.length === 0) {
157
return this._createDecoration(true, null, null, model, value);
158
}
159
const resolvedKeybinding = resolvedKeybindings[0];
160
let usLabel: string | null = null;
161
if (resolvedKeybinding instanceof WindowsNativeResolvedKeybinding) {
162
usLabel = resolvedKeybinding.getUSLabel();
163
}
164
if (!resolvedKeybinding.isWYSIWYG()) {
165
const uiLabel = resolvedKeybinding.getLabel();
166
if (typeof uiLabel === 'string' && value.value.toLowerCase() === uiLabel.toLowerCase()) {
167
// coincidentally, this is actually WYSIWYG
168
return null;
169
}
170
return this._createDecoration(false, resolvedKeybinding.getLabel(), usLabel, model, value);
171
}
172
if (/abnt_|oem_/.test(value.value)) {
173
return this._createDecoration(false, resolvedKeybinding.getLabel(), usLabel, model, value);
174
}
175
const expectedUserSettingsLabel = resolvedKeybinding.getUserSettingsLabel();
176
if (typeof expectedUserSettingsLabel === 'string' && !KeybindingEditorDecorationsRenderer._userSettingsFuzzyEquals(value.value, expectedUserSettingsLabel)) {
177
return this._createDecoration(false, resolvedKeybinding.getLabel(), usLabel, model, value);
178
}
179
return null;
180
}
181
return null;
182
}
183
184
static _userSettingsFuzzyEquals(a: string, b: string): boolean {
185
a = a.trim().toLowerCase();
186
b = b.trim().toLowerCase();
187
188
if (a === b) {
189
return true;
190
}
191
192
const aKeybinding = KeybindingParser.parseKeybinding(a);
193
const bKeybinding = KeybindingParser.parseKeybinding(b);
194
if (aKeybinding === null && bKeybinding === null) {
195
return true;
196
}
197
if (!aKeybinding || !bKeybinding) {
198
return false;
199
}
200
return aKeybinding.equals(bKeybinding);
201
}
202
203
private _createDecoration(isError: boolean, uiLabel: string | null, usLabel: string | null, model: ITextModel, keyNode: Node): IModelDeltaDecoration {
204
let msg: MarkdownString;
205
let className: string;
206
let overviewRulerColor: ThemeColor;
207
208
if (isError) {
209
// this is the error case
210
msg = new MarkdownString().appendText(NLS_KB_LAYOUT_ERROR_MESSAGE);
211
className = 'keybindingError';
212
overviewRulerColor = themeColorFromId(overviewRulerError);
213
} else {
214
// this is the info case
215
if (usLabel && uiLabel !== usLabel) {
216
msg = new MarkdownString(
217
nls.localize({
218
key: 'defineKeybinding.kbLayoutLocalAndUSMessage',
219
comment: [
220
'Please translate maintaining the stars (*) around the placeholders such that they will be rendered in bold.',
221
'The placeholders will contain a keyboard combination e.g. Ctrl+Shift+/'
222
]
223
}, "**{0}** for your current keyboard layout (**{1}** for US standard).", uiLabel, usLabel)
224
);
225
} else {
226
msg = new MarkdownString(
227
nls.localize({
228
key: 'defineKeybinding.kbLayoutLocalMessage',
229
comment: [
230
'Please translate maintaining the stars (*) around the placeholder such that it will be rendered in bold.',
231
'The placeholder will contain a keyboard combination e.g. Ctrl+Shift+/'
232
]
233
}, "**{0}** for your current keyboard layout.", uiLabel)
234
);
235
}
236
className = 'keybindingInfo';
237
overviewRulerColor = themeColorFromId(overviewRulerInfo);
238
}
239
240
const startPosition = model.getPositionAt(keyNode.offset);
241
const endPosition = model.getPositionAt(keyNode.offset + keyNode.length);
242
const range = new Range(
243
startPosition.lineNumber, startPosition.column,
244
endPosition.lineNumber, endPosition.column
245
);
246
247
// icon + highlight + message decoration
248
return {
249
range: range,
250
options: {
251
description: 'keybindings-widget',
252
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
253
className: className,
254
hoverMessage: msg,
255
overviewRuler: {
256
color: overviewRulerColor,
257
position: OverviewRulerLane.Right
258
}
259
}
260
};
261
}
262
263
}
264
265
function isInterestingEditorModel(editor: ICodeEditor, userDataProfileService: IUserDataProfileService): boolean {
266
const model = editor.getModel();
267
if (!model) {
268
return false;
269
}
270
return isEqual(model.uri, userDataProfileService.currentProfile.keybindingsResource);
271
}
272
273
registerEditorContribution(DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, DefineKeybindingEditorContribution, EditorContributionInstantiation.AfterFirstRender);
274
275