I'm trying to make an editor based on the generativity
crate. Pattern is described in this blog post. My editor will have many commands that operate on text. I want to maximize type safety by having an Index<'id>(usize, Id<'id>)
newtype that is branded with a non-unifiable lifetime 'id
and guarantees it is in-bounds for any FileContents<'id>
.
The FileContents
represents contents of each file:
#[repr(transparent)]
pub struct FileContents<'id>(String, Id<'id>);
The Index<'id>
newtype represents a usize
that is known to always be in-bounds for any FileContents
with the identical 'id
#[repr(transparent)]
pub struct Index<'id>(usize, Id<'id>);
impl Index {
/// **Safety:** `index` must be in-bounds for `TextContents` with the same `'id`
unsafe fn new(index: usize, id: Id<'id>) {
Self(index, id)
}
}
FileContents[Index]
is an infallible operation.
unsafe
code should be able to rely on the property that an Index<'id>
can never hold a usize
that is out-of-bounds for the FileContents<'id>
FileContents<'id>
must be invalidated on any mutation that could invalidate existing Index<'id>
s, so we must consume self
and return the modified version:
impl<'id> FileContents<'id> {
pub fn new(content: String, guard: Guard<'id>) -> Self {
FileContents(content, guard.into())
}
/// If the `index` is in-bounds, promote it to `Index<'id>` which is
/// statically known to always be in-bounds
pub fn char_index(&self, index: usize) -> Option<Index<'id>> {
if index < self.0.len() {
// SAFETY: We just checked that `index` is in-bounds
Some(unsafe { Index::new(index, self.1) })
} else {
None
}
}
/// This method mutates our file. Because mutation can invalidate existing indexes,
/// we must consume the old one and return new with the `'new_id` lifetime.
///
/// This means that any existing `Index<'id>` are invalidated, which is what we want
pub fn insert_at<'new_id>(
self,
content: &str,
index: Index<'id>,
new_guard: Guard<'new_id>,
) -> FileContents<'new_id> {
let mut s = self.0;
s.insert_str(index.0, content);
FileContents(s, new_guard.into())
}
pub fn content(&self) -> &str {
&self.0
}
}
I managed to implement an editor that can only edit or keep 1 file in memory, ever. That was easy, I just had a single Guard<'id>
at the top which outlives everything. When I need to mutate FileContents
, I safely transmute
it.
fn main() {
make_guard!(guard);
let guard: Guard<'_ /* long */> = guard;
// Just have a single file in our "editor"
let mut file: FileContents<'_ /* 'long */> = FileContents::new("foo", guard);
let mut insert_at = 2; // after "foo"
// Simulate user typing 4 characters
for ch in " baz".chars() {
let index: Index<'_ /* long */> = file.char_index(insert_at).unwrap();
make_guard!(edit_guard);
let new_file: FileContents<'_ /* 'short */> = file.insert_at(ch, index, edit_guard);
// SAFETY: Each iteration of the loop creates a "new world", so we can safely transmute
// 'short to 'long because 'long outlives 'short, and we don't use `index` created here
// in the next iteration of the loop.
let new_file: FileContents<'_ /* 'long */> = unsafe { std::mem::transmute(new_file) };
file = new_file;
insert_at += 1;
}
assert_eq!(file.content(), "foo bar");
}
Now here is the hard part, and one that, I don't even know if it is possible. Editors can handle as many files as they want, they can have many of them open at the same time. I want to have a global context that I pass around, which contains all of my opened files:
struct EditorContext {
files: Vec<FileContents<'?>>,
current_file: usize
}
impl EditorContext {
fn insert_text(&mut self, text: &str) { ??? }
}
But putting the 'id
lifetime on the EditorContext
will be incorrect - each item there must have its own unique lifetime.
I'm struggling to come up with an architecture for an Editor that makes use of this pattern. We start with 0 files. We can open files 1, 2, 3 4. They each have unique 'id
lifetimes. We then remove some files. Execute commands on the 1st file and 2nd file etc. How is any of this possible?
Is the thing I'm trying to do just fundamentally impossible / unsound, or are there some workarounds I could do? I really want to create an editor where every command is as type-safe as possible. Where I can guarantee that given an Index
, LineIndex
etc that they are in-bounds.
Would appreciate some help!
1 post - 1 participant
Read full topic
🏷️ Rust_feed