wip(stage2a): name provenance plus the View rename hook
`BufferNameOrigin` records where a buffer's name came from instead of inferring it from the string: a path-backed buffer's name is the path *as given*, so a relative open is named `foo.rs` while its stored path is absolute, and a user may legitimately choose a name that normalizes to its own file's path. Rename reconciliation asks the bit. Every path-backed creation site is audited onto the new `set_path_derived_name` door: `EditorCore::get_or_load_buffer`, the `NotFound` arm of `resolve_target_buffer`, `pmacs.buffer.from_file`, and `pmacs.buffer.find_or_open`. Ordinary `Buffer::set_name` records `Explicit`. `View::rename_resource` is the seam that re-roots a URI-keyed overlay in place, so it keeps its position in the window's composition order; `DiagnosticView` overrides it, whose `uri` is private and set once at construction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
This commit is contained in:
parent
e003b81cdd
commit
4f6135263b
|
|
@ -148,6 +148,26 @@ struct EditDescription {
|
|||
inserted_len: u64,
|
||||
}
|
||||
|
||||
/// Provenance of a [`Buffer`]'s name (dired Stage 2a, Q#DR30).
|
||||
///
|
||||
/// A rename must move a name that merely *renders* the file's path and
|
||||
/// must leave a name the user chose alone. String inspection cannot
|
||||
/// tell those apart — a user may legitimately name a buffer with a
|
||||
/// string that normalizes to its own path — so the fact is recorded at
|
||||
/// the moment the name is written instead of being reconstructed
|
||||
/// later.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BufferNameOrigin {
|
||||
/// A caller named this buffer: `Buffer::new`/`from_bytes`, an
|
||||
/// ordinary [`Buffer::set_name`], or the `pmacs.buffer.set_name`
|
||||
/// binding. A rename leaves the name alone.
|
||||
Explicit,
|
||||
/// The name was derived from the buffer's backing path by a
|
||||
/// path-backed creation site, through
|
||||
/// [`Buffer::set_path_derived_name`]. A rename rewrites it.
|
||||
PathDerived,
|
||||
}
|
||||
|
||||
/// The unit of editable content: rope + identity + views + undo.
|
||||
///
|
||||
/// # Threading
|
||||
|
|
@ -159,6 +179,14 @@ pub struct Buffer {
|
|||
id: BufferId,
|
||||
rope: Rope,
|
||||
name: String,
|
||||
/// Where [`Self::name`] came from. Recorded rather than inferred,
|
||||
/// because a path-backed buffer's name is **not** reliably its
|
||||
/// path: `get_or_load_buffer` takes the name from the path *as
|
||||
/// given* and normalizes only the stored `file_path`, so a
|
||||
/// relative open is named `foo.rs` while its path is absolute.
|
||||
/// Rename reconciliation asks this bit, never the string
|
||||
/// (dired Stage 2a, Q#DR30).
|
||||
name_origin: BufferNameOrigin,
|
||||
/// The buffer's single active major mode, if one has been selected.
|
||||
major_mode: Option<String>,
|
||||
is_modified: bool,
|
||||
|
|
@ -247,6 +275,10 @@ impl Buffer {
|
|||
id,
|
||||
rope,
|
||||
name: name.into(),
|
||||
// Construction names a buffer explicitly. A path-backed
|
||||
// creation site re-records provenance through
|
||||
// `set_path_derived_name` right after binding the path.
|
||||
name_origin: BufferNameOrigin::Explicit,
|
||||
major_mode: None,
|
||||
is_modified: false,
|
||||
read_only: false,
|
||||
|
|
@ -449,9 +481,35 @@ impl Buffer {
|
|||
&self.name
|
||||
}
|
||||
|
||||
/// Set the buffer's name. Used by save-as and rename operations.
|
||||
/// Set the buffer's name, recording it as **explicitly chosen**
|
||||
/// ([`BufferNameOrigin::Explicit`]).
|
||||
///
|
||||
/// This is the user-facing door — `pmacs.buffer.set_name` and
|
||||
/// save-as go through it — and it is deliberately explicit even
|
||||
/// when the string happens to denote the file: naming a buffer
|
||||
/// `notes` for `${cwd}/notes` is still a naming operation, and a
|
||||
/// later rename must not overwrite it. Path-backed creation sites
|
||||
/// use [`Self::set_path_derived_name`] instead.
|
||||
pub fn set_name(&mut self, name: impl Into<String>) {
|
||||
self.name = name.into();
|
||||
self.name_origin = BufferNameOrigin::Explicit;
|
||||
}
|
||||
|
||||
/// Set the buffer's name **and** record that it was derived from
|
||||
/// the buffer's backing path ([`BufferNameOrigin::PathDerived`]).
|
||||
///
|
||||
/// Every site that creates or re-binds a path-backed buffer uses
|
||||
/// this door, including rename reconciliation itself — so a second
|
||||
/// rename still follows the path.
|
||||
pub fn set_path_derived_name(&mut self, name: impl Into<String>) {
|
||||
self.name = name.into();
|
||||
self.name_origin = BufferNameOrigin::PathDerived;
|
||||
}
|
||||
|
||||
/// Where this buffer's name came from (dired Stage 2a, Q#DR30).
|
||||
#[must_use]
|
||||
pub fn name_origin(&self) -> BufferNameOrigin {
|
||||
self.name_origin
|
||||
}
|
||||
|
||||
/// This buffer's active major mode, if any.
|
||||
|
|
|
|||
11
src/diag.rs
11
src/diag.rs
|
|
@ -489,6 +489,17 @@ impl DiagnosticView {
|
|||
}
|
||||
|
||||
impl View for DiagnosticView {
|
||||
/// Re-root this view when the buffer's file was renamed (dired
|
||||
/// Stage 2a, §5). The URI field is private and `View` has no
|
||||
/// downcast, so this hook is the only way an outside sweep can
|
||||
/// reach it — and mutating in place preserves this overlay's
|
||||
/// position in the window's composition order.
|
||||
fn rename_resource(&mut self, old_uri: &str, new_uri: &str) {
|
||||
if self.uri == old_uri {
|
||||
self.uri = new_uri.to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
fn kind(&self) -> &'static str {
|
||||
"diagnostic"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -938,11 +938,18 @@ impl EditorCore {
|
|||
}
|
||||
let normalized = normalize_buffer_path(path.to_path_buf());
|
||||
let (bytes, meta) = crate::file_io::load_file(path)?;
|
||||
// The name is the path **as given** — a relative open is named
|
||||
// `foo.rs` while `file_path` below is absolute. Recording the
|
||||
// provenance (Q#DR30) is what lets rename reconciliation move
|
||||
// this name without having to guess from the string.
|
||||
let display_name = path.display().to_string();
|
||||
let id = self
|
||||
.registry
|
||||
.borrow_mut()
|
||||
.create_from_bytes(display_name, &bytes);
|
||||
.create_from_bytes(display_name.clone(), &bytes);
|
||||
if let Ok(b) = self.registry.borrow_mut().get_mut(id) {
|
||||
b.set_path_derived_name(display_name);
|
||||
}
|
||||
self.set_buffer_path(id, Some(normalized));
|
||||
self.set_buffer_meta(id, Some(meta));
|
||||
Ok((id, true))
|
||||
|
|
@ -1001,7 +1008,12 @@ impl EditorCore {
|
|||
}),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
let display_path = path.display().to_string();
|
||||
let buffer_id = self.registry.borrow_mut().create(display_path);
|
||||
let buffer_id = self.registry.borrow_mut().create(display_path.clone());
|
||||
// Path-backed creation site (Q#DR30): the name is the
|
||||
// path, so a later rename may move it.
|
||||
if let Ok(b) = self.registry.borrow_mut().get_mut(buffer_id) {
|
||||
b.set_path_derived_name(display_path);
|
||||
}
|
||||
self.set_buffer_path(buffer_id, Some(path.to_path_buf()));
|
||||
"[new file]".clone_into(&mut self.status);
|
||||
Ok(ResolvedTarget::Buffer {
|
||||
|
|
|
|||
|
|
@ -3244,6 +3244,11 @@ fn install_buffer_module(lua: &Lua, registry: &SharedRegistry) -> mlua::Result<T
|
|||
))
|
||||
})?;
|
||||
let id = reg.borrow_mut().create_from_bytes(path.clone(), &bytes);
|
||||
// Path-backed creation site (Q#DR30): this name is the
|
||||
// path as given, so rename reconciliation may move it.
|
||||
if let Ok(b) = reg.borrow_mut().get_mut(id) {
|
||||
b.set_path_derived_name(path.clone());
|
||||
}
|
||||
if let Some(core) = lua.app_data_ref::<SharedCore>() {
|
||||
let mut core = core.borrow_mut();
|
||||
core.switch_active_buffer(id)
|
||||
|
|
@ -3307,6 +3312,10 @@ fn install_buffer_module(lua: &Lua, registry: &SharedRegistry) -> mlua::Result<T
|
|||
))
|
||||
})?;
|
||||
let id = reg.borrow_mut().create_from_bytes(path.clone(), &bytes);
|
||||
// Path-backed creation site (Q#DR30), as in `from_file`.
|
||||
if let Ok(b) = reg.borrow_mut().get_mut(id) {
|
||||
b.set_path_derived_name(path.clone());
|
||||
}
|
||||
if let Some(core) = lua.app_data_ref::<SharedCore>() {
|
||||
let mut core = core.borrow_mut();
|
||||
core.switch_active_buffer(id)
|
||||
|
|
|
|||
14
src/view.rs
14
src/view.rs
|
|
@ -310,6 +310,20 @@ pub trait View {
|
|||
fn clone_for_split(&self) -> Option<Box<dyn View>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Retarget this overlay from `old_uri` to `new_uri` after a
|
||||
/// resource rename (dired Stage 2a, §5). Default: no-op — a view
|
||||
/// that renders nothing URI-keyed is unaffected.
|
||||
///
|
||||
/// Mutates **in place**, so the overlay keeps its position in the
|
||||
/// window's composition order. That is the reason this is a trait
|
||||
/// hook rather than a remove-and-re-push at the call site: overlays
|
||||
/// are an ordered `Vec` merged in sequence, and re-pushing would
|
||||
/// move a diagnostic underline to the end of the stack. It is also
|
||||
/// how *passive* windows are reached at all — the Lua attach path
|
||||
/// (`pmacs.diag._attach_view`) can only touch the active window,
|
||||
/// while the sweep that drives this walks every window.
|
||||
fn rename_resource(&mut self, _old_uri: &str, _new_uri: &str) {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue