You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

224 lines
5.3 KiB

  1. 'use strict';
  2. import { commands, Disposable } from 'vscode';
  3. import { CommandContext, extensionId, setCommandContext } from './constants';
  4. import { Logger } from './logger';
  5. import { log } from './system/decorators/log';
  6. export declare interface KeyCommand {
  7. onDidPressKey?(key: Keys): void | Promise<void>;
  8. }
  9. const keyNoopCommand = Object.create(null) as KeyCommand;
  10. export { keyNoopCommand as KeyNoopCommand };
  11. export const keys = [
  12. 'left',
  13. 'alt+left',
  14. 'ctrl+left',
  15. 'right',
  16. 'alt+right',
  17. 'ctrl+right',
  18. 'alt+,',
  19. 'alt+.',
  20. 'escape'
  21. ] as const;
  22. export type Keys = typeof keys[number];
  23. export type KeyMapping = { [K in Keys]?: KeyCommand | (() => Promise<KeyCommand>) };
  24. type IndexableKeyMapping = KeyMapping & {
  25. [index: string]: KeyCommand | (() => Promise<KeyCommand>) | undefined;
  26. };
  27. const mappings: KeyMapping[] = [];
  28. export class KeyboardScope implements Disposable {
  29. private readonly _mapping: IndexableKeyMapping;
  30. constructor(mapping: KeyMapping) {
  31. this._mapping = mapping;
  32. for (const key in this._mapping) {
  33. this._mapping[key] = this._mapping[key] || keyNoopCommand;
  34. }
  35. mappings.push(this._mapping);
  36. }
  37. @log({
  38. args: false,
  39. prefix: context => `${context.prefix}[${mappings.length}]`
  40. })
  41. async dispose() {
  42. const index = mappings.indexOf(this._mapping);
  43. const cc = Logger.getCorrelationContext();
  44. if (cc) {
  45. cc.exitDetails = ` \u2022 index=${index}`;
  46. }
  47. if (index === mappings.length - 1) {
  48. mappings.pop();
  49. await this.updateKeyCommandsContext(mappings[mappings.length - 1]);
  50. } else {
  51. mappings.splice(index, 1);
  52. }
  53. }
  54. private _paused = true;
  55. get paused() {
  56. return this._paused;
  57. }
  58. @log<KeyboardScope['clearKeyCommand']>({
  59. args: false,
  60. prefix: (context, key) => `${context.prefix}[${mappings.length}](${key})`
  61. })
  62. async clearKeyCommand(key: Keys) {
  63. const cc = Logger.getCorrelationContext();
  64. const mapping = mappings[mappings.length - 1];
  65. if (mapping !== this._mapping || !mapping[key]) {
  66. if (cc) {
  67. cc.exitDetails = ' \u2022 skipped';
  68. }
  69. return;
  70. }
  71. mapping[key] = undefined;
  72. await setCommandContext(`${CommandContext.Key}:${key}`, false);
  73. }
  74. @log({
  75. args: false,
  76. prefix: context => `${context.prefix}(paused=${context.instance._paused})`
  77. })
  78. async pause(keys?: Keys[]) {
  79. if (this._paused) return;
  80. this._paused = true;
  81. const mapping = (Object.keys(this._mapping) as Keys[]).reduce(
  82. (accumulator, key) => {
  83. accumulator[key] = keys === undefined ? false : keys.includes(key) ? false : this._mapping[key];
  84. return accumulator;
  85. },
  86. {} as any
  87. );
  88. await this.updateKeyCommandsContext(mapping);
  89. }
  90. @log({
  91. args: false,
  92. prefix: context => `${context.prefix}(paused=${context.instance._paused})`
  93. })
  94. async resume() {
  95. if (!this._paused) return;
  96. this._paused = false;
  97. await this.updateKeyCommandsContext(this._mapping);
  98. }
  99. async start() {
  100. await this.resume();
  101. }
  102. @log<KeyboardScope['setKeyCommand']>({
  103. args: false,
  104. prefix: (context, key) => `${context.prefix}[${mappings.length}](${key})`
  105. })
  106. async setKeyCommand(key: Keys, command: KeyCommand | (() => Promise<KeyCommand>)) {
  107. const cc = Logger.getCorrelationContext();
  108. const mapping = mappings[mappings.length - 1];
  109. if (mapping !== this._mapping) {
  110. if (cc) {
  111. cc.exitDetails = ' \u2022 skipped';
  112. }
  113. return;
  114. }
  115. const set = Boolean(mapping[key]);
  116. mapping[key] = command;
  117. if (!set) {
  118. await setCommandContext(`${CommandContext.Key}:${key}`, true);
  119. }
  120. }
  121. private async updateKeyCommandsContext(mapping: KeyMapping) {
  122. await Promise.all(
  123. keys.map(key => setCommandContext(`${CommandContext.Key}:${key}`, Boolean(mapping && mapping[key])))
  124. );
  125. }
  126. }
  127. export class Keyboard implements Disposable {
  128. private _disposable: Disposable;
  129. constructor() {
  130. const subscriptions = keys.map(key =>
  131. commands.registerCommand(`${extensionId}.key.${key}`, () => this.execute(key), this)
  132. );
  133. this._disposable = Disposable.from(...subscriptions);
  134. }
  135. dispose() {
  136. this._disposable && this._disposable.dispose();
  137. }
  138. @log<Keyboard['createScope']>({
  139. args: false,
  140. prefix: (context, mapping) =>
  141. `${context.prefix}[${mappings.length}](${mapping === undefined ? '' : Object.keys(mapping).join(',')})`
  142. })
  143. createScope(mapping?: KeyMapping): KeyboardScope {
  144. return new KeyboardScope({ ...mapping });
  145. }
  146. @log<Keyboard['beginScope']>({
  147. args: false,
  148. prefix: (context, mapping) =>
  149. `${context.prefix}[${mappings.length}](${mapping === undefined ? '' : Object.keys(mapping).join(',')})`
  150. })
  151. async beginScope(mapping?: KeyMapping): Promise<KeyboardScope> {
  152. const scope = this.createScope(mapping);
  153. await scope.start();
  154. return scope;
  155. }
  156. @log()
  157. async execute(key: Keys): Promise<{} | undefined> {
  158. const cc = Logger.getCorrelationContext();
  159. if (!mappings.length) {
  160. if (cc) {
  161. cc.exitDetails = ' \u2022 skipped, no mappings';
  162. }
  163. return undefined;
  164. }
  165. try {
  166. const mapping = mappings[mappings.length - 1];
  167. let command = mapping[key] as KeyCommand | (() => Promise<KeyCommand>);
  168. if (typeof command === 'function') {
  169. command = await command();
  170. }
  171. if (!command || typeof command.onDidPressKey !== 'function') {
  172. if (cc) {
  173. cc.exitDetails = ' \u2022 skipped, no callback';
  174. }
  175. return undefined;
  176. }
  177. await command.onDidPressKey(key);
  178. return undefined;
  179. } catch (ex) {
  180. Logger.error(ex, cc);
  181. return undefined;
  182. }
  183. }
  184. }