Skip to content

Towards a text editor construction kit #1187

Open
@cmyr

Description

Towards a text editor construction kit

One of my goals in writing the rust playground for macOS was to see how much work would be involved in reusing components from the xi-editor core library (xi-core).

Basically: xi-core contains all the logic for handling user actions (such as editing the buffer or modifying the selection) but currently the architecture of xi-core is sort of all-or-nothing; it assumes that if you're using it you're using it in a certain way.

As an example of this: currently, all of the logic for various editing operations is attached to the Editor struct. This means that if you want to use xi-rope as the backing store for some text editor, you can't currently use xi's implementations of complex operations like backspace without also using xi's View struct, Editor struct, and plumbing.

This is less than ideal. We've put a lot of effort over the past few years into writing good, high-performance implementations of a bunch of text-editing primitives, and it would be nice if they were easy to use.

Improving code reuse and modularity

I believe that the xi-editor project should provide a set of components and functions for various text editing tasks, and these components and functions should be mix-and-match; you can use whatever works for you, and fill in the blanks however you wish.

This approach has obvious practical advantages, but it has some secondary advantages as well; most importantly, it encourages us to write code that is easier to test and profile.

Below, I will outline what this means, some specific things I think could be improved.

Good primitive text editing data structures

The most basic thing we would like to provide is a set of well-tested and robust data structures for text editing that work well together.

In general, we don't have a ton of work to do here; this is all (mostly) already in place, and working.

  • A text buffer: xi-rope is in quite good shape.
  • Selections: the current implementation works, but we might consider replacing it with a immutable version, but I'd want this project to be backed up with good profiling
  • line breaks (mostly fine)
  • style spans (okay for now)
  • annotations (in progress)

Move selection modification out of View

Functions that set and transform selections should be callable directly. For these, we need the ability to navigate between lines (which may be either visual (soft breaks / word wrap) or logical (newline characteres)). Currently these functions (in movement.rs) take a View struct, which exposes functions like offset_of_line, and line_of_offset. To make these work without needing a View, we'll need some sort of abstraction (like a trait) over this behaviour.

Ideally, then, all movement becomes a function with a signature like,

// LineMovement is a trait for things that implement fns like `line_of_offset`.
fn move_left(text: &Rope, selection: &Selection, view: &dyn LineMovement) -> Selection;

(it may be worth making the HorizPos a field on SelRegion to simplify some of this logic.)

move editing functions out of Editor

Editing operations need a few pieces of state: The current buffer, the current selections, the 'view' (or whatever is tracking line state) and likely certain config settings; for instance, the insert_tab edit operation needs to know whether to insert a tab character or spaces.

Edit operations produce deltas. In the general case, the selection state following an edit can be inferred (any selection regions are collapsed, as their contents are replaced/deleted; carets move based no the inserted or deleted text) but in certain cases an edit can also produce explicit selections, such as in transpose, where the selection moves along with the swapped regions.

Edit operations should be moved out of Editor, and be callable directly.

fn backspace(
    text: &Rope,
    selection: &Selection,
    view: &dyn LineMovement,
    config: &Config)
-> (Delta, Option<Selection>);

fn indent(
    text: &Rope,
    selection: &Selection,
    view: &dyn LineMovement,
    config: &Config)
-> (Delta, Option<Selection>);

Alternatively, we could some sort of context for this state, and hang edit functions off of that:

struct EditContext<'a> {
    text: &'a Rope,
    selection: &'a Selection,
    view: &'a dyn LineMovement,
    config: &'a Config,
}

impl<'a> EventContext<'a> {
    fn backspace(&self) -> (Delta, Option<Selection>) { .. }
    fn insert_text<T: Into<Rope>>(&self, text: T) -> (Delta, Option<Selection>) { .. }
}

It's unclear that this offers much advantage.

Find & Replace

Find and replace should be moved out of View. there's room for some design thinking here; for instance I could imagine wanting to use a FindContext struct that keeps track of find state, and implements methods for moving between results.

Other editor features

We should provide good standalone implementations of things like syntax highlighting, line breaking, auto-indent, and diffing, which can be computed incrementally where possible.

In addition, it would be nice if we could provide a basic non-CRDT undo implementation.

Other misc stuff: encoding / decoding files? a general purpose file system watcher? More generic support for configuration?

Conclusion

This is intended as a discussion issue; if there is general agreement with this approach, I'll open a tracking issue for specific tasks. If anyone has any thoughts about this, feel free to comment.

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions