mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(file-tree): fold a filename's line breaks where it is drawn
A filename is bytes to the kernel, so `touch $'a\nb'` makes a file the tree has to draw, and gpui breaks text on `\n` whatever the row says. The row grows to two lines, its `text_ellipsis` contract stops holding, and the rows below it slide down — one odd file misaligns the column. The terminal's history rows already met this and already carry the fold (`one_line`), so reuse it rather than spell a second one. What differs here is where it may be applied: a history entry is only ever drawn, but a filename is also handed to `join`, `rename` and `remove`. Folding at ingestion would splice a `↵` into a name that has to match what is on disk, so the fold sits at the two `.child(...)` sites and nothing on the operations path sees it.
This commit is contained in:
+29
-1
@@ -6213,7 +6213,16 @@ fn one_line_char(c: char) -> char {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn one_line(text: &str) -> String {
|
/// `text` with every line break and control character folded to a visible
|
||||||
|
/// stand-in, so it draws on exactly one row.
|
||||||
|
///
|
||||||
|
/// The hazard is not this module's alone. Anything that draws a name it did
|
||||||
|
/// not compose is exposed to it — a filename is bytes to the kernel, so
|
||||||
|
/// `touch $'a\nb'` is a file the tree has to draw. Fold at the point of
|
||||||
|
/// drawing, never at the point of reading: the unfolded name is what
|
||||||
|
/// `join`/`rename`/`remove` are handed, and a `↵` spliced into it names
|
||||||
|
/// nothing on disk.
|
||||||
|
pub(crate) fn one_line(text: &str) -> String {
|
||||||
text.chars().map(one_line_char).collect()
|
text.chars().map(one_line_char).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6766,6 +6775,25 @@ mod tests {
|
|||||||
assert_eq!(unescape_mark_text("cargo build"), "cargo build");
|
assert_eq!(unescape_mark_text("cargo build"), "cargo build");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_name_drawn_on_one_row_keeps_its_line_breaks_visible() {
|
||||||
|
use super::one_line;
|
||||||
|
// A newline is the whole point: gpui breaks on it whatever the row
|
||||||
|
// says, so it has to become something that occupies a column instead.
|
||||||
|
assert_eq!(one_line("a\nb"), "a↵b");
|
||||||
|
assert_eq!(one_line("a\r\nb"), "a ↵b");
|
||||||
|
// Every other control character folds to a space rather than
|
||||||
|
// vanishing, so the name's length still reflects what is on disk.
|
||||||
|
assert_eq!(one_line("a\tb\u{7}c"), "a b c");
|
||||||
|
// One char in, one char out — a fuzzy matcher's byte positions are
|
||||||
|
// handed to this and still have to address the same characters.
|
||||||
|
for s in ["plain.rs", "a\nb", "a\tb", "日本語\n"] {
|
||||||
|
assert_eq!(one_line(s).chars().count(), s.chars().count(), "{s:?}");
|
||||||
|
}
|
||||||
|
// Text with nothing to fold comes back untouched.
|
||||||
|
assert_eq!(one_line("README.md"), "README.md");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_busy_command_name_stays_short_enough_to_read() {
|
fn a_busy_command_name_stays_short_enough_to_read() {
|
||||||
use super::{BUSY_COMMAND_MAX, clamp_command};
|
use super::{BUSY_COMMAND_MAX, clamp_command};
|
||||||
|
|||||||
+3
-2
@@ -6,6 +6,7 @@ use std::sync::Arc;
|
|||||||
use crate::core::config::RightPanelTab;
|
use crate::core::config::RightPanelTab;
|
||||||
use crate::core::git::status::{DecoStatus, DirRollup, StatusIndex};
|
use crate::core::git::status::{DecoStatus, DirRollup, StatusIndex};
|
||||||
use crate::terminal::git_data::index_of;
|
use crate::terminal::git_data::index_of;
|
||||||
|
use crate::terminal::view::one_line;
|
||||||
use crate::ui::app::Tty7App;
|
use crate::ui::app::Tty7App;
|
||||||
use crate::ui::file_copy;
|
use crate::ui::file_copy;
|
||||||
use crate::ui::host_ops::{ByHost, HostId, HostOps, InFlight, SharedHost, WatchSub};
|
use crate::ui::host_ops::{ByHost, HostId, HostOps, InFlight, SharedHost, WatchSub};
|
||||||
@@ -1815,7 +1816,7 @@ impl Tty7App {
|
|||||||
.when(deco.strike, |d| d.line_through())
|
.when(deco.strike, |d| d.line_through())
|
||||||
.when(deco.bold, |d| d.font_weight(gpui::FontWeight::SEMIBOLD))
|
.when(deco.bold, |d| d.font_weight(gpui::FontWeight::SEMIBOLD))
|
||||||
.when(row.is_root, |d| d.font_weight(gpui::FontWeight::MEDIUM))
|
.when(row.is_root, |d| d.font_weight(gpui::FontWeight::MEDIUM))
|
||||||
.child(SharedString::from(row.entry.name.clone()))
|
.child(SharedString::from(one_line(&row.entry.name)))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2117,7 +2118,7 @@ impl gpui::Render for DragGhost {
|
|||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(Icon::new(IconName::File).size(px(ROW_GLYPH)))
|
.child(Icon::new(IconName::File).size(px(ROW_GLYPH)))
|
||||||
.child(SharedString::from(self.name.clone()))
|
.child(SharedString::from(one_line(&self.name)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user