Bilah gulir yang gagal


Baru-baru ini merilis versi baru Terminal Windows. Semuanya akan baik-baik saja, tetapi kinerja bilah gulirnya meninggalkan banyak yang harus diinginkan. Karena itu, sekarang saatnya untuk menempelkan sedikit tongkat padanya dan memainkan rebana.

Apa yang biasanya dilakukan pengguna dengan versi baru aplikasi apa pun? Itu benar, apa yang tidak dilakukan oleh penguji. Karena itu, setelah menggunakan terminal secara singkat untuk tujuan yang dimaksudkan, saya mulai melakukan hal-hal buruk dengannya. Baiklah, baiklah, saya baru saja menumpahkan kopi di keyboard dan tanpa sengaja menjepit <Enter> ketika saya menyeka. Apa yang terjadi pada akhirnya?



Ya, sepertinya tidak terlalu mengesankan, tapi jangan buru-buru melempari saya dengan batu. Perhatikan sisi kanan. Pertama-tama cobalah mencari tahu apa yang salah dengannya. Berikut screenshot untuk sebuah petunjuk:


Tentu saja, judul artikel itu adalah spoiler yang serius. :)

Jadi, ada masalah dengan scrollbar. Pindah ke baris baru berkali-kali, setelah melewati batas bawah, Anda biasanya mengharapkan bilah gulir muncul, dan Anda dapat menggulir ke atas. Namun, ini tidak terjadi sampai kita menulis perintah dengan output dari sesuatu. Anggap saja tingkah lakunya aneh. Namun, ini mungkin tidak terlalu kritis jika scrollbar berfungsi ...

Setelah diuji sedikit, saya menemukan bahwa beralih ke baris baru tidak meningkatkan buffer. Ini hanya membuat output dari perintah. Jadi whoami di atas akan meningkatkan buffer hanya dengan satu baris. Karena itu, lama kelamaan kita akan kehilangan banyak sejarah, terutama setelah jelas.

Hal pertama yang muncul di benak saya adalah menggunakan penganalisa kami dan melihat apa yang dikatakannya:


Kesimpulannya, tentu saja, dari ukuran yang mengesankan, jadi saya akan mengambil keuntungan dari kekuatan pemfilteran dan memotong segala sesuatu kecuali yang mengandung ScrollBar :


Saya tidak bisa mengatakan bahwa ada banyak pesan ... Ya, mungkin kemudian ada sesuatu yang terkait dengan buffer?


Penganalisa tidak gagal dan menemukan sesuatu yang menarik. Saya menyoroti peringatan ini di atas. Mari kita lihat apa yang salah di sana:

V501 . Ada sub-ekspresi identik ke kiri dan ke kanan operator '-': 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);
  ....
}

Kode ini disertai dengan komentar: "Siapkan ketinggian ScrollViewer dan kisi yang kami gunakan untuk memalsukan ketinggian gulir kami."

Mensimulasikan ketinggian gulir tentu saja bagus, tetapi mengapa kita menempatkan maksimum 0? Beralih ke dokumentasi , menjadi jelas bahwa kode tersebut tidak terlalu mencurigakan. Jangan salah paham: mengurangkan variabel dari dirinya sendiri, tentu saja, mencurigakan, tetapi kita mendapatkan nol pada output, yang tidak membahayakan kita. Bagaimanapun, saya mencoba menentukan nilai default (1) di bidang Maksimum :


Bilah gulir muncul, tetapi juga tidak berfungsi:



Jika ada, maka saya menjepit <Enter> selama 30 detik. Rupanya ini bukan masalah, jadi mari kita biarkan apa adanya, kecuali dengan mengganti bufferHeight - bufferHeight dengan 0:

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

  ScrollBar().Maximum(0); // <=   
  ScrollBar().Minimum(0);
  ScrollBar().Value(0);
  ScrollBar().ViewportSize(bufferHeight);
  ....
}

Jadi, kita tidak terlalu dekat dengan penyelesaian masalah. Dengan tidak adanya tawaran yang lebih baik untuk masuk ke debag. Pada awalnya kami bisa meletakkan breakpoint pada baris yang diubah, tetapi saya ragu itu akan membantu kami. Oleh karena itu, pertama-tama kita perlu menemukan fragmen yang bertanggung jawab atas offset Viewport relatif terhadap buffer.

Sedikit tentang cara kerja scrollbar lokal (dan kemungkinan besar lainnya). Kami memiliki satu buffer besar yang menyimpan semua output. Untuk berinteraksi dengannya, beberapa jenis abstraksi digunakan untuk menggambar di layar, dalam hal ini, viewport .

Dengan menggunakan dua primitif ini, kita dapat memahami apa masalah kita. Pergi ke baris baru tidak meningkatkan buffer, dan karena ini, kita tidak punya tempat untuk pergi. Karena itu, masalahnya ada di dalamnya.

Berbekal pengetahuan umum ini, kami melanjutkan debat heroik kami. Setelah sedikit berjalan di sekitar fungsi, saya menarik perhatian pada fragmen ini:

// 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);

Setelah kami mengkonfigurasi ScrollBar di atas , kami mengkonfigurasi berbagai fungsi panggilan balik dan menjalankan __connection.Start () untuk jendela baru kami yang dicetak. Setelah itu lambda di atas disebut. Karena ini adalah pertama kalinya kami menulis sesuatu ke buffer, saya sarankan memulai debug kami dari sana.

Kami menetapkan breakpoint di dalam lambda dan mencari di _terminal :



Sekarang kita memiliki dua variabel yang sangat penting bagi kita - _buffer dan _mutableViewport . Letakkan breakpoint pada mereka dan temukan di mana mereka berubah. Benar, dengan _viewport saya akan sedikit curang dan meletakkan breakpoint bukan pada variabel itu sendiri, tetapi pada bidang atasnya (kita hanya membutuhkannya).

Sekarang klik pada <F5>, dan tidak ada yang terjadi ... Baiklah, maka mari kita tekan beberapa lusin kali <Enter>. Tidak ada yang terjadi. Rupanya, pada _buffer kami menetapkan breakpoint terlalu ceroboh, dan _viewport , seperti yang diharapkan, tetap di bagian atas buffer, yang tidak bertambah besar.

Dalam hal ini, masuk akal untuk memasukkan perintah yang menyebabkan titik diperbarui._viewport . Setelah itu, kami bangun dengan sepotong kode yang sangat menarik:

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

Saya menunjukkan komentar di mana kami tinggalkan. Jika Anda melihat komentar pada fragmen itu, menjadi jelas bahwa kami lebih dekat dengan solusi daripada sebelumnya. Di tempat inilah bagian yang terlihat bergeser relatif ke buffer, dan kami mendapat kesempatan untuk menggulir. Setelah mengamati perilaku sedikit, saya perhatikan satu hal menarik: ketika pindah ke baris baru, nilai kursorPosAfter.Y variabel sama dengan nilai viewport , jadi kami tidak menghilangkannya dan tidak ada yang berhasil. Selain itu, ada masalah serupa dengan variabel newViewTop . Oleh karena itu, mari kita tambahkan nilai cursorPosAfter.Y per satu dan lihat apa yang terjadi:

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

Dan hasil peluncurannya:


Keajaiban! Saya memasukkan jumlah dan bilah gulir berfungsi. Benar, sampai saat kami memperkenalkan sesuatu ... Untuk mendemonstrasikan file, saya akan melampirkan gif:


Tampaknya, kami melakukan beberapa lompatan ekstra ke baris baru. Mari kita coba membatasi transisi kita menggunakan koordinat X. Kita hanya akan menggeser garis ketika X adalah 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;
    }
  }
  ....
}

Fragmen yang dituliskan di atas akan menggeser koordinat Y untuk kursor. Kemudian kami memperbarui posisi kursor. Secara teori, ini harus berhasil ... Apa yang terjadi?



Yah, tentu saja, lebih baik. Namun, ada masalah saat kami menggeser titik output, tetapi jangan menggeser buffer. Karena itu, kita melihat dua panggilan dari perintah yang sama. Tentu saja, sepertinya saya tahu apa yang saya lakukan, tetapi tidak demikian. :)

Pada titik ini, saya memutuskan untuk memeriksa isi buffer, jadi saya kembali ke titik di mana saya memulai debug:

// 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);

Saya menetapkan breakpoint di tempat yang sama seperti terakhir kali, dan mulai melihat isi dari variabel str . Mari kita mulai dengan apa yang saya lihat di layar saya:


Menurut Anda apa yang akan ada di string str ketika saya menekan <Enter>?

  1. String "DESKRIPSI PANJANG" .
  2. Seluruh buffer yang sekarang kita lihat.
  3. Seluruh buffer, tetapi tanpa baris pertama.

Saya tidak akan merana - seluruh buffer, tetapi tanpa baris pertama. Dan ini adalah masalah yang cukup besar, karena justru karena inilah kita kehilangan sejarah, terlebih lagi, secara langsung. Beginilah tampilan fragmen hasil bantuan kami setelah menuju ke baris baru:


Dengan panah aku menandai tempat "LONG DESCRIPTOIN" berada . Mungkin kemudian menimpa buffer dengan offset satu baris? Ini akan berhasil jika panggilan balik ini tidak dipanggil untuk setiap bersin.

Saya telah menemukan setidaknya tiga situasi ketika dipanggil,

  • Ketika kita memasukkan karakter apa pun;
  • Saat kita menelusuri sejarah;
  • Ketika kita menjalankan perintah.

Masalahnya adalah Anda hanya perlu memindahkan buffer saat kami menjalankan perintah, atau masukkan <Enter>. Dalam kasus lain, melakukan ini adalah ide yang buruk. Jadi kita perlu entah bagaimana menentukan di dalam apa yang perlu digeser.

Kesimpulan


Gambar 18


Artikel ini adalah upaya untuk menunjukkan betapa terampilnya PVS-Studio dapat menemukan kode yang rusak yang mengarah ke kesalahan yang saya perhatikan. Pesan tentang topik mengurangkan variabel dari dirinya sendiri sangat memotivasi saya, dan saya mulai menulis teks. Tetapi seperti yang Anda lihat, kegembiraan saya terlalu dini dan semuanya ternyata jauh lebih rumit.

Jadi saya memutuskan untuk berhenti. Seseorang masih bisa menghabiskan beberapa malam, tetapi semakin lama saya melakukan ini, semakin banyak masalah muncul. Yang bisa saya lakukan adalah berharap para pengembang Terminal Windows semoga berhasil dalam memperbaiki bug ini. :)

Saya harap saya tidak mengecewakan pembaca bahwa saya tidak menyelesaikan penelitian dan menarik bagi saya untuk berjalan-jalan di sepanjang bagian dalam proyek. Sebagai kompensasi, saya sarankan menggunakan kode promo #WindowsTerminal, terima kasih Anda akan menerima versi demo PVS-Studio tidak selama seminggu, tetapi langsung selama sebulan. Jika Anda belum mencoba penganalisis statis PVS-Studio dalam praktiknya, ini adalah alasan yang tepat untuk melakukan hal itu. Cukup masukkan "#WindowsTerminal" di bidang "Pesan" di halaman unduhan .

Dan juga, mengambil kesempatan ini, saya ingin mengingatkan Anda bahwa segera akan ada versi penganalisa C # yang bekerja di Linux dan macOS. Dan sekarang Anda dapat mendaftar untuk pengujian pendahuluan.


Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan ke terjemahan: Maxim Zvyagintsev. Scrollbar Kecil Yang Tidak Bisa .

All Articles