-
Notifications
You must be signed in to change notification settings - Fork 1
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. Tudo nesta página vive em
eQuantic.UI.Primitives, lógica pura com zero dependências e sem pixels, que é o que permite a um
app construído sobre isso 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.
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 widget 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 + coluna × columnWidth), o que só funciona enquanto TODA parte do editor concorda com
esses números. Três 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.
| 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. |
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 de C#, 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,
};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 widget é 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; 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. - 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; 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.
Estes são contratos que o app implementa; o trabalho do editor é posicionar o que eles devolvem.
public interface ICodeCompletionProvider
{
IReadOnlyList<char> TriggerCharacters => ['.'];
Task<IReadOnlyList<CodeCompletionItem>> CompleteAsync(
CodeDocument document, CodePosition position, CancellationToken cancellation);
}
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. |
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 widget 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")
{
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 CodeKeymap.Handle(editor, key, modifiers, 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: o host do
macOS a partir do keyDown dele, o browser a partir do keydown dele. Nada sobre o que ⌥← ou ⇧Tab
significam é decidido num realizador.
| Tecla | O que faz |
|---|---|
| ←→↑↓ | caractere / linha; ⌥ anda por palavra, ⌘ vai à borda da linha |
⌘↑ ⌘↓ ⌘Home ⌘End
|
o documento inteiro |
⇧ + qualquer uma delas |
estende a partir da âncora |
| Enter | linha nova, herdando a indentação (um nível a mais depois de {) |
Tab / ⇧Tab |
indenta / desindenta: a seleção, ou até a próxima parada de tabulação |
| Backspace / Delete | um caractere; ⌥ leva a palavra; espaço em branco inicial vai um passo inteiro |
⌘Z / ⇧⌘Z
|
desfazer / refazer, juntando uma sequência de digitação numa coisa só |
⌘A ⌘C ⌘X ⌘V
|
selecionar tudo, copiar, recortar, colar; copiar sem seleção leva a linha |
⌘/ |
alternar comentário de linha (nada numa linguagem que não tem) |
| Escape | SAI do editor; um que prende o Escape é um do qual você não consegue sair |
Caracteres digitados não passam pelo keymap: o que um toque de tecla produz é assunto da plataforma
(uma tecla morta, um método de entrada, "á" a partir de três eventos), então o texto chega como string e
vai para o Type, onde vivem os pares que se fecham sozinhos e a regra de passar por cima do
fechamento.
A face é monoespaçada, então uma (linha, coluna) É
(contentTop + linha × lineHeight, contentLeft + coluna × 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.
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.
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:
Box (a laje, cortada)
└ ScrollView (vertical, quando o MaxHeight a limita)
└ ScrollView (horizontal)
└ CodeSurface ← move com o código, então as marcas também
└ CodeBlock ← Standalone = false: conteúdo cru, sem laje, sem viewport
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 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. Enter e as setas percorrem; a
contagem lê 3/17.
Uma IDE com a própria interface de busca pula tudo isso e define Search / SearchMatchCase
diretamente.
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 com um MaxHeight 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.
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.
O CodeSurface rebaixa para uma div focável com o cursor e as faixas de seleção como filhos
posicionados de forma absoluta, e o keydown dela chama o MESMO CodeKeymap.Handle que o host do macOS
chama. O controller, o documento, os tokenizadores e o histórico de desfazer por baixo dela são saída do
eqc a partir do mesmo C#. Nada no caminho do browser reimplementa um comportamento de editor, que é o
único jeito de os dois alvos não conseguirem divergir.
O code-editor.spec.ts dirige a superfície do jeito que um browser dirige: um keydown com flags de
modificador, 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 | ✅ |
O modelo, a superfície e os comportamentos de acabamento estão cobertos em
eQuantic.UI.Native.Engine.Tests (CodeModelTests, CodeEditorControllerTests,
CodeEditorSurfaceTests, CodeEditorFinishTests). 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
📱 Write-once
- Componentes write-once
- Superfície declarativa
- Motor Photon
- Design System
- Capacidades
- Armazenamento
- Formulários
- Editor de código
- Markdown
- Mermaid
- Renderização de Email
🏗️ Arquitetura
⚙️ Compilação
- Compilador
- Avaliação em tempo de compilação
- Recursos C# suportados
- Resolução de tipos externos
- Fluxo de build
- Diagnósticos
⚡ Runtime
🔌 Servidor
🎨 Ecossistema
🚀 Desenvolvimento