TextBufferSession
class exported by @mwillbanks/tuil-editor.
class
Public class exported by @mwillbanks/tuil-editor.
export class TextBufferSession implements EditorSession {
readonly #id: string;
readonly #type: string;
readonly #readOnly: boolean;
readonly #masked: boolean;
readonly #clipboard?: EditorProviderOptions["clipboard"];
readonly #onDocumentChange?: EditorProviderOptions["onDocumentChange"];
readonly #observers = new Set<(snapshot: EditorSnapshot) => void>();
readonly #undo: HistoryEntry[] = [];
readonly #redo: HistoryEntry[] = [];
#text: string;
#version = 0;
#selections: readonly EditorSelection[];
#decorations: readonly EditorDecoration[] = [];
#diagnostics: readonly EditorDiagnostic[] = [];
#mode: string;
#viewportAnchor?: EditorPosition;
#disposed = false;
constructor(options: EditorProviderOptions = {}) {
this.#id = options.id ?? "document";
this.#type = options.documentType ?? "text/plain";
this.#text = options.value ?? "";
this.#readOnly = options.readOnly ?? false;
this.#masked = options.masked ?? false;
this.#clipboard = options.clipboard;
this.#onDocumentChange = options.onDocumentChange;
this.#mode = options.mode ?? "insert";
this.#viewportAnchor = options.viewportAnchor;
this.#selections = [selection(position(0, 0))];
}
snapshot(): EditorSnapshot {
return Object.freeze({
document: Object.freeze({
id: this.#id,
type: this.#type,
version: this.#version,
text: this.#exportText(),
}),
selections: this.#selections,
decorations: this.#decorations,
diagnostics: this.#diagnostics,
mode: this.#mode,
readOnly: this.#readOnly,
canUndo: this.#undo.length > 0,
canRedo: this.#redo.length > 0,
viewportAnchor: this.#viewportAnchor,
});
}
dispatch(transaction: EditorTransaction): EditorSnapshot {
this.#assertActive();
const changes = [...(transaction.changes ?? [])];
if (changes.length > 0 && this.#readOnly) {
throw new Error("Editor session is read-only");
}
this.#recordTransactionHistory(changes, transaction.addToHistory);
const edits = prepareEdits(this.#text, changes);
this.#text = applyEdits(this.#text, edits);
this.#applyTransactionSelection(edits, transaction.selections);
if (changes.length > 0 || transaction.selections) this.#version += 1;
if (changes.length > 0) this.#onDocumentChange?.(this.#text);
this.#notify();
return this.snapshot();
}
#recordTransactionHistory(
changes: readonly EditorChange[],
addToHistory: boolean | undefined,
): void {
if (changes.length === 0 || addToHistory === false) return;
this.#undo.push({ text: this.#text, selections: this.#selections });
this.#redo.length = 0;
}
#applyTransactionSelection(
edits: readonly PreparedEdit[],
selections: readonly EditorSelection[] | undefined,
): void {
if (selections) {
this.#selections = freezeSelections(this.#text, selections);
return;
}
if (edits.length === 0) return;
this.#selections = edits
.toSorted((left, right) => left.index - right.index)
.map((edit) => {
const precedingDelta = edits
.filter((candidate) => candidate.offsets[0] < edit.offsets[0])
.reduce(
(total, candidate) =>
total +
candidate.change.insert.length -
(candidate.offsets[1] - candidate.offsets[0]),
0,
);
return selection(
positionAt(
this.#text,
edit.offsets[0] + edit.change.insert.length + precedingDelta,
),
);
});
}
execute(
command: string | EditorCommand,
argument?: unknown,
): boolean | Promise<boolean> {
this.#assertActive();
if (typeof command !== "string") return command.execute(this, argument);
const clipboard = executeClipboardCommand(this, command, argument);
if (clipboard !== undefined) return clipboard;
return this.#executeSelectionCommand(command);
}
#executeSelectionCommand(command: string): boolean {
if (command === "select-all") {
this.#selections = [
selection(position(0, 0), positionAt(this.#text, this.#text.length)),
];
} else if (command === "delete-selection") {
this.dispatch({
changes: this.#selections.map((range) => ({ range, insert: "" })),
});
return true;
} else if (command === "cursor-start") {
this.dispatch({
selections: [selection(position(0, 0))],
addToHistory: false,
});
return true;
} else if (command === "cursor-end") {
this.dispatch({
selections: [selection(positionAt(this.#text, this.#text.length))],
addToHistory: false,
});
return true;
} else {
return false;
}
this.#notify();
return true;
}
copy(): string | Promise<string> {
this.#assertActive();
const copied = this.#selections
.map((range) => {
const [start, end] = offsets(this.#text, range);
const value = this.#text.slice(start, end);
return this.#masked ? maskText(value) : value;
})
.join("\n");
if (!this.#clipboard) return copied;
return Promise.resolve(this.#clipboard.write(copied)).then(() => copied);
}
cut(): string | Promise<string> {
return cutSelections(this, this.copy());
}
paste(value?: string): boolean | Promise<boolean> {
return pasteSelections(this, this.#clipboard, value);
}
undo(): boolean {
return this.#restoreHistory(this.#undo, this.#redo);
}
redo(): boolean {
return this.#restoreHistory(this.#redo, this.#undo);
}
#restoreHistory(
source: HistoryEntry[],
destination: HistoryEntry[],
): boolean {
this.#assertActive();
const entry = source.pop();
if (!entry) return false;
destination.push({ text: this.#text, selections: this.#selections });
this.#text = entry.text;
this.#selections = entry.selections;
this.#version += 1;
this.#onDocumentChange?.(this.#text);
this.#notify();
return true;
}
search(query: string | RegExp): readonly EditorRange[] {
this.#assertActive();
if (typeof query === "string" && query.length === 0) return [];
const expression = createGlobalSearchExpression(query);
const ranges: EditorRange[] = [];
for (const match of this.#text.matchAll(expression)) {
ranges.push(
Object.freeze({
anchor: positionAt(this.#text, match.index),
head: positionAt(this.#text, match.index + match[0].length),
}),
);
if (match[0].length === 0) expression.lastIndex += 1;
}
return Object.freeze(ranges);
}
replace(query: string | RegExp, replacement: string, all = true): number {
const ranges = this.search(query);
const selected = all ? ranges : ranges.slice(0, 1);
if (selected.length === 0) return 0;
this.dispatch({
changes: selected.map((range) => ({
range,
insert: replacement,
})),
});
return selected.length;
}
serialize(format: "text" | "json" | "markdown" = "text"): string {
const text = this.#exportText();
return format === "json"
? JSON.stringify(
this.#masked
? { type: this.#type, text, masked: true }
: { type: this.#type, text },
)
: text;
}
#exportText(): string {
return this.#masked ? maskText(this.#text) : this.#text;
}
subscribe(observer: (snapshot: EditorSnapshot) => void): () => void {
this.#assertActive();
this.#observers.add(observer);
return () => this.#observers.delete(observer);
}
setDecorations(decorations: readonly EditorDecoration[]): void {
this.#decorations = Object.freeze([...decorations]);
this.#notify();
}
setDiagnostics(diagnostics: readonly EditorDiagnostic[]): void {
this.#diagnostics = Object.freeze([...diagnostics]);
this.#notify();
}
setMode(mode: string): void {
this.#mode = mode;
this.#notify();
}
setViewportAnchor(anchor: EditorPosition): void {
this.#viewportAnchor = clamp(this.#text, anchor);
this.#notify();
}
dispose(): void {
this.#disposed = true;
this.#observers.clear();
this.#undo.length = 0;
this.#redo.length = 0;
}
#notify(): void {
const snapshot = this.snapshot();
for (const observer of this.#observers) observer(snapshot);
}
#assertActive(): void {
if (this.#disposed) throw new Error("Editor session is disposed");
}
}Members
| Member | Type | Required | Description | Related types |
|---|---|---|---|---|
#id | string | Yes | The #id member uses the string contract. | — |
#type | string | Yes | The #type member uses the string contract. | — |
#readOnly | boolean | Yes | The #readOnly member uses the boolean contract. | — |
#masked | boolean | Yes | The #masked member uses the boolean contract. | — |
#clipboard | import("./index").EditorClipboardAdapter | undefined | No | The #clipboard member uses the import("./index").EditorClipboardAdapter | undefined contract. | — |
#onDocumentChange | ((value: string) => void) | undefined | No | The #onDocumentChange member uses the ((value: string) => void) | undefined contract. | — |
#observers | Set<(snapshot: EditorSnapshot) => void> | Yes | The #observers member uses the Set<(snapshot: EditorSnapshot) => void> contract. | EditorSnapshot |
#undo | HistoryEntry[] | Yes | The #undo member uses the HistoryEntry[] contract. | — |
#redo | HistoryEntry[] | Yes | The #redo member uses the HistoryEntry[] contract. | — |
#text | string | Yes | The #text member uses the string contract. | — |
#version | number | Yes | The #version member uses the number contract. | — |
#selections | readonly EditorSelection[] | Yes | The #selections member uses the readonly EditorSelection[] contract. | EditorSelection |
#decorations | readonly EditorDecoration[] | Yes | The #decorations member uses the readonly EditorDecoration[] contract. | EditorDecoration |
#diagnostics | readonly EditorDiagnostic[] | Yes | The #diagnostics member uses the readonly EditorDiagnostic[] contract. | EditorDiagnostic |
#mode | string | Yes | The #mode member uses the string contract. | — |
#viewportAnchor | EditorPosition | undefined | No | The #viewportAnchor member uses the EditorPosition | undefined contract. | EditorPosition |
#disposed | boolean | Yes | The #disposed member uses the boolean contract. | — |
__constructor | any | Yes | The __constructor member uses the any contract. | — |
snapshot | () => EditorSnapshot | Yes | The snapshot member uses the () => EditorSnapshot contract. | EditorSnapshot |
dispatch | (transaction: EditorTransaction) => EditorSnapshot | Yes | The dispatch member uses the (transaction: EditorTransaction) => EditorSnapshot contract. | EditorSnapshot, EditorTransaction |
#recordTransactionHistory | (changes: readonly EditorChange[], addToHistory: boolean | undefined) => void | Yes | The #recordTransactionHistory member uses the (changes: readonly EditorChange[], addToHistory: boolean | undefined) => void contract. | EditorChange |
#applyTransactionSelection | (edits: readonly PreparedEdit[], selections: readonly EditorSelection[] | undefined) => void | Yes | The #applyTransactionSelection member uses the (edits: readonly PreparedEdit[], selections: readonly EditorSelection[] | undefined) => void contract. | EditorSelection |
execute | (command: string | EditorCommand, argument?: unknown) => boolean | Promise<boolean> | Yes | The execute member uses the (command: string | EditorCommand, argument?: unknown) => boolean | Promise<boolean> contract. | EditorCommand |
#executeSelectionCommand | (command: string) => boolean | Yes | The #executeSelectionCommand member uses the (command: string) => boolean contract. | — |
copy | () => string | Promise<string> | Yes | The copy member uses the () => string | Promise<string> contract. | — |
cut | () => string | Promise<string> | Yes | The cut member uses the () => string | Promise<string> contract. | — |
paste | (value?: string) => boolean | Promise<boolean> | Yes | The paste member uses the (value?: string) => boolean | Promise<boolean> contract. | — |
undo | () => boolean | Yes | The undo member uses the () => boolean contract. | — |
redo | () => boolean | Yes | The redo member uses the () => boolean contract. | — |
#restoreHistory | (source: HistoryEntry[], destination: HistoryEntry[]) => boolean | Yes | The #restoreHistory member uses the (source: HistoryEntry[], destination: HistoryEntry[]) => boolean contract. | — |
search | (query: string | RegExp) => readonly EditorRange[] | Yes | The search member uses the (query: string | RegExp) => readonly EditorRange[] contract. | EditorRange |
replace | (query: string | RegExp, replacement: string, all?: boolean) => number | Yes | The replace member uses the (query: string | RegExp, replacement: string, all?: boolean) => number contract. | — |
serialize | (format?: "text" | "json" | "markdown") => string | Yes | The serialize member uses the (format?: "text" | "json" | "markdown") => string contract. | — |
#exportText | () => string | Yes | The #exportText member uses the () => string contract. | — |
subscribe | (observer: (snapshot: EditorSnapshot) => void) => () => void | Yes | The subscribe member uses the (observer: (snapshot: EditorSnapshot) => void) => () => void contract. | EditorSnapshot |
setDecorations | (decorations: readonly EditorDecoration[]) => void | Yes | The setDecorations member uses the (decorations: readonly EditorDecoration[]) => void contract. | EditorDecoration |
setDiagnostics | (diagnostics: readonly EditorDiagnostic[]) => void | Yes | The setDiagnostics member uses the (diagnostics: readonly EditorDiagnostic[]) => void contract. | EditorDiagnostic |
setMode | (mode: string) => void | Yes | The setMode member uses the (mode: string) => void contract. | — |
setViewportAnchor | (anchor: EditorPosition) => void | Yes | The setViewportAnchor member uses the (anchor: EditorPosition) => void contract. | EditorPosition |
dispose | () => void | Yes | The dispose member uses the () => void contract. | — |
#notify | () => void | Yes | The #notify member uses the () => void contract. | — |
#assertActive | () => void | Yes | The #assertActive member uses the () => void contract. | — |
Parameters
This declaration has no public members.
Returns
This declaration does not return a value.
Throws
No thrown errors are documented for this declaration.
Related types
EditorChangeEditorCommandEditorDecorationEditorDiagnosticEditorPositionEditorProviderOptionsEditorRangeEditorSelectionEditorSessionEditorSnapshotEditorTransaction
Source
View the secondary source reference