Editor de Texto no Terminal em Rust

Construa um editor de texto minimalista no terminal com Rust e crossterm: abertura e salvamento de arquivos, movimentação, busca e barra de status.

Neste projeto vamos construir um editor de texto minimalista para o terminal, inspirado no nano, usando Rust e a crate crossterm. O editor permite abrir arquivos, navegar pelo conteúdo com as teclas de seta, inserir e apagar caracteres, exibir números de linha, realizar buscas com Ctrl+F e salvar alterações com Ctrl+S. Tudo isso funcionando diretamente no terminal, sem interface gráfica.

Este é um projeto que exercita profundamente o gerenciamento de estado em Rust: manter um buffer de texto editável, sincronizar a posição do cursor com a janela de visualização, lidar com entrada de teclado em modo raw e renderizar a tela completa a cada alteração. Ao final, você terá um editor funcional que pode usar no dia a dia para edições rápidas.

O Que Vamos Construir

Um editor de texto com as seguintes funcionalidades:

  • Abertura de arquivos existentes passados como argumento
  • Criação de novos arquivos do zero
  • Movimentação do cursor com setas (cima, baixo, esquerda, direita)
  • Teclas Home, End, Page Up e Page Down
  • Inserção de caracteres e novas linhas (Enter)
  • Exclusão com Backspace e Delete
  • Números de linha na margem esquerda
  • Barra de status com nome do arquivo, posição e indicador de modificação
  • Barra de mensagens para feedback ao usuário
  • Busca de texto com Ctrl+F (navega entre ocorrências)
  • Salvamento com Ctrl+S
  • Rolagem vertical e horizontal automática

Estrutura do Projeto

editor-texto/
├── Cargo.toml
└── src/
    └── main.rs

Configurando o Projeto

Crie o projeto com o Cargo:

cargo new editor-texto
cd editor-texto

Edite o Cargo.toml:

[package]
name = "editor-texto"
version = "0.1.0"
edition = "2021"

[dependencies]
crossterm = "0.28"

Usamos apenas crossterm para controle do terminal (modo raw, eventos de teclado, posicionamento do cursor e estilos). Todo o restante é implementado com a biblioteca padrão.

Passo 1: Estruturas de Dados do Editor

Vamos definir as estruturas que mantêm o estado completo do editor: o buffer de texto, a posição do cursor, a janela de visualização (scroll) e os metadados do arquivo.

use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
    execute, queue,
    style::{self, Attribute, Stylize},
    terminal::{self, ClearType},
};
use std::env;
use std::fs;
use std::io::{self, Write};
use std::time::{Duration, Instant};

/// Largura da coluna de números de linha
const LARGURA_NUMEROS: usize = 5;

/// Estado completo do editor
struct Editor {
    /// Linhas do documento (cada linha é um Vec de caracteres)
    linhas: Vec<Vec<char>>,
    /// Posição do cursor no documento (coluna, linha)
    cursor_x: usize,
    cursor_y: usize,
    /// Offset de rolagem (qual linha/coluna está no topo-esquerdo da tela)
    scroll_y: usize,
    scroll_x: usize,
    /// Dimensões do terminal
    largura_tela: usize,
    altura_tela: usize,
    /// Nome do arquivo aberto
    nome_arquivo: Option<String>,
    /// Indica se o documento foi modificado desde o último salvamento
    modificado: bool,
    /// Mensagem exibida na barra inferior
    mensagem: Option<(String, Instant)>,
    /// Indica se o editor deve encerrar
    encerrar: bool,
}

impl Editor {
    /// Cria um novo editor vazio
    fn novo() -> Self {
        let (largura, altura) = terminal::size().unwrap_or((80, 24));

        Editor {
            linhas: vec![Vec::new()], // Começa com uma linha vazia
            cursor_x: 0,
            cursor_y: 0,
            scroll_y: 0,
            scroll_x: 0,
            largura_tela: largura as usize,
            altura_tela: altura as usize - 2, // Reserva 2 linhas (status + mensagem)
            nome_arquivo: None,
            modificado: false,
            mensagem: None,
            encerrar: false,
        }
    }

    /// Abre um arquivo existente para edição
    fn abrir_arquivo(&mut self, caminho: &str) -> io::Result<()> {
        let conteudo = fs::read_to_string(caminho)?;

        self.linhas = conteudo
            .lines()
            .map(|linha| linha.chars().collect())
            .collect();

        // Garante que existe ao menos uma linha
        if self.linhas.is_empty() {
            self.linhas.push(Vec::new());
        }

        self.nome_arquivo = Some(caminho.to_string());
        self.modificado = false;
        self.definir_mensagem(&format!("Arquivo '{}' aberto ({} linhas)", caminho, self.linhas.len()));

        Ok(())
    }

    /// Define uma mensagem temporária na barra inferior
    fn definir_mensagem(&mut self, texto: &str) {
        self.mensagem = Some((texto.to_string(), Instant::now()));
    }

    /// Retorna o comprimento da linha atual
    fn comprimento_linha_atual(&self) -> usize {
        self.linhas[self.cursor_y].len()
    }

    /// Área editável disponível (descontando a coluna de números)
    fn largura_editavel(&self) -> usize {
        self.largura_tela.saturating_sub(LARGURA_NUMEROS + 1)
    }
}

Cada linha é armazenada como Vec<char> em vez de String para facilitar inserções e exclusões em posições arbitrárias – acessar e modificar o caractere na posição N é O(1) em vez de O(N) como seria com bytes UTF-8. A rolagem é controlada por scroll_y e scroll_x, que indicam qual porção do documento está visível na tela.

Passo 2: Movimentação do Cursor e Edição

Agora implementamos todas as operações de movimentação e edição de texto: mover o cursor, inserir caracteres, apagar com Backspace/Delete e criar novas linhas.

impl Editor {
    /// Move o cursor para cima
    fn mover_cima(&mut self) {
        if self.cursor_y > 0 {
            self.cursor_y -= 1;
            // Ajusta a coluna se a linha de cima for mais curta
            let comprimento = self.linhas[self.cursor_y].len();
            if self.cursor_x > comprimento {
                self.cursor_x = comprimento;
            }
        }
    }

    /// Move o cursor para baixo
    fn mover_baixo(&mut self) {
        if self.cursor_y < self.linhas.len() - 1 {
            self.cursor_y += 1;
            let comprimento = self.linhas[self.cursor_y].len();
            if self.cursor_x > comprimento {
                self.cursor_x = comprimento;
            }
        }
    }

    /// Move o cursor para a esquerda
    fn mover_esquerda(&mut self) {
        if self.cursor_x > 0 {
            self.cursor_x -= 1;
        } else if self.cursor_y > 0 {
            // Volta para o final da linha anterior
            self.cursor_y -= 1;
            self.cursor_x = self.linhas[self.cursor_y].len();
        }
    }

    /// Move o cursor para a direita
    fn mover_direita(&mut self) {
        let comprimento = self.linhas[self.cursor_y].len();
        if self.cursor_x < comprimento {
            self.cursor_x += 1;
        } else if self.cursor_y < self.linhas.len() - 1 {
            // Avança para o início da próxima linha
            self.cursor_y += 1;
            self.cursor_x = 0;
        }
    }

    /// Move o cursor para o início da linha
    fn mover_inicio(&mut self) {
        self.cursor_x = 0;
    }

    /// Move o cursor para o final da linha
    fn mover_final(&mut self) {
        self.cursor_x = self.linhas[self.cursor_y].len();
    }

    /// Move uma página para cima
    fn pagina_cima(&mut self) {
        if self.cursor_y > self.altura_tela {
            self.cursor_y -= self.altura_tela;
        } else {
            self.cursor_y = 0;
        }
        let comprimento = self.linhas[self.cursor_y].len();
        if self.cursor_x > comprimento {
            self.cursor_x = comprimento;
        }
    }

    /// Move uma página para baixo
    fn pagina_baixo(&mut self) {
        self.cursor_y = (self.cursor_y + self.altura_tela).min(self.linhas.len() - 1);
        let comprimento = self.linhas[self.cursor_y].len();
        if self.cursor_x > comprimento {
            self.cursor_x = comprimento;
        }
    }

    /// Insere um caractere na posição do cursor
    fn inserir_caractere(&mut self, c: char) {
        self.linhas[self.cursor_y].insert(self.cursor_x, c);
        self.cursor_x += 1;
        self.modificado = true;
    }

    /// Insere uma nova linha (Enter)
    fn inserir_nova_linha(&mut self) {
        // Divide a linha atual na posição do cursor
        let restante: Vec<char> = self.linhas[self.cursor_y].drain(self.cursor_x..).collect();
        self.cursor_y += 1;
        self.linhas.insert(self.cursor_y, restante);
        self.cursor_x = 0;
        self.modificado = true;
    }

    /// Apaga o caractere antes do cursor (Backspace)
    fn apagar_atras(&mut self) {
        if self.cursor_x > 0 {
            self.cursor_x -= 1;
            self.linhas[self.cursor_y].remove(self.cursor_x);
            self.modificado = true;
        } else if self.cursor_y > 0 {
            // Junta a linha atual com a anterior
            let linha_atual = self.linhas.remove(self.cursor_y);
            self.cursor_y -= 1;
            self.cursor_x = self.linhas[self.cursor_y].len();
            self.linhas[self.cursor_y].extend(linha_atual);
            self.modificado = true;
        }
    }

    /// Apaga o caractere na posição do cursor (Delete)
    fn apagar_frente(&mut self) {
        let comprimento = self.linhas[self.cursor_y].len();
        if self.cursor_x < comprimento {
            self.linhas[self.cursor_y].remove(self.cursor_x);
            self.modificado = true;
        } else if self.cursor_y < self.linhas.len() - 1 {
            // Junta a próxima linha com a atual
            let proxima_linha = self.linhas.remove(self.cursor_y + 1);
            self.linhas[self.cursor_y].extend(proxima_linha);
            self.modificado = true;
        }
    }

    /// Ajusta a rolagem para manter o cursor visível
    fn ajustar_scroll(&mut self) {
        // Rolagem vertical
        if self.cursor_y < self.scroll_y {
            self.scroll_y = self.cursor_y;
        }
        if self.cursor_y >= self.scroll_y + self.altura_tela {
            self.scroll_y = self.cursor_y - self.altura_tela + 1;
        }

        // Rolagem horizontal
        let largura = self.largura_editavel();
        if self.cursor_x < self.scroll_x {
            self.scroll_x = self.cursor_x;
        }
        if self.cursor_x >= self.scroll_x + largura {
            self.scroll_x = self.cursor_x - largura + 1;
        }
    }
}

A movimentação do cursor lida com todos os casos limites: ao se mover para a esquerda no início de uma linha, o cursor salta para o final da linha anterior; ao se mover para baixo em uma linha mais longa, a coluna é ajustada para não ultrapassar o comprimento da nova linha. O Backspace no início de uma linha junta as duas linhas, e o Delete no final faz o mesmo com a próxima.

Passo 3: Busca e Salvamento

Implementamos a funcionalidade de busca (Ctrl+F), que permite digitar um termo e navegar entre as ocorrências, e o salvamento (Ctrl+S).

impl Editor {
    /// Salva o arquivo atual
    fn salvar(&mut self) -> io::Result<()> {
        let caminho = match &self.nome_arquivo {
            Some(c) => c.clone(),
            None => {
                // Pede o nome do arquivo ao usuário
                match self.ler_prompt("Salvar como: ") {
                    Some(nome) => {
                        self.nome_arquivo = Some(nome.clone());
                        nome
                    }
                    None => {
                        self.definir_mensagem("Salvamento cancelado.");
                        return Ok(());
                    }
                }
            }
        };

        // Converte as linhas para texto
        let conteudo: String = self.linhas
            .iter()
            .map(|linha| linha.iter().collect::<String>())
            .collect::<Vec<_>>()
            .join("\n");

        fs::write(&caminho, &conteudo)?;

        let bytes = conteudo.len();
        self.modificado = false;
        self.definir_mensagem(&format!(
            "Salvo '{}' ({} linhas, {} bytes)",
            caminho,
            self.linhas.len(),
            bytes
        ));

        Ok(())
    }

    /// Realiza busca interativa no documento
    fn buscar(&mut self) {
        let termo = match self.ler_prompt("Buscar: ") {
            Some(t) if !t.is_empty() => t,
            _ => {
                self.definir_mensagem("Busca cancelada.");
                return;
            }
        };

        // Busca a partir da posição atual do cursor
        let mut encontrou = false;

        for y in self.cursor_y..self.linhas.len() {
            let texto: String = self.linhas[y].iter().collect();
            let inicio_busca = if y == self.cursor_y {
                self.cursor_x + 1
            } else {
                0
            };

            if let Some(pos) = texto[inicio_busca..].find(&termo) {
                self.cursor_y = y;
                self.cursor_x = inicio_busca + pos;
                encontrou = true;
                self.definir_mensagem(&format!(
                    "Encontrado na linha {}. Ctrl+F para próximo.",
                    y + 1
                ));
                break;
            }
        }

        // Se não encontrou a partir da posição atual, volta ao início
        if !encontrou {
            for y in 0..=self.cursor_y {
                let texto: String = self.linhas[y].iter().collect();
                let limite = if y == self.cursor_y {
                    self.cursor_x
                } else {
                    texto.len()
                };

                if let Some(pos) = texto[..limite].find(&termo) {
                    self.cursor_y = y;
                    self.cursor_x = pos;
                    encontrou = true;
                    self.definir_mensagem(&format!(
                        "Encontrado na linha {} (voltou ao início).",
                        y + 1
                    ));
                    break;
                }
            }
        }

        if !encontrou {
            self.definir_mensagem(&format!("'{}' não encontrado.", termo));
        }
    }

    /// Lê uma entrada do usuário na barra de mensagens (para busca e nome de arquivo)
    fn ler_prompt(&mut self, prompt: &str) -> Option<String> {
        let mut entrada = String::new();
        let mut stdout = io::stdout();

        loop {
            // Desenha o prompt na linha de mensagem
            let y_msg = self.altura_tela as u16 + 1;
            queue!(
                stdout,
                cursor::MoveTo(0, y_msg),
                terminal::Clear(ClearType::CurrentLine),
                style::PrintStyledContent(prompt.bold()),
                style::Print(&entrada),
                cursor::Show,
            )
            .unwrap();
            stdout.flush().unwrap();

            // Lê evento de teclado
            if event::poll(Duration::from_millis(100)).unwrap() {
                if let Event::Key(KeyEvent { code, .. }) = event::read().unwrap() {
                    match code {
                        KeyCode::Enter => return Some(entrada),
                        KeyCode::Esc => return None,
                        KeyCode::Backspace => {
                            entrada.pop();
                        }
                        KeyCode::Char(c) => {
                            entrada.push(c);
                        }
                        _ => {}
                    }
                }
            }
        }
    }
}

A busca implementa wraparound: se o termo não for encontrado entre a posição do cursor e o final do documento, a busca recomeça do início. Isso permite que o usuário pressione Ctrl+F repetidamente para navegar por todas as ocorrências de forma cíclica. O ler_prompt reutiliza a barra de mensagens como campo de entrada, criando uma interface similar ao nano.

Passo 4: Renderização e Loop Principal

Finalmente, implementamos a renderização da tela e o loop principal que une tudo.

impl Editor {
    /// Renderiza o estado completo do editor no terminal
    fn renderizar(&mut self, stdout: &mut io::Stdout) {
        self.ajustar_scroll();

        queue!(stdout, cursor::Hide, cursor::MoveTo(0, 0)).unwrap();

        // Renderiza cada linha visível
        for i in 0..self.altura_tela {
            let linha_doc = self.scroll_y + i;

            // Limpa a linha
            queue!(stdout, terminal::Clear(ClearType::CurrentLine)).unwrap();

            if linha_doc < self.linhas.len() {
                // Número da linha (com padding à direita)
                let numero = format!("{:>4} ", linha_doc + 1);
                queue!(stdout, style::PrintStyledContent(numero.dark_grey())).unwrap();

                // Conteúdo da linha (com rolagem horizontal)
                let texto: String = self.linhas[linha_doc].iter().collect();
                let largura = self.largura_editavel();

                if self.scroll_x < texto.len() {
                    let fim = (self.scroll_x + largura).min(texto.len());
                    let visivel = &texto[self.scroll_x..fim];
                    queue!(stdout, style::Print(visivel)).unwrap();
                }
            } else {
                // Linhas além do documento: mostra til (~)
                queue!(stdout, style::PrintStyledContent("~".dark_grey())).unwrap();
            }

            // Nova linha (exceto na última)
            if i < self.altura_tela - 1 {
                queue!(stdout, style::Print("\r\n")).unwrap();
            }
        }

        // Barra de status (linha penúltima)
        queue!(stdout, style::Print("\r\n")).unwrap();
        self.renderizar_barra_status(stdout);

        // Barra de mensagens (última linha)
        queue!(stdout, style::Print("\r\n")).unwrap();
        self.renderizar_barra_mensagem(stdout);

        // Posiciona o cursor na tela
        let cursor_tela_x = (LARGURA_NUMEROS + self.cursor_x - self.scroll_x) as u16;
        let cursor_tela_y = (self.cursor_y - self.scroll_y) as u16;
        queue!(
            stdout,
            cursor::MoveTo(cursor_tela_x, cursor_tela_y),
            cursor::Show,
        )
        .unwrap();

        stdout.flush().unwrap();
    }

    /// Renderiza a barra de status com informações do arquivo
    fn renderizar_barra_status(&self, stdout: &mut io::Stdout) {
        queue!(stdout, terminal::Clear(ClearType::CurrentLine)).unwrap();

        let nome = self.nome_arquivo.as_deref().unwrap_or("[Sem nome]");
        let modificado = if self.modificado { " [Modificado]" } else { "" };
        let lado_esquerdo = format!(" {}{}", nome, modificado);

        let lado_direito = format!(
            "Ln {}, Col {} | {} linhas ",
            self.cursor_y + 1,
            self.cursor_x + 1,
            self.linhas.len()
        );

        // Preenche o espaço entre os lados com espaços
        let espacos = self.largura_tela
            .saturating_sub(lado_esquerdo.len() + lado_direito.len());
        let barra = format!(
            "{}{}{}",
            lado_esquerdo,
            " ".repeat(espacos),
            lado_direito
        );

        queue!(
            stdout,
            style::PrintStyledContent(
                barra
                    .clone()
                    .with(style::Color::Black)
                    .on(style::Color::White)
                    .attribute(Attribute::Bold)
            ),
        )
        .unwrap();
    }

    /// Renderiza a barra de mensagens
    fn renderizar_barra_mensagem(&mut self, stdout: &mut io::Stdout) {
        queue!(stdout, terminal::Clear(ClearType::CurrentLine)).unwrap();

        // Mostra a mensagem se existir e se foi definida há menos de 5 segundos
        if let Some((ref texto, instante)) = self.mensagem {
            if instante.elapsed() < Duration::from_secs(5) {
                let msg = if texto.len() > self.largura_tela {
                    &texto[..self.largura_tela]
                } else {
                    texto
                };
                queue!(stdout, style::Print(msg)).unwrap();
            } else {
                self.mensagem = None;
                // Mostra atalhos padrão
                queue!(
                    stdout,
                    style::PrintStyledContent(
                        " Ctrl+S: Salvar | Ctrl+F: Buscar | Ctrl+Q: Sair".dark_grey()
                    )
                )
                .unwrap();
            }
        } else {
            queue!(
                stdout,
                style::PrintStyledContent(
                    " Ctrl+S: Salvar | Ctrl+F: Buscar | Ctrl+Q: Sair".dark_grey()
                )
            )
            .unwrap();
        }
    }

    /// Processa um evento de teclado
    fn processar_tecla(&mut self, evento: KeyEvent) {
        match (evento.modifiers, evento.code) {
            // Atalhos com Ctrl
            (KeyModifiers::CONTROL, KeyCode::Char('q')) => {
                if self.modificado {
                    self.definir_mensagem(
                        "Arquivo modificado! Ctrl+Q novamente para sair sem salvar."
                    );
                    // Permite sair na segunda tentativa
                    self.modificado = false;
                    return;
                }
                self.encerrar = true;
            }
            (KeyModifiers::CONTROL, KeyCode::Char('s')) => {
                if let Err(e) = self.salvar() {
                    self.definir_mensagem(&format!("Erro ao salvar: {}", e));
                }
            }
            (KeyModifiers::CONTROL, KeyCode::Char('f')) => {
                self.buscar();
            }

            // Movimentação
            (_, KeyCode::Up) => self.mover_cima(),
            (_, KeyCode::Down) => self.mover_baixo(),
            (_, KeyCode::Left) => self.mover_esquerda(),
            (_, KeyCode::Right) => self.mover_direita(),
            (_, KeyCode::Home) => self.mover_inicio(),
            (_, KeyCode::End) => self.mover_final(),
            (_, KeyCode::PageUp) => self.pagina_cima(),
            (_, KeyCode::PageDown) => self.pagina_baixo(),

            // Edição
            (_, KeyCode::Enter) => self.inserir_nova_linha(),
            (_, KeyCode::Backspace) => self.apagar_atras(),
            (_, KeyCode::Delete) => self.apagar_frente(),
            (_, KeyCode::Tab) => {
                // Insere 4 espaços no lugar de tab
                for _ in 0..4 {
                    self.inserir_caractere(' ');
                }
            }
            (_, KeyCode::Char(c)) => {
                self.inserir_caractere(c);
            }

            _ => {} // Ignora teclas não mapeadas
        }
    }
}

fn main() {
    let args: Vec<String> = env::args().collect();

    // Cria o editor
    let mut editor = Editor::novo();

    // Abre o arquivo se fornecido como argumento
    if args.len() >= 2 {
        if let Err(e) = editor.abrir_arquivo(&args[1]) {
            eprintln!("Erro ao abrir '{}': {}", args[1], e);
            eprintln!("Criando novo arquivo.");
            editor.nome_arquivo = Some(args[1].clone());
        }
    } else {
        editor.definir_mensagem("Editor de Texto em Rust | Novo arquivo | Ctrl+Q para sair");
    }

    // Entra em modo raw e tela alternativa
    terminal::enable_raw_mode().expect("Falha ao ativar modo raw");
    let mut stdout = io::stdout();
    execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)
        .expect("Falha ao configurar terminal");

    // Loop principal do editor
    loop {
        editor.renderizar(&mut stdout);

        // Espera por um evento de teclado
        if event::poll(Duration::from_millis(100)).unwrap() {
            match event::read().unwrap() {
                Event::Key(evento) => editor.processar_tecla(evento),
                Event::Resize(largura, altura) => {
                    editor.largura_tela = largura as usize;
                    editor.altura_tela = (altura as usize).saturating_sub(2);
                }
                _ => {}
            }
        }

        if editor.encerrar {
            break;
        }
    }

    // Restaura o terminal ao estado original
    execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)
        .expect("Falha ao restaurar terminal");
    terminal::disable_raw_mode().expect("Falha ao desativar modo raw");
    println!("Editor encerrado.");
}

O loop principal segue o padrão clássico de aplicações interativas: renderizar, ler evento, processar, repetir. O Event::Resize captura redimensionamento do terminal e ajusta as dimensões dinamicamente. O Ctrl+Q implementa uma proteção contra perda de dados: se o arquivo foi modificado, a primeira tentativa mostra um aviso e somente a segunda efetivamente encerra. O Tab insere 4 espaços, evitando problemas de renderização com tabulações de largura variável.

Como Executar

Compile e execute o projeto:

# Compilar
cargo build --release

# Abrir um novo arquivo vazio
cargo run

# Abrir um arquivo existente
cargo run -- meu_arquivo.txt

# Ou usar o binário diretamente
./target/release/editor-texto codigo.rs

Controles do editor:

  • Setas: Mover o cursor
  • Home / End: Início / final da linha
  • Page Up / Page Down: Rolar uma página
  • Enter: Nova linha
  • Backspace: Apagar caractere antes do cursor
  • Delete: Apagar caractere na posição do cursor
  • Tab: Insere 4 espaços
  • Ctrl+S: Salvar arquivo
  • Ctrl+F: Buscar texto
  • Ctrl+Q: Sair (pede confirmação se houver alterações)

A barra de status na parte inferior mostra o nome do arquivo, indicador de modificação, número da linha e coluna atuais, e total de linhas. A barra de mensagens exibe feedback das operações e os atalhos disponíveis.

Desafios para Expandir

  1. Syntax highlighting – Implemente destaque de sintaxe para linguagens como Rust, Python e Markdown, reconhecendo palavras-chave, strings, comentários e números com cores diferentes usando os estilos do crossterm.

  2. Desfazer e refazer (Undo/Redo) – Implemente um sistema de histórico usando uma pilha de operações. Cada inserção, exclusão e junção de linhas empilha uma operação inversa. Ctrl+Z desfaz e Ctrl+Y refaz.

  3. Seleção e clipboard – Adicione seleção de texto com Shift+setas (mantendo posição âncora e posição atual), recortar (Ctrl+X), copiar (Ctrl+C) e colar (Ctrl+V) usando um buffer interno.

  4. Múltiplos buffers – Permita abrir vários arquivos simultaneamente e alternar entre eles com Ctrl+Tab, exibindo o nome de cada buffer na barra de status.

  5. Busca e substituição – Estenda o Ctrl+F para incluir substituição com Ctrl+H, com opções de substituir a ocorrência atual, pular ou substituir todas de uma vez.

Veja Também