最近发布了Windows终端的新版本。一切都会好起来的,但是她的滚动条的性能仍有很多不足之处。因此,是时候向他稍加黏附并演奏手鼓了。用户通常会对新版本的任何应用程序做什么?没错,正是测试人员没有做的。因此,在将终端短暂用于预期目的之后,我开始使用它进行可怕的事情。好吧,好吧,我只是将咖啡洒在键盘上,并在擦拭时不小心夹住了<Enter>键。结局是什么?是的,它看起来并不令人印象深刻,但不要急于向我扔石头。注意右侧。首先尝试找出她的问题所在。这是提示的屏幕截图:当然,文章的标题是一个严重的破坏者。 :)因此,滚动条存在问题。越过下边框,移至新行很多次了,通常希望滚动条出现,然后可以向上滚动。但是,直到我们编写带有输出内容的命令后,这种情况才会发生。我们只说行为很奇怪。但是,如果滚动条可以正常工作,这可能并不是那么重要。经过一些测试,我发现切换到新行不会增加缓冲区。这仅使命令输出。因此,上述whoami仅会使缓冲区增加一行。因此,随着时间的流逝,我们将失去很多历史,尤其是在清除之后。我想到的第一件事就是使用我们的分析仪,看看它说了什么:结论当然是令人印象深刻的,因此,我将利用过滤和裁剪除包含ScrollBar的内容之外的所有内容的强大功能:我不能说有很多消息...好吧,也许那与缓冲区有关吗?分析仪没有失败,发现了一些有趣的东西。我在上面强调了此警告。让我们看看那里出了什么问题:V501。在'-'运算符的左侧和右侧有相同的子表达式:bufferHeight-bufferHeight TermControl.cpp 592bool TermControl::_InitializeTerminal()
{
....
auto bottom = _terminal->GetViewport().BottomExclusive();
auto bufferHeight = bottom;
ScrollBar().Maximum(bufferHeight - bufferHeight);
ScrollBar().Minimum(0);
ScrollBar().Value(0);
ScrollBar().ViewportSize(bufferHeight);
....
}
该代码带有注释:“设置ScrollViewer的高度以及我们用来伪造滚动高度的网格。”当然,模拟滚动高度是很好的,但是为什么我们将最大值设置为0?转到文档,很明显该代码不是很可疑。不要误会我的意思:从变量中减去一个变量当然是可疑的,但是我们在输出中得到的是零,这不会损害我们。无论如何,我尝试在“ 最大值”字段中指定默认值(1):滚动条出现了,但是它也不起作用:如果有的话,我将<Enter>固定了30秒。显然这不是问题,所以让它保持原样,除非将bufferHeight - bufferHeight替换为0:bool TermControl::_InitializeTerminal()
{
....
auto bottom = _terminal->GetViewport().BottomExclusive();
auto bufferHeight = bottom;
ScrollBar().Maximum(0);
ScrollBar().Minimum(0);
ScrollBar().Value(0);
ScrollBar().ViewportSize(bufferHeight);
....
}
因此,我们并不是特别接近解决问题。在没有更好的报价的情况下,请放进购物袋。起初,我们可以在更改的行上设置一个断点,但是我怀疑它是否可以以某种方式帮助我们。因此,我们首先需要找到负责视口相对于缓冲区偏移的片段。关于本地(最可能是其他)滚动条的工作原理。我们有一个大缓冲区来存储所有输出。为了与之交互,在屏幕上使用了某种抽象画法(在本例中为viewport)。使用这两个原语,我们可以了解问题所在。换行不会增加缓冲区,因此,我们无处可去。因此,问题就出在其中。有了这种平常的知识,我们继续英勇的背囊。在对该函数进行了一些漫步之后,我提请注意此片段:
auto onReceiveOutputFn = [this](const hstring str) {
_terminal->Write(str);
};
_connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn);
在配置完上面的ScrollBar之后,我们配置了各种回调函数并为新创建的窗口执行__connection.Start()。之后将调用上述lambda。由于这是我们第一次向缓冲区写入内容,因此建议从此处开始调试。我们在lambda中设置一个断点,然后查看_terminal:现在,我们有两个对我们极为重要的变量-_buffer和_mutableViewport。在它们上放置断点,并找到它们的变化点。没错,使用_viewport,我会作弊一点,并将断点放在变量本身上,而不是变量的顶部字段上(我们只需要它)。现在单击<F5>,没有任何反应...好吧,让我们按几十次<Enter>。什么都没有发生。显然,在_buffer上,我们过分鲁 set地设置了一个断点,而_viewport仍然如预期那样保留在缓冲区的顶部,并且其大小并未增加。在这种情况下,输入命令以更新顶点是有意义的。_viewport。之后,我们编写了一段非常有趣的代码:void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
{
....
if (cursorPosAfter.Y > _mutableViewport.BottomInclusive())
{
const auto newViewTop =
std::max(0, cursorPosAfter.Y - (_mutableViewport.Height() - 1));
if (newViewTop != _mutableViewport.Top())
{
_mutableViewport = Viewport::FromDimensions(....);
notifyScroll = true;
}
}
....
}
我指出了我们停下来的地方。如果您查看有关该片段的评论,很显然我们比以往任何时候都更接近解决方案。正是在这个地方,可见部分相对于缓冲区发生了移动,我们有了滚动的机会。观察了一下行为之后,我注意到了一个有趣的观点:移至新行时,cursorPosAfter.Y变量的值等于视口的值,因此我们没有忽略它,没有任何效果。另外,newViewTop变量也存在类似的问题。因此,让我们将cursorPosAfter.Y的值增加一,然后看看发生了什么:void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
{
....
if (cursorPosAfter.Y + 1 > _mutableViewport.BottomInclusive())
{
const auto newViewTop =
std::max(0, cursorPosAfter.Y + 1 - (_mutableViewport.Height() - 1));
if (newViewTop != _mutableViewport.Top())
{
_mutableViewport = Viewport::FromDimensions(....);
notifyScroll = true;
}
}
....
}
以及启动结果:奇迹!我输入了数量,滚动条起作用了。没错,直到我们介绍一些内容为止……为了演示该文件,我将附上gif:显然,我们正在做一些额外的跳转到新的一行。然后让我们尝试使用X坐标来限制过渡,我们只会在X为0 时移动直线:void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
{
....
if ( proposedCursorPosition.X == 0
&& proposedCursorPosition.Y == _mutableViewport.BottomInclusive())
{
proposedCursorPosition.Y++;
}
ursor.SetPosition(proposedCursorPosition);
const COORD cursorPosAfter = cursor.GetPosition();
if (cursorPosAfter.Y > _mutableViewport.BottomInclusive())
{
const auto newViewTop =
std::max(0, cursorPosAfter.Y - (_mutableViewport.Height() - 1));
if (newViewTop != _mutableViewport.Top())
{
_mutableViewport = Viewport::FromDimensions(....);
notifyScroll = true;
}
}
....
}
上面写的片段将移动光标的Y坐标。然后我们更新光标位置。从理论上讲,这应该起作用。发生了什么事?好吧,当然更好。但是,存在的问题是我们移动输出点,但不移动缓冲区。因此,我们看到同一命令的两个调用。当然,似乎我知道自己在做什么,但事实并非如此。:)至此,我决定检查缓冲区的内容,因此回到了开始调试的地方:
auto onReceiveOutputFn = [this](const hstring str) {
_terminal->Write(str);
};
_connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn);
我在与上次相同的位置设置了一个断点,然后开始查看str变量的内容。让我们从我在屏幕上看到的内容开始:当我按<Enter>键时,
您认为str字符串中会有什么?- 字符串“ LONG description”。
- 我们现在看到的整个缓冲区。
- 整个缓冲区,但没有第一行。
我不会疲倦-整个缓冲区,但是没有第一行。这是一个很大的问题,因为正是由于这个原因,我们正在失去历史性的,有目的的。这是我们的帮助输出片段在换行后的样子:我用箭头标记了“ LONG DESCRIPTOIN”所在的位置。也许然后用一行偏移量覆盖缓冲区?如果没有每次打喷嚏都调用此回调,则此方法有效。调用它时,我至少发现了三种情况,- 当我们输入任何字符时;
- 当我们穿越历史时;
- 当我们执行命令时。
问题是仅在执行命令时才需要移动缓冲区,或者输入<Enter>。在其他情况下,这样做不是一个好主意。因此,我们需要以某种方式确定内部需要转移的内容。结论
本文试图说明PVS-Studio如何熟练地发现导致我注意到的错误的有缺陷的代码。关于从自身中减去变量的主题的信息极大地激励了我,因此我积极编写了本书。但是,正如您所看到的,我的喜悦还为时过早,结果一切都变得更加复杂。所以我决定停下来。一个人仍然可以度过几个晚上,但是我做的时间越长,出现的问题就越多。我所能做的就是希望Windows终端开发人员在修复此错误时祝您好运。:)我希望我不会让读者失望,因为我还没有完成研究,对我来说,在项目内部进行漫步很有趣。作为补偿,我建议使用#WindowsTerminal促销代码,由于该代码,您将在一周内而不是一个月内收到PVS-Studio的演示版。如果您实际上没有尝试过PVS-Studio静态分析仪,那么这就是一个很好的理由。只需在下载页面的“消息”字段中输入“ #WindowsTerminal” 。另外,借此机会,我想提醒您,很快将有一个在Linux和macOS下运行的C#分析器版本。现在,您可以注册进行初步测试。
如果您想与讲英语的读者分享这篇文章,请使用以下链接:Maxim Zvyagintsev。无法滚动的小滚动条。