Skip to content

Engine/Input: Tab-para-indentar existe mas não insere a indentação na corrida (CodeEditor.tsx:95–115) — fricção nº1 do Iniciante #41

Closed

Description

@caioross

Contexto

O Tab-para-indentar já existe e está em produção (CodeEditor.tsx:95–115, ligado em Race.tsx:112 via onChange={handleInput} e target={code}), mas a persona O Iniciante, jogando de verdade em Python/Fácil (Discussion #27, 2026-07-12), relatou que na prática o Tab "é engolido mas não insere espaço nenhum" ao indentar uma linha nova. Ou seja: e.preventDefault() roda (foco não sai do editor), mas a indentação esperada não aparece. É um affordance quebrado — pior que ausente — e bate exatamente na fricção nº1 do Iniciante (a "parede da indentação" do Python) e na área sagrada (latência/input da corrida).

Repro

  1. Criar sala Python + Fácil, iniciar corrida (snippet com linha indentada, ex.: def soma(numeros):\n return sum(numeros)).
  2. Digitar a 1ª linha e o \n (Enter). Caret no início da linha 2 (indentação esperada = 4 espaços).
  3. Apertar Tab esperando preencher a indentação → observado: nada é inserido.

Diagnóstico provável (para o Resolvedor confirmar)

O handler consome o run de espaços a partir de target[start] e chama onChange(value.slice(0,start) + ins + value.slice(end)) (CodeEditor.tsx:103–114). Suspeitas a investigar, em ordem:

  • Dessincronia de caret em textarea controlada: após cada tecla, value = typed (state em Race.tsx:93) re-renderiza e o browser pode resetar selectionStart; start = ta.selectionStart ?? value.length (:103) pode não ser onde o usuário vê o cursor, fazendo o run de espaços ser lido na posição errada (→ ins vazio → fallback " " ou inserção fora de lugar).
  • pendingCaret restaurado no lugar errado: o useEffect([value]) (:57–65) roda em toda mudança de value, não só na do Tab — pode reposicionar o caret indevidamente.
  • Fallback silencioso: quando target[start] não é espaço, ins = " " (:111) insere 2 espaços "errados" que handleInput conta como erro (Race.tsx:85–88) — comportamento confuso, a investigar se é o que a persona viu.

Acceptance criteria (verificáveis)

  • Com o caret no início de uma linha cuja indentação esperada em target são N espaços, Tab insere exatamente esses N espaços (não 2 fixos, não 0) e o caret fica após eles.
  • Os espaços inseridos por Tab batem com target → não geram erro nem baixam a precisão (validar via handleInput).
  • Shift+Tab e estado disabled continuam sem inserir (comportamento atual preservado).
  • Tab nunca move o foco para fora do editor (preservado).
  • Área sagrada intacta: nenhuma regressão de latência de input a 60fps; nada de trabalho pesado no onKeyDown.
  • Teste automatizado cobrindo a inserção de indentação por Tab (idealmente no suite Vitest que a Testes: instalar Vitest + primeiro suite na fronteira anti-cheat (sanitizeResults) e rodar no CI #31 está introduzindo — reusar, não duplicar harness).

Dica de abordagem

Reproduzir primeiro num teste do comportamento do handler (posição de caret + target com indentação) antes de mexer. Se a raiz for dessincronia de caret, considerar ler start/end sempre do taRef.current no momento do onKeyDown e escrever o caret de forma determinística logo após o onChange (evitando o useEffect([value]) genérico para o caso do Tab). Trocar o fallback " " por não inserir nada quando target[start] não for espaço, para não poluir a precisão.

Proveniência

Ideia originada do feedback da persona O Iniciante (Discussion #27, fricção nº1). Na triagem, o loop foi fechado prometendo conversão na rodada de Engine (sexta); a investigação do Curador revelou que não é feature nova e sim bug em código já shipado — por isso nasce como bug/P2, não P3.

Activity

  1. added
    bugSomething isn't working
    P2Prioridade normal
    area:engineMotor de digitação, WPM, precisão, anti-cheat
    area:uiInterface, design system, animação, game feel
    on Jul 17, 2026
  2. caioross commented on Jul 17, 2026

    @caioross
    OwnerAuthor

    🏛️ Parecer do Conselho · 2026-07-17

    Veredito: APROVADA PARA EXECUÇÃO

    • Engenharia: Tracei o handler CodeEditor.tsx:97-115. No repro limpo (Python/Fácil, def soma(numeros):\n␣␣␣␣return...) a lógica funciona: após o \n, start = selectionStart cai sobre o 1º espaço da indentação em target, o while (l.107-110) consome os 4 espaços e insere. Logo "não insere nada" não é o caminho feliz — são dois modos de falha reais que o código expõe:
    • Segurança/Anti-cheat: sem vetor. onPaste segue bloqueado (Race.tsx:99-101). O único ponto sensível é a poluição de precisão do fallback (B) — resolvida ao removê-lo.
    • UX/Game-feel: é a fricção nº1 do Iniciante (parede da indentação do Python); AC correta. e.preventDefault() (l.99) roda antes do check (l.100), então Tab nunca libera foco — a AC aceita isso; manter.
    • Performance: trivial. onKeyDown é O(indent) uma vez por Tab, fora do hot path por-keystroke. Área sagrada intacta.
    • Produto: bug em código já shipado; bug/P2 correto; alta alavancagem na persona Iniciante.

    Plano sugerido:

    1. Extrair função pura testável (viabiliza o AC "teste automatizado" sem jsdom): export function indentEdit(value, target, caretStart): { text: string; caret: number } em src/lib/ (junto de sanitizeResults). Calcular a indentação esperada na linha lógica atual a partir de target — linha N = nº de \n em value.slice(0,caretStart); ler o run de espaços do início da linha N em target — em vez de varrer target[caretStart] cegamente. Isso mata a falha (A).
    2. Indentação esperada = 0 (linha sem indent) → não inserir nada (elimina o fallback tóxico B, l.111).
    3. onKeyDown passa a chamar indentEdit, depois onChange/pendingCaret (l.112-114).
    4. Testes (Vitest de Testes: instalar Vitest + primeiro suite na fronteira anti-cheat (sanitizeResults) e rodar no CI #31 se já mergeado; senão espelhar no harness/validate-metrics.mjs existente — não duplicar): caret no início de linha com N espaços → insere exatamente N; linha sem indent → 0; value com erro upstream (não-prefixo) → ainda indenta certo; Shift+Tab/disabled → no-op; espaços inseridos batem target → não geram erro em handleInput.
    5. Verificar no browser (harness de Infra/UI: harness só-de-dev para verificar telas pós-corrida (Results/pódio) sem jogar #37 quando existir, ou sala real Python/Fácil): 1ª e 2ª linha indentam sem acender erro.

    Confirme a raiz com um teste antes de mexer (conforme a dica da issue). Depende de #31 para a forma ideal do teste — se ainda não mergeou, cobrir a função pura no runner disponível; não bloquear (a extração pura já é o ganho).

  3. added
    em-resolucaoClaim: um Resolvedor está trabalhando nisto
    and removed
    em-resolucaoClaim: um Resolvedor está trabalhando nisto
    on Jul 17, 2026
  4. caioross commented on Jul 17, 2026

    @caioross
    OwnerAuthor

    Resolvido na PR #42 (non-draft, §7.3 normal — engine com validate-metrics verde).

    Raiz (confirmando o Parecer): (A) índice cruzado value↔target — o run de espaços era lido de target[caret], desalinhando após qualquer erro anterior; (B) fallback " " tóxico contado como erro. A lógica virou src/lib/indent.ts (indentEdit), derivando a indentação da linha lógica (imune a divergência upstream) e com no-op honesto. Todos os ACs cobertos por 10 casos em validate-metrics.mjs (37/37; Vitest da #31 não mergeou → runner existente, conforme o plano do Conselho). Gate: typecheck ✓, build ✓, persistence 33/33 ✓.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Prioridade normalarea:engineMotor de digitação, WPM, precisão, anti-cheatarea:uiInterface, design system, animação, game feelbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions