abstractKeybindingService.js 11 KB

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