vim窗口垂直分屏和水平分屏对终端控制序列的微妙影响

news/2025/9/18 18:00:52/文章来源:https://www.cnblogs.com/tsecer/p/19099323

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,一经查实,立即删除!

相关文章

Java基本语句-分支语句

Java基本语句-分支语句Day05 如何在API字典中寻找自己想要的Scanner类型 1.点击搜索 输入Scanner 2.字典中回显示各种类型的获取方式: nextByte()、nextShort()、nextInt()、nextLong()、nextdouble()、nextFloat()、n…

丘成桐谈AI

很多重要的科学发现,是在平凡的事情里面突然有个突破。 观念上的突破,在我看人工智能有困难做不到,现在全民学人工智能, 听起来很好听,但是师资不够, 跟数学的整个合作是刚开始, AI看见万千数据 记者:您第一次…

异常检测在网络安全中的应用 - 实践

异常检测在网络安全中的应用 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco…

人小鼠免疫细胞maker基因 - un

人小鼠ref:https://www.abcam.cn/primary-antibodies/immune-cell-markers-poster作者:un-define出处:https://www.cnblogs.com/mmtinfo/p/19099316本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此…

HyperWorks许可配置

在工程设计和仿真领域,正确的软件许可配置是确保工作流程顺畅、提高生产效率和实现最佳投资回报的关键。HyperWorks作为业界领先的工程仿真软件,其灵活的许可配置功能为用户提供了广泛的定制选项,确保软件能够完全满…

AI --- LLM 之 模型大比拼

AI --- LLM 之 模型大比拼如何成为高级的安卓逆向分析工程师,请告诉我详细的学习路径qwen3-next-80b-a3b-thinking 成为高级安卓逆向分析工程师需要系统性学习、大量实战和持续精进。这是一条技术深度+实战经验+思维模…

国标GB28181视频平台EasyGBS如何解决安防视频融合与级联管理的核心痛点?

国标GB28181视频平台EasyGBS如何解决安防视频融合与级联管理的核心痛点?在平安城市、雪亮工程等大型安防项目中,如何解决不同品牌设备与平台之间的互联互通难题?本文深度解析基于国标GB/T28181协议的EasyGBS视频平台…

python基础-推导式

1.列表推导式 : 有规律的快速创建或者控制列表1.1 创建列表 eg: list1 = [ i for i in range(10)]1.2 带条件判断的列表推导式eg: list1 = [ i for i in range(50) if i % 3 == 0]3.多个for循环实现的列表推导式eg: …

人 CD 抗原完全指南 - un

设立分化簇 (CD) 命名系统的目的是对白细胞表面抗原进行分类。 最初,表面抗原是根据与它们结合的对应单克隆抗体进行命名。随着各实验室逐渐发现抗原常能刺激产生多种单克隆抗体,因此需要采用一种统一的命名系统。19…

IOT——维度追光框架

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Java入门知识

Java的特性和优势 简单性 面向对象 可移植性 (“Write once ,run anywhere”) 高性能 分布式 动态性 (反射机制) 多线程 (同时进行) 安全性 (异常机制,防病毒防篡改) 健壮性 在学习过程中爱上它,能够不断主动…

从ppm到ppb:全面解读浓度单位转换的诀窍 - 实践

从ppm到ppb:全面解读浓度单位转换的诀窍 - 实践2025-09-18 17:41 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display…

Scikit-learn 简单介绍入门和常用API汇总 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

AUTOSAR网络管理

汽车行业的网络管理一般有两种,一种是AutoSar另一种是OSEK,为啥汽车要网络管理,其实是为了降低车辆电池消耗,当车辆不工作时所有总线上的ECU通讯模块或整个ECU处于低功耗状态。网络管理一般用在电池供电的ECU,比如…

写用例注意点

写用例注意点: 1、测试标题 明确测试点 2、写用例的前几条用例都是主要场景的用例先写 微信个人能发微信红包 微信群发能发拼手气红包 微信群发能发拼手气红包 微信群发能发专属气红包 3、测试标题尽量写内容不要写案…

12 路低延迟推流!米尔 RK3576 赋能智能安防 360 环视

在智慧城市建设加速与社区安防需求升级的双重驱动下,“360 无死角监控 + 实时响应” 已成为安防领域的核心诉求。传统监控方案常受限于摄像头接入数量不足、编解码效率低、推流延迟高三大痛点,难以覆盖社区、园区等复…

Xilinx DDR3仿真 DBG

Xilinx DDR3仿真 DBG emmmm,其实这个错误不用去管,直接运行也不影响仿真的进行。 https://blog.csdn.net/qq_42959171/article/details/139726943

A公司一面:类加载的过程是怎么样的? 双亲委派的优点和缺点? 产生fullGC的情况有哪些? spring的动态代理有哪些?区别是什么? 如何排查CPU使用率过高?

A公司一面:类加载的过程是怎么样的? 双亲委派的优点和缺点? 产生fullGC的情况有哪些? spring的动态代理有哪些?区别是什么? 如何排查CPU使用率过高?摘要 A公司的面经JVM的类加载的过程是怎么样的? 双亲委派模型…

redis-hash类型参数基本命令

redis-hash类型参数基本命令redis存储数据的value可以是hash类型的,也称之为hash表,字典等。hash表就是一个map,由key-value组成。 我们把hash表的key称为field,值称为value。注意:redis的hash表的field和value都…