T18 round 2 fixes: a folder the terminal cannot enter stops the start, and links read combining clusters whole

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-26 01:50:58 -04:00
1 parent 47f3c7d505
commit 99d9bf45d4
7 files changed
+73 -14

No files matched your search

Generated
+1
View File
@@ -1600,6 +1600,7 @@ dependencies = [
"raw-window-handle",
"regex",
"rust-embed",
"rustix 1.1.5",
"serde",
"serde_json",
"smallvec",
+3
View File
@@ -39,6 +39,9 @@ cocoa = "0.26"
objc = "0.2"
raw-window-handle = "0.6"
[target.'cfg(unix)'.dependencies]
rustix = { version = "1", default-features = false, features = ["std", "fs"] }
[dev-dependencies]
env_logger = "0.11"
+2 -1
View File
@@ -240,4 +240,5 @@ Per-component lines live in `tasks/`. Tags there:
| T17d | 2 | PASS | |
| T17e | 1 | PASS | |
| 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 |
| T18 | 2 | FAIL | a folder it could not enter still started the shell elsewhere; links read cluster cells as spaces |
| T18 | 3 | — | pending review |
+12 -7
View File
@@ -49,19 +49,24 @@ pub(crate) struct Frame {
pub cursor: Option<(usize, usize, CursorShape)>,
}
impl Row {
/// Each cell's text, a combining cluster whole.
pub(crate) fn cells(&self) -> Vec<String> {
let mut cells: Vec<String> = self.text.chars().map(String::from).collect();
for (column, cluster, _) in &self.clusters {
cells[*column] = cluster.clone();
}
cells
}
}
impl Frame {
/// The screen's text, trailing spaces gone.
pub(crate) fn text(&self) -> String {
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()
})
.map(|row| row.cells().concat().trim_end().to_string())
.collect();
lines.join("\n").trim_end().to_string()
}
+20 -5
View File
@@ -144,11 +144,26 @@ impl Terminal {
}
/// The link a row shows at `column`: its columns and target.
fn link_at(&self, row: usize, column: usize) -> Option<(usize, Range<usize>, Target)> {
let text = &self.shown.rows.get(row)?.text;
links(text).into_iter().find_map(|(bytes, target)| {
let start = text[..bytes.start].chars().count();
let end = start + text[bytes].chars().count();
pub(crate) fn link_at(
&self,
row: usize,
column: usize,
) -> Option<(usize, Range<usize>, Target)> {
let cells = self.shown.rows.get(row)?.cells();
let text = cells.concat();
let column_of = |byte: usize| {
let mut from = 0;
cells
.iter()
.position(|cell| {
let past = from >= byte;
from += cell.len();
past
})
.unwrap_or(cells.len())
};
links(&text).into_iter().find_map(|(bytes, target)| {
let (start, end) = (column_of(bytes.start), column_of(bytes.end));
(start..end)
.contains(&column)
.then_some((row, start..end, target))
+24
View File
@@ -55,6 +55,10 @@ pub(crate) fn spawn(launch: &Launch, size: TermSize, cell: (u16, u16)) -> anyhow
"a terminal cannot start in {}: no such folder",
cwd.display()
);
#[cfg(unix)]
rustix::fs::access(cwd, rustix::fs::Access::EXEC_OK).map_err(|error| {
anyhow::anyhow!("a terminal cannot enter {}: {error}", cwd.display())
})?;
}
let (sender, events) = unbounded();
let listener = Listener(sender);
@@ -110,6 +114,26 @@ mod tests {
use super::*;
#[test]
fn a_folder_that_cannot_be_entered_stops_the_start() {
use std::os::unix::fs::PermissionsExt;
let folder = std::env::temp_dir().join(format!("ely-shut-{}", std::process::id()));
std::fs::create_dir_all(&folder).expect("a temp folder");
std::fs::set_permissions(&folder, std::fs::Permissions::from_mode(0o600))
.expect("mode 600");
let launch = Launch {
program: Some(("/bin/sh".into(), vec!["-c".into(), "true".into()])),
cwd: Some(folder.clone()),
..Launch::default()
};
let started = spawn(&launch, TermSize::new(20, 4), (8, 16));
std::fs::set_permissions(&folder, std::fs::Permissions::from_mode(0o700))
.expect("mode 700");
std::fs::remove_dir(&folder).expect("the temp folder goes");
let error = started.err().expect("a folder it cannot enter is an error");
assert!(error.to_string().contains("ely-shut"));
}
#[test]
fn a_missing_folder_stops_the_start() {
let launch = Launch {
+11 -1
View File
@@ -1,6 +1,6 @@
use gpui::{Entity, TestAppContext, VisualTestContext};
use super::Terminal;
use super::{Target, Terminal};
use crate::{editor::FindOptions, theme::Theme};
fn replayed<'a>(
@@ -69,3 +69,13 @@ fn clearing_drops_the_matches(cx: &mut TestAppContext) {
assert_eq!(terminal.matches(), (0, None));
});
}
#[gpui::test]
fn a_link_keeps_its_combining_marks(cx: &mut TestAppContext) {
let (terminal, cx) = replayed("a https://e.dev/cafe\u{301}/x".as_bytes(), cx);
terminal.read_with(cx, |terminal, _| {
let (_, columns, target) = terminal.link_at(0, 10).expect("a link under column 10");
assert_eq!(columns, 2..22);
assert_eq!(target, Target::Url("https://e.dev/cafe\u{301}/x".into()));
});
}