شريط التمرير الذي فشل


أصدرت مؤخرًا إصدارًا جديدًا من Windows Terminal. كل شيء سيكون على ما يرام ، لكن أداء شريط التمرير الخاص بها ترك الكثير مما هو مرغوب فيه. لذلك ، حان الوقت لإلصاق عصا صغيرة به ولعب الدف.

ماذا يفعل المستخدمون عادةً بالإصدار الجديد من أي تطبيق؟ هذا صحيح ، بالضبط ما لم يفعله المختبرون. لذلك ، بعد استخدام قصير للمحطة للغرض المقصود ، بدأت في فعل أشياء رهيبة معها. حسنًا ، حسنًا ، لقد انسكبت قهوة على لوحة المفاتيح وفرضت <Enter> بطريق الخطأ عندما مسحتها. ماذا حدث في النهاية؟



نعم ، إنها لا تبدو مثيرة للإعجاب للغاية ، لكن لا تتسرع في رمي الحجارة علي. انتبه إلى الجانب الأيمن. حاول أولاً معرفة ما هو الخطأ معها. إليك لقطة شاشة لتلميح:


بالطبع ، كان عنوان المقال مفسدًا خطيرًا. :)

لذا ، هناك مشكلة في شريط التمرير. الانتقال إلى خط جديد عدة مرات ، بعد عبور الحد السفلي ، تتوقع عادةً ظهور شريط التمرير ، ويمكنك التمرير لأعلى. ومع ذلك ، لا يحدث هذا حتى نكتب أمرًا بإخراج شيء ما. دعنا نقول فقط أن السلوك غريب. ومع ذلك ، قد لا يكون هذا مهمًا للغاية إذا عمل شريط التمرير ...

بعد الاختبار قليلاً ، وجدت أن التبديل إلى خط جديد لا يزيد من المخزن المؤقت. هذا فقط يجعل إخراج الأوامر. لذا فإن whoami أعلاه سيزيد من المخزن المؤقت بخط واحد فقط. وبسبب هذا ، سنفقد الكثير من التاريخ بمرور الوقت ، خاصة بعد فترة واضحة.

كان أول ما يتبادر إلى ذهني هو استخدام محللنا ومعرفة ما يقوله:


الاستنتاج ، بطبيعة الحال ، ذو حجم مثير للإعجاب ، لذلك سأستفيد من قوة التصفية وقص كل شيء ما عدا ذلك الذي يحتوي على شريط التمرير :


لا أستطيع أن أقول أن هناك الكثير من الرسائل ... حسنًا ، ربما هناك شيء متعلق بالعازل؟


لم يفشل المحلل ووجد شيئًا مثيرًا للاهتمام. أبرزت هذا التحذير أعلاه. دعونا نرى ما هو الخطأ هناك:

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

يرافق هذا الرمز تعليق: "قم بإعداد ارتفاع برنامج التمرير والشبكة التي نستخدمها لتزييف ارتفاع التمرير."

محاكاة ارتفاع التمرير جيدة بالطبع ، ولكن لماذا نضع 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);
  ....
}

لذا ، نحن لسنا قريبين بشكل خاص من حل المشكلة. في حالة عدم وجود عرض أفضل للذهاب إلى debag. في البداية يمكننا وضع نقطة توقف على الخط المتغير ، ولكن أشك في أنه سيساعدنا بطريقة ما. لذلك ، نحتاج أولاً إلى العثور على الجزء المسؤول عن إزاحة إطار العرض نسبة إلى المخزن المؤقت.

القليل عن كيفية عمل شريط التمرير المحلي (وعلى الأرجح أي شريط آخر). لدينا مخزن مؤقت كبير يخزن كل المخرجات. للتفاعل معها ، يتم استخدام نوع من التجريد للرسم على الشاشة ، في هذه الحالة ، إطار العرض .

باستخدام هذين البدائيين ، يمكننا أن نفهم ما هي مشكلتنا. لا يؤدي الانتقال إلى خط جديد إلى زيادة المخزن المؤقت ، ولهذا السبب ليس لدينا مكان نذهب إليه. لذلك ، المشكلة في ذلك.

مسلحين بهذه المعرفة الشائعة ، نواصل تصحيحنا البطولي. بعد المشي قليلاً حول الوظيفة ، لفتت الانتباه إلى هذا الجزء:

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

بعد تكوين شريط التمرير أعلاه ، نقوم بتكوين العديد من وظائف رد الاتصال وتنفيذ __connection.Start () لنافذة جديدة تم سكها. وبعد ذلك تسمى لامدا أعلاه. نظرًا لأن هذه هي المرة الأولى التي نكتب فيها شيئًا إلى المخزن المؤقت ، أقترح بدء تصحيح الأخطاء من هناك.

نضع نقطة توقف داخل لامدا ونبحث في _terminal :



الآن لدينا متغيرين مهمان للغاية بالنسبة لنا - _buffer و _mutableViewport . ضع نقاط توقف عليها واعثر على مكان تغيرها. صحيح ، مع _viewport ، سأغش قليلاً وأضع نقطة توقف ليس على المتغير نفسه ، ولكن في مجاله العلوي (نحن بحاجة إليه فقط).

انقر الآن على <F5> ، ولن يحدث شيء ... حسنًا ، فلنضرب بضع مرات <Enter>. لم يحدث شيء. على ما يبدو ، في _buffer ، وضعنا نقطة توقف بشكل متهور للغاية ، وظل _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;
    }
  }
  ....
}

أشرت إلى تعليق حيث توقفنا. إذا نظرت إلى التعليق على الجزء ، يصبح من الواضح أننا أقرب إلى الحل من أي وقت مضى. في هذا المكان يتم نقل الجزء المرئي بالنسبة إلى المخزن المؤقت ، ونحصل على فرصة التمرير. بعد أن لاحظت السلوك قليلاً ، لاحظت نقطة واحدة مثيرة للاهتمام: عند الانتقال إلى سطر جديد ، فإن قيمة المؤشر posAfter.Y يساوي قيمة إطار العرض ، لذلك لا نحذفه ولا يعمل شيء. بالإضافة إلى ذلك ، هناك مشكلة مشابهة مع المتغير newViewTop . لذلك ، دعنا نزيد من قيمة المؤشر POSAfter.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 . لنبدأ بما رأيته على شاشتي:


ما رأيك سيكون في سلسلة str عندما أضغط على <Enter>؟

  1. السلسلة "LONG DESCRIPTION" .
  2. المخزن المؤقت بأكمله الذي نراه الآن.
  3. المخزن المؤقت بأكمله ، ولكن بدون السطر الأول.

لن أضعف - المخزن المؤقت بأكمله ، ولكن بدون السطر الأول. وهذه مشكلة كبيرة ، لأن هذا هو السبب في أننا نفقد التاريخ ، علاوة على ذلك ، بشكل نقطي. هذه هي الطريقة التي سيبدو بها جزء ناتج المساعدة بعد الانتقال إلى سطر جديد:


باستخدام السهم ، قمت بتمييز المكان الذي كان فيه "LONG DESCRIPTOIN" . ثم ربما الكتابة فوق المخزن المؤقت بإزاحة سطر واحد؟ قد ينجح هذا إذا لم يتم استدعاء هذا الرد لكل عطس.

اكتشفت ثلاث حالات على الأقل عندما يتم استدعاؤها ،

  • عندما ندخل أي حرف ؛
  • عندما نتحرك عبر التاريخ ؛
  • عندما نقوم بتنفيذ الأمر.

تكمن المشكلة في أنك لا تحتاج إلى نقل المخزن المؤقت إلا عند تنفيذ الأمر ، أو إدخال <Enter>. في حالات أخرى ، يعد القيام بذلك فكرة سيئة. لذلك نحن بحاجة إلى تحديد ما يحتاج إلى تغيير بطريقة أو بأخرى.

استنتاج


صورة 18


كانت هذه المقالة محاولة لإظهار مدى قدرة PVS-Studio بمهارة على العثور على رمز معيب يؤدي إلى الخطأ الذي لاحظته. لقد حفزتني الرسالة المتعلقة بموضوع طرح متغير من نفسه بقوة ، وشرعت بقوة في كتابة النص. ولكن كما ترون ، كان فرحي سابق لأوانه وتبين أن كل شيء أكثر تعقيدًا.

لذا قررت التوقف. لا يزال بإمكان المرء قضاء أمسيتين ، ولكن كلما فعلت ذلك لفترة أطول ، ظهرت مشاكل أكثر فأكثر. كل ما يمكنني فعله هو أن أتمنى حظًا جيدًا لمطوري Windows Terminal في إصلاح هذا الخطأ. :)

آمل ألا أكون قد خيبت أمل القارئ لأنني لم أنتهي من البحث وكان من المثير للاهتمام بالنسبة لي أن أتجول داخل المشروع. كتعويض ، أقترح استخدام الرمز الترويجي #WindowsTerminal ، والذي ستحصل بفضله على نسخة تجريبية من PVS-Studio ليس لمدة أسبوع ، ولكن فورًا لمدة شهر. إذا لم تكن قد جربت محلل PVS-Studio الثابت عمليًا ، فهذا سبب وجيه للقيام بذلك. فقط أدخل "#WindowsTerminal" في حقل "الرسالة" في صفحة التنزيل .

وأغتنم هذه الفرصة أيضًا ، وأود أن أذكركم أنه سيكون هناك قريبًا إصدار من محلل C # يعمل في Linux و macOS. والآن يمكنك التسجيل للاختبار الأولي.


إذا كنت تريد مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فيرجى استخدام رابط الترجمة: Maxim Zvyagintsev. شريط التمرير الصغير الذي لم يستطع .

All Articles