Repository navigation
CodeEditor pt BR
🌐 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.
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
CodeEditormede a grade e a entrega aoCodeBlock(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
FontWeightrebaixa para um nome de membro; um canvas que receberegular 11.5px …mantém10px sans-serife 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.
| 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. |
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á.
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 deleUm 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.
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 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-
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.
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.
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. |
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 arquivoUm 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.
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 é.
| 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.
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.
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.
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 emaria-controlse a opção selecionada emaria-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.
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.
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.
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.
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 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.
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,
\txpõe oxna 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,
ecom 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).
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.
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
scrollIntoViewnum 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.
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.
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.
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.
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.
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.
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.
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.
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 linhasO 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#.
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 comCodeEditor.InitialCode,Modifiedabre 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.
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
keydownleva só o que o keymap REIVINDICA como comando. Uma tecla que umShortcutdo app reivindicou não chega ao handler próprio de nenhum elemento, como no Photon. -
O texto chega como input, nunca do
keydown: obeforeinputpara 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 eventoscopy,cutepasteda própria área de transferência. Uma superfície que lia caracteres dokeydownperdia 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
pointerdowne só conta nomousedown). -
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.
| 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.
- 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.
🌐 English · Português
🏁 Comece aqui
🏗️ Arquitetura
- Visão geral da arquitetura
- Componentes write-once
- Superfície declarativa
- Arquitetura de pacotes
- Componentes
- Estilo
- Localização
- Analytics e GTM
📱 Write-once
- Motor Photon
- Design System
- Capacidades
- Armazenamento
- Formulários
- Editor de código
- Markdown
- Mermaid
- Renderização de Email
⚙️ Compilação
⚡ Runtime
🔌 Servidor
🎨 Ecossistema
🚀 Desenvolvimento