Barra de desplazamiento que falló


Recientemente lanzó una nueva versión de Windows Terminal. Todo estaría bien, pero el rendimiento de su barra de desplazamiento dejaba mucho que desear. Por lo tanto, es hora de pegarle un palito y tocar la pandereta.

¿Qué suelen hacer los usuarios con la nueva versión de cualquier aplicación? Así es, exactamente lo que los evaluadores no hicieron. Por lo tanto, después de un breve uso del terminal para su propósito previsto, comencé a hacer cosas terribles con él. Está bien, está bien, acabo de derramar café en el teclado y sujeté accidentalmente <Enter> cuando lo limpié. ¿Que pasó al final?



Sí, no se ve muy impresionante, pero no te apresures a tirarme piedras. Presta atención al lado derecho. Primero trate de descubrir qué le pasa. Aquí hay una captura de pantalla para una pista:


Por supuesto, el título del artículo era un serio spoiler. :)

Entonces, hay un problema con la barra de desplazamiento. Al pasar a una nueva línea muchas veces, después de cruzar el borde inferior, generalmente espera que aparezca una barra de desplazamiento y puede desplazarse hacia arriba. Sin embargo, esto no sucede hasta que escribimos un comando con la salida de algo. Digamos que el comportamiento es extraño. Sin embargo, esto podría no haber sido tan crítico si la barra de desplazamiento funcionara ...

Después de probar un poco, descubrí que cambiar a una nueva línea no aumenta el búfer. Esto solo genera la salida de comandos. Entonces, el whoami anterior aumentará el búfer en solo una línea. Debido a esto, con el tiempo perderemos mucha historia, especialmente después de despejar.

Lo primero que se me ocurrió fue usar nuestro analizador y ver qué dice:


La conclusión, por supuesto, es de un tamaño impresionante, por lo que aprovecharé el poder de filtrar y recortar todo excepto el que contiene la barra de desplazamiento :


No puedo decir que hay muchos mensajes ... Bueno, ¿tal vez hay algo relacionado con el búfer?


El analizador no falló y encontró algo interesante. Destaqué esta advertencia arriba. Veamos qué está mal allí:

V501 . Hay subexpresiones idénticas a la izquierda y a la derecha del operador '-': 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);
  ....
}

Este código va acompañado de un comentario: "Configure la altura del ScrollViewer y la cuadrícula que estamos utilizando para simular nuestra altura de desplazamiento".

Simular la altura de desplazamiento es, por supuesto, bueno, pero ¿por qué ponemos 0 al máximo? En cuanto a la documentación , quedó claro que el código no es muy sospechoso. No me malinterpreten: restar una variable de sí mismo es, por supuesto, sospechoso, pero obtenemos un cero en la salida, lo que no nos perjudica. En cualquier caso, intenté especificar el valor predeterminado (1) en el campo Máximo :


Aparece la barra de desplazamiento, pero tampoco funciona:



En todo caso, sujeté <Enter> durante 30 segundos. Aparentemente este no es el problema, así que dejémoslo como estaba, excepto reemplazando bufferHeight - bufferHeight con 0:

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

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

Por lo tanto, no estamos particularmente cerca de resolver el problema. En ausencia de una mejor oferta para entrar en debate. Al principio podríamos poner un punto de quiebre en la línea cambiada, pero dudo que nos ayude de alguna manera. Por lo tanto, primero tenemos que encontrar el fragmento responsable del desplazamiento de la ventana gráfica en relación con el búfer.

Un poco sobre cómo funciona la barra de desplazamiento local (y muy probablemente cualquier otra). Tenemos un gran búfer que almacena toda la salida. Para interactuar con él, se utiliza algún tipo de abstracción para dibujar en la pantalla, en este caso, viewport .

Usando estas dos primitivas, podemos entender cuál es nuestro problema. Ir a una nueva línea no aumenta el búfer, y debido a esto, simplemente no tenemos a dónde ir. Por lo tanto, el problema está en eso.

Armados con este conocimiento común, continuamos nuestro debate heroico. Después de un pequeño paseo por la función, llamé la atención sobre este fragmento:

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

Después de haber configurado el ScrollBar anterior , podemos configurar diversas funciones de devolución de llamada y ejecutar __connection.Start () de la ventana de nuevo cuño. Después de lo cual se llama la lambda anterior. Como esta es la primera vez que estamos escribiendo algo en el búfer, sugiero comenzar nuestra depuración desde allí.

Establecemos un punto de interrupción dentro de la lambda y miramos en _terminal :



Ahora tenemos dos variables que son extremadamente importantes para nosotros: _buffer y _mutableViewport . Ponles puntos de quiebre y averigua dónde cambian. Es cierto, con _viewport haré un poco de trampa y pondré un punto de interrupción no en la variable en sí, sino en su campo superior (solo lo necesitamos).

Ahora haga clic en <F5>, y no pasa nada ... Bueno, entonces peguemos un par de docenas de veces <Enter>. No pasó nada. Aparentemente, en _buffer establecimos un punto de interrupción demasiado imprudentemente, y _viewport , como se esperaba, permaneció en la parte superior del búfer, que no aumentó de tamaño.

En este caso, tiene sentido ingresar un comando para provocar una actualización de vértice._viewport . Después de eso, nos encontramos con un código muy interesante:

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

Señalé un comentario donde lo dejamos. Si observa los comentarios sobre el fragmento, queda claro que estamos más cerca de la solución que nunca. Es en este lugar que la parte visible se desplaza en relación con el búfer, y tenemos la oportunidad de desplazarnos. Después de observar un poco el comportamiento, noté un punto interesante: al pasar a una nueva línea, el valor de la variable cursorPosAfter.Y es igual al valor de la ventana gráfica , por lo que no lo omitimos y nada funciona. Además, hay un problema similar con la variable newViewTop . Por lo tanto, aumentemos el valor de cursorPosAfter.Y en uno y veamos qué sucedió:

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

Y el resultado del lanzamiento:


Maravillas! Ingresé una cantidad ne y la barra de desplazamiento funciona. Es cierto, hasta el momento en que presentamos algo ... Para demostrar el archivo, adjuntaré un gif:


Aparentemente, estamos haciendo algunos saltos adicionales a una nueva línea. Entonces tratemos de limitar nuestras transiciones usando la coordenada X. Solo cambiaremos la línea cuando X sea ​​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;
    }
  }
  ....
}

El fragmento escrito arriba desplazará la coordenada Y para el cursor. Luego actualizamos la posición del cursor. En teoría, esto debería funcionar ... ¿Qué pasó?



Bueno, por supuesto, mejor. Sin embargo, hay un problema en que cambiamos el punto de salida, pero no cambiamos el búfer. Por lo tanto, vemos dos llamadas del mismo comando. Por supuesto, puede parecer que sé lo que estoy haciendo, pero esto no es así. :)

En este punto, decidí verificar el contenido del búfer, así que volví al punto en el que comencé la depuración:

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

Establecí un punto de interrupción en el mismo lugar que la última vez, y comencé a mirar el contenido de la variable str . Comencemos con lo que vi en mi pantalla:


¿Qué crees que estará en la cadena str cuando presione <Enter>?

  1. Cadena "LARGA DESCRIPCIÓN" .
  2. Todo el búfer que ahora vemos.
  3. Todo el búfer, pero sin la primera línea.

No languideceré: todo el búfer, pero sin la primera línea. Y este es un problema considerable, porque es precisamente por eso que estamos perdiendo la historia, además, puntiagudos. Así es como se verá nuestro fragmento de salida de ayuda después de ir a una nueva línea:


Con una flecha marqué el lugar donde estaba "DESCRIPCIÓN LARGA" . ¿Quizás entonces sobrescribir el búfer con un desplazamiento de una línea? Esto funcionaría si no se llamara a esta devolución de llamada por cada estornudo.

He descubierto al menos tres situaciones cuando se llama,

  • Cuando ingresamos cualquier personaje;
  • Cuando nos movemos por la historia;
  • Cuando ejecutamos el comando.

El problema es que solo necesita mover el búfer cuando ejecutamos el comando o ingresar <Enter>. En otros casos, hacer esto es una mala idea. Por lo tanto, debemos determinar de alguna manera dentro de lo que se debe cambiar.

Conclusión


Cuadro 18


Este artículo fue un intento de mostrar cuán hábilmente PVS-Studio pudo encontrar código defectuoso que condujo al error que noté. El mensaje sobre el tema de restar una variable de sí mismo me motivó fuertemente y procedí vigorosamente a escribir el texto. Pero como puede ver, mi alegría fue prematura y todo resultó ser mucho más complicado.

Entonces decidí parar. Todavía se podía pasar un par de noches, pero cuanto más tiempo hacía esto, surgían más y más problemas. Todo lo que puedo hacer es desear buena suerte a los desarrolladores de Windows Terminal para solucionar este error. :)

Espero no haber decepcionado al lector de que no terminé la investigación y fue interesante para mí dar un paseo por el interior del proyecto. Como compensación, sugiero usar el código de promoción #WindowsTerminal, gracias al cual recibirá una versión demo de PVS-Studio no durante una semana, sino inmediatamente durante un mes. Si no ha probado el analizador estático PVS-Studio en la práctica, esta es una buena razón para hacerlo. Simplemente ingrese "#WindowsTerminal" en el campo "Mensaje" en la página de descarga .

Y también, aprovechando esta oportunidad, quiero recordarles que pronto habrá una versión del analizador C # que funcione bajo Linux y macOS. Y ahora puedes inscribirte para las pruebas preliminares.


Si desea compartir este artículo con una audiencia de habla inglesa, utilice el enlace a la traducción: Maxim Zvyagintsev. La pequeña barra de desplazamiento que no pudo .

All Articles