Skip to content

Documents and edits ​

new DocumentModel(text?) starts a document with one caret at offset 0 and an empty history. The model keeps its text in a persistent rope, so an edit copies only the chunks it touches and an old snapshot keeps its own text.

Text and lines ​

getText() flattens the rope once per revision and caches it. A feature that needs a small region reads it with getLength(), getLineCount(), getLine(line) or slice(from, to) instead.

ts
const document = new DocumentModel('first\r\nsecond');

document.getLineCount(); // 2
document.getLine(1); // { start: 7, end: 13, next: 13, text: 'second' }
document.positionAt(9); // { line: 1, column: 2 }
document.offsetAt({ line: 1, column: 99 }); // 13, the end of the line

The model keeps the line endings it was given. A line ends at \n, and a \r\n counts as one break whose \r is not part of the line's text. A lone \r does not end a line. An empty document has one line, and a trailing break adds an empty last line.

getLine clamps a line number out of range. positionAt and offsetAt clamp to the document and to the line. slice is a plain substring and can cut a surrogate pair in half.

Selections ​

getSelections() returns copies in the order the carets were added. The last one is the primary caret, which getPrimary() returns: the view scrolls to it and a language feature asks about it.

setSelections(selections) clamps the offsets, moves an end out of the middle of a surrogate pair or a \r\n, and merges selections that touch or overlap. A merge that takes in the primary caret keeps the result last. An empty list becomes one caret at offset 0. A call to setSelections also ends the current typing group in the history.

ts
const document = new DocumentModel('red blue red');

document.setSelections([
    { anchor: 0, head: 3 },
    { anchor: 9, head: 12 }
]);
document.typeText('green', { language: 'text' });
document.getText(); // 'green blue green'

Edits ​

applyEdits(edits, options?) applies a batch of TextEdits at once. Every range is in the coordinates of the text before the batch, so the order of the list does not matter. The batch is one undo step.

ts
const document = new DocumentModel('red blue red');
const revision = document.getRevision();

document.applyEdits(
    [
        { from: 0, to: 3, text: 'green' },
        { from: 9, to: 12, text: 'green' }
    ],
    { expectedRevision: revision, source: 'command' }
);

It returns true when the text changed. It returns false, with nothing changed, for a batch that leaves the text as it was or for an expectedRevision the document moved past. It throws a RangeError for an offset outside the document, a range with to before from, an edge inside a surrogate pair, or two edits that overlap; two different insertions at one offset count as overlapping. A non-string text throws a TypeError. Nothing changes when it throws.

DocumentEditOptionsMeaning
expectedRevisionRefuse the batch unless the document is at this revision
selectionsThe selections after the edit; without them the current ones move along with the edits
source'input', 'command' or 'external' (the default), passed on to listeners
historyGroupJoins this batch to the previous undo step of the same group

setText(text) replaces everything as one undoable external edit. It keeps the history; to start over without one, make a new model.

Revisions ​

getRevision() goes up by one with every change of the text, undo and redo included, and never returns to an earlier value. A selection change leaves it alone. Take the revision before asking something slow, such as a language server or a model, and pass it as expectedRevision with the answer. A false then means the person typed in between and the answer needs asking again.

History ​

undo() and redo() return whether there was a step to take. A new edit clears the redo stack, and the history keeps the last 200 steps.

Edits with the same historyGroup join one undo step until something closes the group: a setSelections, an undo, an edit without the group, or any other change of the text or the selections in between. typeText uses the group typing unless told otherwise, so a word typed letter by letter undoes at once.

Snapshots and listeners ​

getSnapshot() returns an EditorSnapshot: text, selections, revision, canUndo and canRedo. Its text is read lazily from the rope of that revision, so a snapshot kept across later edits still returns its own text.

subscribe(listener) returns a Disposable and calls the listener after every change, synchronously, with a fresh snapshot. It does not call it right away. A snapshot of a text change adds three fields:

  • source: what made the change.
  • changes: one list of DocumentChanges (from, to, insertedLength) per transaction in the step, each in the coordinates of the text before that transaction. An undo of a typing group has one list per keystroke.
  • contentEdits: the same change as ContentEdits with line and column positions, each in the text the edit before it left. That is the order a language server's didChange expects.
ts
const subscription = document.subscribe((snapshot) => {
    if (snapshot.contentEdits === undefined) {
        return; // only the selection moved
    }
    send(snapshot.contentEdits);
});

subscription.dispose();

The model has no dispose of its own. Dispose its subscriptions and drop the reference; a retained snapshot keeps the rope of its revision alive.

Licenses and third-party notices are listed in each package.