Skip to content

CodeEditor es

Edgar Mesquita edited this page Oct 14, 2026 · 1 revision

Editor de código

🌐 Esta página en: English · Português · Español · Français · Deutsch

El SDK entrega el modelo de un editor de código, no solo una caja con sintaxis coloreada: un documento hecho de líneas, un resaltador incremental, un historial de deshacer que piensa en palabras, y un controlador cuyos métodos son los comandos que un IDE pone en sus menús. El modelo vive en eQuantic.UI.Code, un ensamblado propio entre Primitives y Components que el SDK trae con ellos: lógica pura sin píxeles, que es lo que deja a una app construida sobre él probar por unidad sus propios comandos de edición sin pantalla, y lo que deja al mismo editor correr como píxeles de GPU en nativo y como DOM en la web. Primitives conserva solo el nodo CodeSurface y el protocolo por el que un realizador le habla al editor (ICodeSurfaceModel), así que un realizador no sabe nada del motor y el motor nada de ningún realizador.

¿Por qué una capa de modelo? Porque «un editor» es 90% aritmética: en qué columna aterriza un caret después de ↓ por una línea corta, qué hace Backspace dentro de la indentación, qué llave empareja con cuál. Conecta eso a un control y solo se puede probar haciendo clic. Mantenlo aquí y se prueba afirmando.


Una sola rejilla, y un caret que puedes ver

El caret y la banda de selección se colocan por aritmética (contentTop + line × lineHeight, contentLeft + cell × columnWidth), lo cual solo funciona mientras TODA parte del editor concuerde en esos números. Cuatro reglas los mantienen de acuerdo, cada una de ellas un defecto que se entregó una vez:

  • Una sola medición. CodeEditor mide la rejilla y se la entrega a CodeBlock (Metrics), que nunca mide la suya. Dos mediciones se separan en el momento en que las dos mitades se construyen con contextos que difieren, y un caret entonces se sienta entre líneas.
  • La tinta de las marcas viaja en el nodo (CaretColor / SelectionColor), no en el realizador. Un editor sobre una losa inversa escribe con tinta propia; pintado desde el tema de la página, un caret es invisible exactamente en la superficie en la que la gente teclea. Solo el parpadeo y la puerta de foco son mecánica de hoja de estilos: 500ms por fase, la misma con la que el host nativo parpadea desde su propio reloj.
  • Mide con una fuente que CSS pueda parsear. FontWeight baja a un nombre de miembro; un canvas al que se le da regular 11.5px … conserva 10px sans-serif y responde con un avance proporcional, en silencio.
  • Un solo mapa de una columna a su celda (CodeLineCells). Una columna de documento cuenta unidades UTF-16 y la rejilla cuenta celdas, y las dos se separan en un tabulador, un carácter ancho o un emoji. El caret, la selección, el clic, las flechas, Backspace y el dibujo todos leen el mismo mapa: ver Las columnas no son celdas.

Las piezas

Tipo Qué es
CodeDocument El texto, guardado como líneas. Inmutable: toda edición devuelve un documento nuevo (eso es lo que guarda el deshacer).
CodePosition / CodeRange Una línea+columna, y un par dirigido (ancla → foco) para que shift+flecha sepa qué extremo está arrastrando.
ICodeLanguage Un tokenizador de una línea a la vez con estado que se arrastra, más las Rules del lenguaje.
CodeHighlighter Los colores de un documento, mantenidos al día incrementalmente.
CodeHistory Deshacer y rehacer que fusiona una tirada de tecleo en un solo paso.
CodeEditorController Todo comando de edición, sobre un documento y una selección: el editor menos los píxeles.
CodeLineCells Dónde aterriza una línea en la rejilla: la celda en la que empieza cada elemento de texto, y el ancho de la línea en celdas.
ICodeSurfaceModel Lo que un realizador puede decirle al editor (una tecla, algo de texto, una composición, el puntero, el portapapeles, el foco) y preguntarle (los carets que pintar). En Primitives, al lado de CodeSurface.

Los documentos son líneas

var document = CodeDocument.FromText(File.ReadAllText(path));   // CRLF, CR and LF all accepted
document.LineCount;                    // 42
document.Line(7);                      // "    public void Run()"
document.OffsetOf(new CodePosition(7, 4));   // ↔ PositionOf(offset)

Líneas en vez de una sola cadena porque todo lo que hace un editor tiene forma de línea: el margen las numera, el tokenizador las colorea de una en una, el caret se mueve entre ellas, y una pulsación no debe volver a copiar un megabyte.

Una sola primitiva de edición, reemplazar un rango con texto, cubre insertar (rango vacío), borrar (texto vacío) y teclear sobre una selección (las dos):

var next = document.Replace(range, "renamed", out var caret);

Clamp fija cualquier posición dentro del documento, que es por lo que ninguna navegación tiene que pensar en los bordes. LineStart implementa el Home que tiene todo editor: el primer carácter no blanco, y la columna cero solo cuando el caret ya está ahí.


Lenguajes

Incluidos: C#, TypeScript/JavaScript, Python, JSON, XML (y .csproj/.plist con él), y texto plano como la reserva que siempre renderiza.

var language = CodeLanguages.For("cs");          // by name or extension; PlainText when unknown
CodeLanguages.Register("sql", new SqlLanguage()); // an app brings its own dialect

Un tokenizador lee una línea y devuelve el estado en el que empieza la siguiente:

int Tokenize(string line, int state, List<CodeToken> into);

Esa forma es lo que hace barato volver a colorear una pulsación, y es la única manera de que una construcción que abarca líneas pueda funcionar en absoluto: un comentario de bloque, una cadena textual o cruda de C# (@"…", """…""", interpolada o no), un literal de plantilla de JS, un docstring de Python. Las clases de token son un conjunto cerrado y pequeño (Keyword, Type, String, Number, Comment, Operator, Punctuation, Function, Attribute, Property, Constant, Plain), porque un sistema de diseño tiene una sola paleta para el código.

Cada lenguaje declara además sus reglas, y todo comportamiento se construye desde ellas:

public CodeLanguageRules Rules { get; } = new()
{
    LineComment = "#",                       // ⌘/ ; null = el comando no hace nada (JSON)
    IndentAfter = [':', '(', '[', '{'],      // qué abre un nivel (Python indenta después de dos puntos)
    OutdentOn  = [')', ']', '}'],
    IndentWidth = 4,
    InsertSpaces = true,
};

Y sus palabras, IReadOnlyList<string> Keywords: las palabras reservadas, los tipos integrados y las constantes, que una completación ofrece donde sea que empiece una palabra. Un lenguaje derivado de CurlyBraceLanguage las obtiene de las tablas con las que colorea (ReservedWords, TypeWords, ConstantWords), así que los colores y la completación leen una sola lista. Desde 0.2.0-preview.61.


Resaltado incremental

var highlighter = new CodeHighlighter(CodeLanguages.CSharp);
var tokens = highlighter.TokensFor(document, line);

// después de una edición
int repaintThrough = highlighter.LineChanged(document, line);

LineChanged vuelve a tokenizar esa línea y sigue solo mientras el estado final siga saliendo distinto, lo cual ocurre cuando un comentario de bloque o una cadena multilínea abre o cierra, y en ningún otro caso. Devuelve cuán lejos se movieron los colores, así que quien llama puede repintar solo eso.


El controlador

CodeEditorController es el comportamiento del editor. Un IDE lo maneja desde su propio mapa de teclas, su propio menú o su propio servidor de lenguaje; el componente es solo lo que lo dibuja.

var editor = new CodeEditorController(text, CodeLanguages.CSharp);

editor.Type('(');                 // se autocierra, el caret aterriza dentro
editor.InsertNewLine();           // hereda la indentación, abre un bloque, suelta la llave de cierre
editor.Indent();                  // caret → siguiente parada de tabulación; selección → toda línea
editor.ToggleLineComment();       // ⌘/ agrega, o quita cuando todas las líneas ya lo están
editor.Move(CodeMotion.Line, CodeDirection.Forward, extend: true);
editor.Undo();  editor.Redo();
editor.FindNext("needle");
editor.MatchingBracket(editor.Caret);
editor.Apply(range, "renamed");   // un refactor: se deshace como cualquier cosa tecleada

Los comportamientos que vienen gratis

  • Pares: una llave de apertura se cierra sola; teclear la mitad de cierre sobre el gemelo autoinsertado lo salta en vez de doblarlo; borrar la mitad de apertura se lleva el cierre con ella; una comilla dentro de una palabra se queda apóstrofo (don't).
  • Indentación: una línea nueva hereda la indentación actual y gana un nivel después de {; Enter entre {} abre el bloque y suelta el cierre a su propia línea; una llave de cierre tecleada donde solo hay indentación delante retrocede un nivel, al bloque que cierra; Backspace en el blanco inicial quita un paso entero; Tab va a la siguiente parada, no a un número fijo de espacios.
  • Una selección sobrevive a su edición: Tab, ⇧Tab y ⌘/ dejan seleccionadas las líneas que indentaron o comentaron, así que volver a pulsar hace lo mismo otra vez.
  • Movimiento: una tirada de ↓ por líneas irregulares recuerda la columna de la que empezó; los pasos de palabra paran donde pararía un lector; un → simple colapsa una selección a su borde.
  • Deshacer: una tirada de tecleo es un paso; mover el caret termina la tirada; teclear sobre una selección es un paso con lo que la reemplazó, y un pegado es un paso propio; una edición nueva mata la rama de rehacer.

Eventos

editor.Changed += edit => { /* dirty flag, language server, diff */ };
editor.SelectionChanged += range => { /* status bar: Ln 12, Col 4 */ };

Changed lleva el CodeEdit: rango, texto quitado, texto insertado, selección en cada lado. Un IDE se suscribe a ediciones, no a pulsaciones, porque un pegado y un refactor son ediciones que nadie tecleó. El texto insertado es el texto que el documento contiene, con sus líneas cortadas donde el documento las corta: un pegado que llevaba CR o CRLF llega con LF, y el deshacer después restaura el documento exactamente.


Puntos de extensión para un IDE

Estos son contratos que implementa la app; el trabajo del editor es colocar lo que devuelven.

public interface ICodeCompletionProvider
{
    IReadOnlyList<char> TriggerCharacters => [];
    Task<CodeCompletionList> CompleteAsync(CodeDocument document, CodePosition position,
        CodeCompletionContext context, CancellationToken cancellation);
    Task<CodeCompletionItem> ResolveAsync(CodeCompletionItem item, CancellationToken cancellation) =>
        Task.FromResult(item);
}

public interface ICodeHoverProvider   { Task<CodeHover?> HoverAsync(…); }
public interface ICodeFoldProvider    { IReadOnlyList<CodeFold> FoldsFor(CodeDocument document); }

Asíncronos porque la respuesta normalmente cruza una frontera de proceso, y un editor que se bloquea en ella es un editor que tartamudea. IndentationFoldProvider es el proveedor de pliegues por defecto: funciona para todo lenguaje, incluidos los que nadie escribió un parser.

Datos que una app entrega por fotograma:

Tipo Para
CodeDiagnostic El subrayado ondulado bajo el código y la fila en la lista de problemas. Un solo record, Range + Severity + Message (+ Code, Source).
CodeDecoration Cualquier marca extra sobre un rango: coincidencias de búsqueda, el símbolo bajo el caret, una llave emparejada, un trozo de diff. Highlight/Squiggle/Outline/Strike.
CodeGutterMarker Puntos de ruptura, estado de git, la sentencia en la que un depurador se detuvo.

Completación

Desde 0.2.0-preview.61. CodeEditorController.Completion es la completación del editor: los proveedores a los que pregunta, y la lista mientras una está abierta, como un modelo que una prueba maneja con pulsaciones y que cualquier host dibuja. CodeEditor dibuja la lista (abajo); un IDE que dibuja la suya lee IsOpen, IsLoading, Items, Selected y Start, y escucha Changed.

var editor = new CodeEditorController(source, CodeLanguages.CSharp);
editor.Completion.Providers.Add(languageService);                   // lo que un IDE sabe
editor.Completion.Providers.Add(new CodeKeywordCompletionProvider()); // las palabras del lenguaje
editor.Completion.Providers.Add(new CodeWordCompletionProvider());    // las palabras ya en el archivo

Un controlador empieza sin proveedor, y sin ninguno no se abre lista y ninguna tecla va a una: una lista que nada dibuja nunca debe tomar Enter. CodeEditor entrega a su controlador las palabras del lenguaje y las del documento salvo que se le diga otra cosa (Completions).

CodeWordCompletionProvider responde antes de que la pulsación retorne, así que lee primero las líneas más cercanas al caret, y no más de 50.000 caracteres de ellas, la línea propia del caret alrededor del caret: una palabra empezada cuesta lo mismo en un archivo de cualquier largo, y lo que un archivo largo (o una línea minificada) deja fuera es lo que queda más lejos de la palabra que se teclea.

Cuándo pregunta

Una lista pregunta a sus proveedores una vez, cuando empieza una palabra, y filtra lo que respondieron en cada pulsación posterior, aquí y de inmediato: a un servicio de lenguaje se le pregunta por palabra, nunca por tecla.

Qué Pregunta a Para la palabra que empieza
Uno de los TriggerCharacters de un proveedor (el . tras un nombre) ese proveedor solo después del carácter
Una palabra empezada al teclear, fuera de un comentario o una cadena, y que no es un número todo proveedor al inicio de la palabra
⌃Espacio (Ctrl+Espacio en Windows y Linux) todo proveedor en la palabra antes del caret, o en el caret

A un proveedor que responde IsIncomplete se le pregunta otra vez cuando la palabra cambia, con el disparador Incomplete; los demás conservan sus respuestas. Una respuesta que llega después de que su palabra fue dejada, o después de una petición más nueva, se descarta, y el token de su petición se cancela. Una respuesta que termina en otro hilo se aplica en el hilo de UI, como lo hace SetState.

Las teclas, mientras una lista se muestra

Tecla Hace
↓ ↑ Recorren la lista, en círculo de la última entrada a la primera. Con una sola entrada mostrándose, mueven el caret.
PageDown PageUp Una página (PageSize), parando en cualquiera de los extremos.
Tab Acepta la entrada seleccionada.
Enter La acepta cuando eso cambia el texto; una palabra tecleada por completo termina su línea en su lugar.
Escape Cierra la lista, y solo la lista: la trampa sobre Tab sigue armada y un diálogo alrededor del editor sigue abierto.
Un carácter de confirmación (CommitCharacters) Acepta, y luego teclea el carácter: Console. toma Console y sigue a sus miembros.

Toda otra tecla, ⌃Espacio y las propias de la lista entre ellas, vuelve a armar la trampa sobre Tab: después de Escape, ⌃Espacio y Escape, Tab todavía indenta.

Aceptar escribe sobre la palabra tecleada, o sobre el rango propio del proveedor (Replacing), cuyo final conserva su distancia al final de la línea: lo que se teclea o se borra en el caret lo mueve, y un movimiento del caret no. Deja el caret después del texto, y es un paso de deshacer. La lista se cierra cuando el caret deja la palabra (otra línea, antes de su inicio, una selección, un carácter que termina una palabra), cuando el editor pierde el teclado y cuando su documento se reemplaza.

Clasificación

CodeFuzzyMatch es el filtro, y es público para cualquier otra cosa que estreche mientras una persona teclea: los caracteres del patrón en orden, dejando el caso aparte, el primero donde empieza una parte de la palabra (bc encuentra BarChart, col encuentra Column, lum no encuentra nada), y la mejor manera de poner uno sobre el otro, una tirada por encima de los mismos caracteres separados. Las coincidencias iguales van por el SortText del proveedor, y luego por la etiqueta. Entre las mejores coincidencias, se selecciona una entrada que un proveedor marca Preselect. Dos proveedores que ofrecen una entrada (una etiqueta, un texto insertado) la listan una vez, como la copia del primer proveedor que coincide con la palabra.

Escribir un proveedor

public sealed class ColumnNames(IReadOnlyList<string> columns) : ICodeCompletionProvider
{
    public Task<CodeCompletionList> CompleteAsync(CodeDocument document, CodePosition position,
        CodeCompletionContext context, CancellationToken cancellation) =>
        Task.FromResult(new CodeCompletionList(
            columns.Select(name => new CodeCompletionItem(name, CodeCompletionKind.Field)).ToList()));
}

Los contratos son los del Language Server Protocol, tipados, así que un cliente de LSP es un adaptador: el disparador Typing del contexto es el Invoked de LSP, IsIncomplete su isIncomplete, Replacing su rango de edición, ResolveAsync su completionItem/resolve, y el token su $/cancelRequest. CodeCompletionKind tiene toda clase que tiene LSP.

El fallo de un proveedor es suyo: uno que lanza, cuya respuesta falla, o cuyo callback sobre el token lanza cuando la lista lo cancela (una conexión que se cayó) se reporta por Failed, y la lista sigue con lo que ofrecieron los demás. Ninguna pulsación falla por él. Una OperationCanceledException es la cancelación propia de la petición solo cuando la lista la canceló, y el fallo del proveedor en caso contrario.

La lista que dibuja CodeEditor

Desde 0.2.0-preview.61. Mientras su completación muestra una lista, CodeEditor la dibuja en la palabra que completa, por la superficie de código y en las coordenadas propias del código, así que la lista se mueve con el código a medida que el código se desplaza.

new CodeEditor(source, "csharp")
{
    Height = SizeValue.Fill,
    // De qué completa, en este orden. Null, el valor por defecto, son las palabras del lenguaje y
    // las del documento; una lista vacía es ninguna completación.
    Completions = [languageService, new CodeKeywordCompletionProvider(), new CodeWordCompletionProvider()],
}

Un editor de solo lectura no completa nada, se le dé lo que se le dé, y volver un editor de solo lectura cierra la lista que muestra y descarta una respuesta todavía en camino. El editor entrega los proveedores a su controlador una vez, y otra vez solo cuando la lista contiene otros proveedores (una lista que el padre cambió en el sitio incluida), así que un padre que reconstruye con los mismos no cambia nada, y una lista que ya se muestra sigue con los proveedores con los que se pidió. Saca solo lo que puso: un proveedor que una app agregó a Editor.Completion.Providers ella misma se queda al lado de ellos.

  • Dónde se para. Una línea debajo de la palabra, con sus etiquetas alineadas con lo que se tecleó. Cuando una página de filas no cabe debajo dentro del viewport del editor y cabe arriba, se para sobre la línea; cuando ninguno de los dos lados aguanta una página, toma el más holgado y muestra tantas filas como caben, nunca menos de una. Su borde derecho se queda dentro del viewport. Un editor que se ajusta a su código tiene la altura del código como viewport, así que un editor de dos líneas muestra una lista corta; el código de un editor acotado llena su viewport, así que un archivo corto en un panel alto tiene el espacio del panel.
  • Qué dice una fila. Las filas son las líneas propias del código, la altura de línea del editor en su tipografía de código: la clase de la entrada como una letra, en el color que el código da a lo que nombra, su etiqueta con lo que la palabra coincidió en el color de acento, y su detalle, atenuado. La entrada seleccionada está lavada, y su documentación se muestra bajo la lista (sobre ella, cuando la lista se para encima de la línea) en cuanto su proveedor la resolvió, como hasta cuatro líneas de texto plano. Toma el espacio que las filas dejan en ese lado y no más, tantas de sus líneas como caben o ninguna: las filas nunca le ceden, así que la lista no salta a medida que la selección se mueve entre entradas con y sin documentación. Una fila más larga que la lista corta primero su detalle y después su etiqueta, cada uno terminando en puntos suspensivos, contando las celdas que la rejilla del código da a cada carácter (un carácter ancho o un emoji toma dos) y nunca cortando dentro de uno, y su nombre para la tecnología asistiva conserva los dos enteros.
  • Una página. La lista muestra hasta doce filas y construye solo esas, la primera siguiendo a la selección más allá de cualquiera de los extremos. PageUp y PageDown avanzan por las filas mostradas, y una marca a la derecha dice dónde está la página en el conjunto.
  • Una presión sobre una fila la acepta, y el teclado se queda en el código (Pressable.CanRequestFocus). Una presión en la lista que ninguna fila toma no mueve ningún caret.
  • Tecnología asistiva. En la web la lista es el listbox del input de código: mientras se muestra, el input dice aria-autocomplete="list", nombra la lista en aria-controls y la opción seleccionada en aria-activedescendant. En Photon las filas se anuncian como opciones después del campo de código, la seleccionada seleccionada.
Letra Clases
m método, función, constructor
v campo, variable
p propiedad
e evento
C S I E T N clase, struct, interfaz, enum, parámetro de tipo, módulo
c constante, miembro de enum
# valor, unidad, color
k palabra clave
s fragmento
o operador
r f d referencia, archivo, carpeta
a texto (una palabra del documento)

Cercado, a propósito: una rueda sobre la lista desplaza el código y no la lista, que recorren las flechas y las teclas de página; la documentación es texto plano hasta que la tarjeta de hover renderice Markdown; y en Photon la lista se para a un ancho de borde de su gemela web hasta que una caja con borde mantenga a su hijo dentro del borde (#629), mientras que el margen de toque de una fila puede tomar una presión destinada a la fila de arriba (#630): 13dp de él bajo un dedo, y 3dp bajo un puntero, cuyo objetivo conserva un suelo de 24dp.


CodeBlock: la superficie de solo lectura

El modelo se dibuja por un solo componente. Toda línea se vuelve una Row de series de Text coloreadas, que es por lo que no necesita soporte del motor más allá de la tipografía monoespaciada: el mismo árbol renderiza como píxeles de GPU y como DOM.

new CodeBlock(source, "csharp")
{
    ShowLineNumbers = true,
    FirstLineNumber = 120,          // un fragmento citado desde la línea 120 dice 120
    MaxHeight = 320,                // acota la altura y desplaza más allá
    ActiveLine = 4,                 // la línea actual del depurador
    GutterMarkers = [new CodeGutterMarker(4, CodeGutterKind.Breakpoint)],
    Decorations  = [new CodeDecoration(range, CodeDecorationKind.Search)],
    OnGutterPressed = line => ToggleBreakpoint(line),
    OnCopy = () => clipboard.Write(source),
    Caption = "Program.cs",
}
Propiedad Para qué sirve
Inverse Una losa oscura en LOS DOS modos: código como figura en documentación, no como control.
Highlighter Reúsalo entre fotogramas para que el coloreado siga siendo incremental (un editor lo hace; un fragmento no lo necesita).
Size El tamaño propio del código; el margen lo sigue.
Standalone Si el bloque es el componente entero (su propia losa, su propio viewport) o contenido desnudo que algo de fuera enmarca y desplaza. Por defecto true; CodeEditor lo pone en false.
ViewportWidth Cuán ancho resultó el viewport, devuelto desde la maquetación. El contenido nunca es más estrecho que esto y nunca más ancho de lo que necesita.

Dos reglas que el componente conserva y que es fácil equivocar:

  • El margen se MIDE, no se adivina: context.MeasureText(lastNumber + "0", style). Un archivo con 1000 líneas necesita una columna que uno con 10 no.
  • Las líneas largas se desplazan de lado, nunca se envuelven. Una línea de código envuelta ha perdido lo único que su indentación te estaba diciendo.

Medir es parte del contexto

ComponentContext.MeasureText(text, style) y MonoAdvance(style) responden cuán ancha SERÍA una cadena, en dp, antes de que se maquete. El nativo pregunta al servicio de texto de la plataforma; la web pregunta al navegador por un contexto 2D de canvas usando las mismas pilas de fuentes que usa el CSS, así que los dos responden con los mismos números con los que cada destino maquetará el texto, que es de lo que depende mapear un clic a una columna.

CodeEditor: la superficie editable

El mismo dibujo, más las tres cosas que lo hacen un editor: un caret, una selección y un teclado.

new CodeEditor(source, "csharp")
{
    Height = SizeValue.Fill,        // tan alto como su lugar: el panel de un IDE
    OnChanged = text => _dirty = true,
    OnSelectionChanged = range => _status = $"Ln {range.Focus.Line + 1}, Col {range.Focus.Column + 1}",
    Autofocus = true,
    ReadOnly = false,
}

El componente es dueño de un CodeEditorController y lo entrega a un nodo CodeSurface. Un IDE alcanza editor.Editor para ejecutar comandos que nadie tecleó (un formateador, un renombrado, la edición de un servidor de lenguaje), y se deshacen como cualquier otra cosa, porque pasan por la misma primitiva.

OnChanged se levanta para una edición y OnSelectionChanged para un movimiento, cada uno solo cuando su propia cosa cambió: una flecha no es una edición, y una barra de estado escucha el segundo. Los dos se levantaban juntos para cualquier cosa que hiciera la superficie, así que una app que volvía a leer el documento en cada cambio lo hacía en cada flecha.

Un mapa de teclas, dos superficies, dos tradiciones

CodeKeymap.Handle(editor, key, modifiers, convention, clipboard) es donde un NOMBRE de tecla se vuelve un comando. Es C# simple, así que transpila con todo lo demás y las dos superficies llaman a la misma función: un host de Photon desde su evento de tecla, el navegador desde su keydown. Nada sobre lo que significan ⌥← o ⇧Tab se decide en un realizador.

Desde 0.2.0-preview.58

Habla las dos tradiciones de teclado, porque discrepan exactamente en las teclas de las que vive un editor: ⌘← es «inicio de línea» en un Mac y Ctrl+← es «una palabra atrás» en todos los demás lugares. El host dice en cuál viven sus usuarios (KeyboardConvention.Apple en macOS y iPadOS, Standard en todos los demás), y KeyModifiers.Command es ⌘ en uno y Ctrl en el otro. Un mapa de teclas que solo conocía el de Apple tenía a los usuarios de Windows y Linux saltando al inicio de la línea cada vez que querían pasar por encima de una palabra.

Tecla Apple Estándar
← → un carácter; ⌥ una palabra; ⌘ el borde de la línea un carácter; Ctrl una palabra
↑ ↓ una línea; ⌘ el borde del documento una línea (Ctrl+↑ y Ctrl+↓ desplazan una vista, y se dejan al host)
Home / End el borde de la línea; ⌘ el del documento el borde de la línea; Ctrl el del documento
PageUp / PageDown una página una página
⇧ + cualquiera de ellas extiende desde el ancla extiende desde el ancla
Enter una línea nueva, heredando la indentación (un nivel más después de {) lo mismo
Tab / ⇧Tab indentar / desindentar: la selección, o a la siguiente parada de tabulación lo mismo
Backspace / Delete un carácter; ⌥ una palabra; ⌘⌫ la línea hasta el caret; el blanco inicial se va un paso entero un carácter; Ctrl una palabra
deshacer / rehacer ⌘Z / ⇧⌘Z, y ⌘Y Ctrl+Z / Ctrl+Shift+Z, y Ctrl+Y
seleccionar todo, copiar, cortar, pegar ⌘A ⌘C ⌘X ⌘V; una copia sin nada seleccionado toma la línea Ctrl+A, C, X, V, lo mismo
comentar ⌘/ alterna comentarios de línea (nada en un lenguaje que no tiene) Ctrl+/
Escape libera Tab (abajo) lo mismo

Tab es del editor, y Escape lo devuelve. Un editor toma Tab, que es el punto, y un usuario de teclado tiene que seguir pudiendo salir de él. Escape libera la trampa, así que el siguiente Tab mueve el foco en vez de indentar, y cualquier otra tecla la vuelve a poner. Escape mismo se deja sin reclamar, así que todavía significa lo que significa alrededor del editor (cierra el diálogo en el que el editor se sienta), y en Photon además saca el teclado del editor. Un editor de solo lectura nunca toma Tab en absoluto. Dentro de un diálogo es lo mismo: el diálogo cicla solo un Tab que el editor no tomó, así que un editor en cualquiera de los extremos de un diálogo todavía indenta, y después de Escape el siguiente Tab sigue a medida que el diálogo cicla.

El texto viene de la plataforma, nunca de una tecla. Lo que produce una pulsación es asunto de la plataforma (una tecla muerta, un método de entrada, AltGr en una distribución europea, dictado, «á» desde tres eventos), así que el texto llega como una cadena y va a HandleText, donde viven los pares que se autocierran y la regla de pasar por encima del cierre. La composición de un método de entrada está EN el documento mientras se construye, subrayada en la tinta propia del código, y se confirma como una sola edición; cancelarla deja el documento como estaba. El portapapeles también es de la plataforma: en la web las teclas de copiar se dejan a los eventos propios de copiar, cortar y pegar del navegador, que llevan el texto, así que un pegado desde otra aplicación aterriza.

La geometría es aritmética

La tipografía es monoespaciada, así que una (línea, celda) ES (contentTop + line × lineHeight, contentLeft + cell × columnWidth), y un caret se repinta en cada pulsación sin medir nada ni volver a maquetar. Los dos realizadores usan los mismos números, y CodeBlock.MetricsFor es el único lugar del que vienen; dos cálculos independientes se separarían por un píxel y luego por un carácter. El motor hace la aritmética (editor.Grid tiene las métricas), así que un realizador pinta rectángulos que se le entregan y nunca vuelve una columna en píxeles.

Una selección es una BANDA POR LÍNEA, nunca un rectángulo sobre el rango: un solo rectángulo cubriría la indentación de líneas que el rango nunca tocó. El componente dibuja las bandas bajo el texto, en las capas propias del código, así que se ven igual en todo destino; un realizador pinta solo los carets, que tienen que parpadear.

Las columnas no son celdas

Desde 0.2.0-preview.58

Una COLUMNA de documento es un desplazamiento en el texto UTF-16 de la línea, y una CELDA es un avance de la tipografía mono en pantalla. Son el mismo número solo mientras todo carácter sea de una unidad de largo y de una celda de ancho, y tres cosas no lo son:

  • Un tabulador corre hasta la siguiente parada, un múltiplo del ancho de indentación del lenguaje: con paradas de 4, \tx pone x en la celda 4.
  • Un carácter ancho (ancho o de ancho completo del Asia oriental, y un emoji) toma dos celdas.
  • Un elemento de texto (un clúster de grafemas extendido: 👍🏽, una bandera, e con un acento combinante) nunca se parte: sus marcas, uniones, modificadores y la segunda mitad de un par subrogado no toman celda propia, y ningún caret para dentro de él.

CodeLineCells es el único mapa de cada uno al otro, y todo lo lee: el caret y la selección se colocan por él, el bloque dibuja las celdas que nombra, un clic es su inverso (un punto en la mitad izquierda de un tabulador aterriza antes de él, en la mitad derecha después), las flechas y Backspace avanzan por elemento, y los pasos de palabra y los dobles clics seleccionan elementos enteros. En la web los elementos vienen de Intl.Segmenter, que parte el texto como lo hace el StringInfo de .NET. Qué caracteres son anchos es una tabla que el motor conserva, y se compara, en los dos lados y para todo punto de código, con la convención de terminal que sigue el Bun incrustado del SDK (CellWidthOracleTests).

La tabla sigue el Unicode que sigue Bun, que es 17 ya que el SDK incrusta Bun 1.4.3 (Desde 0.2.0-preview.62). Los símbolos del Yijing y del Tai Xuan Jing, las varillas de conteo y los emoji que Unicode 16 y 17 agregaron toman dos celdas, y una incrustación, anulación o aislamiento direccional no toma ninguna. Una sílaba Hangul escrita en jamo conjunto toma las dos celdas de su consonante: la vocal y la final son parte de su elemento. Una vocal o una final sin consonante delante conserva una celda, ya que una fuente la dibuja sola.

Un solo espacio de coordenadas

Desde 0.2.0-preview.21

Las marcas se dibujan contra la SUPERFICIE que las contiene, así que nada puede desplazarse dentro de ella. El viewport vive FUERA de CodeSurface (el editor lo construye), y la superficie viaja con el código:

Stack (las capas, siempre ahí)
 ├ Box (la losa, cortada)
 │  └ ScrollView (vertical, cuando el editor está acotado)
 │     └ Row
 │        ├ el margen       ← al lado del scroll lateral, dentro del vertical
 │        └ ScrollView (horizontal)
 │           └ CodeSurface  ← se mueve con el código, así que las marcas también
 │              └ CodeBlock ← Standalone = false: contenido desnudo, sin losa, sin viewport
 ├ la esquina (el título)
 └ la barra de búsqueda, mientras está abierta

El código es la PRIMERA capa esté o no algo encima. Abrir la búsqueda solía envolver el código en un Stack que no tenía antes, y una superficie que se mueve en el árbol es una superficie nueva para todo host: el desplazamiento volvió arriba, «siguiente» no reveló nada, y en Photon el teclado apuntaba a una ruta que nada tenía ya.

Equivocar esto no es sutil en cuanto lo buscas, y es invisible hasta que lo haces: un bloque que se desplaza DENTRO de la superficie pone el código en un espacio y el caret en otro. Desplaza una línea larga de lado y el texto viaja mientras el caret se queda atrás; haz clic, y la columna se lee como si nada se hubiera desplazado. Poner el viewport fuera hace verdadera toda una de esas sumas por construcción: no queda nada que mantener sincronizado.

Dos consecuencias que vale declarar, porque cada una fue un defecto:

  • El contenido tiene el ancho del VIEWPORT, nunca menos que la línea más larga. Llenar solo es por lo que el desplazamiento de lado nunca se desplazaba: una vista de scroll cuyo contenido es exactamente su propio tamaño no tiene nada que mover. Dimensionar al código solo es el error opuesto: un clic en el espacio vacío a la derecha de una línea corta aterrizaría en nada.
  • El ancho vuelve DESDE la maquetación (ViewportWidth), como la altura ya lo hacía. Los dos destinos discrepan sobre qué significa llenar dentro de una vista de scroll lateral (una página resuelve 100% contra el desplazador, Photon mide el contenido sin acotar en el eje de scroll), y un número reportado es la única aritmética con la que los dos realizadores concuerdan.

El caret vuelve a la vista

Desde 0.2.0-preview.22

Irse con las flechas del borde de una línea larga, o abajo más allá de la última visible, solía dejar el caret donde la aritmética lo puso: fuera de la caja. Dos cosas tienen que estar bien, y cada una es fácil de equivocar de una manera que parece implementada:

  • Qué elemento. Toda pulsación reconstruye el árbol, así que la superficie sobre la que corrió el manejador está desprendida para cuando algo corre después, y scrollIntoView sobre un caret desprendido tiene éxito en silencio. La superficie lleva su ruta (data-eq-code), y la revelación se resuelve por ella.
  • Cuándo. El render se descarga en un fotograma de animación, así que un microtask encuentra un caret que todavía no se ha movido y decide correctamente que ya está en pantalla. Espera el fotograma después de la descarga, y TAMBIÉN un tiempo de espera, el mismo par que conserva el programador de render, porque una pestaña oculta o limitada deja de entregar fotogramas y el render ocurre de todos modos.

El margen se queda donde está

Desde 0.2.0-preview.25

Los números son una columna propia, AL LADO del desplazamiento lateral y dentro del vertical: viajan hacia abajo por el archivo con el código y se quedan quietos mientras se desliza de lado.

Se probaron antes otras dos disposiciones y las dos estaban mal de la misma manera. Dentro del scroll, los números se iban con el código y un lector perdía el número de la línea que estaba leyendo. Superpuestos encima de él, el código se deslizaba POR DEBAJO de una columna opaca y caracteres reales desaparecían: using se volvía eQuantic.UI.Core;, lo cual parece un defecto de renderizado y en realidad es uno de capas.

Al lado, los dos siguen siendo verdad y ninguno compensa al otro. El precio se declara en las métricas:

ContentLeft  =  el padding izquierdo propio del código  ← donde empieza la columna 0
             ≠  margen + padding                         ← lo que era antes

Esa única línea es por lo que esto llevó un cambio deliberado y no un retoque. La columna cero es donde el caret, la banda de selección y toda decoración empiezan a contar, así que mover su origen mueve las tres a la vez, que es exactamente por lo que es una propiedad y no tres, y por lo que todas pudieron moverse en una sola edición. El HUECO entre los números y el código pertenece al margen ahora, no al código: el relleno dentro del scroll se desliza fuera, y los dígitos acabaron tocando el primer carácter.

Buscar, emparejar, y archivos demasiado largos para construir

Buscar

⌘F (Ctrl+F) abre una barra sobre la esquina superior derecha, sobre y no encima: el código que salta cuando abres la búsqueda ha perdido la línea que estabas mirando. Toda coincidencia está lavada y la ACTUAL está delineada, porque una «siguiente coincidencia» que mueve algo invisible no te ha dicho nada.

Desde 0.2.0-preview.58

  • El campo toma el teclado cuando la barra abre, y lo conserva: Enter y los galones avanzan por las coincidencias, y la cuenta lee 3/17. Cuenta lo que busca SU campo, así que un campo vacío no muestra cuenta.
  • Un paso selecciona la coincidencia, lo cual la revela, y la app oye el movimiento en OnSelectionChanged, como lo hace cuando una tecla mueve el caret.
  • Escape cierra la barra donde sea que esté el teclado, en la barra o en el código, y devuelve el teclado al código.
  • El botón de cerrar se nombra en el idioma de la interfaz (SdkStrings.CloseFind).

Un IDE con su propia UI de búsqueda se salta todo eso y pone Search / SearchMatchCase directamente: sus coincidencias se marcan mientras la barra propia del editor está cerrada o su campo vacío.

Llaves

MatchBrackets (encendido por defecto) delinea la llave contra la que está el caret y la que empareja con ella. Un caret se sienta ENTRE caracteres, así que pertenece a la llave de cualquiera de sus lados, y gana la de DETRÁS: habiendo tecleado ) recién, esa es la que quieres decir.

Las dos marcas son CodeDecorationKind.Outline, no un lavado: un lavado esconderían el carácter al que la marca apunta.

Las decoraciones son rangos

Una decoración es un RANGO, y se dibuja como un rectángulo por línea que abarca, la misma aritmética que usa la banda de selección.

Clase Qué dibuja
Highlight un lavado de fondo: una coincidencia de búsqueda, un símbolo bajo el caret
Outline una caja alrededor del rango: una llave emparejada
Squiggle una regla debajo: un diagnóstico
Strike una regla a través: borrado en un diff, código inalcanzable

Sobre una losa Inverse cada una de ellas toma la mitad OSCURA de su color, por la misma razón que lo hacen los tokens: un token de modo claro sobre código oscuro se lee como un fallo de renderizado.

Virtualización

Un CodeEditor ACOTADO construye solo las líneas que el viewport puede mostrar, más un margen a cada lado para que un desplazamiento de una línea no construya nada. Arriba y abajo de la ventana se sienta un espaciador cada uno, así que el contenido sigue siendo tan alto como el archivo y la barra de desplazamiento dice la verdad.

Desde 0.2.0-preview.58

Height dice cuán alto es el editor. Hug, el valor por defecto, es tan alto como el código, hasta MaxHeight cuando se fija uno; Fill toma la altura que le da su lugar, que es cómo lo usa el panel de un IDE; y una altura fija son esos dp. Cualquier cosa menos un Hug sin tope está acotada, y un editor acotado con un MaxHeight toma la altura de su lugar hasta el tope. Sin una cota no hay viewport y no hay ventana: toda pulsación construía toda línea, de 100 a 213 ms por tecla en un archivo de 3000 líneas.

El código de un editor acotado llena su viewport por corto que sea el archivo: una presión bajo la última fila pone el caret al final del documento, como en cualquier editor de código, y un arrastre desde ahí selecciona de vuelta hasta donde se detiene. Los rellenos de un diff después de su última línea también son filas, y una presión en uno aterriza en esa línea en su columna, como en cualquier relleno. Desde 0.2.0-preview.61.

Fill sigue la regla de la maquetación: en un padre sin cota propia, una página que se desplaza por ejemplo, es tan alto como el código, en los dos destinos. Dale un lugar con una altura.

Todo lo demás que dibuja una construcción se enventana con las líneas: las bandas de la selección, las coincidencias de una búsqueda (encontradas una vez por búsqueda, no por construcción) y toda otra marca se hacen solo para las líneas a la vista, y la línea más ancha, que fija el ancho del código, se mide una vez por documento. Un paso de desplazamiento sobre 50.000 líneas con todo seleccionado y una búsqueda encendida pasó de 81 ms a 3.

Los dos números vienen de la maquetación, por dos canales nuevos en ScrollView:

new ScrollView(content)
{
    OnScrolled = offset => …,          // donde ESTÁ, cuando eso cambia
    OnViewportChanged = height => …,   // cuán alto resultó ser
}

Son el canal de salida al canal de entrada de Offset, y son lo que hace posible cualquier lista larga: sin ellos el desplazamiento vive en el host y ningún componente puede preguntar. El primer fotograma no tiene ninguno y construye todo, lo cual es correcto para un fragmento; el segundo sabe los dos y estrecha. En la web el viewport se mide una vez que el render se ha escrito, y otra vez cuando sea que cambie de tamaño sin render alguno (la ventana cambió de tamaño, se arrastró un divisor), así que un editor Fill en un panel que creció construye las filas que el panel muestra ahora.

Pedir el teclado

Desde 0.2.0-preview.58

Un IDE devuelve el teclado al código después de que un panel sobre él cierra, o cuando un archivo abre:

editor.Editor.RequestFocus();

La petición pertenece al MODELO (ICodeSurfaceModel.FocusVersion), así que el host que dibuje la superficie la honra, una vez: una petición hecha antes del primer fotograma se honra cuando la superficie se dibuja, y una ya honrada no se honra otra vez cuando la superficie se dibuja en otro lugar. La barra de búsqueda propia del editor la usa al cerrarse.

Autofocus se honra una vez por MONTAJE, como hace un navegador: un campo o un editor que aparece toma el teclado de lo que lo tuviera, porque apareció porque alguien lo abrió, y uno que se va y vuelve lo pide otra vez.

En una página renderizada en el servidor

Desde 0.2.0-preview.58

El servidor escribe la superficie (el código, sus carets y su input), pero no tiene fuentes: mide toda cadena como de 0 de ancho. Un componente cuyo propio Build midió texto en el servidor queda marcado (data-eq-unmeasured), y la hidratación dibuja ese subárbol otra vez en el cliente en vez de adoptar marcado construido con anchos que nadie midió. El margen de un bloque de código en una página renderizada en el servidor es tan ancho como sus números gracias a eso.

Comparar dos textos

Desde 0.2.0-preview.59

CodeDiffer responde qué difiere entre dos textos: las líneas, y dentro de cada región cambiada las palabras. Es lo que dibuja una vista de diff y lo que lee un IDE para mostrar un archivo contra su último commit.

var changes = CodeDiffer.Compare(original, modified);   // two CodeDocuments
foreach (var change in changes)
{
    // OriginalCount líneas desde OriginalStart se volvieron ModifiedCount líneas desde ModifiedStart.
    // Una cuenta de 0 es una inserción, o un borrado, en esa línea.
    foreach (var inner in change.Inner)
    {
        // inner.Original se volvió inner.Modified: las palabras, en las posiciones de cada lado.
    }
}

CodeDiffer.CompareLines(originalLines, modifiedLines);   // or two lists of lines

El diff de líneas es el script de edición más corto de Myers, el que git computa con --minimal: tan pocas líneas quitadas y agregadas como los dos textos permiten. La cabeza y la cola comunes se recortan primero, así que el costo sigue el tamaño del cambio en vez del tamaño del archivo. El diff de palabras corre el mismo algoritmo sobre los tokens de una región cambiada (tiradas de caracteres de palabra, tiradas de blanco, cualquier otro carácter por sí mismo, y un par subrogado entero), con un salto de línea entre dos líneas, así que una línea partida en dos se lee como el salto que es.

Dos rangos demasiado apartados para buscar, pasadas 2.000 rondas, se marcan enteros: un script más largo que el más corto, y todavía uno correcto. Una cuenta de rondas y no un reloj, así que .NET y la web paran en el mismo punto y responden lo mismo. Una región cambiada con más de 20.000 tokens de un lado se marca entera también, sin palabras, y se sabe en cuanto se pasa el límite: los tokens pasados él nunca se construyen, así que una línea de un millón de signos de puntuación cuesta lo que cuestan 20.000.

Lo que lo fija: las formas que una vista de diff muestra, a mano; 500 pares aleatorios contra una subsecuencia común más larga (el script reconstruye el texto modificado, y es tan corto como puede ser); las cuentas propias de --minimal de git sobre archivos reales de la historia del repositorio; y el gemelo en el runtime servido, comparado cambio por cambio, palabras incluidas, con el C#.

La vista de diff

Desde 0.2.0-preview.59

CodeDiff dibuja dos versiones de un texto y qué difiere entre ellas, como hace una revisión. Es un componente de escritura única: el mismo árbol en la web y en Photon.

new CodeDiff(before, after, "csharp")
{
    OriginalCaption = "Invoice.cs (main)",
    ModifiedCaption = "Invoice.cs",
    MaxHeight = 460,
    OnChanged = text => Save(text),   // toda edición del lado modificado, con su texto entero
}

// Un cambio como lo escribe git: un archivo de un parche, que solo se lee.
CodeDiff.OfPatch(CodePatch.Parse(patchText)[0], "csharp")
  • Lado a lado, los dos lados están nivelados en todo cambio: el lado con menos líneas se rellena, así que la línea después de un cambio está en la misma fila en los dos. En línea (Inline = true, o el interruptor de la barra de herramientas), las líneas que un cambio quitó se dibujan entre las líneas que las reemplazaron, con los números del original al lado de los del modificado.
  • Una línea cambiada está lavada por toda su fila, y las palabras que cambiaron dentro de ella están marcadas, desde los cambios internos de CodeDiffer.
  • Una tirada de líneas sin cambios más larga que Context (3 a cada lado de un cambio) se pliega en una sola fila que dice cuántas líneas contiene, y abre con una presión. Un paso, una búsqueda o un caret que aterriza dentro de un pliegue lo abre.
  • F7 y Shift+F7 avanzan al cambio siguiente y al anterior (el Alt+F5 de VS Code también), y lo mismo hacen las flechas de la barra de herramientas, dando la vuelta en cualquiera de los extremos. Un paso mueve los dos carets, que es lo que revela el cambio en los dos lados. Las teclas son propias del diff: de dos diffs en una página, avanza el que tiene el teclado.
  • Cada lado se numera como se numera su archivo. Los lados de un parche se numeran como dice el parche, y las líneas que un parche deja fuera son una fila con la cabecera de su trozo en su lugar.
  • El lado modificado EDITA cuando el diff es de dos textos y no es ReadOnly, y se compara otra vez después de toda edición. Como con CodeEditor.InitialCode, Modified abre el documento, y el documento es propio del diff desde la primera pulsación. Editor es el controlador del lado modificado, que un IDE maneja como el de cualquier editor.
  • Los dos lados están en un solo scroll vertical, así que no pueden separarse, cada uno con su propio scroll lateral. Height es Hug (hasta MaxHeight), Fill o fija, y solo se construyen las filas a la vista.

Debajo de ella, CodeBlock dibuja FILAS en vez de líneas cuando se le da un mapa CodeRows (Rows): una fila de relleno (CodeFiller), una línea de otro documento dibujada entre dos de las suyas (FillerDocument), y un pliegue (CodeCollapse). Un diff es un uso de ello, y una vista propia de una app que necesita filas que su documento no tiene es otro.

Lo que lo fija: el componente sobre la maquetación de Photon (CodeDiffComponentTests: los lados nivelados, el orden en línea, un pliegue que abre con una presión por el despacho propio del host, los pasos y su vuelta, una edición comparada otra vez, la vista de un parche, las teclas de dos diffs), las filas debajo de ello (CodeBlockRowsTests), y la página /diff del sample, recorrida en un navegador.

La mitad web

CodeSurface baja a un div con los carets como hijos posicionados absolutamente y un TEXTAREA en el caret primario. Todo comportamiento es del motor, llamado por ICodeSurfaceModel: el controlador, el documento, los tokenizadores y el historial de deshacer son salida de eqc del mismo C#, y nada en el camino del navegador reimplementa un comportamiento de editor, que es la única manera de que los dos destinos no puedan separarse.

  • keydown lleva solo lo que el mapa de teclas RECLAMA como comando. Una tecla que el Shortcut de una app reclamó no alcanza el manejador propio de ningún elemento, como en Photon.
  • El texto llega como input, nunca desde keydown: beforeinput para teclear, teclas muertas, AltGr y dictado; los eventos de composición para un método de entrada, cuyo texto se dibuja en el documento mientras se construye y se confirma una vez; y los propios eventos copy, cut y paste del portapapeles. Una superficie que leía caracteres de keydown perdía cada uno de esos.
  • El puntero lo mapea el motor: una presión, un arrastre que extiende la selección, Shift+clic, doble y triple clic (la cuenta viene de la presión, que Chrome reporta como 0 en pointerdown y solo cuenta en mousedown).
  • La superficie se encuentra por su ruta (data-eq-code) siempre que algo tenga que pasar después del render (una revelación, una petición de foco), porque el elemento sobre el que corrió un manejador queda reemplazado por él.

code-editor.spec.ts maneja la superficie como lo hace un navegador: un keydown con banderas de modificador, un evento de input, un pointerdown con coordenadas de cliente. Es la prueba de escritura única del editor: todo comportamiento que el host nativo afirma se ejercita también en el camino web.

Qué incluye el editor

Capa
Documento, posiciones, rangos ✅
Tokenizadores (C#, TS/JS, Python, JSON, XML, texto) ✅
Resaltador incremental ✅
Deshacer y rehacer con fusión ✅
Controlador: teclear, pares, indentar, comentar, movimiento, buscar, emparejar llaves ✅
Contratos de IDE: completación, hover, pliegues, diagnósticos, decoraciones, margen ✅
Componente CodeBlock (píxeles de solo lectura, margen, marcadores, decoraciones) ✅
MeasureText / MonoAdvance en el contexto (los dos destinos) ✅
Componente CodeEditor (caret, selección, teclado, ratón) ✅
CodeKeymap, un mapeo de teclas que los dos destinos llaman ✅
La superficie web, manejada por su propia spec (code-editor.spec.ts) ✅
Buscar (⌘F), emparejado de llaves, decoraciones por rango ✅
Virtualización: una ventana sobre las líneas, los dos números desde la maquetación ✅
Las dos tradiciones de teclado (KeyboardConvention), y Escape liberando Tab ✅
Texto desde la plataforma: métodos de entrada, teclas muertas, los eventos propios del portapapeles ✅
Columnas mapeadas a celdas: tabuladores, caracteres anchos, elementos de texto enteros (CodeLineCells) ✅
Height: Hug, Fill o fija, con todo dibujado enventanado a las líneas a la vista ✅
Peticiones de foco (RequestFocus) y Autofocus una vez por montaje ✅
Renderizado en el servidor: la superficie escrita por el servidor, redibujada donde no pudo medir ✅
Comparar dos textos: líneas y palabras (CodeDiffer) ✅ desde 0.2.0-preview.59
La vista de diff: lado a lado y en línea, pliegues, pasos, edición, parches (CodeDiff) ✅ desde 0.2.0-preview.59
Completación: la sesión, su filtro, sus teclas, las palabras del lenguaje y del documento (CodeCompletion) ✅ desde 0.2.0-preview.61
La lista de completación, en la palabra que completa (CodeEditor.Completions) ✅ desde 0.2.0-preview.61

El modelo, la superficie y los comportamientos de acabado están cubiertos en eQuantic.UI.Native.Engine.Tests (CodeModelTests, CodeEditorControllerTests, CodeEditorSurfaceTests, CodeEditorFinishTests, CodeViewModelTests, CodeEditorComponentTests, CodeEditorPerfTests), y la tabla de anchos en los dos lados en CellWidthOracleTests. Todo comportamiento de arriba se afirma ahí, que es además el mejor lugar para leer qué promete el editor.


Relacionado

Clone this wiki locally