From cbfe5248bb320fb344ca26c12c7982ce1ea00fa8 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 12:29:20 -0800 Subject: [PATCH] MCP: a readable library's id keeps a hidden path that it holds after its start A reply hid a hidden library's absolute path wherever it appeared, also inside another loaded library's id when the match began after the id's start. With `/libs/Kit.gpl` hidden, the readable id `loaded//project/libs/Kit.gpl` came back as `loaded//projectKit.gpl`, and a request that used it failed. Each whole readable id in a reply (with or without `loaded/`, or in a `file:` URL with no host or `localhost`) is now found first, and a hidden id or path that lies inside it stays. The same path after a drive or in a URL with another host is still hidden. --- crates/ui-egui/src/control.rs | 55 +++++++++++++++++++++++++---- crates/ui-egui/src/control/tests.rs | 24 +++++++++++++ docs/mcp.md | 4 +-- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 32d680b549..c1e6b7d0db 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -650,7 +650,10 @@ fn occurrences(text: &str, needle: &str, fold: bool) -> Vec { /// changed. fn replace_text(s: &str, r: &Rewrite) -> Option { let decoded = if has_percent_escape(s) { decoded_with_spans(s).filter(|(decoded, _)| decoded != s) } else { None }; - let mut found = Found::default(); + let mut found = Found { kept: readable_ids_in(s, None, r), ..Found::default() }; + if let Some((text, spans)) = &decoded { + found.kept.extend(readable_ids_in(text, Some(spans.as_slice()), r)); + } for group in &r.pairs { for fold in [false, true].into_iter().take(if r.fold { 2 } else { 1 }) { for (from, to) in group { @@ -677,24 +680,28 @@ fn replace_text(s: &str, r: &Rewrite) -> Option { Some(out) } -/// The spans of a text that [`replace_text`] replaces, each with its replacement, and every span it has -/// taken: the ones it replaces and the loaded library ids it keeps. +/// The spans of a text that [`replace_text`] replaces, each with its replacement, every span it has +/// taken (the ones it replaces and the loaded library ids it keeps), and the whole readable library ids +/// the text holds ([`readable_ids_in`]). #[derive(Default)] struct Found<'a> { cuts: Vec<(usize, usize, &'a str)>, taken: Vec<(usize, usize)>, + kept: Vec<(usize, usize)>, } impl<'a> Found<'a> { - /// Takes each occurrence of `from` in `text` that overlaps no span taken: as a span to write as `to`, - /// or, where it begins a loaded library's id ([`begins_known_id`]), that id's span, kept. `spans` - /// gives the range of the original text each byte of a decoded `text` was read from. + /// Takes each occurrence of `from` in `text` that overlaps no span taken and lies inside no whole + /// readable library id: as a span to write as `to`, or, where it begins a loaded library's id + /// ([`begins_known_id`]), that id's span, kept. `spans` gives the range of the original text each + /// byte of a decoded `text` was read from. fn claim(&mut self, text: &str, spans: Option<&[(usize, usize)]>, from: &str, to: &'a str, fold: bool, r: &Rewrite) { let span = |at: usize, end: usize| spans.map_or((at, end), |s| (s[at].0, s[end - 1].1)); for at in occurrences(text, from, fold) { let end = at + from.len(); let (start, stop) = span(at, end); - if self.taken.iter().any(|&(a, b)| start < b && a < stop) || ends_a_longer_path(text, at, from) { + let inside_readable = self.kept.iter().any(|&(a, b)| a <= start && stop <= b); + if inside_readable || self.taken.iter().any(|&(a, b)| start < b && a < stop) || ends_a_longer_path(text, at, from) { continue; } match begins_known_id(text, at, end, r) { @@ -823,6 +830,40 @@ fn begins_known_id(text: &str, at: usize, end: usize, r: &Rewrite) -> Option