Skip to content

CodeEditor pt BR

Edgar Mesquita edited this page Oct 10, 2026 · 21 revisions

Editor de código

🌐 Esta página em: English · Português

O SDK publica o modelo de um editor de código, não só uma caixa com realce de sintaxe: um documento feito de linhas, um realçador incremental, um histórico de desfazer que pensa em palavras, e um controller cujos métodos são os comandos que uma IDE põe nos menus dela. O modelo vive em eQuantic.UI.Code, um assembly próprio entre o Primitives e o Components que o SDK traz junto com eles: lógica pura e sem pixels, que é o que permite a um app construído sobre ele testar unitariamente os próprios comandos de edição sem tela, e o que permite ao mesmo editor rodar como pixels de GPU no nativo e como DOM no web. O Primitives guarda só o nó CodeSurface e o protocolo pelo qual um realizador fala com o editor (ICodeSurfaceModel), então um realizador não sabe nada do motor e o motor não sabe nada de realizador nenhum.

Por que uma camada de modelo? Porque "um editor" é 90% aritmética: em qual coluna um cursor cai depois de ↓ por uma linha curta, o que o Backspace faz dentro da indentação, qual chave casa com qual. Costure isso num controle e só dá para testar clicando. Mantenha aqui e testa-se afirmando.


Uma grade, e um cursor que dá para ver

O cursor e a faixa de seleção são posicionados por aritmética (contentTop + linha × lineHeight, contentLeft + célula × columnWidth), o que só funciona enquanto TODA parte do editor concorda com esses números. Quatro regras os mantêm concordando, cada uma delas um bug que já foi publicado:

  • Uma medição. O CodeEditor mede a grade e a entrega ao CodeBlock (Metrics), que nunca mede a dele. Duas medições divergem no momento em que as duas metades são construídas com contextos diferentes, e aí um cursor fica entre as linhas.
  • A tinta das marcas anda no nó (CaretColor / SelectionColor), não no realizador. Um editor sobre uma laje inversa escreve com uma tinta própria; pintado a partir do tema da página, um cursor fica invisível exatamente na superfície em que as pessoas digitam. Só o piscar e a porteira de foco são mecânica de folha de estilo: 500ms por fase, o mesmo que o host nativo pisca pelo relógio dele.
  • Meça com uma fonte que o CSS consiga parsear. O FontWeight rebaixa para um nome de membro; um canvas que recebe regular 11.5px … mantém 10px sans-serif e responde com um avanço proporcional, em silêncio.
  • Um mapa de uma coluna para a célula dela (CodeLineCells). Uma coluna do documento conta unidades UTF-16 e a grade conta células, e as duas se separam num tab, num caractere largo ou num emoji. O cursor, a seleção, o clique, as setas, o Backspace e o desenho leem todos o mesmo mapa: veja Colunas não são células.

As peças

Tipo O que é
CodeDocument O texto, guardado como linhas. Imutável: cada edição devolve um documento novo (é isso que o desfazer guarda).
CodePosition / CodeRange Uma linha+coluna, e um par direcionado (âncora → foco) para que shift+seta saiba qual ponta está arrastando.
ICodeLanguage Um tokenizador de uma linha por vez com estado carregado adiante, mais as Rules da linguagem.
CodeHighlighter As cores de um documento, mantidas atualizadas incrementalmente.
CodeHistory Desfazer/refazer que junta uma sequência de digitação num passo só.
CodeEditorController Todo comando de edição, sobre um documento e uma seleção: o editor menos os pixels.
CodeLineCells Onde uma linha cai na grade: a célula em que cada elemento de texto começa, e a largura da linha em células.
ICodeSurfaceModel O que um realizador pode dizer ao editor (uma tecla, algum texto, uma composição, o ponteiro, a área de transferência, o foco) e perguntar a ele (os cursores a pintar). No Primitives, ao lado do CodeSurface.

Documentos são linhas

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

Linhas em vez de uma string só porque tudo que um editor faz tem formato de linha: a calha as numera, o tokenizador as colore uma por vez, o cursor se move entre elas, e um toque de tecla não pode recopiar um megabyte.

Uma primitiva de edição, substituir um intervalo por texto, cobre inserir (intervalo vazio), apagar (texto vazio) e digitar sobre uma seleção (os dois):

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

O Clamp prende qualquer posição dentro do documento, que é por que nenhuma navegação precisa pensar nas bordas. O LineStart implementa o Home que todo editor tem: o primeiro caractere não branco, e só a coluna zero quando o cursor já está lá.


Linguagens

Incluídas: C#, TypeScript/JavaScript, Python, JSON, XML (e .csproj/.plist junto), e texto puro como o fallback que sempre renderiza.

var language = CodeLanguages.For("cs");          // por nome ou extensão; PlainText quando desconhecida
CodeLanguages.Register("sql", new SqlLanguage()); // um app traz o dialeto dele

Um tokenizador lê uma linha e devolve o estado em que a próxima linha começa:

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

Esse formato é o que torna barato recolorir um toque de tecla, e é o único jeito de uma construção que atravessa linhas funcionar: um comentário de bloco, uma string verbatim ou raw de C# (@"…", """…""", interpolada ou não), um template literal de JS, uma docstring de Python. Os tipos de token são um conjunto pequeno e fechado (Keyword, Type, String, Number, Comment, Operator, Punctuation, Function, Attribute, Property, Constant, Plain), porque um design system tem uma paleta para código.

Cada linguagem também declara as regras dela, e todo comportamento é construído a partir delas:

public CodeLanguageRules Rules { get; } = new()
{
    LineComment = "#",                       // ⌘/ ; null = o comando não faz nada (JSON)
    IndentAfter = [':', '(', '[', '{'],      // o que abre um nível (Python indenta depois de dois pontos)
    OutdentOn  = [')', ']', '}'],
    IndentWidth = 4,
    InsertSpaces = true,
};

E as suas palavras, IReadOnlyList<string> Keywords: as palavras reservadas, os tipos embutidos e as constantes, que uma completion oferece onde quer que uma palavra comece. Uma linguagem derivada de CurlyBraceLanguage as tira das tabelas com que colore (ReservedWords, TypeWords, ConstantWords), então as cores e a completion leem uma lista só. Desde 0.2.0-preview.61.


Realce incremental

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

// depois de uma edição
int repaintThrough = highlighter.LineChanged(document, line);

O LineChanged re-tokeniza aquela linha e continua só enquanto o estado final continuar saindo diferente, o que acontece quando um comentário de bloco ou uma string de várias linhas abre ou fecha, e em nenhum outro caso. Ele devolve até onde as cores se moveram, para que quem chamou repinte só isso.


O controller

O CodeEditorController é o comportamento do editor. Uma IDE o dirige a partir do mapa de teclas dela, do menu dela ou do language server dela; o componente é só o que o desenha.

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

editor.Type('(');                 // fecha sozinho, o cursor cai dentro
editor.InsertNewLine();           // herda a indentação, abre um bloco, solta a chave de fechamento
editor.Indent();                  // cursor → próxima parada de tabulação; seleção → todas as linhas
editor.ToggleLineComment();       // ⌘/ adiciona, ou remove quando todas as linhas já são comentário
editor.Move(CodeMotion.Line, CodeDirection.Forward, extend: true);
editor.Undo();  editor.Redo();
editor.FindNext("needle");
editor.MatchingBracket(editor.Caret);
editor.Apply(range, "renamed");   // um refactor: desfaz como qualquer coisa digitada

Os comportamentos que você ganha de graça

  • Pares: um colchete de abertura se fecha sozinho; digitar a metade de fechamento sobre o gêmeo autoinserido passa por cima dele em vez de duplicá-lo; apagar a metade de abertura leva o fechamento junto; uma aspa dentro de uma palavra continua sendo um apóstrofo (don't).
  • Indentação: uma linha nova herda a indentação atual e ganha um nível depois de {; Enter entre {} abre o bloco e solta o fechamento na linha dele; um colchete de fechamento digitado onde só há indentação antes dele recua um nível, até o bloco que ele fecha; Backspace no espaço em branco inicial remove um passo inteiro; Tab vai para a próxima parada, não um número fixo de espaços.
  • Uma seleção sobrevive à própria edição: Tab, ⇧Tab e ⌘/ deixam selecionadas as linhas que indentaram ou comentaram, então apertar de novo faz a mesma coisa de novo.
  • Movimento: uma sequência de ↓ por linhas irregulares lembra a coluna de onde começou; os passos por palavra param onde um leitor pararia; um → simples colapsa a seleção na borda dela.
  • Desfazer: uma sequência de digitação é um passo; mover o cursor termina a sequência; digitar sobre uma seleção é um passo só com o que a substituiu, e uma colagem é um passo próprio; uma edição nova mata o ramo de refazer.

Eventos

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

O Changed carrega o CodeEdit: intervalo, texto removido, texto inserido, seleção de cada lado. Uma IDE assina edições, não toques de tecla, porque uma colagem e um refactor são edições que ninguém digitou. O texto inserido é o texto que o documento guarda, com as linhas quebradas onde o documento as quebra: uma colagem que tinha CR ou CRLF chega com LF, e o desfazer depois dela restaura o documento exatamente.


Pontos de extensão para uma IDE

Estes são contratos que o app implementa; o trabalho do editor é posicionar o que eles devolvem.

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

Assíncronos porque a resposta normalmente cruza uma fronteira de processo, e um editor que bloqueia nela é um editor que engasga. O IndentationFoldProvider é o provedor de dobras padrão: ele funciona para toda linguagem, incluindo aquelas para as quais ninguém escreveu um parser.

Dados que um app entrega por frame:

Tipo Para
CodeDiagnostic O rabisco sob o código e a linha na lista de problemas. Um record, Range + Severity + Message (+ Code, Source).
CodeDecoration Qualquer marca extra sobre um intervalo: resultados de busca, o símbolo sob o cursor, uma chave casada, um trecho de diff. Highlight/Squiggle/Outline/Strike.
CodeGutterMarker Breakpoints, status do git, a instrução em que um depurador parou.

Completion

Desde 0.2.0-preview.61. CodeEditorController.Completion é a completion do editor: os provedores que ele consulta, e a lista enquanto uma está aberta, como um modelo que um teste dirige com teclas e que qualquer host desenha. O CodeEditor desenha a lista (abaixo); uma IDE que desenha a sua lê IsOpen, IsLoading, Items, Selected e Start, e escuta Changed.

var editor = new CodeEditorController(source, CodeLanguages.CSharp);
editor.Completion.Providers.Add(languageService);                   // o que uma IDE sabe
editor.Completion.Providers.Add(new CodeKeywordCompletionProvider()); // as palavras da própria linguagem
editor.Completion.Providers.Add(new CodeWordCompletionProvider());    // as palavras que já estão no arquivo

Um controller começa sem provedor, e sem nenhum nenhuma lista abre e nenhuma tecla vai para uma: uma lista que nada desenha nunca pode tomar o Enter. O CodeEditor entrega ao seu controller as palavras da linguagem e as do documento, a menos que lhe digam outra coisa (Completions).

O CodeWordCompletionProvider responde antes de a tecla voltar, então lê primeiro as linhas mais perto do cursor, e não mais que 50.000 caracteres delas, a própria linha do cursor em volta do cursor: uma palavra começada custa o mesmo num arquivo de qualquer tamanho, e o que um arquivo longo (ou uma linha minificada) deixa de fora é o que fica mais longe da palavra que está sendo digitada.

Quando ela consulta

Uma lista consulta os seus provedores uma vez, quando uma palavra começa, e filtra o que eles responderam a cada tecla depois disso, aqui e na hora: um serviço de linguagem é consultado por palavra, nunca por tecla.

O quê Consulta Para a palavra que começa
Um dos TriggerCharacters de um provedor (o . depois de um nome) só esse provedor depois do caractere
Uma palavra começada ao digitar, fora de um comentário ou de uma string, e que não é um número todos os provedores no início da palavra
⌃Space (Ctrl+Space no Windows e no Linux) todos os provedores na palavra antes do cursor, ou no cursor

Um provedor que responde IsIncomplete é consultado de novo sempre que a palavra muda, com o gatilho Incomplete; os outros guardam as suas respostas. Uma resposta que chega depois que a sua palavra foi deixada, ou depois de um pedido mais novo, é descartada, e o token do seu pedido é cancelado. Uma resposta que termina em outra thread é aplicada na thread de UI, como o SetState é.

As teclas, enquanto uma lista aparece

Tecla Faz
↓ ↑ Percorrem a lista, dando a volta da última entrada para a primeira. Com uma entrada só, movem o cursor.
PageDown PageUp Uma página (PageSize), parando em cada ponta.
Tab Aceita a entrada selecionada.
Enter Aceita quando isso muda o texto; uma palavra digitada por inteiro termina a sua linha em vez disso.
Escape Fecha a lista, e só a lista: a armadilha do Tab continua armada e um diálogo em volta do editor continua aberto.
Um caractere de commit (CommitCharacters) Aceita, e depois digita o caractere: Console. pega Console e segue para os seus membros.

Qualquer outra tecla, o ⌃Space e as da própria lista entre elas, arma de novo a armadilha do Tab: depois de Escape, ⌃Space e Escape, o Tab ainda indenta.

Aceitar escreve sobre a palavra digitada, ou sobre o intervalo do próprio provedor (Replacing), cujo fim guarda a sua distância do fim da linha: o que é digitado ou apagado no cursor o move, e um movimento do cursor não. Deixa o cursor depois do texto, e é um passo de desfazer. A lista fecha quando o cursor deixa a palavra (outra linha, antes do seu início, uma seleção, um caractere que termina uma palavra), quando o editor perde o teclado e quando o seu documento é trocado.

Ordenação

CodeFuzzyMatch é o filtro, e é público para qualquer outra coisa que estreita enquanto alguém digita: os caracteres do padrão em ordem, sem ligar para maiúsculas, o primeiro onde uma parte da palavra começa (bc acha BarChart, col acha Column, lum não acha nada), e o melhor jeito de pôr um sobre o outro, uma sequência acima dos mesmos caracteres separados. Casamentos iguais vão pelo SortText do provedor, depois pelo rótulo. Entre os melhores casamentos, uma entrada que um provedor marca com Preselect é a selecionada. Dois provedores que oferecem a mesma entrada (um rótulo, um texto inserido) a listam uma vez, como a cópia do primeiro provedor que casa com a palavra.

Escrevendo um provedor

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

Os contratos são os do Language Server Protocol, tipados, então um cliente LSP é um adaptador: o gatilho Typing do contexto é o Invoked do LSP, IsIncomplete o seu isIncomplete, Replacing o seu intervalo de edição, ResolveAsync o seu completionItem/resolve, e o token o seu $/cancelRequest. CodeCompletionKind tem todos os tipos que o LSP tem.

A falha de um provedor é dele: um que lança exceção, cuja resposta falha, ou cujo callback no token lança quando a lista o cancela (uma conexão que caiu) é reportado por Failed, e a lista segue com o que os outros ofereceram. Nenhuma tecla falha por causa dele. Uma OperationCanceledException é o cancelamento do próprio pedido só quando a lista o cancelou, e a falha do provedor nos outros casos.

A lista que o CodeEditor desenha

Desde 0.2.0-preview.61. Enquanto a completion mostra uma lista, o CodeEditor a desenha na palavra que ela completa, pela superfície do código e nas coordenadas do próprio código, então a lista se move com o código quando o código rola.

new CodeEditor(source, "csharp")
{
    Height = SizeValue.Fill,
    // De onde ele completa, nesta ordem. Null, o padrão, são as palavras da linguagem e as do
    // documento; uma lista vazia é nenhuma completion.
    Completions = [languageService, new CodeKeywordCompletionProvider(), new CodeWordCompletionProvider()],
}

Um editor somente leitura não completa nada, seja o que for que lhe derem, e tornar um editor somente leitura fecha a lista que ele mostra e descarta uma resposta ainda a caminho. O editor entrega os provedores ao seu controller uma vez, e de novo só quando a lista traz outros provedores (inclusive uma lista que o pai mudou no lugar), então um pai que reconstrói com os mesmos não muda nada, e uma lista que já está aparecendo segue com os provedores a que foi pedida. Ele só tira o que pôs: um provedor que a app acrescentou ao próprio Editor.Completion.Providers fica ao lado deles.

  • Onde ela fica. Uma linha abaixo da palavra, com os rótulos alinhados com o que foi digitado. Quando uma página de linhas não cabe abaixo dentro do viewport do editor e cabe acima, ela fica acima da linha; quando nenhum dos lados comporta uma página, ela fica no lado com mais espaço e mostra tantas linhas quantas couberem, nunca menos de uma. A borda direita fica dentro do viewport. Um editor que abraça o código tem a altura do código como viewport, então um editor de duas linhas mostra uma lista curta; o código de um editor limitado preenche o viewport, então um arquivo curto num painel alto tem o espaço do painel.
  • O que uma linha diz. As linhas são as do próprio código, a altura de linha do editor na sua fonte de código: o tipo da entrada como uma letra, na cor que o código dá ao que ela nomeia, o rótulo com o que a palavra casou na cor de destaque, e o detalhe, esmaecido. A entrada selecionada fica realçada, e a documentação dela aparece abaixo da lista (acima, quando a lista fica acima da linha) assim que o provedor a resolve, em até quatro linhas de texto simples. Ela ocupa o espaço que as linhas deixam desse lado e nada mais, tantas linhas dela quantas couberem ou nenhuma: as linhas da lista nunca cedem a ela, então a lista não pula quando a seleção passa entre entradas com e sem documentação. Uma linha mais longa que a lista corta primeiro o detalhe e depois o rótulo, cada um terminando em reticências, contando as células que a grade do código dá a cada caractere (um caractere largo ou um emoji ocupa duas) e nunca cortando dentro de um, e o nome dela para a tecnologia assistiva guarda os dois inteiros.
  • Uma página. A lista mostra até doze linhas e constrói só essas, a primeira seguindo a seleção quando ela passa de qualquer ponta. PageUp e PageDown andam pelas linhas mostradas, e uma marca à direita diz onde a página está no todo.
  • Um toque numa linha a aceita, e o teclado fica no código (Pressable.CanRequestFocus). Um toque na lista que nenhuma linha pega não move cursor nenhum.
  • Tecnologia assistiva. Na web a lista é o listbox do input do código: enquanto ela aparece, o input diz aria-autocomplete="list", nomeia a lista em aria-controls e a opção selecionada em aria-activedescendant. No Photon as linhas são anunciadas como opções depois do campo de código, a selecionada como selecionada.
Letra Tipos
m método, função, construtor
v campo, variável
p propriedade
e evento
C S I E T N classe, struct, interface, enum, parâmetro de tipo, módulo
c constante, membro de enum
# valor, unidade, cor
k palavra-chave
s snippet
o operador
r f d referência, arquivo, pasta
a texto (uma palavra do documento)

Cercado, de propósito: a roda do mouse sobre a lista rola o código e não a lista, que as setas e as teclas de página percorrem; a documentação é texto simples até o cartão de hover renderizar Markdown; e no Photon a lista fica à distância da largura de uma borda da sua gêmea da web até uma caixa com borda manter o filho dentro da borda (#629), enquanto a margem de toque de uma linha pode pegar um toque destinado à linha acima dela (#630): 13dp dela sob um dedo, e 3dp sob um ponteiro, cujo alvo guarda um piso de 24dp.


CodeBlock: a superfície somente leitura

O modelo desenha por um componente. Cada linha vira uma Row de trechos Text coloridos, que é por que ele não precisa de suporte de motor além da face monoespaçada: a mesma árvore renderiza como pixels de GPU e como DOM.

new CodeBlock(source, "csharp")
{
    ShowLineNumbers = true,
    FirstLineNumber = 120,          // um fragmento citado da linha 120 diz 120
    MaxHeight = 320,                // limita a altura e rola além disso
    ActiveLine = 4,                 // a linha atual do depurador
    GutterMarkers = [new CodeGutterMarker(4, CodeGutterKind.Breakpoint)],
    Decorations  = [new CodeDecoration(range, CodeDecorationKind.Search)],
    OnGutterPressed = line => ToggleBreakpoint(line),
    OnCopy = () => clipboard.Write(source),
    Caption = "Program.cs",
}
Propriedade Para que serve
Inverse Uma laje escura nos DOIS modos: código como figura numa documentação, não como controle.
Highlighter Reuse um entre frames para que a coloração continue incremental (um editor reusa; um trecho isolado não precisa).
Size O tamanho do próprio código; a calha o segue.
Standalone Se o bloco é o componente inteiro (laje própria, viewport próprio) ou conteúdo cru que algo de fora enquadra e rola. Verdadeiro por padrão; o CodeEditor o põe como falso.
ViewportWidth Quão largo o viewport acabou sendo, devolvido pelo layout. O conteúdo nunca é mais estreito que isso e nunca mais largo do que precisa.

Duas regras que o componente mantém e que é fácil errar:

  • A calha é MEDIDA, não adivinhada: context.MeasureText(lastNumber + "0", style). Um arquivo com 1000 linhas precisa de uma coluna que um de 10 não precisa.
  • Linhas longas rolam para o lado, nunca quebram. Uma linha de código quebrada perdeu a única coisa que a indentação dela estava te dizendo.

Medir faz parte do contexto

O ComponentContext.MeasureText(text, style) e o MonoAdvance(style) respondem quão larga uma string SERIA, em dp, antes de ser posicionada. O nativo pergunta ao serviço de texto da plataforma; o web pergunta ao browser por um contexto 2D de canvas usando as mesmas pilhas de fonte que o CSS usa, então os dois respondem com os mesmos números com que cada alvo vai posicionar o texto, que é do que depende mapear um clique para uma coluna.

CodeEditor: a superfície editável

O mesmo desenho, mais as três coisas que fazem dele um editor: um cursor, uma seleção e um teclado.

new CodeEditor(source, "csharp")
{
    Height = SizeValue.Fill,        // tão alto quanto o lugar dele: o painel de uma IDE
    OnChanged = text => _dirty = true,
    OnSelectionChanged = range => _status = $"Ln {range.Focus.Line + 1}, Col {range.Focus.Column + 1}",
    Autofocus = true,
    ReadOnly = false,
}

O componente é dono de um CodeEditorController e o entrega a um nó CodeSurface. Uma IDE recorre ao editor.Editor para rodar comandos que ninguém digitou (um formatador, uma renomeação, a edição de um language server), e eles desfazem como qualquer outra coisa, porque passam pela mesma primitiva.

O OnChanged é disparado por uma edição e o OnSelectionChanged por um movimento, cada um só quando a coisa dele mudou: uma seta não é uma edição, e uma barra de status escuta o segundo. Os dois eram disparados juntos para qualquer coisa que a superfície fizesse, então um app que relia o documento a cada mudança fazia isso a cada seta.

Um keymap, duas superfícies, duas tradições

O CodeKeymap.Handle(editor, key, modifiers, convention, clipboard) é onde um NOME de tecla vira um comando. Ele é C# puro, então transpila junto com todo o resto e as duas superfícies chamam a mesma função: um host do Photon a partir do evento de tecla dele, o browser a partir do keydown dele. Nada sobre o que ⌥← ou ⇧Tab significam é decidido num realizador.

Desde 0.2.0-preview.58

Ele fala as duas tradições de teclado, porque elas discordam exatamente nas teclas de que um editor vive: ⌘← é "início da linha" num Mac e Ctrl+← é "uma palavra para trás" em todo o resto. O host diz em qual delas os usuários dele vivem (KeyboardConvention.Apple no macOS e no iPadOS, Standard em todo o resto), e o KeyModifiers.Command é ⌘ numa e Ctrl na outra. Um keymap que só conhecia a da Apple fazia usuários de Windows e Linux pularem para o início da linha toda vez que queriam passar por cima de uma palavra.

Tecla Apple Standard
← → um caractere; ⌥ uma palavra; ⌘ a borda da linha um caractere; Ctrl uma palavra
↑ ↓ uma linha; ⌘ a borda do documento uma linha (Ctrl+↑ e Ctrl+↓ rolam uma view, e ficam a cargo do host)
Home / End a borda da linha; ⌘ a do documento a borda da linha; Ctrl a do documento
PageUp / PageDown uma página uma página
⇧ + qualquer uma delas estende a partir da âncora estende a partir da âncora
Enter uma linha nova, herdando a indentação (um nível a mais depois de {) igual
Tab / ⇧Tab indenta / desindenta: a seleção, ou até a próxima parada de tabulação igual
Backspace / Delete um caractere; ⌥ uma palavra; ⌘⌫ a linha até o cursor; espaço em branco inicial vai um passo inteiro um caractere; Ctrl uma palavra
desfazer / refazer ⌘Z / ⇧⌘Z, e ⌘Y Ctrl+Z / Ctrl+Shift+Z, e Ctrl+Y
selecionar tudo, copiar, recortar, colar ⌘A ⌘C ⌘X ⌘V; uma cópia sem nada selecionado leva a linha Ctrl+A, C, X, V, igual
comentário ⌘/ alterna comentários de linha (nada numa linguagem que não tem) Ctrl+/
Escape libera o Tab (abaixo) igual

O Tab é do editor, e o Escape o devolve. Um editor fica com o Tab, que é o objetivo, e um usuário de teclado ainda tem que conseguir sair dele. O Escape solta a armadilha, então o próximo Tab leva o foco adiante em vez de indentar, e qualquer outra tecla a arma de novo. O próprio Escape não é reivindicado, então continua significando o que significa em volta do editor (ele fecha o diálogo em que o editor está), e no Photon ele também tira o teclado do editor. Um editor somente leitura nunca fica com o Tab. Dentro de um diálogo é igual: o diálogo só cicla um Tab que o editor não pegou, então um editor em qualquer ponta de um diálogo continua indentando, e depois do Escape o próximo Tab segue como o diálogo cicla.

O texto vem da plataforma, nunca de uma tecla. O que um toque de tecla produz é assunto da plataforma (uma tecla morta, um método de entrada, o AltGr num layout europeu, ditado, "á" a partir de três eventos), então o texto chega como string e vai para o HandleText, onde vivem os pares que se fecham sozinhos e a regra de passar por cima do fechamento. A composição de um método de entrada fica DENTRO do documento enquanto está sendo montada, sublinhada com a tinta do próprio código, e é confirmada como uma edição só; cancelá-la deixa o documento como estava. A área de transferência também é da plataforma: no web as teclas de cópia ficam para os eventos de copiar, recortar e colar do próprio browser, que carregam o texto, então uma colagem vinda de outro aplicativo chega.

A geometria é aritmética

A face é monoespaçada, então uma (linha, célula) É (contentTop + linha × lineHeight, contentLeft + célula × columnWidth), e um cursor repinta a cada toque de tecla sem medir nada nem refazer o layout. Os dois realizadores usam os mesmos números, e o CodeBlock.MetricsFor é o único lugar de onde eles vêm; dois cálculos independentes divergiriam por um pixel e depois por um caractere. O motor faz a aritmética (o editor.Grid guarda as métricas), então um realizador pinta os retângulos que recebe e nunca transforma uma coluna em pixels.

Uma seleção é uma FAIXA POR LINHA, nunca um retângulo sobre o intervalo: um retângulo único cobriria a indentação de linhas que o intervalo nunca tocou. O componente desenha as faixas sob o texto, nas camadas do próprio código, então elas ficam iguais em todo alvo; um realizador pinta só os cursores, que precisam piscar.

Colunas não são células

Desde 0.2.0-preview.58

Uma COLUNA do documento é um deslocamento no texto UTF-16 da linha, e uma CÉLULA é um avanço da face mono na tela. Elas são o mesmo número só enquanto todo caractere tem uma unidade de comprimento e uma célula de largura, e três coisas não têm:

  • Um tab vai até a próxima parada, um múltiplo da largura de indentação da linguagem: com paradas de 4, \tx põe o x na célula 4.
  • Um caractere largo (largo ou de largura total do Leste Asiático, e um emoji) ocupa duas células.
  • Um elemento de texto (um cluster de grafemas estendido: 👍🏽, uma bandeira, e com um acento combinante) nunca é partido: as marcas, os joiners e os modificadores dele, e a segunda metade de um par substituto, não ocupam célula própria, e nenhum cursor cai dentro dele.

O CodeLineCells é o único mapa de uma para a outra, e tudo o lê: o cursor e a seleção são posicionados por ele, o bloco desenha as células que ele nomeia, um clique é o inverso dele (um ponto na metade esquerda de um tab cai antes dele, na metade direita depois), as setas e o Backspace andam por elemento, e os passos por palavra e os cliques duplos selecionam elementos inteiros. No web os elementos vêm do Intl.Segmenter, que divide o texto como o StringInfo do .NET divide. Quais caracteres são largos é uma tabela que o motor mantém, e ela é comparada, nos dois lados e para todo ponto de código, com a convenção de terminal que o Bun embarcado do SDK segue (CellWidthOracleTests).

Um espaço de coordenadas

Desde 0.2.0-preview.21

As marcas são desenhadas contra a SUPERFÍCIE que as segura, então nada pode rolar dentro dela. O viewport vive FORA do CodeSurface (o editor o constrói), e a superfície viaja com o código:

Stack (as camadas, sempre lá)
 ├ Box (a laje, cortada)
 │  └ ScrollView (vertical, quando o editor é limitado)
 │     └ Row
 │        ├ a calha         ← ao lado da rolagem lateral, dentro da vertical
 │        └ ScrollView (horizontal)
 │           └ CodeSurface  ← move com o código, então as marcas também
 │              └ CodeBlock ← Standalone = false: conteúdo cru, sem laje, sem viewport
 ├ o canto (a legenda)
 └ a barra de busca, enquanto está aberta

O código é a PRIMEIRA camada haja ou não alguma coisa sobre ele. Abrir a busca embrulhava o código num Stack que ele não tinha antes, e uma superfície que se move na árvore é uma superfície nova para todo host: a rolagem voltava ao topo, o "próximo" não revelava nada, e no Photon o teclado apontava para um caminho que já não pertencia a nada.

Errar isso não é sutil quando você procura, e é invisível até procurar: um bloco que rola DENTRO da superfície põe o código num espaço e o cursor em outro. Role uma linha longa para o lado e o texto viaja enquanto o cursor fica para trás; clique, e a coluna é lida como se nada tivesse rolado. Pôr o viewport do lado de fora torna cada uma dessas somas verdadeira por construção: não sobra nada para manter em sincronia.

Duas consequências que vale declarar, porque cada uma foi um bug:

  • O conteúdo tem a largura do VIEWPORT, nunca menos que a linha mais longa. Só preencher é por que a rolagem lateral nunca rolava: uma view de rolagem cujo conteúdo tem exatamente o tamanho dela não tem o que mover. Dimensionar só pelo código é o erro oposto: um clique no espaço vazio à direita de uma linha curta cairia em nada.
  • A largura volta DO layout (ViewportWidth), do jeito que a altura já voltava. Os dois alvos discordam sobre o que preencher significa dentro de uma view de rolagem lateral (uma página resolve 100% contra o rolador, o Photon mede o conteúdo sem limite no eixo da rolagem), e um número reportado é a aritmética com que os dois realizadores concordam.

O cursor volta para a vista

Desde 0.2.0-preview.22

Andar com a seta para fora da borda de uma linha longa, ou para baixo além da última visível, deixava o cursor onde a aritmética o punha: fora da caixa. Duas coisas têm que estar certas, e cada uma é fácil de errar de um jeito que parece implementado:

  • Qual elemento. Cada toque de tecla reconstrói a árvore, então a superfície em que o handler rodou já está desanexada quando qualquer coisa roda depois, e o scrollIntoView num cursor desanexado tem sucesso em silêncio. A superfície carrega o caminho dela (data-eq-code), e a revelação resolve por ele.
  • Quando. O render é despejado num frame de animação, então um microtask acha um cursor que ainda não se moveu e corretamente decide que ele já está na tela. Ele espera o frame depois do despejo, e TAMBÉM um timeout, o mesmo par que o agendador de render mantém, porque uma aba escondida ou estrangulada para de entregar frames e o render acontece de qualquer jeito.

A calha fica onde está

Desde 0.2.0-preview.25

Os números são uma coluna própria, AO LADO da rolagem lateral e dentro da vertical: eles descem o arquivo com o código e ficam parados enquanto ele desliza de lado.

Dois outros arranjos foram tentados antes e os dois estavam errados do mesmo jeito. Dentro da rolagem, os números iam embora com o código e o leitor perdia o número da linha que estava lendo. Sobrepostos por cima dela, o código deslizava POR BAIXO de uma coluna opaca e caracteres reais sumiam: using virava eQuantic.UI.Core;, o que parece um bug de renderização e na verdade é de camada.

Ao lado, os dois continuam verdadeiros e nenhum compensa o outro. O preço está declarado nas métricas:

ContentLeft  =  o padding esquerdo do próprio código      ← onde a coluna 0 começa
             ≠  calha + padding                            ← o que era antes

Essa linha é por que isto exigiu uma mudança deliberada e não um retoque. A coluna zero é de onde o cursor, a faixa de seleção e toda decoração começam a contar, então mover a origem dela move as três de uma vez, que é exatamente por que ela é uma propriedade e não três, e por que todas puderam ser movidas numa edição. O ESPAÇO entre os números e o código pertence à calha agora, não ao código: um padding dentro da rolagem desliza embora, e os dígitos acabavam encostando no primeiro caractere.

Buscar, casar, e arquivos longos demais para construir

Buscar

O ⌘F (Ctrl+F) abre uma barra sobre o canto superior direito, sobre e não acima: código que salta quando você abre a busca perdeu a linha que você estava olhando. Todo resultado é lavado e o ATUAL é contornado, porque um "próximo resultado" que move algo invisível não te disse nada.

Desde 0.2.0-preview.58

  • O campo fica com o teclado quando a barra abre, e não o larga: Enter e as setas percorrem os resultados, e a contagem lê 3/17. A contagem é do que o PRÓPRIO campo procura, então um campo vazio não mostra contagem.
  • Um passo seleciona o resultado, o que o revela, e o app ouve o movimento pelo OnSelectionChanged, como ouve quando uma tecla move o cursor.
  • O Escape fecha a barra onde quer que o teclado esteja, na barra ou no código, e devolve o teclado ao código.
  • O botão de fechar é nomeado no idioma da interface (SdkStrings.CloseFind).

Uma IDE com a própria interface de busca pula tudo isso e define Search / SearchMatchCase diretamente: os resultados dela são marcados enquanto a barra do próprio editor está fechada ou com o campo vazio.

Chaves e colchetes

O MatchBrackets (ligado por padrão) contorna o delimitador contra o qual o cursor está e o par dele. Um cursor fica ENTRE caracteres, então ele pertence ao delimitador de qualquer um dos lados, e o de TRÁS vence: tendo acabado de digitar ), é esse que você quer dizer.

As duas marcas são CodeDecorationKind.Outline, não uma lavagem: uma lavagem esconderia o caractere para o qual a marca está apontando.

Decorações são intervalos

Uma decoração é um INTERVALO, e ela desenha como um retângulo por linha que atravessa, a mesma aritmética que a faixa de seleção usa.

Tipo O que desenha
Highlight uma lavagem de fundo: um resultado de busca, um símbolo sob o cursor
Outline uma caixa em volta do intervalo: um delimitador casado
Squiggle um filete abaixo: um diagnóstico
Strike um filete atravessando: apagado num diff, código inalcançável

Numa laje Inverse cada uma delas pega a metade ESCURA da cor dela, pela mesma razão que os tokens pegam: um token de modo claro sobre código escuro lê como falha de renderização.

Virtualização

Um CodeEditor LIMITADO constrói só as linhas que o viewport consegue mostrar, mais uma margem de cada lado para que uma rolagem de uma linha não construa nada. Acima e abaixo da janela fica um espaçador cada, então o conteúdo continua tão alto quanto o arquivo e a barra de rolagem diz a verdade.

Desde 0.2.0-preview.58

O Height diz quão alto o editor é. O Hug, o padrão, é tão alto quanto o código, até o MaxHeight quando há um; o Fill pega a altura que o lugar dele dá, que é como o painel de uma IDE o usa; e uma altura fixa é essa quantidade de dp. Qualquer coisa que não seja um Hug sem teto é limitada, e um editor limitado com um MaxHeight pega a altura do lugar dele até o teto. Sem um limite não há viewport nem janela: cada toque de tecla construía todas as linhas, de 100 a 213 ms por tecla num arquivo de 3000 linhas.

O código de um editor limitado preenche o viewport por mais curto que seja o arquivo: um toque abaixo da última fileira põe o cursor no fim do documento, como em qualquer editor de código, e um arrasto a partir dali seleciona de volta até onde ele para. Os preenchimentos de um diff depois da última linha também são fileiras, e um toque num deles cai nessa linha, na coluna dele, como em qualquer preenchimento. Desde 0.2.0-preview.61.

O Fill segue a regra do layout: num pai sem limite próprio, uma página que rola por exemplo, ele é tão alto quanto o código, nos dois alvos. Dê a ele um lugar com altura.

Todo o resto que uma construção desenha também fica restrito à janela, como as linhas: as faixas da seleção, os resultados de uma busca (achados uma vez por busca, não por construção) e toda outra marca são feitos só para as linhas à vista, e a linha mais larga, que define a largura do código, é medida uma vez por documento. Um passo de rolagem sobre 50.000 linhas com tudo selecionado e uma busca ativa foi de 81 ms para 3.

Os dois números vêm do layout, por dois canais novos no ScrollView:

new ScrollView(content)
{
    OnScrolled = offset => …,          // onde ELA ESTÁ, sempre que isso muda
    OnViewportChanged = height => …,   // quão alta ela acabou sendo
}

Eles são o canal de saída para o canal de entrada do Offset, e são o que torna qualquer lista longa possível: sem eles o deslocamento vive no host e nenhum componente consegue perguntar. O primeiro frame não tem nenhum dos dois e constrói tudo, o que está certo para um trecho; o segundo sabe os dois e estreita. No web o viewport é medido assim que o render foi escrito, e de novo sempre que ele muda de tamanho sem render nenhum (a janela redimensionada, um divisor arrastado), então um editor Fill num painel que cresceu constrói as linhas que o painel agora mostra.

Pedir o teclado

Desde 0.2.0-preview.58

Uma IDE devolve o teclado ao código depois que um painel sobre ele fecha, ou quando um arquivo abre:

editor.Editor.RequestFocus();

O pedido pertence ao MODELO (ICodeSurfaceModel.FocusVersion), então qualquer host que desenhe a superfície o atende, uma vez: um pedido feito antes do primeiro frame é atendido quando a superfície é desenhada, e um já atendido não é atendido de novo quando a superfície é desenhada em outro lugar. A própria barra de busca do editor o usa ao fechar.

O Autofocus é atendido uma vez por MONTAGEM, como um browser faz: um campo ou um editor que aparece toma o teclado de quem o segurava, porque ele apareceu porque alguém o abriu, e um que sai e volta pede de novo.

Numa página renderizada no servidor

Desde 0.2.0-preview.58

O servidor escreve a superfície (o código, os cursores e a entrada dela), mas não tem fontes: ele mede toda string com largura 0. Um componente cujo próprio Build mediu texto no servidor é marcado (data-eq-unmeasured), e a hidratação desenha aquela subárvore de novo no cliente em vez de adotar uma marcação construída sobre larguras que ninguém mediu. A calha de um bloco de código numa página renderizada no servidor tem a largura dos números dela por causa disso.

Comparar dois textos

Desde 0.2.0-preview.59

O CodeDiffer responde o que difere entre dois textos: as linhas, e dentro de cada região alterada as palavras. É o que uma vista de diff desenha e o que uma IDE lê para mostrar um arquivo contra o seu último commit.

var changes = CodeDiffer.Compare(original, modified);   // dois CodeDocuments
foreach (var change in changes)
{
    // OriginalCount linhas a partir de OriginalStart viraram ModifiedCount linhas a partir de ModifiedStart.
    // Uma contagem 0 é uma inserção, ou uma remoção, naquela linha.
    foreach (var inner in change.Inner)
    {
        // inner.Original virou inner.Modified: as palavras, nas posições de cada lado.
    }
}

CodeDiffer.CompareLines(originalLines, modifiedLines);   // ou duas listas de linhas

O diff de linhas é o script de edição mais curto de Myers, o que o git calcula com --minimal: tão poucas linhas removidas e adicionadas quanto os dois textos permitem. O começo e o fim em comum são aparados primeiro, então o custo acompanha o tamanho da mudança e não o do arquivo. O diff de palavras roda o mesmo algoritmo sobre os tokens de uma região alterada (sequências de caracteres de palavra, sequências de espaço, qualquer outro caractere sozinho, e um par substituto inteiro), com uma quebra de linha entre duas linhas, então uma linha partida em duas se lê como a quebra que é.

Dois trechos distantes demais para procurar, depois de 2.000 rodadas, são marcados inteiros: um script mais longo que o mais curto, e ainda assim certo. Uma contagem de rodadas e não um relógio, para que o .NET e o web parem no mesmo ponto e respondam o mesmo. Uma região alterada com mais de 20.000 tokens de um lado também é marcada inteira, sem palavras, e isso se sabe assim que o limite é passado: os tokens depois dele nunca são construídos, e uma linha de um milhão de sinais de pontuação custa o que custam 20.000.

O que o fixa: as formas que uma vista de diff mostra, à mão; 500 pares aleatórios contra uma maior subsequência comum (o script reconstrói o texto modificado, e é tão curto quanto pode ser); as contagens --minimal do próprio git em arquivos reais do histórico do repositório; e o gêmeo no runtime servido, comparado mudança a mudança, palavras incluídas, com o C#.

A vista de diff

Desde 0.2.0-preview.59

CodeDiff desenha duas versões de um texto e o que difere entre elas, como uma revisão faz. É um componente write-once: a mesma árvore no web e no Photon.

new CodeDiff(before, after, "csharp")
{
    OriginalCaption = "Invoice.cs (main)",
    ModifiedCaption = "Invoice.cs",
    MaxHeight = 460,
    OnChanged = text => Save(text),   // cada edição do lado modificado, com o texto inteiro
}

// Uma alteração como o git a escreve: um arquivo de um patch, que só se lê.
CodeDiff.OfPatch(CodePatch.Parse(patchText)[0], "csharp")
  • Lado a lado, os dois lados ficam nivelados em cada alteração: o lado com menos linhas recebe preenchimento, e a linha depois de uma alteração fica na mesma fileira nos dois. Em linha (Inline = true, ou o botão da barra), as linhas que uma alteração removeu são desenhadas entre as linhas que as substituíram, com os números do original ao lado dos do modificado.
  • Uma linha alterada ganha um fundo de ponta a ponta, e as palavras que mudaram dentro dela são marcadas, a partir das alterações internas do CodeDiffer.
  • Uma sequência de linhas inalteradas maior que Context (3 de cada lado de uma alteração) se dobra numa fileira que diz quantas linhas guarda, e abre com um toque. Um passo, uma busca ou um cursor que cai dentro de uma dobra a abre.
  • F7 e Shift+F7 vão para a próxima alteração e para a anterior (o Alt+F5 do VS Code também), assim como as setas da barra, voltando ao começo em cada ponta. Um passo move os dois cursores, que é o que revela a alteração dos dois lados. As teclas são do próprio diff: de dois diffs numa página, anda o que tem o teclado.
  • Cada lado é numerado como o seu arquivo. Os lados de um patch são numerados como o patch diz, e as linhas que um patch deixa de fora viram uma fileira com o cabeçalho do seu hunk no lugar delas.
  • O lado modificado EDITA quando o diff é de dois textos e não é ReadOnly, e é comparado de novo a cada edição. Como com CodeEditor.InitialCode, Modified abre o documento, e o documento é do próprio diff desde a primeira tecla. Editor é o controlador do lado modificado, que uma IDE conduz como o de qualquer editor.
  • Os dois lados ficam numa só rolagem vertical, para não se desalinharem, cada um com a sua rolagem lateral. Height é Hug (até MaxHeight), Fill ou fixo, e só as fileiras à vista são construídas.

Por baixo, o CodeBlock desenha FILEIRAS em vez de linhas quando recebe um mapa CodeRows (Rows): uma fileira de preenchimento (CodeFiller), uma linha de outro documento desenhada entre duas das suas (FillerDocument), e uma dobra (CodeCollapse). Um diff é um uso disso, e uma vista da própria app que precise de fileiras que o seu documento não tem é outro.

O que a fixa: o componente no layout do Photon (CodeDiffComponentTests: os lados nivelados, a ordem em linha, uma dobra que abre com um toque pelo despacho do próprio host, os passos e a volta, uma edição comparada de novo, a vista de um patch, as teclas de dois diffs), as fileiras por baixo (CodeBlockRowsTests), e a página /diff do sample, percorrida num browser.

A metade web

O CodeSurface rebaixa para uma div com os cursores como filhos posicionados de forma absoluta e uma TEXTAREA no cursor principal. Todo comportamento é do motor, chamado pelo ICodeSurfaceModel: o controller, o documento, os tokenizadores e o histórico de desfazer são saída do eqc a partir do mesmo C#, e nada no caminho do browser reimplementa um comportamento de editor, que é o único jeito de os dois alvos não conseguirem divergir.

  • O keydown leva só o que o keymap REIVINDICA como comando. Uma tecla que um Shortcut do app reivindicou não chega ao handler próprio de nenhum elemento, como no Photon.
  • O texto chega como input, nunca do keydown: o beforeinput para digitação, teclas mortas, AltGr e ditado; os eventos de composição para um método de entrada, cujo texto é desenhado no documento enquanto é montado e é confirmado uma vez só; e os eventos copy, cut e paste da própria área de transferência. Uma superfície que lia caracteres do keydown perdia cada um desses.
  • O ponteiro é mapeado pelo motor: um pressionamento, um arrasto que estende a seleção, Shift+clique, clique duplo e triplo (a contagem vem do pressionamento, que o Chrome reporta como 0 no pointerdown e só conta no mousedown).
  • A superfície é achada pelo caminho dela (data-eq-code) sempre que algo tem que acontecer depois do render (uma revelação, um pedido de foco), porque o render substitui o elemento em que um handler rodou.

O code-editor.spec.ts dirige a superfície do jeito que um browser dirige: um keydown com flags de modificador, um evento de input, um pointerdown com coordenadas de cliente. Ele é a prova write-once do editor: todo comportamento que o host nativo afirma é exercitado no caminho web também.

O que o editor inclui

Camada
Documento, posições, intervalos ✅
Tokenizadores (C#, TS/JS, Python, JSON, XML, texto) ✅
Realçador incremental ✅
Desfazer/refazer com junção ✅
Controller: digitação, pares, indentação, comentário, movimento, busca, casamento de delimitadores ✅
Contratos de IDE: completação, hover, dobras, diagnósticos, decorações, calha ✅
Componente CodeBlock (pixels somente leitura, calha, marcadores, decorações) ✅
MeasureText / MonoAdvance no contexto (nos dois alvos) ✅
Componente CodeEditor (cursor, seleção, teclado, mouse) ✅
CodeKeymap, um mapeamento de teclas que os dois alvos chamam ✅
A superfície web, dirigida pela spec própria (code-editor.spec.ts) ✅
Busca (⌘F), casamento de delimitadores, decorações por intervalo ✅
Virtualização: uma janela sobre as linhas, os dois números vindos do layout ✅
As duas tradições de teclado (KeyboardConvention), e o Escape liberando o Tab ✅
Texto da plataforma: métodos de entrada, teclas mortas, os eventos da própria área de transferência ✅
Colunas mapeadas para células: tabs, caracteres largos, elementos de texto inteiros (CodeLineCells) ✅
Height: Hug, Fill ou fixa, com tudo que é desenhado restrito às linhas à vista ✅
Pedidos de foco (RequestFocus) e Autofocus uma vez por montagem ✅
Renderização no servidor: a superfície escrita pelo servidor, redesenhada onde ele não conseguiu medir ✅
Comparar dois textos: linhas e palavras (CodeDiffer) ✅ desde 0.2.0-preview.59
A vista de diff: lado a lado e em linha, dobras, passos, edição, patches (CodeDiff) ✅ desde 0.2.0-preview.59
Completion: a sessão, o seu filtro, as suas teclas, as palavras da linguagem e do documento (CodeCompletion) ✅ desde 0.2.0-preview.61
A lista de completion, na palavra que ela completa (CodeEditor.Completions) ✅ desde 0.2.0-preview.61

O modelo, a superfície e os comportamentos de acabamento estão cobertos em eQuantic.UI.Native.Engine.Tests (CodeModelTests, CodeEditorControllerTests, CodeEditorSurfaceTests, CodeEditorFinishTests, CodeViewModelTests, CodeEditorComponentTests, CodeEditorPerfTests), e a tabela de larguras nos dois lados em CellWidthOracleTests. Todo comportamento acima é afirmado lá, que também é o melhor lugar para ler o que o editor promete.


Relacionado

  • Design System: a escala de tipo (incluindo a face mono) e a paleta de tokens com que o editor colore.
  • Componentes write-once: como a camada de componentes acima deste modelo alcança os dois alvos.

Clone this wiki locally