vim内置terminal的使用及实现

intro

在使用vim的时候,一个非常常用的功能就是搜索功能。

搜索

在缺少工程级别搜索的情况下,搜索通常不是一次完成的:通常的场景是提供一个最明显的关键字(并且忽略大小写),然后从结果中再缩小搜索范围,直到找到搜索结果。在这个迭代的过程中,可能需要从上次的输出中拷贝一部分、简单编辑之后再次搜索。这拷贝/编辑功能是vim的强项。如果能将命令的输出不断拷贝/粘贴/编辑是一个很好的功能。

耗时命令

在功能验证时,需要不断的构建和测试,这两个可能都是耗时比较长的操作,如果在vim中直接执行就不能执行vim操作,例如浏览代码。

另外,构建错误可能会输出错误文件的名字和行号,这就免不了需要打开这些文件。如果这些文件在一个vim的buffer中,直接通过gf就可以打开文件。

指令序列

通过git提交代码的时候,经常需要先执行git status查看修改了哪些文件,通过git diff查看修改的内容是会否符合预期,然后git add,git commit。

这样每次执行完之后再切换回vim跟平时的操作流并不一致,而程序员都是不喜欢这种不连续的切换状态。

vim提供的term功能,正好可以解决这些问题:启动一个终端,可有执行命令,并且可以进入多Normal模式下使用vim的编辑器功能。当然,前面的问题都不是只能使用term解决(通常也可以在cmd中启动bash来实现),只是通过这种方法解决看起来更加流畅和自然。

这个term功能尽管看起来并不太常用,但是这个功能在vim的源代码中出镜率还很高:在src文件夹下有专门的libvterm文件夹,这算是vim源代码中依赖的一个不太常规的外部库(readline算是比较常用的)。

在vim的主循环函数main_loop中

/*
 * Main loop: Execute Normal mode commands until exiting Vim.
 * Also used to handle commands in the command-line window, until the window
 * is closed.
 * Also used to handle ":visual" command after ":global": execute Normal mode
 * commands, return when entering Ex mode.  "noexmode" is TRUE then.
 */
    void
main_loop(
    int		cmdwin,	    // TRUE when working in the command-line window
    int		noexmode)   // TRUE when return on entering Ex mode
{
///...
	/*
	 * If we're invoked as ex, do a round of ex commands.
	 * Otherwise, get and execute a normal mode command.
	 */
	if (exmode_active)
	{
	    if (noexmode)   // End of ":global/path/visual" commands
		goto theend;
	    do_exmode(exmode_active == EXMODE_VIM);
	}
	else
	{
#ifdef FEAT_TERMINAL
	    if (term_use_loop()
		    && oa.op_type == OP_NOP && oa.regname == NUL
		    && !VIsual_active
		    && !skip_term_loop)
	    {
		// If terminal_loop() returns OK we got a key that is handled
		// in Normal mode.  With FAIL we first need to position the
		// cursor and the screen needs to be redrawn.
		if (terminal_loop(TRUE) == OK)
		    normal_cmd(&oa, TRUE);
	    }
	    else
#endif
	    {
#ifdef FEAT_TERMINAL
		skip_term_loop = FALSE;
#endif
		normal_cmd(&oa, TRUE);
	    }
	}
///...
"

libvterm

direction

一个直觉的疑问是:vim本身不就是在处理终端吗,为什么还需要一个vterm?这个其实就是term的两个方面:当vim作为一个编辑器时,它是一个term的使用者,只需要按照需求向终端输出控制命令(例如移动光标),而在vim中执行term命令时则是作为终端的输出方。这意味着需要解析类似移动光标/切换背景颜色之类的控制命令序列,

这一点并不是vim的主要功能,所以需要引入一个专门解析并执行终端控制命令的功能库。这个功能库接收并执行终端命令,并将处理之后的文字存储在特定位置,这样vim就可以直接使用处理后的内容,只需要将结果显示到buffer中即可。这里可以大哥不太恰当的比喻:libvterm就是一个电脑,可以将输入转换为屏幕像素信息,而vim作为一个显示器只需要按照像素信息显示即可。这也是真实/物理终端的功能:接收控制序列并展示在终端屏幕上。只是libvterm的作用是接收控制数列,将结果以数据结构的形式保存在内存中(可以供业务进程来取度)。

功能

  • 回调

当使用libvterm创建一个终端时,需要提供vterm相关回调,其中最关键的是handle_damage,用来处理输出内容的变化。


static VTermScreenCallbacks screen_callbacks = {
  handle_damage,	// damage
  handle_moverect,	// moverect
  handle_movecursor,	// movecursor
  handle_settermprop,	// settermprop
  handle_bell,		// bell
  handle_resize,	// resize
  handle_pushline,	// sb_pushline
  NULL,			// sb_popline
  NULL			// sb_clear
};
  • 变化内容

libvterm提供了获取屏幕上每个cell信息的接口。

/*
 * Fill one screen line from a line of the terminal.
 * Advances "pos" to past the last column.
 */
    static void
term_line2screenline(
	term_T		*term,
	win_T		*wp,
	VTermScreen	*screen,
	VTermPos	*pos,
	int		max_col)
{
    int off = screen_get_current_line_off();

    for (pos->col = 0; pos->col < max_col; )
    {
	VTermScreenCell cell;
	int		c;

	if (vterm_screen_get_cell(screen, *pos, &cell) == 0)
	    CLEAR_FIELD(cell);
///...

获取到的cell包括了字符内容(例如汉字的utf-8),文字及背景颜色,是否斜体/斜体/下划线等。有了这些信息,vim只负责展示即可。

typedef struct {
  uint32_t chars[VTERM_MAX_CHARS_PER_CELL];
  char     width;
  VTermScreenCellAttrs attrs;
  VTermColor fg, bg;
} VTermScreenCell;

typedef struct {
    unsigned int bold      : 1;
    unsigned int underline : 2;
    unsigned int italic    : 1;
    unsigned int blink     : 1;
    unsigned int reverse   : 1;
    unsigned int conceal   : 1;
    unsigned int strike    : 1;
    unsigned int font      : 4; /* 0 to 9 */
    unsigned int dwl       : 1; /* On a DECDWL or DECDHL line */
    unsigned int dhl       : 2; /* On a DECDHL line (1=top 2=bottom) */
    unsigned int small     : 1;
    unsigned int baseline  : 2;
} VTermScreenCellAttrs;
// VIM: this was a union, but that doesn't always work.
typedef struct {
  /**
   * Tag indicating which member is actually valid.
   * Please use the `VTERM_COLOR_IS_*` test macros to check whether a
   * particular type flag is set.
   */
  uint8_t type;

  uint8_t red, green, blue;

  uint8_t index;
} VTermColor;

伪终端

在terminal的帮助文档中,有这么一段话

On Unix a pty is used to make it possible to run all kinds of commands. You
can even run Vim in the terminal! That's used for debugging, see below.

这里的伪终端(pty)有事什么用的呢?这个其实理解起来相对简单一些:它是libvterm的输入。在典型的运行shell的终端中,这些控制序列由bash生成,当然也可以由vim生成,或者任何进程向自己标准输出打印生成。

这就是文档里说的“You can even run Vim in the terminal!”(甚至还加了个惊叹号)的原因。

按键处理

谁处理输入

在vim的主循环中,会判断当前活跃的buffer是不是一个终端对应的buffer。全局变量curbuf保存当前(光标所在)buffer,而每个buffer结构中的b_term保存终端指针,如果buffer是由终端生成,则会指向对应终端对象。

 * Returns TRUE if the current window contains a terminal and we are sending
 * keys to the job.
 * If "check_job_status" is TRUE update the job status.
 */
    static int
term_use_loop_check(int check_job_status)
{
    term_T *term = curbuf->b_term;

    return term != NULL
	&& !term->tl_normal_mode
	&& term->tl_vterm != NULL
	&& term_job_running_check(term, check_job_status);
}

/*
 * Returns TRUE if the current window contains a terminal and we are sending
 * keys to the job.
 */
    int
term_use_loop(void)
{
    return term_use_loop_check(FALSE);
}

此时,主流程执行转移到terminal_loop函数,并继续的等待/读取/处理用户的按键输入。

    int
terminal_loop(int blocking)
{
    while (blocking || vpeekc_nomap() != NUL)
    {
    ///...
	raw_c = term_vgetc();
    ///...
}
}

特殊按键

在term使用中,真正需要感兴趣的是哪些按键是特殊的。

从代码上看,在终端中真正有特殊意义的就是 ctrl-w(或者其他配置的termkey)或者ctrl-backslash引导的序列。由于ctrl-w刚好和bash的前向删除单个word冲突,所以这个需要注意下。从代码(和文档)可以看到,在terminal中通过 Ctrl -w .组合按键来向job发送Ctrl-W,并且还可以通过Ctrl-w “访问寄存器内容。

    int
terminal_loop(int blocking)
{
///...
	// Was either CTRL-W (termwinkey) or CTRL-\ pressed?
	// Not in a system terminal.
	if ((c == (termwinkey == 0 ? Ctrl_W : termwinkey) || c == Ctrl_BSL)
#ifdef FEAT_GUI
		&& !curbuf->b_term->tl_system
#endif
		)
	{
	    int	    prev_c = c;
	    int	    prev_raw_c = raw_c;
	    int	    prev_mod_mask = mod_mask;

	    if (add_to_showcmd(c))
		out_flush();

	    raw_c = term_vgetc();
	    c = raw_c_to_ctrl(raw_c);

	    clear_showcmd();

	    if (!term_use_loop_check(TRUE)
					 || in_terminal_loop != curbuf->b_term)
		// job finished while waiting for a character
		break;

	    if (prev_c == Ctrl_BSL)
	    {
		if (c == Ctrl_N)
		{
		    // CTRL-\ CTRL-N : go to Terminal-Normal mode.
		    term_enter_normal_mode();
		    ret = FAIL;
		    goto theend;
		}
		// Send both keys to the terminal, first one here, second one
		// below.
		send_keys_to_term(curbuf->b_term, prev_raw_c, prev_mod_mask,
									 TRUE);
	    }
	    else if (c == Ctrl_C)
	    {
		// "CTRL-W CTRL-C" or 'termwinkey' CTRL-C: end the job
		mch_signal_job(curbuf->b_term->tl_job, (char_u *)"kill");
	    }
	    else if (c == '.')
	    {
		// "CTRL-W .": send CTRL-W to the job
		// "'termwinkey' .": send 'termwinkey' to the job
		raw_c = ctrl_to_raw_c(termwinkey == 0 ? Ctrl_W : termwinkey);
	    }
	    else if (c == Ctrl_BSL)
	    {
		// "CTRL-W CTRL-\": send CTRL-\ to the job
		raw_c = ctrl_to_raw_c(Ctrl_BSL);
	    }
	    else if (c == 'N')
	    {
		// CTRL-W N : go to Terminal-Normal mode.
		term_enter_normal_mode();
		ret = FAIL;
		goto theend;
	    }
	    else if (c == '"')
	    {
		term_paste_register(prev_c);
		continue;
	    }
	    else if (termwinkey == 0 || c != termwinkey)
	    {
		// space for CTRL-W, modifier, multi-byte char and NUL
		char_u buf[1 + 3 + MB_MAXBYTES + 1];

		// Put the command into the typeahead buffer, when using the
		// stuff buffer KeyStuffed is set and 'langmap' won't be used.
		buf[0] = Ctrl_W;
		buf[special_to_buf(c, mod_mask, FALSE, buf + 1) + 1] = NUL;
		ins_typebuf(buf, REMAP_NONE, 0, TRUE, FALSE);
		ret = OK;
		goto theend;
	    }
	}
///...
}

outro

尽管很多人都不会用到vim的term功能,但是这些功能的代码在vim中却是实实在在存在的,并且影响了vim的代码结构和流程。了解这些功能更便于理解vim这个整体及其它相关模块的功能。

vim作虽然是一个无处不在的、徒手可得的编辑器,但实现并不简单。由于vim的大部分功能都是基于常见功能、基础设备实现,所以涉及到了不少设备的本质和用法,而且有不少优秀的设计/实现思路可以借鉴。

posted on 2024-07-12 20:24  tsecer  阅读(213)  评论(0编辑  收藏  举报

导航