Barre de défilement qui a échoué


Récemment publié une nouvelle version de Windows Terminal. Tout irait bien, mais les performances de sa barre de défilement laissaient beaucoup à désirer. Par conséquent, il est temps de lui coller un petit bâton et de jouer du tambourin.

Que font généralement les utilisateurs avec la nouvelle version d'une application? C'est vrai, exactement ce que les testeurs n'ont pas fait. Par conséquent, après une brève utilisation du terminal à sa destination, j'ai commencé à faire des choses terribles avec lui. D'accord, d'accord, je viens de renverser du café sur le clavier et j'ai accidentellement serré <Entrée> lorsque je l'ai essuyé. Ce qui est arrivé à la fin?



Oui, ça n'a pas l'air très impressionnant, mais ne vous précipitez pas pour me jeter des pierres. Faites attention au côté droit. Essayez d'abord de comprendre ce qui ne va pas avec elle. Voici une capture d'écran pour un indice:


Bien sûr, le titre de l'article était un spoiler sérieux. :)

Donc, il y a un problème avec la barre de défilement. Passer à une nouvelle ligne plusieurs fois, après avoir franchi la bordure inférieure, vous vous attendez généralement à ce qu'une barre de défilement apparaisse, et vous pouvez faire défiler vers le haut. Cependant, cela ne se produit que lorsque nous écrivons une commande avec la sortie de quelque chose. Disons simplement que le comportement est étrange. Cependant, cela n'aurait peut-être pas été aussi critique si la barre de défilement fonctionnait ...

Après avoir testé un peu, j'ai trouvé que le passage à une nouvelle ligne n'augmente pas le tampon. Cela ne fait que la sortie des commandes. Ainsi, le whoami ci-dessus augmentera le tampon d'une seule ligne. Pour cette raison, au fil du temps, nous allons perdre beaucoup d'histoire, surtout après clair.

La première chose qui m'est venue à l'esprit était d'utiliser notre analyseur et de voir ce qu'il dit:


La conclusion est, bien sûr, de taille impressionnante, je vais donc profiter de la puissance du filtrage et tout recadrer sauf le ScrollBar contenant :


Je ne peux pas dire qu'il y a beaucoup de messages ... Eh bien, peut-être qu'il y a alors quelque chose lié au tampon?


L'analyseur n'a pas échoué et a trouvé quelque chose d'intéressant. J'ai souligné cet avertissement ci-dessus. Voyons ce qui ne va pas:

V501 . Il existe des sous-expressions identiques à gauche et à droite de l'opérateur '-': 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);
  ....
}

Ce code est accompagné d'un commentaire: "Configurez la hauteur du ScrollViewer et la grille que nous utilisons pour simuler notre hauteur de défilement."

La simulation de la hauteur de défilement est bien sûr bonne, mais pourquoi mettons-nous 0 au maximum? En ce qui concerne la documentation , il est devenu clair que le code n'est pas très suspect. Ne vous méprenez pas: soustraire une variable d'elle-même est, bien sûr, suspect, mais nous obtenons un zéro à la sortie, ce qui ne nous nuit pas. Dans tous les cas, j'ai essayé de spécifier la valeur par défaut (1) dans le champ Maximum :


La barre de défilement est apparue, mais elle ne fonctionne pas non plus:



Si quoi que ce soit, j'ai bloqué <Entrée> pendant 30 secondes. Apparemment, ce n'est pas le problème, alors laissez-le tel quel, sauf en remplaçant bufferHeight - bufferHeight par 0:

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

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

Nous ne sommes donc pas particulièrement près de résoudre le problème. En l'absence d'une meilleure offre pour déboguer. Au début, nous pourrions mettre un point d'arrêt sur la ligne modifiée, mais je doute que cela nous aide d'une manière ou d'une autre. Par conséquent, nous devons d'abord trouver le fragment qui est responsable du décalage de la fenêtre d'affichage par rapport au tampon.

Un peu sur le fonctionnement de la barre de défilement locale (et probablement toute autre). Nous avons un grand tampon qui stocke toutes les sorties. Pour interagir avec elle, une sorte d'abstraction est utilisée pour dessiner sur l'écran, dans ce cas, la fenêtre .

En utilisant ces deux primitives, nous pouvons comprendre quel est notre problème. Aller sur une nouvelle ligne n'augmente pas la mémoire tampon, et pour cette raison, nous n'avons tout simplement nulle part où aller. Par conséquent, le problème est là-dedans.

Armés de cette connaissance banale, nous continuons notre héroïque débag. Après une petite promenade autour de la fonction, j'ai attiré l'attention sur ce fragment:

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

Après avoir configuré le ScrollBar ci - dessus , il faut configurer les différentes fonctions de rappel et exécutons __connection.Start () pour notre fenêtre , nouvellement créé. Après quoi le lambda ci-dessus est appelé. Puisque c'est la première fois que nous écrivons quelque chose dans le tampon, je suggère de commencer notre débogage à partir de là.

Nous définissons un point d'arrêt à l'intérieur du lambda et regardons dans _terminal :



Nous avons maintenant deux variables qui sont extrêmement importantes pour nous - _buffer et _mutableViewport . Mettez des points d'arrêt sur eux et trouvez où ils changent. Certes, avec _viewport, je vais tricher un peu et mettre un point d'arrêt non pas sur la variable elle-même, mais sur son champ supérieur (nous en avons juste besoin).

Maintenant, cliquez sur <F5>, et rien ne se passe ... Eh bien, frappons une douzaine de fois <Entrée>. Rien ne s'est passé. Apparemment, sur _buffer, nous avons défini un point d'arrêt trop imprudemment, et _viewport , comme prévu, est resté au sommet du tampon, qui n'a pas augmenté de taille.

Dans ce cas, il est logique d'entrer une commande pour provoquer une mise à jour du sommet._viewport . Après cela, nous nous sommes mis sur un morceau de code très intéressant:

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

J'ai signalé un commentaire là où nous nous sommes arrêtés. Si vous regardez le commentaire sur le fragment, il devient clair que nous sommes plus près de la solution que jamais. C'est à cet endroit que la partie visible est décalée par rapport au tampon, et nous avons la possibilité de faire défiler. Après avoir observé un peu le comportement, j'ai remarqué un point intéressant: lorsque vous passez à une nouvelle ligne, la valeur de la variable cursorPosAfter.Y est égale à la valeur de la fenêtre d'affichage , nous ne l' omettons donc pas et rien ne fonctionne. En outre, il existe un problème similaire avec la variable newViewTop . Par conséquent, augmentons la valeur de cursorPosAfter.Y de un et voyons ce qui s'est passé:

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

Et le résultat du lancement:


Merveilles! J'ai entré une quantité et la barre de défilement fonctionne. Certes, jusqu'au moment où nous introduisons quelque chose ... Pour démontrer le fichier, je vais joindre un gif:


Apparemment, nous faisons quelques sauts supplémentaires vers une nouvelle ligne. Essayons alors de limiter nos transitions en utilisant la coordonnée X. Nous ne déplacerons la ligne que lorsque X est 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;
    }
  }
  ....
}

Le fragment écrit ci-dessus décale la coordonnée Y du curseur. Ensuite, nous mettons à jour la position du curseur. En théorie, cela devrait fonctionner ... Que s'est-il passé?



Eh bien, bien sûr, mieux. Cependant, il y a un problème en ce que nous décalons le point de sortie, mais ne décalons pas le tampon. Par conséquent, nous voyons deux appels de la même commande. Il peut bien sûr sembler que je sais ce que je fais, mais ce n'est pas le cas. :)

À ce stade, j'ai décidé de vérifier le contenu du tampon, donc je suis revenu au point où j'ai commencé le débogage:

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

J'ai défini un point d'arrêt au même endroit que la dernière fois et j'ai commencé à regarder le contenu de la variable str . Commençons par ce que j'ai vu sur mon écran:


Que pensez-vous qu'il y aura dans la chaîne str lorsque j'appuierai sur <Entrée>?

  1. Chaîne "LONG DESCRIPTION" .
  2. L'ensemble du tampon que nous voyons maintenant.
  3. Le tampon entier, mais sans la première ligne.

Je ne languirai pas - tout le tampon, mais sans la première ligne. Et c'est un problème considérable, car c'est précisément à cause de cela que nous perdons l'histoire, d'ailleurs, ponctuellement. Voici à quoi ressemblera notre fragment de sortie d' aide après être passé à une nouvelle ligne:


Avec une flèche, j'ai marqué l'endroit où se trouvait «LONG DESCRIPTOIN» . Peut-être alors écraser le tampon avec un décalage d'une ligne? Cela fonctionnerait si ce rappel n'était pas appelé pour chaque éternuement.

J'ai découvert au moins trois situations quand on l'appelle,

  • Lorsque nous entrons un caractère;
  • Quand nous passons à travers l'histoire;
  • Lorsque nous exécutons la commande.

Le problème est que vous devez déplacer le tampon uniquement lorsque nous exécutons la commande, ou entrez <Entrée>. Dans d'autres cas, cela est une mauvaise idée. Nous devons donc en quelque sorte déterminer à l'intérieur ce qui doit être déplacé.

Conclusion


Image 18


Cet article était une tentative de montrer à quel point PVS-Studio était capable de trouver un code défectueux menant à l'erreur que j'ai remarquée. Le message sur le sujet de la soustraction d'une variable à elle-même m'a fortement motivé et j'ai vigoureusement commencé à écrire le texte. Mais comme vous pouvez le voir, ma joie était prématurée et tout s'est avéré beaucoup plus compliqué.

J'ai donc décidé d'arrêter. On pouvait encore passer quelques soirées, mais plus je le faisais, plus il y avait de problèmes. Tout ce que je peux faire, c'est souhaiter bonne chance aux développeurs du terminal Windows pour corriger ce bogue. :)

J'espère que je n'ai pas déçu le lecteur que je n'ai pas terminé la recherche et il était intéressant pour moi de me promener à l'intérieur du projet. En guise de compensation, je suggère d'utiliser le code promo #WindowsTerminal, grâce auquel vous recevrez une version de démonstration de PVS-Studio non pas pour une semaine, mais immédiatement pour un mois. Si vous n'avez pas essayé l'analyseur statique PVS-Studio dans la pratique, c'est une bonne raison de le faire. Entrez simplement "#WindowsTerminal" dans le champ "Message" sur la page de téléchargement .

Et aussi, saisissant cette opportunité, je tiens à vous rappeler que bientôt il y aura une version de l'analyseur C # fonctionnant sous Linux et macOS. Et maintenant, vous pouvez vous inscrire aux tests préliminaires.


Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Maxim Zvyagintsev. La petite barre de défilement qui ne pouvait pas .

All Articles