Loading...
墨滴

前端家园

2021/12/13  阅读:40  主题:默认主题

Theia快捷键实现原理(一):快捷键注册

在theia或vscode中都内置了很多快捷键,可以很方便的用于触发一个command(命令),我们今天来学习下theia快捷键的注册原理。

使用

任何东西我们在学习其原理之前,都需要先知道怎么使用,不然就会一头雾水,不知从何看起。

首先我们看下在theia中如何注册一个快捷键,例如注册一个剪切的快捷键:

registry.registerKeybinding({
    command: CommonCommands.CUT.id,
    keybinding: 'ctrlcmd+x'
});

可以看到注册快捷键的主入口函数是registerKeybinding,那么我们再来看下这个函数的定义:

/**
 * Register a default keybinding to the registry.
 *
 * Keybindings registered later have higher priority during evaluation.
 *
 * @param binding the keybinding to be registered
 */

registerKeybinding(binding: common.Keybinding): Disposable {
    return this.doRegisterKeybinding(binding);
}

可以看到这里又调用了另一个方法doRegisterKeybinding,在我们继续往下看之前,我们先来看下这个binding参数主要定义了哪些内容。

定义

Keybinding的定义如下:

export interface Keybinding {
    /**
     * Unique command identifier of the command to be triggered by this keybinding.
     */

    command: string;
    /**
     * The key sequence for the keybinding as defined in packages/keymaps/README.md.
     */

    keybinding: string;
    /**
     * The optional keybinding context where this binding belongs to.
     * If not specified, then this keybinding context belongs to the NOOP
     * keybinding context.
     *
     * @deprecated use `when` closure instead
     */

    context?: string;
    /**
     * An optional clause defining the condition when the keybinding is active, e.g. based on the current focus.
     * See {@link https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts} for more details.
     */

    when?: string;

    /**
     * Optional arguments that will be passed to the command when it gets triggered via this keybinding.
     * Needs to be specified when the triggered command expects arguments to be passed to the command handler.
     */

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    args?: any;
}

command:表示快捷键需要触发的命令的id

keybinding:表示快捷键的组合方式

context:表示快捷键运行的上下文(已经废弃了,用when取代)

when:表示快捷键执行的时机,与vscode的快捷键里的when定义是一样的,可以参考https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts

args:传给command的参数

注册

接下来我们继续看doRegisterKeybinding的实现逻辑:

protected doRegisterKeybinding(binding: common.Keybinding, scope: KeybindingScope = KeybindingScope.DEFAULT): Disposable {
    try {
        this.resolveKeybinding(binding);
        const scoped = Object.assign(binding, { scope });
        this.insertBindingIntoScope(scoped, scope);
        return Disposable.create(() => {
            const index = this.keymaps[scope].indexOf(scoped);
            if (index !== -1) {
                this.keymaps[scope].splice(index, 1);
            }
        });
    } catch (error) {
        this.logger.warn(`Could not register keybinding:\n  ${common.Keybinding.stringify(binding)}\n${error}`);
        return Disposable.NULL;
    }
}

主要逻辑就是根据scopekeymaps中将快捷键存储起来。

keymaps

根据scope生成的用于存储快捷键的二维数组

protected readonly keymaps: ScopedKeybinding[][] = [...Array(KeybindingScope.length)].map(() => []);
scope

scope用于表示快捷键的作用域:

export enum KeybindingScope {
    DEFAULT,
    USER,
    WORKSPACE,
    END
}

DEFAULT表示内置的快捷键,USER表示用户定义的快捷键,WORKSPACE表示工作区的快捷键,END只用于遍历。

优先级:WORKSPACE > USER > DEFAULT

resolveKeybinding
/**
 * Ensure that the `resolved` property of the given binding is set by calling the KeyboardLayoutService.
 */

resolveKeybinding(binding: ResolvedKeybinding): KeyCode[] {
    if (!binding.resolved) {
        const sequence = KeySequence.parse(binding.keybinding);
        binding.resolved = sequence.map(code => this.keyboardLayoutService.resolveKeyCode(code));
    }
    return binding.resolved;
}

binding.resolved用于缓存解析结果,如果之前解析过则直接返回,这函数主要是两个功能:

  1. 解析keybinding为KeyCode实例
  2. 根据当前键盘布局返回KeyCode,主要是为了兼容其他非标准布局的键盘类型

KeyCode用于表示快捷键信息,核心代码如下:

/**
 * Representation of a pressed key combined with key modifiers.
 */

export class KeyCode {

  public readonly key: Key | undefined;
  public readonly ctrl: boolean;
  public readonly shift: boolean;
  public readonly alt: boolean;
  public readonly meta: boolean;
  public readonly character: string | undefined;

  public constructor(schema: KeyCodeSchema) {
      const key = schema.key;
      if (key) {
          if (key.code && key.keyCode && key.easyString) {
              this.key = key as Key;
          } else if (key.code) {
              this.key = Key.getKey(key.code);
          } else if (key.keyCode) {
              this.key = Key.getKey(key.keyCode);
          }
      }
      this.ctrl = !!schema.ctrl;
      this.shift = !!schema.shift;
      this.alt = !!schema.alt;
      this.meta = !!schema.meta;
      this.character = schema.character;
  }
  ...
}
insertBindingIntoScope
/**
 * Ensures that keybindings are inserted in order of increasing length of binding to ensure that if a
 * user triggers a short keybinding (e.g. ctrl+k), the UI won't wait for a longer one (e.g. ctrl+k enter)
 */

protected insertBindingIntoScope(item: common.Keybinding & { scope: KeybindingScope; }, scope: KeybindingScope): void {
    const scopedKeymap = this.keymaps[scope];
    const getNumberOfKeystrokes = (binding: common.Keybinding): number => (binding.keybinding.trim().match(/\s/g)?.length ?? 0) + 1;
    const numberOfKeystrokesInBinding = getNumberOfKeystrokes(item);
    const indexOfFirstItemWithEqualStrokes = scopedKeymap.findIndex(existingBinding => getNumberOfKeystrokes(existingBinding) === numberOfKeystrokesInBinding);
    if (indexOfFirstItemWithEqualStrokes > -1) {
        scopedKeymap.splice(indexOfFirstItemWithEqualStrokes, 0, item);
    } else {
        scopedKeymap.push(item);
    }
}

根据快捷键的组合长度来决定插入快捷键到keymaps中的位置,长度越大的优先级越低例如:如果同时注册了ctrl+k enter,和ctrl+k,这样能确保ctrl+k要优先执行。长度一样的则插入到该长度序列中的首个位置,因为越往前优先级越高。

概括来说就是:在同一个scope中,前面的优先级要大于后面的,长度短的要大于长度长的。

到此就完成了快捷键的注册流程。

总结

整个流程也比较简单,就是通过调用registerKeybinding方法,将binding参数中的keybinding字符串解析为KeyCode实例,并缓存在binding.resolved字段中,然后再将binding根据scope存入到keymaps数组中。

后续文章会继续讲解KeySequence.parse函数,看是如何将字符串解析为KeyCode实例的,敬请期待。

关注公众号: 前端家园

前端家园

2021/12/13  阅读:40  主题:默认主题

作者介绍

前端家园