90ccbc083c75e0758a0ddfddd92d9996efe8779e50c91548591521d48bd2d9cd58e2c121de04240ffeac67b35777268053753d6e35eaae0a01ee68b4cea4a9 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  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. import { IntervalTimer, TimeoutTimer } from '../../../base/common/async.js';
  6. import { Emitter, Event } from '../../../base/common/event.js';
  7. import { Disposable } from '../../../base/common/lifecycle.js';
  8. import * as nls from '../../../nls.js';
  9. const HIGH_FREQ_COMMANDS = /^(cursor|delete)/;
  10. export class AbstractKeybindingService extends Disposable {
  11. constructor(_contextKeyService, _commandService, _telemetryService, _notificationService, _logService) {
  12. super();
  13. this._contextKeyService = _contextKeyService;
  14. this._commandService = _commandService;
  15. this._telemetryService = _telemetryService;
  16. this._notificationService = _notificationService;
  17. this._logService = _logService;
  18. this._onDidUpdateKeybindings = this._register(new Emitter());
  19. this._currentChord = null;
  20. this._currentChordChecker = new IntervalTimer();
  21. this._currentChordStatusMessage = null;
  22. this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY;
  23. this._currentSingleModifier = null;
  24. this._currentSingleModifierClearTimeout = new TimeoutTimer();
  25. this._logging = false;
  26. }
  27. get onDidUpdateKeybindings() {
  28. return this._onDidUpdateKeybindings ? this._onDidUpdateKeybindings.event : Event.None; // Sinon stubbing walks properties on prototype
  29. }
  30. dispose() {
  31. super.dispose();
  32. }
  33. _log(str) {
  34. if (this._logging) {
  35. this._logService.info(`[KeybindingService]: ${str}`);
  36. }
  37. }
  38. getKeybindings() {
  39. return this._getResolver().getKeybindings();
  40. }
  41. lookupKeybinding(commandId, context) {
  42. const result = this._getResolver().lookupPrimaryKeybinding(commandId, context || this._contextKeyService);
  43. if (!result) {
  44. return undefined;
  45. }
  46. return result.resolvedKeybinding;
  47. }
  48. dispatchEvent(e, target) {
  49. return this._dispatch(e, target);
  50. }
  51. softDispatch(e, target) {
  52. this._log(`/ Soft dispatching keyboard event`);
  53. const keybinding = this.resolveKeyboardEvent(e);
  54. if (keybinding.isChord()) {
  55. console.warn('Unexpected keyboard event mapped to a chord');
  56. return null;
  57. }
  58. const [firstPart,] = keybinding.getDispatchParts();
  59. if (firstPart === null) {
  60. // cannot be dispatched, probably only modifier keys
  61. this._log(`\\ Keyboard event cannot be dispatched`);
  62. return null;
  63. }
  64. const contextValue = this._contextKeyService.getContext(target);
  65. const currentChord = this._currentChord ? this._currentChord.keypress : null;
  66. return this._getResolver().resolve(contextValue, currentChord, firstPart);
  67. }
  68. _enterChordMode(firstPart, keypressLabel) {
  69. this._currentChord = {
  70. keypress: firstPart,
  71. label: keypressLabel
  72. };
  73. this._currentChordStatusMessage = this._notificationService.status(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
  74. const chordEnterTime = Date.now();
  75. this._currentChordChecker.cancelAndSet(() => {
  76. if (!this._documentHasFocus()) {
  77. // Focus has been lost => leave chord mode
  78. this._leaveChordMode();
  79. return;
  80. }
  81. if (Date.now() - chordEnterTime > 5000) {
  82. // 5 seconds elapsed => leave chord mode
  83. this._leaveChordMode();
  84. }
  85. }, 500);
  86. }
  87. _leaveChordMode() {
  88. if (this._currentChordStatusMessage) {
  89. this._currentChordStatusMessage.dispose();
  90. this._currentChordStatusMessage = null;
  91. }
  92. this._currentChordChecker.cancel();
  93. this._currentChord = null;
  94. }
  95. _dispatch(e, target) {
  96. return this._doDispatch(this.resolveKeyboardEvent(e), target, /*isSingleModiferChord*/ false);
  97. }
  98. _singleModifierDispatch(e, target) {
  99. const keybinding = this.resolveKeyboardEvent(e);
  100. const [singleModifier,] = keybinding.getSingleModifierDispatchParts();
  101. if (singleModifier) {
  102. if (this._ignoreSingleModifiers.has(singleModifier)) {
  103. this._log(`+ Ignoring single modifier ${singleModifier} due to it being pressed together with other keys.`);
  104. this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY;
  105. this._currentSingleModifierClearTimeout.cancel();
  106. this._currentSingleModifier = null;
  107. return false;
  108. }
  109. this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY;
  110. if (this._currentSingleModifier === null) {
  111. // we have a valid `singleModifier`, store it for the next keyup, but clear it in 300ms
  112. this._log(`+ Storing single modifier for possible chord ${singleModifier}.`);
  113. this._currentSingleModifier = singleModifier;
  114. this._currentSingleModifierClearTimeout.cancelAndSet(() => {
  115. this._log(`+ Clearing single modifier due to 300ms elapsed.`);
  116. this._currentSingleModifier = null;
  117. }, 300);
  118. return false;
  119. }
  120. if (singleModifier === this._currentSingleModifier) {
  121. // bingo!
  122. this._log(`/ Dispatching single modifier chord ${singleModifier} ${singleModifier}`);
  123. this._currentSingleModifierClearTimeout.cancel();
  124. this._currentSingleModifier = null;
  125. return this._doDispatch(keybinding, target, /*isSingleModiferChord*/ true);
  126. }
  127. this._log(`+ Clearing single modifier due to modifier mismatch: ${this._currentSingleModifier} ${singleModifier}`);
  128. this._currentSingleModifierClearTimeout.cancel();
  129. this._currentSingleModifier = null;
  130. return false;
  131. }
  132. // When pressing a modifier and holding it pressed with any other modifier or key combination,
  133. // the pressed modifiers should no longer be considered for single modifier dispatch.
  134. const [firstPart,] = keybinding.getParts();
  135. this._ignoreSingleModifiers = new KeybindingModifierSet(firstPart);
  136. if (this._currentSingleModifier !== null) {
  137. this._log(`+ Clearing single modifier due to other key up.`);
  138. }
  139. this._currentSingleModifierClearTimeout.cancel();
  140. this._currentSingleModifier = null;
  141. return false;
  142. }
  143. _doDispatch(keybinding, target, isSingleModiferChord = false) {
  144. let shouldPreventDefault = false;
  145. if (keybinding.isChord()) {
  146. console.warn('Unexpected keyboard event mapped to a chord');
  147. return false;
  148. }
  149. let firstPart = null; // the first keybinding i.e. Ctrl+K
  150. let currentChord = null; // the "second" keybinding i.e. Ctrl+K "Ctrl+D"
  151. if (isSingleModiferChord) {
  152. const [dispatchKeyname,] = keybinding.getSingleModifierDispatchParts();
  153. firstPart = dispatchKeyname;
  154. currentChord = dispatchKeyname;
  155. }
  156. else {
  157. [firstPart,] = keybinding.getDispatchParts();
  158. currentChord = this._currentChord ? this._currentChord.keypress : null;
  159. }
  160. if (firstPart === null) {
  161. this._log(`\\ Keyboard event cannot be dispatched in keydown phase.`);
  162. // cannot be dispatched, probably only modifier keys
  163. return shouldPreventDefault;
  164. }
  165. const contextValue = this._contextKeyService.getContext(target);
  166. const keypressLabel = keybinding.getLabel();
  167. const resolveResult = this._getResolver().resolve(contextValue, currentChord, firstPart);
  168. this._logService.trace('KeybindingService#dispatch', keypressLabel, resolveResult === null || resolveResult === void 0 ? void 0 : resolveResult.commandId);
  169. if (resolveResult && resolveResult.enterChord) {
  170. shouldPreventDefault = true;
  171. this._enterChordMode(firstPart, keypressLabel);
  172. this._log(`+ Entering chord mode...`);
  173. return shouldPreventDefault;
  174. }
  175. if (this._currentChord) {
  176. if (!resolveResult || !resolveResult.commandId) {
  177. this._log(`+ Leaving chord mode: Nothing bound to "${this._currentChord.label} ${keypressLabel}".`);
  178. this._notificationService.status(nls.localize('missing.chord', "The key combination ({0}, {1}) is not a command.", this._currentChord.label, keypressLabel), { hideAfter: 10 * 1000 /* 10s */ });
  179. shouldPreventDefault = true;
  180. }
  181. }
  182. this._leaveChordMode();
  183. if (resolveResult && resolveResult.commandId) {
  184. if (!resolveResult.bubble) {
  185. shouldPreventDefault = true;
  186. }
  187. this._log(`+ Invoking command ${resolveResult.commandId}.`);
  188. if (typeof resolveResult.commandArgs === 'undefined') {
  189. this._commandService.executeCommand(resolveResult.commandId).then(undefined, err => this._notificationService.warn(err));
  190. }
  191. else {
  192. this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs).then(undefined, err => this._notificationService.warn(err));
  193. }
  194. if (!HIGH_FREQ_COMMANDS.test(resolveResult.commandId)) {
  195. this._telemetryService.publicLog2('workbenchActionExecuted', { id: resolveResult.commandId, from: 'keybinding' });
  196. }
  197. }
  198. return shouldPreventDefault;
  199. }
  200. mightProducePrintableCharacter(event) {
  201. if (event.ctrlKey || event.metaKey) {
  202. // ignore ctrl/cmd-combination but not shift/alt-combinatios
  203. return false;
  204. }
  205. // weak check for certain ranges. this is properly implemented in a subclass
  206. // with access to the KeyboardMapperFactory.
  207. if ((event.keyCode >= 31 /* KeyCode.KeyA */ && event.keyCode <= 56 /* KeyCode.KeyZ */)
  208. || (event.keyCode >= 21 /* KeyCode.Digit0 */ && event.keyCode <= 30 /* KeyCode.Digit9 */)) {
  209. return true;
  210. }
  211. return false;
  212. }
  213. }
  214. class KeybindingModifierSet {
  215. constructor(source) {
  216. this._ctrlKey = source ? source.ctrlKey : false;
  217. this._shiftKey = source ? source.shiftKey : false;
  218. this._altKey = source ? source.altKey : false;
  219. this._metaKey = source ? source.metaKey : false;
  220. }
  221. has(modifier) {
  222. switch (modifier) {
  223. case 'ctrl': return this._ctrlKey;
  224. case 'shift': return this._shiftKey;
  225. case 'alt': return this._altKey;
  226. case 'meta': return this._metaKey;
  227. }
  228. }
  229. }
  230. KeybindingModifierSet.EMPTY = new KeybindingModifierSet(null);