T18 review fixes: a missing folder stops the start, combining marks paint on their cell, the terminal is a Tab stop and keeps its wheel, clear drops find matches; the gallery starts a fresh shell per pick and picks the shell tab before typing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8374b77906
commit
47f3c7d505
9 files changed
+134
-26
No files matched your search
@@ -239,4 +239,5 @@ Per-component lines live in `tasks/`. Tags there:
|
|||||||
| T17d | 1 | FAIL | a path match marked a project name mid-character; pin and remove also opened the row; number settings showed no places |
|
| T17d | 1 | FAIL | a path match marked a project name mid-character; pin and remove also opened the row; number settings showed no places |
|
||||||
| T17d | 2 | PASS | |
|
| T17d | 2 | PASS | |
|
||||||
| T17e | 1 | PASS | |
|
| T17e | 1 | PASS | |
|
||||||
| T18 | 1 | — | pending review |
|
| T18 | 1 | FAIL | a missing folder ran in the app's own; combining marks dropped; Tab skipped the terminal; its wheel scrolled the page too; clear kept find matches; New bash reused the old shell; the dark script typed into the replay |
|
||||||
|
| T18 | 2 | — | pending review |
|
||||||
@@ -50,10 +50,11 @@ fn launch(shell: usize) -> Launch {
|
|||||||
/// A pane group of shells, and the shell each pane holds.
|
/// A pane group of shells, and the shell each pane holds.
|
||||||
type Panes = (Entity<PaneGroup>, Entity<Vec<(PaneId, Entity<Terminal>)>>);
|
type Panes = (Entity<PaneGroup>, Entity<Vec<(PaneId, Entity<Terminal>)>>);
|
||||||
|
|
||||||
fn panes(shell: usize, window: &mut Window, cx: &mut App) -> Panes {
|
/// Panes of `shell`, fresh for each `start`: a pick starts new shells even of the same kind.
|
||||||
let sessions = window.use_keyed_state(("terminal-sessions", shell), cx, |_, _| Vec::new());
|
fn panes(shell: usize, start: usize, window: &mut Window, cx: &mut App) -> Panes {
|
||||||
|
let sessions = window.use_keyed_state(("terminal-sessions", start), cx, |_, _| Vec::new());
|
||||||
let held = sessions.clone();
|
let held = sessions.clone();
|
||||||
let group = window.use_keyed_state(("terminal-panes", shell), cx, move |_, _| {
|
let group = window.use_keyed_state(("terminal-panes", start), cx, move |_, _| {
|
||||||
PaneGroup::new(move |pane, _, cx| session(&held, pane, shell, cx).into_any_element())
|
PaneGroup::new(move |pane, _, cx| session(&held, pane, shell, cx).into_any_element())
|
||||||
});
|
});
|
||||||
(group, sessions)
|
(group, sessions)
|
||||||
@@ -85,9 +86,9 @@ fn field(key: &'static str, text: &str, window: &mut Window, cx: &mut App) -> En
|
|||||||
|
|
||||||
pub fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
pub fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||||
let tab = keep("terminal-tab", || SharedString::from("shell"), window, cx);
|
let tab = keep("terminal-tab", || SharedString::from("shell"), window, cx);
|
||||||
let shell = keep("terminal-shell-choice", || 0usize, window, cx);
|
let shell = keep("terminal-shell-choice", || (0usize, 0usize), window, cx);
|
||||||
let (now_tab, now_shell) = (tab.read(cx).clone(), *shell.read(cx));
|
let (now_tab, (now_shell, start)) = (tab.read(cx).clone(), *shell.read(cx));
|
||||||
let (group, sessions) = panes(now_shell, window, cx);
|
let (group, sessions) = panes(now_shell, start, window, cx);
|
||||||
let build = window.use_keyed_state("terminal-build", cx, |_, cx| {
|
let build = window.use_keyed_state("terminal-build", cx, |_, cx| {
|
||||||
Terminal::replay(BUILD.as_bytes(), 92, 12, cx)
|
Terminal::replay(BUILD.as_bytes(), 92, 12, cx)
|
||||||
});
|
});
|
||||||
@@ -124,7 +125,7 @@ pub fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.default_shell(now_shell)
|
.default_shell(now_shell)
|
||||||
.on_pick(move |ix, _, cx| set(&choose, ix, cx));
|
.on_pick(move |ix, _, cx| set(&choose, (ix, start + 1), cx));
|
||||||
let on_shell = now_tab.as_ref() == "shell";
|
let on_shell = now_tab.as_ref() == "shell";
|
||||||
let exit = active
|
let exit = active
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ pub const PAGE: Page = Page {
|
|||||||
|
|
||||||
const SCRIPT: &[Step] = &[
|
const SCRIPT: &[Step] = &[
|
||||||
Step::Rest,
|
Step::Rest,
|
||||||
|
Step::DownAt("terminal-tabs", 46.0, 16.0),
|
||||||
|
Step::UpAt("terminal-tabs", 46.0, 16.0),
|
||||||
|
Step::Wait(300),
|
||||||
Step::DownAt("terminal", 200.0, 150.0),
|
Step::DownAt("terminal", 200.0, 150.0),
|
||||||
Step::UpAt("terminal", 200.0, 150.0),
|
Step::UpAt("terminal", 200.0, 150.0),
|
||||||
Step::Wait(400),
|
Step::Wait(400),
|
||||||
|
|||||||
+44
-3
@@ -27,6 +27,8 @@ pub(crate) struct Style {
|
|||||||
pub(crate) struct Row {
|
pub(crate) struct Row {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub runs: Vec<(usize, Style)>,
|
pub runs: Vec<(usize, Style)>,
|
||||||
|
/// Cells with combining marks, painted on their own at their column: the column, the cluster and its style.
|
||||||
|
pub clusters: Vec<(usize, String, Style)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Why cells are lit: selected, or a find match, the current one stronger.
|
/// Why cells are lit: selected, or a find match, the current one stronger.
|
||||||
@@ -50,7 +52,17 @@ pub(crate) struct Frame {
|
|||||||
impl Frame {
|
impl Frame {
|
||||||
/// The screen's text, trailing spaces gone.
|
/// The screen's text, trailing spaces gone.
|
||||||
pub(crate) fn text(&self) -> String {
|
pub(crate) fn text(&self) -> String {
|
||||||
let lines: Vec<&str> = self.rows.iter().map(|row| row.text.trim_end()).collect();
|
let lines: Vec<String> = self
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.map(|row| {
|
||||||
|
let mut cells: Vec<String> = row.text.chars().map(String::from).collect();
|
||||||
|
for (column, cluster, _) in &row.clusters {
|
||||||
|
cells[*column] = cluster.clone();
|
||||||
|
}
|
||||||
|
cells.concat().trim_end().to_string()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
lines.join("\n").trim_end().to_string()
|
lines.join("\n").trim_end().to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,11 +90,27 @@ pub(crate) fn frame<T: EventListener>(
|
|||||||
let spacer = cell
|
let spacer = cell
|
||||||
.flags
|
.flags
|
||||||
.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER);
|
.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER);
|
||||||
|
let look = style(cell, ink, content.colors);
|
||||||
|
let cluster = cell
|
||||||
|
.zerowidth()
|
||||||
|
.filter(|marks| !marks.is_empty())
|
||||||
|
.map(|marks| {
|
||||||
|
std::iter::once(cell.c)
|
||||||
|
.chain(marks.iter().copied())
|
||||||
|
.collect::<String>()
|
||||||
|
});
|
||||||
push(
|
push(
|
||||||
&mut rows[row],
|
&mut rows[row],
|
||||||
if spacer { ' ' } else { cell.c },
|
if spacer || cluster.is_some() {
|
||||||
style(cell, ink, content.colors),
|
' '
|
||||||
|
} else {
|
||||||
|
cell.c
|
||||||
|
},
|
||||||
|
look,
|
||||||
);
|
);
|
||||||
|
if let Some(cluster) = cluster {
|
||||||
|
rows[row].clusters.push((point.column.0, cluster, look));
|
||||||
|
}
|
||||||
let why = if content.selection.is_some_and(|range| range.contains(point)) {
|
let why = if content.selection.is_some_and(|range| range.contains(point)) {
|
||||||
Some(Lit::Selected)
|
Some(Lit::Selected)
|
||||||
} else {
|
} else {
|
||||||
@@ -178,6 +206,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combining_marks_stay_on_their_cell() {
|
||||||
|
let term = replayed("e\u{301}x".as_bytes());
|
||||||
|
let shown = frame(&term, &ink(), &[], None);
|
||||||
|
assert_eq!(shown.text(), "e\u{301}x");
|
||||||
|
assert_eq!(
|
||||||
|
&shown.rows[0].text[..2],
|
||||||
|
" x",
|
||||||
|
"the cluster's cell keeps its column"
|
||||||
|
);
|
||||||
|
assert_eq!(shown.rows[0].clusters[0].1, "e\u{301}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cells_join_into_runs_of_one_style() {
|
fn cells_join_into_runs_of_one_style() {
|
||||||
let term = replayed(b"\x1b[31mred\x1b[0m ok\r\n\x1b[7minv\x1b[0m");
|
let term = replayed(b"\x1b[31mred\x1b[0m ok\r\n\x1b[7minv\x1b[0m");
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wheel(&mut self, event: &ScrollWheelEvent, _: &mut Window, cx: &mut Context<Self>) {
|
fn wheel(&mut self, event: &ScrollWheelEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
cx.stop_propagation();
|
||||||
self.rest += event.delta.pixel_delta(self.cell.height).y;
|
self.rest += event.delta.pixel_delta(self.cell.height).y;
|
||||||
let lines = (self.rest / self.cell.height).trunc() as i32;
|
let lines = (self.rest / self.cell.height).trunc() as i32;
|
||||||
if lines == 0 {
|
if lines == 0 {
|
||||||
|
|||||||
+34
-14
@@ -8,7 +8,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
frame::{Frame, Lit},
|
frame::{Frame, Lit, Style},
|
||||||
links::Target,
|
links::Target,
|
||||||
view::Terminal,
|
view::Terminal,
|
||||||
};
|
};
|
||||||
@@ -28,6 +28,23 @@ pub(crate) struct Look {
|
|||||||
pub link: Hsla,
|
pub link: Hsla,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The face a style prints in.
|
||||||
|
fn font(style: &Style, base: &Font) -> Font {
|
||||||
|
Font {
|
||||||
|
weight: if style.bold {
|
||||||
|
FontWeight::BOLD
|
||||||
|
} else {
|
||||||
|
FontWeight::NORMAL
|
||||||
|
},
|
||||||
|
style: if style.italic {
|
||||||
|
FontStyle::Italic
|
||||||
|
} else {
|
||||||
|
FontStyle::Normal
|
||||||
|
},
|
||||||
|
..base.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The grid, fitted to its box: it tells the terminal its size and origin, then paints each row.
|
/// The grid, fitted to its box: it tells the terminal its size and origin, then paints each row.
|
||||||
pub(crate) fn grid(
|
pub(crate) fn grid(
|
||||||
terminal: Entity<Terminal>,
|
terminal: Entity<Terminal>,
|
||||||
@@ -63,19 +80,7 @@ pub(crate) fn grid(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(len, style)| TextRun {
|
.map(|(len, style)| TextRun {
|
||||||
len: *len,
|
len: *len,
|
||||||
font: Font {
|
font: font(style, &look.font),
|
||||||
weight: if style.bold {
|
|
||||||
FontWeight::BOLD
|
|
||||||
} else {
|
|
||||||
FontWeight::NORMAL
|
|
||||||
},
|
|
||||||
style: if style.italic {
|
|
||||||
FontStyle::Italic
|
|
||||||
} else {
|
|
||||||
FontStyle::Normal
|
|
||||||
},
|
|
||||||
..look.font.clone()
|
|
||||||
},
|
|
||||||
color: style.fg,
|
color: style.fg,
|
||||||
background_color: style.bg,
|
background_color: style.bg,
|
||||||
underline: style.underline.then_some(UnderlineStyle {
|
underline: style.underline.then_some(UnderlineStyle {
|
||||||
@@ -110,6 +115,21 @@ pub(crate) fn grid(
|
|||||||
shaped
|
shaped
|
||||||
.paint(at(ix, 0), cell.height, window, cx)
|
.paint(at(ix, 0), cell.height, window, cx)
|
||||||
.expect("a terminal row paints");
|
.expect("a terminal row paints");
|
||||||
|
for (column, cluster, style) in &row.clusters {
|
||||||
|
let run = TextRun {
|
||||||
|
len: cluster.len(),
|
||||||
|
font: font(style, &look.font),
|
||||||
|
color: style.fg,
|
||||||
|
background_color: None,
|
||||||
|
underline: None,
|
||||||
|
strikethrough: None,
|
||||||
|
};
|
||||||
|
window
|
||||||
|
.text_system()
|
||||||
|
.shape_line(cluster.clone().into(), look.size, &[run], None)
|
||||||
|
.paint(at(ix, *column), cell.height, window, cx)
|
||||||
|
.expect("a terminal cluster paints");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some((row, columns, _)) = &hovered {
|
if let Some((row, columns, _)) = &hovered {
|
||||||
let under = at(*row, columns.start) + point(Pixels::ZERO, cell.height - look.rule);
|
let under = at(*row, columns.start) + point(Pixels::ZERO, cell.height - look.rule);
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ pub(crate) fn detached(size: TermSize) -> Session {
|
|||||||
|
|
||||||
/// Starts `launch` on a pseudo-terminal of `size` cells, each `cell` pixels.
|
/// Starts `launch` on a pseudo-terminal of `size` cells, each `cell` pixels.
|
||||||
pub(crate) fn spawn(launch: &Launch, size: TermSize, cell: (u16, u16)) -> anyhow::Result<Session> {
|
pub(crate) fn spawn(launch: &Launch, size: TermSize, cell: (u16, u16)) -> anyhow::Result<Session> {
|
||||||
|
if let Some(cwd) = &launch.cwd {
|
||||||
|
anyhow::ensure!(
|
||||||
|
cwd.is_dir(),
|
||||||
|
"a terminal cannot start in {}: no such folder",
|
||||||
|
cwd.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
let (sender, events) = unbounded();
|
let (sender, events) = unbounded();
|
||||||
let listener = Listener(sender);
|
let listener = Listener(sender);
|
||||||
let term = Arc::new(FairMutex::new(Term::new(
|
let term = Arc::new(FairMutex::new(Term::new(
|
||||||
@@ -103,6 +110,19 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_missing_folder_stops_the_start() {
|
||||||
|
let launch = Launch {
|
||||||
|
program: Some(("/bin/sh".into(), vec!["-c".into(), "true".into()])),
|
||||||
|
cwd: Some("/nonexistent/ely".into()),
|
||||||
|
..Launch::default()
|
||||||
|
};
|
||||||
|
let error = spawn(&launch, TermSize::new(20, 4), (8, 16))
|
||||||
|
.err()
|
||||||
|
.expect("a missing folder is an error");
|
||||||
|
assert!(error.to_string().contains("/nonexistent/ely"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_program_prints_into_the_grid_through_a_pseudo_terminal() {
|
fn a_program_prints_into_the_grid_through_a_pseudo_terminal() {
|
||||||
let launch = Launch {
|
let launch = Launch {
|
||||||
|
|||||||
@@ -49,3 +49,23 @@ fn find_counts_matches_and_steps_round(cx: &mut TestAppContext) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn tab_reaches_the_terminal(cx: &mut TestAppContext) {
|
||||||
|
let (terminal, cx) = replayed(b"ok", cx);
|
||||||
|
let focused = cx.update(|window, cx| {
|
||||||
|
window.focus_next();
|
||||||
|
terminal.read(cx).focus.is_focused(window)
|
||||||
|
});
|
||||||
|
assert!(focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn clearing_drops_the_matches(cx: &mut TestAppContext) {
|
||||||
|
let (terminal, cx) = replayed(b"one two one", cx);
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
assert_eq!(terminal.find("one", FindOptions::default(), cx), Ok(2));
|
||||||
|
terminal.clear(cx);
|
||||||
|
assert_eq!(terminal.matches(), (0, None));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -126,7 +126,7 @@ impl Terminal {
|
|||||||
Self {
|
Self {
|
||||||
term,
|
term,
|
||||||
pty,
|
pty,
|
||||||
focus: cx.focus_handle(),
|
focus: cx.focus_handle().tab_stop(true),
|
||||||
title,
|
title,
|
||||||
exited: None,
|
exited: None,
|
||||||
size,
|
size,
|
||||||
@@ -166,6 +166,7 @@ impl Terminal {
|
|||||||
|
|
||||||
/// Clears the screen and the scrollback; a shell redraws its prompt.
|
/// Clears the screen and the scrollback; a shell redraws its prompt.
|
||||||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.search = Search::default();
|
||||||
{
|
{
|
||||||
let mut term = self.term.lock();
|
let mut term = self.term.lock();
|
||||||
term.clear_screen(ClearMode::Saved);
|
term.clear_screen(ClearMode::Saved);
|
||||||
|
|||||||
Reference in new issue
Block a user