滚动条失败


最近发布了Windows终端的新版本。一切都会好起来的,但是她的滚动条的性能仍有很多不足之处。因此,是时候向他稍加黏附并演奏手鼓了。

用户通常会对新版本的任何应用程序做什么?没错,正是测试人员没有做的。因此,在将终端短暂用于预期目的之后,我开始使用它进行可怕的事情。好吧,好吧,我只是将咖啡洒在键盘上,并在擦拭时不小心夹住了<Enter>键。结局是什么?



是的,它看起来并不令人印象深刻,但不要急于向我扔石头。注意右侧。首先尝试找出她的问题所在。这是提示的屏幕截图:


当然,文章的标题是一个严重的破坏者。 :)

因此,滚动条存在问题。越过下边框,移至新行很多次了,通常希望滚动条出现,然后可以向上滚动。但是,直到我们编写带有输出内容的命令后,这种情况才会发生。我们只说行为很奇怪。但是,如果滚动条可以正常工作,这可能并不是那么重要。

经过一些测试,我发现切换到新行不会增加缓冲区。这仅使命令输出。因此,上述whoami仅会使缓冲区增加一行。因此,随着时间的流逝,我们将失去很多历史,尤其是在清除之后

我想到的第一件事就是使用我们的分析仪,看看它说了什么:


结论当然是令人印象深刻的,因此,我将利用过滤和裁剪除包含ScrollBar的内容之外的所有内容的强大功能


我不能说有很多消息...好吧,也许那与缓冲区有关吗?


分析仪没有失败,发现了一些有趣的东西。我在上面强调了此警告。让我们看看那里出了什么问题:

V501在'-'运算符的左侧和右侧有相同的子表达式:bufferHeight-bufferHeight TermControl.cpp 592

bool 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)

使用这两个原语,我们可以了解问题所在。换行不会增加缓冲区,因此,我们无处可去。因此,问题就出在其中。

有了这种平常的知识,我们继续英勇的背囊。在对该函数进行了一些漫步之后,我提请注意此片段:

// This event is explicitly revoked in the destructor: does not need weak_ref
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)
{
  ....
  // Move the viewport down if the cursor moved below the viewport.
  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)
{
  ....
  // Move the viewport down if the cursor moved below the viewport.
  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++;
  }

  // Update Cursor Position
  ursor.SetPosition(proposedCursorPosition);

  const COORD cursorPosAfter = cursor.GetPosition();

  // Move the viewport down if the cursor moved below the viewport.
  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坐标然后我们更新光标位置。从理论上讲,这应该起作用。发生了什么事?



好吧,当然更好。但是,存在的问题是我们移动输出点,但不移动缓冲区。因此,我们看到同一命令的两个调用。当然,似乎我知道自己在做什么,但事实并非如此。:)

至此,我决定检查缓冲区的内容,因此回到了开始调试的地方:

// This event is explicitly revoked in the destructor: does not need weak_ref
auto onReceiveOutputFn = [this](const hstring str) {
  _terminal->Write(str);
};
_connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn);

我在与上次相同的位置设置了一个断点,然后开始查看str变量的内容让我们从我在屏幕上看到的内容开始:


当我按<Enter>键时, 您认为str字符串中会有什么

  1. 字符串“ LONG description”
  2. 我们现在看到的整个缓冲区。
  3. 整个缓冲区,但没有第一行。

我不会疲倦-整个缓冲区,但是没有第一行。这是一个很大的问题,因为正是由于这个原因,我们正在失去历史性的,有目的的。这是我们的帮助输出片段在换行后的样子


我用箭头标记了“ LONG DESCRIPTOIN”所在的位置也许然后用一行偏移量覆盖缓冲区?如果没有每次打喷嚏都调用此回调,则此方法有效。

调用它时,我至少发现了三种情况,

  • 当我们输入任何字符时;
  • 当我们穿越历史时;
  • 当我们执行命令时。

问题是仅在执行命令时才需要移动缓冲区,或者输入<Enter>。在其他情况下,这样做不是一个好主意。因此,我们需要以某种方式确定内部需要转移的内容。

结论


图片18


本文试图说明PVS-Studio如何熟练地发现导致我注意到的错误的有缺陷的代码。关于从自身中减去变量的主题的信息极大地激励了我,因此我积极编写了本书。但是,正如您所看到的,我的喜悦还为时过早,结果一切都变得更加复杂。

所以我决定停下来。一个人仍然可以度过几个晚上,但是我做的时间越长,出现的问题就越多。我所能做的就是希望Windows终端开发人员在修复此错误时祝您好运。:)

我希望我不会让读者失望,因为我还没有完成研究,对我来说,在项目内部进行漫步很有趣。作为补偿,我建议使用#WindowsTerminal促销代码,由于该代码,您将在一周内而不是一个月内收到PVS-Studio的演示版。如果您实际上没有尝试过PVS-Studio静态分析仪,那么这就是一个很好的理由。只需在下载页面的“消息”字段中输入“ #WindowsTerminal”

另外,借此机会,我想提醒您,很快将有一个在Linux和macOS下运行的C#分析器版本。现在,您可以注册进行初步测试。


如果您想与讲英语的读者分享这篇文章,请使用以下链接:Maxim Zvyagintsev。无法滚动的小滚动条

All Articles