﻿# Скроллбар, который не смог

Недавно вышла новая версия Windows Terminal\. Всё бы ничего, но работоспособность её скроллбара оставляла желать лучшего\. Поэтому настало время немного потыкать в него палкой и сыграть на бубне\.

![0718_WinTerm_ru/image1.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image1.png)

Что обычно пользователи делают с новой версией любого приложения? Правильно, именно то, что не делали тестеры\. Поэтому после недолгого использования терминала по назначению, я начал делать с ним страшные вещи\. Хорошо\-хорошо, я просто пролил кофе на клавиатуру и случайно зажал <Enter\>, когда протирал ее\. Что в итоге получилось?

![0718_WinTerm_ru/image2.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image2.png)

Да, выглядит не очень впечатляюще, но не спешите бросать в меня камни\. Обратите внимание на правую часть\. Попробуйте сперва сами догадаться, что с ней не так\. Вот вам скриншот для подсказки:

![0718_WinTerm_ru/image3.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image3.png)

Конечно, название статьи было серьёзным спойлером\. :\) 

Итак, есть проблема со скроллбаром\. Переходя на новую строку много раз, после пересечения нижней границы, обычно ожидаешь, что появится скроллбар, и можно будет пролистать наверх\. Однако этого не происходит до того момента, как мы не напишем какую\-либо команду с выводом чего\-либо\. Скажем так, поведение странное\. Впрочем, это могло быть не так критично, если бы скроллбар работал\.\.\. 

Немного потестировав, я обнаружил, что переход на новую строку не увеличивает буфер\. Это делает исключительно вывод команд\. Так что вышенаписанная _whoami _увеличит буфер только на одну строку\. Из\-за этого со временем мы потеряем много истории, особенно после _clear\._

Первое, что мне пришло на ум – это воспользоваться нашим анализатором и посмотреть, что он скажет:

![0718_WinTerm_ru/image5.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image5.png)

Вывод, конечно, внушительных размеров, поэтому я воспользуюсь могуществом фильтрации и обрежу всё, кроме содержащего _ScrollBar_:

![0718_WinTerm_ru/image7.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image7.png)

Не могу сказать, что сообщений много\.\.\. Хорошо, может быть тогда есть что\-либо связанное с буфером?

![0718_WinTerm_ru/image9.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image9.png)

Анализатор не подвел и нашел что\-то интересное\. Я выделил это предупреждение выше\. Давайте посмотрим, что там не так:

[**V501**](https://pvs-studio.ru/ru/docs/warnings/v501/)\. There are identical sub\-expressions to the left and to the right of the '\-' operator: bufferHeight \- bufferHeight TermControl\.cpp 592

```cpp
bool TermControl::_InitializeTerminal()
{
  ....
  auto bottom = _terminal->GetViewport().BottomExclusive();
  auto bufferHeight = bottom;

  ScrollBar().Maximum(bufferHeight - bufferHeight); // <= Ошибка тут
  ScrollBar().Minimum(0);
  ScrollBar().Value(0);
  ScrollBar().ViewportSize(bufferHeight);
  ....
}
```

Этот код сопровождается комментарием: _"Set up the height of the ScrollViewer and the grid we're using to fake our scrolling height"\._ 

Имитация высоты прокрутки – это, конечно, хорошо, но почему в максимум мы проставляем 0? Обратившись к [документации](https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.primitives.scrollbar?view=winrt-19041), стало понятно, что код не сильно подозрителен\. Не поймите меня неправильно: вычитание переменной из самой себя, конечно, подозрительно, но мы получаем на выходе ноль, который нам не вредит\. В любом случае, я попробовал указать в поле _Maximum_ значение по умолчанию \(1\): 

![0718_WinTerm_ru/image10.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image10.png)

Скроллбар появился, но он так же не работает:

![0718_WinTerm_ru/image11.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image11.png)

Если что, то я зажал <Enter\> секунд на 30\. Видимо проблема не в этом, поэтому оставим как было, разве что, заменив _bufferHeight_ – _bufferHeight_ на 0: 

```cpp
bool TermControl::_InitializeTerminal()
{
  ....
  auto bottom = _terminal->GetViewport().BottomExclusive();
  auto bufferHeight = bottom;

  ScrollBar().Maximum(0); // <= Замена случилась тут
  ScrollBar().Minimum(0);
  ScrollBar().Value(0);
  ScrollBar().ViewportSize(bufferHeight);
  ....
}
```

Итак, к решению проблемы мы не особенно приблизились\. За неимением лучшего предлагаю отправиться в дебаг\. Сперва мы могли бы поставить точку останова \(breakpoint\) на измененной строке, но сомневаюсь, что это нам как\-то поможет\. Поэтому нам нужно сперва найти фрагмент, который отвечает за смещение Viewport'а относительно буфера\. 

Немного о том, как устроен местный \(и, скорее всего, любой другой\) скроллбар\. Есть у нас один большой буфер, который хранит весь вывод\. Для взаимодействия с ним используется какая\-либо абстракция для отрисовки на экран, в данном случае – _viewport_\.

Используя эти два примитива, можно понять в чем заключается наша проблема\. Переход на новую строку не увеличивает буфер, и из\-за этого нам просто некуда подниматься\. Следовательно, проблема именно в нем\.

Вооружившись этими банальными знаниями, продолжим наш героический дебаг\. Немного погуляв по функции, я обратил внимание на этот фрагмент:

```cpp
// 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_, мы настраиваем различные callback\-функции и выполняем _\_\_connection\.Start\(\)_ для нашего новоиспечённого окна\. После чего происходит вызов вышеуказанной лямбды\. Так как это первый раз, когда мы пишем что\-то в буфер, предлагаю начать наш дебаг именно оттуда\.

Ставим точку останова внутри лямбды и заглядываем в _\_terminal_:

![0718_WinTerm_ru/image12.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image12.png)

Теперь у нас есть две крайне важных для нас переменных – _\_buffer_ и _\_mutableViewport_\. Поставим на них точки останова и найдём, где они меняются\. Правда, с _\_viewport_ я немного схитрю и поставлю точку останова не на саму переменную, а на ее поле _top_ \(оно нам как раз и нужно\)\.

Теперь нажимаем на <F5\>, и ничего не происходит\.\.\. Хорошо, тогда давайте нажмём пару десятков раз <Enter\>\. Ничего не произошло\. Судя по всему, на _\_buffer_ мы поставили точку останова слишком опрометчиво, а _\_viewport_, как и ожидалось, оставался на вершине буфера, который не увеличивался в размере\.

В таком случае есть смысл ввести команду, чтобы вызвать обновление вершины _\_viewport_\. После этого мы встали на очень любопытном фрагменте кода:

```cpp
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_ равно значению _viewport'а_, поэтому мы его не опускаем, и ничего не работает\. К тому же есть аналогичная проблема с переменной _newViewTop_\. Поэтому давайте увеличим значение _cursorPosAfter\.Y_ на один и посмотрим, что получилось:

```cpp
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;
    }
  }
  ....
}
```

И результат запуска:

![0718_WinTerm_ru/image13.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image13.png)

Чудеса\! Я ввёл n\-e количество, и скроллбар работает\. Правда, до того момента, как мы введем что\-либо\.\.\. Для демонстрации фейла, я приложу гифку:

![0718_WinTerm_ru/image14.gif](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image14.gif)

Судя по всему, мы делаем несколько лишних переходов на новую строку\. Давайте тогда попробуем ограничить наши переходы, при помощи координаты X\. Будем смещать строку только тогда, когда _X_ равен 0:

```cpp
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_ для курсора\. После чего мы обновляем позицию курсора\. В теории это должно сработать\.\.\. Что у нас вышло?

![0718_WinTerm_ru/image15.gif](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image15.gif)

Ну, уже, конечно, лучше\. Однако есть проблема в том, что мы смещаем точку вывода, но при этом не сдвигаем буфер\. Поэтому мы видим два вызова одной и той же команды\. Может, конечно, показаться, что я знаю, что делаю, но это не так\. :\)

На этом этапе я решил проверить содержимое буфера, поэтому вернулся к моменту с которого начал дебаг:

```cpp
// 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_\. Начнём с того, что я видел на моём экране:

![0718_WinTerm_ru/image16.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image16.png)

Как вы думаете, что будет в строке _str_, когда я нажму <Enter\>?

* Строка _"LONG DESCRIPTION"_\.
* Весь буфер, который мы сейчас видим\.
* Весь буфер, но без первой строки\.

Не буду томить – весь буфер, но без первой строки\. И это немалая проблема, так как именно из\-за этого мы и теряем историю, причем точечно\. Вот так будет выглядеть наш фрагмент вывода _help_ после перехода на новую строку:

![0718_WinTerm_ru/image17.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image17.png)

Стрелочкой я отметил место, где был _"LONG DESCRIPTOIN"_\. Может быть тогда перезаписывать буфер со смещением на одну строку? Это бы сработало, если бы этот callback вызывался не на каждый чих\.

Я обнаружил как минимум три ситуации, когда он вызывается,

* Когда мы вводим любой символ;
* Когда мы двигаемся по истории;
* Когда мы выполняем команду\.

Проблема в том, что смещать буфер нужно только тогда, когда мы выполняем команду, или же вводим <Enter\>\. В остальных случаях делать это – плохая идея\. Значит нам нужно как\-то определить внутри, что нужно сместить\.

## Заключение

![0718_WinTerm_ru/image18.png](https://import.viva64.com/docx/blog/0718_WinTerm_ru/image18.png)

Эта статья была попыткой показать, как ловко PVS\-Studio смог найти дефектный код, приводящий к замеченной мной ошибке\. Сообщение на тему вычитания переменной самой из себя меня сильно мотивировало, и я бодро приступил к написанию текста\. Но как видите, моя радость была преждевременной и всё оказалось гораздо сложнее\.

Поэтому я решил остановиться\. Можно было бы ещё потратить пару вечерков, но чем дольше я этим занимался, тем больше и больше проблем возникало\. Всё, что я могу сделать, так это пожелать удачи разработчикам Windows Terminal в исправлении этого бага\. :\)

Надеюсь, я не сильно разочаровал читателя, что так и не довёл исследования до конца и ему было интересно вместе со мной совершить прогулку по внутренностям проекта\. В качестве компенсации, я предлагаю воспользоваться промокодом \#WindowsTerminal, благодаря которому вы получите демонстрационную версию PVS\-Studio не на неделю, а сразу на месяц\. Если вы ещё не пробовали статический анализатор PVS\-Studio в деле, это хороший повод как раз это сделать\. Просто введите "\#WindowsTerminal" в поле "Message" на [странице загрузки](https://pvs-studio.ru/ru/pvs-studio/download/)\.

А ещё, пользуясь случаем, хочу напомнить, что скоро появится версия C\# анализатора, работающая под Linux и macOS\. И уже сейчас можно записаться на предварительное тестирование\.