qutebrowser Completer
qutebrowser 作为一个命令驱动的浏览器,良好的补全体验非常关键。在 qutebrowser 中,补全模块的实现比较复杂,即包含 GUI 视图绘制,也包含补全逻辑。因此 qutebrowser 对视图、逻辑进行了拆分,Completer 专门负责补全模块的纯逻辑部分的实现。
_partition
方法的主要目的是将命令行文本分割成多个部分,并围绕光标位置进行分割。
这么说有些抽象,结合使用来看。当用户在命令模式下输入命令,此时是 Completer 介入的时机,为用户提供补全提示。
原理分析
以以下命令输入为例:

其中包含两部分内容:
- 命令内容:即 :open -t mmm
- 光标位置:用户可以移动光标,进行行内编辑
回到 _partition,它的作用是根据命令内容和光标位置,对用户输入进行切分。
在我打字输入过程中,_partition 输入输出如下:
| 用户操作 | 方法输入(切分后) | 方法三元组输出 | 
|---|---|---|
| 输入 o | ['o'] | [] 'o' [] | 
| 输入 p | ['op'] | [] 'op' [] | 
| 输入 e | ['ope'] | [] 'ope' [] | 
| 输入 n | ['open'] | [] 'open' [] | 
| 输入   | ['open', ' '] | ['open'] '' [] | 
| 输入 - | ['open', ' -'] | ['open'] '-' [] | 
| 输入 t | ['open', ' -t'] | ['open'] '-t' [] | 
| 输入   | ['open', ' -t', ' '] | ['open', '-t'] '' [] | 
| 输入 m | ['open', ' -t', ' m'] | ['open', '-t'] 'm' [] | 
| 输入 m | ['open', ' -t', ' mm'] | ['open', '-t'] 'mm' [] | 
| 输入 m | ['open', ' -t', ' mmm'] | ['open', '-t'] 'mmm' [] | 
| 左方向键 | ['open', ' -t', ' mmm'] | ['open', '-t'] 'mmm' [] | 
| 左方向键 | ['open', ' -t', ' mmm'] | ['open', '-t'] 'mmm' [] | 
| 左方向键 | ['open', ' -t', ' mmm'] | ['open', '-t'] 'mmm' [] | 
| 左方向键 | ['open', ' -t', ' mmm'] | ['open'] '-t' ['mmm'] | 
| 左方向键 | ['open', ' -t', ' mmm'] | ['open'] '-t' ['mmm'] | 
以上过程,包含逐字输入,输入完成后,左移光标的过程。
_partition 返回一个元组,包含三个元素:光标前的部分,光标下的部分,光标后的部分。
结合拆解推演过程,对这个结果有了直观理解。
在函数实现上,有几个细节值得关注:
首先,在命令切分上,首先尝试使用 CommandParser 执行切分,如果命令不存在,则降级为 split 分割:
# 使用 CommandParser 解析文本,如果命令不存在,则降级为 split 分割
try:
	parse_result = parser.CommandParser().parse(text, keep=True)
except cmdexc.NoSuchCommandError:
	cmdline = split.split(text, keep=True)
else:
	cmdline = parse_result.cmdline
_partition 的最核心逻辑,是计算当前光标落在哪个命令部分上,具体实现为:获取当前光标据开头的总距离,从头开始,以每个命令长度削减总距离,如果在某个部分总长度被减为负数,这个部分变为当前部分。之前、之后部分基于此便可获得。具体实现:
# 获取光标位置,并确保光标位置不超过文本长度
pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text))  # Qt treats 2-byte UTF-16 chars as 2 chars
# ……
# 它遍历 parts 中的每一部分
for i, part in enumerate(parts):
	# 消耗总长度
	pos -= len(part)
	# 找到当前部分,计算前后部分
	if pos <= 0:
		if part[pos-1:pos+1].isspace():
			# cursor is in a space between two existing words
			parts.insert(i, '')
		prefix = [x.strip() for x in parts[:i]]
		center = parts[i].strip()
		# strip trailing whitespace included as a separate token
		postfix = [x.strip() for x in parts[i+1:] if not x.isspace()]
		return prefix, center, postfix
raise utils.Unreachable(f"Not all parts consumed: {parts}")
何处被使用
Command(输入命令的 EditText 组件)cursorPositionChanged 信号和 textChanged
- Command.update_completion 信号
- Completer.schedule_completion_update
- 通过 Completer._timer触发异步- Completer_update_completion- Completer._partition
 
 
 
- 通过 
 
- Completer.schedule_completion_update
还有其他调用路径,略。
_get_new_completion
主要目的是根据当前的命令文本获取一个新的补全函数。它接收两个参数:光标前的命令块(before_cursor)和光标下的命令块(under_cursor)。
还是以 :open -t mmm 为例,当我在逐字录入时,主观感受:
- 首先通过 :进入命令模式,此时补全界面列出所有命令
- 在我输入 o、p过程中,补全界面不断缩小范围
- 当我输入完成 open后,补全界面变了,原本是补全命令,变成了补全 Url,给出的补全提示是我近期访问的 url 列表
这个交互体验细节非常酷。
代码实现如下:
def _get_new_completion(self, before_cursor, under_cursor):
	# ...
    if not before_cursor:
        log.completion.debug('Starting command completion')
        print('Starting command completion')
        # 首次进入命令模式,列出所有命令的补全
        return miscmodels.command
	# 尝试根据命令获取输入
    try:
        cmd = objects.commands[before_cursor[0]]
    except KeyError:
        log.completion.debug("No completion for unknown command: {}"
                                .format(before_cursor[0]))
        return None
    # ...
    argpos = len(before_cursor) - 1
    try:
	    # 根据命令参数类型,获取对应的补全函数
        func = cmd.get_pos_arg_info(argpos).completion
    except IndexError:
        log.completion.debug("No completion in position {}".format(argpos))
        return None
    # 返回新的补全函数
    return func
- objects.commands(参见 qutebrowser objects) 中包含了 qutebrowser 中所有命令
本文作者:Maeiee
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!
