vim窗口垂直分屏和水平分屏对终端控制序列的微妙影响
intro
vim本质上是在使用终端的控制序列来实现编辑功能:基本的光标移动和字符输出都是需要vim来生成终端的精确控制序列。我们甚至可以把终端本身看成一个和GUI一样的画布,只是GUI系统中画布的基本单位是像素(pixel),而终端的基本单位是"字符"(只是这些字符可以附带一些颜色、下划线之类的属性)。
vim对文本的修改有插入和替换两种模式,尽管插入模式是最经常使用的,但是替换模式应该是最容易实现的:把光标移动到目标位置,然后输入新字符覆盖即可。
而插入模式下,当插入一个新内容时,插入位置之后的内容都需要依次向后移动。特别的,当插入新行之后,这一行之后所有的内容都需要依次下移一行。此时,vim是否需要在终端上将所有行都重新输出一遍,就像CRT显示器中光栅重新刷新一样?
如果是这样,那么远程登录到远端机器,此时和刷新的带宽会不会很大(当然,考虑到现在网卡的速度,这种担心显然是多余的。只是vim出现的时候,硬件还没有这么强大,所以这个问题在当时应该是需要考虑的)?
整体思路
vim工程自带的src/README.md文件,说明了主循环的大体流程:
Updating the screen is mostly postponed until a command or a sequence of commands has finished. The work is done by update_screen(), which calls win_update() for every window, which calls win_line() for every line. See the start of screen.c for more explanations.
文档大致表明了“逻辑和显示分离”的设置思想:用户操作修改内存中buffer内容,然后在loop结束时统一刷新(而不是在每次修改buffer之后立即刷新),这样的一个明显好处在于:当一次操作修改多个部分时可以避免频繁刷新。
drawscreen.c文件开始的注释:
* Commands that change text in the buffer must call changed_bytes() or* changed_lines() to mark the area that changed and will require updating* later. The main loop will call update_screen(), which will update each* window that shows the changed buffer. This assumes text above the change* can remain displayed as it is. Text after the change may need updating for* scrolling, folding and syntax highlighting.
通过特定的接口修改内容,从而在更新屏幕时大致知道(提供hint)哪些windows需要更新。
changed_bytes===>>>changed_common
static void
changed_common(linenr_T lnum,colnr_T col,linenr_T lnume,long xtra)
{
///...// Call update_screen() later, which checks out what needs to be redrawn,// since it notices b_mod_set and then uses b_mod_*.set_must_redraw(UPD_VALID);
///...
}
屏幕更新
window结构
每个window结构中包含了显示的对应buffer的起始行号(w_topline)和结束行号(w_botline)。
/** Structure which contains all information that belongs to a window** All row numbers are relative to the start of the window, except w_winrow.*/
struct window_S
{
///.../** "w_topline", "w_leftcol" and "w_skipcol" specify the offsets for* displaying the buffer.*/linenr_T w_topline; // buffer line number of the line at the// top of the windowchar w_topline_was_set; // flag set to TRUE when topline is set,// e.g. by winrestview()linenr_T w_botline; // number of the line below the bottom of// the window
///...
}
window更新
///@file:vim\src\drawscreen.c
/** Update a single window.** This may cause the windows below it also to be redrawn (when clearing the* screen or scrolling lines).** How the window is redrawn depends on wp->w_redr_type. Each type also* implies the one below it.* UPD_NOT_VALID redraw the whole window* UPD_SOME_VALID redraw the whole window but do scroll when possible* UPD_REDRAW_TOP redraw the top w_upd_rows window lines, otherwise like* UPD_VALID* UPD_INVERTED redraw the changed part of the Visual area* UPD_INVERTED_ALL redraw the whole Visual area* UPD_VALID 1. scroll up/down to adjust for a changed w_topline* 2. update lines at the top when scrolled down* 3. redraw changed text:* - if wp->w_buffer->b_mod_set set, update lines between* b_mod_top and b_mod_bot.* - if wp->w_redraw_top non-zero, redraw lines between* wp->w_redraw_top and wp->w_redraw_bot.* - continue redrawing when syntax status is invalid.* 4. if scrolled up, update lines at the bottom.* This results in three areas that may need updating:* top: from first row to top_end (when scrolled down)* mid: from mid_start to mid_end (update inversion or changed text)* bot: from bot_start to last row (when scrolled up)*/static void
win_update(win_T *wp)
{
///...// Update all the window rows.idx = 0; // first entry in w_lines[].wl_sizerow = 0;srow = 0;for (;;){
行刷新
更新一行的内容。可以看到除了字符本身之外,还包含了每个字符对应的属性。这些属性可能是根据不同的逻辑计算获得:例如可能有些插件会对错误加下划线。
///@file: screen.c
/** screen.c: Lower level code for displaying on the screen.** Output to the screen (console, terminal emulator or GUI window) is minimized* by remembering what is already on the screen, and only updating the parts* that changed.** ScreenLines[off] Contains a copy of the whole screen, as it is currently* displayed (excluding text written by external commands).* ScreenAttrs[off] Contains the associated attributes.* ScreenCols[off] Contains the virtual columns in the line. -1 means not* available or before buffer text.** LineOffset[row] Contains the offset into ScreenLines*[], ScreenAttrs[]* and ScreenCols[] for each line.* LineWraps[row] Flag for each line whether it wraps to the next line.///...* The screen_*() functions write to the screen and handle updating* ScreenLines[].*/void
screen_line(win_T *wp,int row,int coloff,int endcol,int clear_width,colnr_T last_vcol,int flags UNUSED)
{
///...///...
}
屏幕字符是否有更新的判断
全局结构定义
vim内部保存了当前控制的整个屏幕中,每一个位置字符的属性。要注意的是,memfile中的字符并没有这些属性,只有屏幕中显示的内容有这个结构。
///@file: globals.h
EXTERN long Columns INIT(= 80); // nr of columns in the screen/** The characters that are currently on the screen are kept in ScreenLines[].* It is a single block of characters, the size of the screen plus one line.* The attributes for those characters are kept in ScreenAttrs[].* The virtual column in the line is kept in ScreenCols[].** "LineOffset[n]" is the offset from ScreenLines[] for the start of line 'n'.* The same value is used for ScreenLinesUC[], ScreenAttrs[] and ScreenCols[].** Note: before the screen is initialized and when out of memory these can be* NULL.*/
EXTERN schar_T *ScreenLines INIT(= NULL);
EXTERN sattr_T *ScreenAttrs INIT(= NULL);
EXTERN colnr_T *ScreenCols INIT(= NULL);
EXTERN unsigned *LineOffset INIT(= NULL);
EXTERN char_u *LineWraps INIT(= NULL); // line wraps to next line
结构初始化
根据终端属性获得表示屏幕内容的内存,这个可以大致理解为FrameBuffer。
void
screenalloc(int doclear)
{
///...new_ScreenLines = LALLOC_MULT(schar_T, (Rows + 1) * Columns);vim_memset(new_ScreenLinesC, 0, sizeof(u8char_T *) * MAX_MCO);
///...// Use the last line of the screen for the current line.current_ScreenLine = new_ScreenLines + Rows * Columns;
///...
每个字符是否需要更新
当修改这个buffer的某一行时,先将修改之后的内容更新到一个额外的、临时的屏幕行结构中,然后比较更新之后的更新前的内容是否相同,如果不同可以获得一个变化内容。
这种设计思路类似于Unreal中的属性同步机制:比较更新前后的内容来判断属性是否要同步到客户端。
void
screen_line(win_T *wp,int row,int coloff,int endcol,int clear_width,colnr_T last_vcol,int flags UNUSED)
{
///...off_from = (unsigned)(current_ScreenLine - ScreenLines);off_to = LineOffset[row] + coloff;max_off_from = off_from + screen_Columns;max_off_to = LineOffset[row] + screen_Columns;
///...redraw_next = char_needs_redraw(off_from, off_to, endcol - col);
///...
}
每个屏幕字符是否需要重新绘制的判断:
/** Check whether the given character needs redrawing:* - the (first byte of the) character is different* - the attributes are different* - the character is multi-byte and the next byte is different* - the character is two cells wide and the second cell differs.*/static int
char_needs_redraw(int off_from, int off_to, int cols)
{if (cols > 0&& ((ScreenLines[off_from] != ScreenLines[off_to]|| ScreenAttrs[off_from] != ScreenAttrs[off_to])|| (enc_dbcs != 0&& MB_BYTE2LEN(ScreenLines[off_from]) > 1&& (enc_dbcs == DBCS_JPNU && ScreenLines[off_from] == 0x8e? ScreenLines2[off_from] != ScreenLines2[off_to]: (cols > 1 && ScreenLines[off_from + 1]!= ScreenLines[off_to + 1])))|| (enc_utf8&& (ScreenLinesUC[off_from] != ScreenLinesUC[off_to]|| (ScreenLinesUC[off_from] != 0&& comp_char_differs(off_from, off_to))|| ((*mb_off2cells)(off_from, off_from + cols) > 1&& ScreenLines[off_from + 1]!= ScreenLines[off_to + 1])))))return TRUE;return FALSE;
}
strace 确认
生成测试文件
tsecer@harry:for i in `seq 1 100`; do echo $i $i $i; done > seq.txt
然后在30行修改一个字符
下面终端控制序列完成什么动作
\33[?2004l\33[>4;m\33[?25l\33[31;49r\33[31;1H\33[L\33[1;50r\33[50;1H\33[K\33[31;1H\33[?25h
chatgpt的答复,也就是vim发送给终端的并不是修改之后的最终完整数字,而是控制序列。
特别的,当插入一个新的空行时,直接给终端发送在指定位置插入新行的指令(而不是发送所有受影响行之后的完整内容);相对于发送修改之后的显示内容,这种只发送控制指令的方式明显效率更高(考虑到可能是通过远程终端在使用vim,这种效率的提升就更是必不可少的了)。
这些终端控制序列执行的动作如下:
1. `\33[?2004l`:禁用“鼠标可用”模式。
2. `\33[>4;m`:重置特定的图形模式。
3. `\33[?25l`:隐藏光标。
4. `\33[31;49r`:设置终端的滚动区域,可能是红色背景。
5. `\33[31;1H`:将光标移动到指定位置(行1,列1)。
6. `\33[L`:向下插入一行(在当前行上方插入新行)。
7. `\33[1;50r`:设置新的滚动区域(行1到行50)。
8. `\33[50;1H`:将光标移动到行50,列1。
9. `\33[K`:清除从光标到行尾的内容。
10. `\33[31;1H`:再次将光标移动到行1,列1。
11. `\33[?25h`:显示光标。
这些序列通常用于控制终端显示的布局和光标的状态。
screen.c中注释
在类型的最后,包含了USE_REDRAW类型,这种就是可能期待的原始的重绘类型(但是通常都是使用终端内置的添加/删除行指令,从而避免向终端发送太多字符)。
从termdefs.h中的注释可以看到,其中的A表示Add、L表示Line、C表示Count(操作接收数量参数),D表示Delete,S表示Scroll...。
///@file: screen.c
/** The rest of the routines in this file perform screen manipulations. The* given operation is performed physically on the screen. The corresponding* change is also made to the internal screen image. In this way, the editor* anticipates the effect of editing changes on the appearance of the screen.* That way, when we call screenupdate a complete redraw isn't usually* necessary. Another advantage is that we can keep adding code to anticipate* screen changes, and in the meantime, everything still works.*//** types for inserting or deleting lines*/
#define USE_T_CAL 1
#define USE_T_CDL 2
#define USE_T_AL 3
#define USE_T_CE 4
#define USE_T_DL 5
#define USE_T_SR 6
#define USE_NL 7
#define USE_T_CD 8
#define USE_REDRAW 9
终端滚屏
滚屏设置,设置滚屏范围之后,在范围内的输出将会向后移动,滚屏区域外的不受影响。因为通常可能会在最下面有状态栏,所以一般滚动区域并不是整个屏幕区域,而是可见屏幕的一个部分。
例如,vim的屏幕最下一行通常显示的是编辑器的当前状态。
另外,在vim中通常还支持多窗口机制,这样将region限制在一个窗口范围内就更容易通过region来实现。
Go to:To go to a line, send "\e[%d;%dH" where first %d = y coordinate, second one is x coordinate. Note that they are reversed. Top left of the screen is 1,1.Set window:To define a scrollable window, send "\e[%d;%dr" , where first %d is top line, and %d is bottom line. E.g. if you define a window from line 1 to 47, and you write a newline at line 47, lines 2 to 47 will be moved one line up, and line 1 will disappear.Initialization:Figure out how many lines the user wants on the screen. This could be pagelen, but often that is set to a smaller number that the actual number of lines. I use another variable, but default to pagelen. The last line is referred to hereafter as maxLineGoto maxLine,1 (that's y = maxLine, x = 1). This is where user's input will be from now on. Send the VT_CLEAR_LINE code and the VT_SAVECURSOR code to save this cursor position.Assuming a 2-line status bar, the screen should look like:line 1 .. maxLine-3 scrollable region line maxLine-2 and maxLine-1 status bar line maxLine input lineGoto 1,1 now and send the SETWIN_CLEAR code (to clear any previous scrollable regions set) and then send a CLEAR_SCREEN code.Now to define the new scrollable window. Simply send the set window code. If you have 50 lines, you would define line 1 to 47 as scrollable, and would thus send the "\e[1;47r" code. Whenever something is printed in that region, the terminal will know that this is the part of screen to be scrolled. Whenever something is printed outside, no scrolling will happen.
垂直(vertical)多窗口
当存在垂直多窗口时,不同窗口中相同屏幕行的内容必然不能相互影响。但此时Scroll没办法提供列的机制,所以此时可能就需要使用原始的文件对比。
也有专门的帖子提到了这个问题。
I wonder what traffic is generated when I have a split screen session with a shell running in one of theregions
and I hit enter in that shell.If I were running in full screen mode, I assume that only a new line
is sent to the terminal which then scrolls up. But what happens in
the above case?
但是这个帖子没有人回答当垂直拆分之后如何处理。从vim控制序列的数出来看,控制序列使用的是整个屏幕对比增量差异的方法更新的。对应vim的代码,其中有一些对应的注释说明(其中T_CSV是scroll region vertical的意思)。
这意味着一个无用的小知识:使用vim的水平窗口拆分时,插入新行触发的终端更新序列,通常比使用垂直拆分窗口的效率更高。
int
screen_ins_lines(int off,int row,int line_count,int end,int clear_attr,win_T *wp) // NULL or window to use width from
{
///...if (wp != NULL && wp->w_width != Columns && *T_CSV == NUL){// Avoid that lines are first cleared here and then redrawn, which// results in many characters updated twice. This happens with CTRL-F// in a vertically split window. With line-by-line scrolling// USE_REDRAW should be faster.if (line_count > 3)return FAIL;type = USE_REDRAW;}
///...
}
wrap up
简单来说,在插入模式下:如果插入内容没有导致换行,此时vim只会简单的将光标移动到插入位置,并将插入位置之后的所有内容(向终端)重新输出一遍;如果有换行发生,使用终端的Scroll机制插入一行(而不是和行类似逻辑:把新插入行之后的内容全部重新输出一遍),然后回到常规逻辑。
由于终端Scroll区域通常不支持指定宽度,所以vim水平分屏的时候,可能会导致回退到原始的、整个插入行之后所有内容重新刷新一遍的逻辑。
outro
看似平平无奇的vim,可能蕴含了“终端”这种计算机早期交互设备的设计逻辑和发展历史,以及计算机先驱解决问题的巧妙思路:尽管受限于当时的硬件技术水平,但是解决问题的思路和之后的思路有异曲同工之妙。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/907320.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!