Cargo.lock 🔗
@@ -7266,6 +7266,7 @@ name = "git_ui"
version = "0.1.0"
dependencies = [
"agent_settings",
+ "alacritty_terminal",
"anyhow",
"askpass",
"buffer_diff",
Luca Zani created
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
<table>
<tr>
<th align="center">Before</th>
<th align="center">After</th>
</tr>
<tr>
<td>
<img width="882" height="862" alt="Screenshot 2026-04-08 at 21 13 07"
src="https://github.com/user-attachments/assets/58731e80-d864-47ca-8983-d0e86e924843"
/>
</td>
<td>
<img width="882" height="862" alt="Screenshot 2026-04-08 at 21 15 14"
src="https://github.com/user-attachments/assets/7649200a-2d82-4442-88da-e231304911a8"
/>
</td>
</tr>
</table>
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(-)
@@ -7266,6 +7266,7 @@ name = "git_ui"
version = "0.1.0"
dependencies = [
"agent_settings",
+ "alacritty_terminal",
"anyhow",
"askpass",
"buffer_diff",
@@ -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
@@ -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<Workspace>,
) {
let operation = operation.into();
- let buffer = cx.new(|cx| Buffer::local(output, cx));
+
+ let mut handler = GitOutputHandler::default();
+ let mut processor = ansi::Processor::<ansi::StdSyncHandler>::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<Workspace>,
action: impl Into<SharedString>,
@@ -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::<ansi::StdSyncHandler>::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);