From 7e1b636b6fc55fde191b2dcae5ea83197950e50f Mon Sep 17 00:00:00 2001 From: Luca Zani Date: Thu, 9 Apr 2026 09:21:54 +0200 Subject: [PATCH] git_ui: Strip ANSI escape codes from git command output (#53444) When git commands (push, pull, hooks...) produce output containing ANSI escape sequences for colors or formatting, Zed was displaying them as raw escape codes in the output buffer, making the output hard to read. This simply escapes ANSI from the git output ### Before and After
Before After
Screenshot 2026-04-08 at 21 13 07 Screenshot 2026-04-08 at 21 15 14
Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Related to #43817. This PR only addresses the escaping of the ANSI codes; colors and other stuff are not handled Release Notes: - Fixed ANSI escape codes being displayed as raw text in git command output --- Cargo.lock | 1 + crates/git_ui/Cargo.toml | 1 + crates/git_ui/src/git_panel.rs | 54 +++++++++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 966b193d91af67774ed6802d65baf558b5efae9d..8c0f51e2893f53c3e6ef78dbd5ef711f8d9dc95c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7266,6 +7266,7 @@ name = "git_ui" version = "0.1.0" dependencies = [ "agent_settings", + "alacritty_terminal", "anyhow", "askpass", "buffer_diff", diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index e06d16708697f721d9377365223dc444ba7b08ae..6927ae16a5c4aa50e5d91563dbb84b1f2e085fd0 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -17,6 +17,7 @@ test-support = ["multi_buffer/test-support", "remote_connection/test-support"] [dependencies] agent_settings.workspace = true +alacritty_terminal.workspace = true anyhow.workspace = true askpass.workspace = true buffer_diff.workspace = true diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 0cb8ec6b78929d216b700b6e21cbf43a538c6f56..a6f1e097cfe1cc0c012ff77987011571760b3ef0 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -11,6 +11,7 @@ use crate::{ repository_selector::RepositorySelector, }; use agent_settings::AgentSettings; +use alacritty_terminal::vte::ansi; use anyhow::Context as _; use askpass::AskPassDelegate; use collections::{BTreeMap, HashMap, HashSet}; @@ -6407,7 +6408,13 @@ fn open_output( cx: &mut Context, ) { let operation = operation.into(); - let buffer = cx.new(|cx| Buffer::local(output, cx)); + + let mut handler = GitOutputHandler::default(); + let mut processor = ansi::Processor::::default(); + processor.advance(&mut handler, output.as_bytes()); + let plain_text = handler.output; + + let buffer = cx.new(|cx| Buffer::local(plain_text.as_str(), cx)); buffer.update(cx, |buffer, cx| { buffer.set_capability(language::Capability::ReadOnly, cx); }); @@ -6423,6 +6430,32 @@ fn open_output( workspace.add_item_to_center(Box::new(editor), window, cx); } +#[derive(Default)] +struct GitOutputHandler { + output: String, + line_start: usize, +} + +impl ansi::Handler for GitOutputHandler { + fn input(&mut self, c: char) { + self.output.push(c); + } + + fn linefeed(&mut self) { + self.output.push('\n'); + self.line_start = self.output.len(); + } + + fn carriage_return(&mut self) { + self.output.truncate(self.line_start); + } + + fn put_tab(&mut self, count: u16) { + self.output + .extend(std::iter::repeat_n('\t', count as usize)); + } +} + pub(crate) fn show_error_toast( workspace: Entity, action: impl Into, @@ -7863,6 +7896,25 @@ mod tests { assert_eq!(message, Some("Update tracked".to_string())); } + #[test] + fn test_git_output_handler_strips_ansi_codes() { + use alacritty_terminal::vte::ansi; + + let cases = [ + ("no escape codes here\n", "no escape codes here\n"), + ("\x1b[31mhello\x1b[0m", "hello"), + ("\x1b[1;32mfoo\x1b[0m bar", "foo bar"), + ("progress 10%\rprogress 100%\n", "progress 100%\n"), + ]; + + for (input, expected) in cases { + let mut handler = GitOutputHandler::default(); + let mut processor = ansi::Processor::::default(); + processor.advance(&mut handler, input.as_bytes()); + assert_eq!(handler.output, expected); + } + } + #[gpui::test] async fn test_dispatch_context_with_focus_states(cx: &mut TestAppContext) { init_test(cx);