-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #577 from streamich/slices-2
JSON CRDT Peritext slices followup
- Loading branch information
Showing
12 changed files
with
1,002 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,3 +4,7 @@ export const enum ExtensionId { | |
peritext = 2, | ||
quill = 3, | ||
} | ||
|
||
export const enum Chars { | ||
BlockSplitSentinel = '\n', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import {Cursor} from '../slice/Cursor'; | ||
import {Anchor, SliceBehavior} from '../constants'; | ||
import {tick, type ITimestampStruct} from '../../../json-crdt-patch/clock'; | ||
import {PersistedSlice} from '../slice/PersistedSlice'; | ||
import type {Range} from '../slice/Range'; | ||
import type {Peritext} from '../Peritext'; | ||
import type {Printable} from '../../../util/print/types'; | ||
import type {Point} from '../point/Point'; | ||
import type {SliceType} from '../types'; | ||
|
||
export class Editor implements Printable { | ||
/** | ||
* Cursor is the the current user selection. It can be a caret or a | ||
* range. If range is collapsed to a single point, it is a caret. | ||
*/ | ||
public readonly cursor: Cursor; | ||
|
||
constructor(public readonly txt: Peritext) { | ||
const point = txt.point(txt.str.id, Anchor.After); | ||
const cursorId = txt.str.id; // TODO: should be autogenerated to something else | ||
this.cursor = new Cursor(cursorId, txt, point, point.clone()); | ||
} | ||
|
||
/** @deprecated */ | ||
public setCursor(start: number, length: number = 0): void { | ||
this.cursor.setAt(start, length); | ||
} | ||
|
||
/** @deprecated */ | ||
public getCursorText(): string { | ||
return this.cursor.text(); | ||
} | ||
|
||
/** | ||
* Ensures there is no range selection. If user has selected a range, | ||
* the contents is removed and the cursor is set at the start of the range as cursor. | ||
* | ||
* @todo If block boundaries are withing the range, remove the blocks. | ||
* | ||
* @returns Returns the cursor position after the operation. | ||
*/ | ||
public collapseSelection(): ITimestampStruct { | ||
const cursor = this.cursor; | ||
const isCaret = cursor.isCollapsed(); | ||
if (!isCaret) { | ||
const {start, end} = cursor; | ||
const txt = this.txt; | ||
const deleteStartId = start.anchor === Anchor.Before ? start.id : start.nextId(); | ||
const deleteEndId = end.anchor === Anchor.After ? end.id : end.prevId(); | ||
const str = txt.str; | ||
if (!deleteStartId || !deleteEndId) throw new Error('INVALID_RANGE'); | ||
const range = str.findInterval2(deleteStartId, deleteEndId); | ||
const model = txt.model; | ||
const api = model.api; | ||
api.builder.del(str.id, range); | ||
api.apply(); | ||
if (start.anchor === Anchor.After) cursor.setCaret(start.id); | ||
else cursor.setCaret(start.prevId() || str.id); | ||
} | ||
return cursor.start.id; | ||
} | ||
|
||
/** | ||
* Insert inline text at current cursor position. If cursor selects a range, | ||
* the range is removed and the text is inserted at the start of the range. | ||
*/ | ||
public insert(text: string): void { | ||
if (!text) return; | ||
const after = this.collapseSelection(); | ||
const textId = this.txt.ins(after, text); | ||
this.cursor.setCaret(textId, text.length - 1); | ||
} | ||
|
||
/** | ||
* Deletes the previous character at current cursor position. If cursor | ||
* selects a range, deletes the whole range. | ||
*/ | ||
public delete(): void { | ||
const isCollapsed = this.cursor.isCollapsed(); | ||
if (isCollapsed) { | ||
const range = this.txt.findCharBefore(this.cursor.start); | ||
if (!range) return; | ||
this.cursor.set(range.start, range.end); | ||
} | ||
this.collapseSelection(); | ||
} | ||
|
||
public start(): Point | undefined { | ||
const txt = this.txt; | ||
const str = txt.str; | ||
if (!str.length()) return; | ||
const firstChunk = str.first(); | ||
if (!firstChunk) return; | ||
const firstId = firstChunk.id; | ||
const start = txt.point(firstId, Anchor.Before); | ||
return start; | ||
} | ||
|
||
public end(): Point | undefined { | ||
const txt = this.txt; | ||
const str = txt.str; | ||
if (!str.length()) return; | ||
const lastChunk = str.last(); | ||
if (!lastChunk) return; | ||
const lastId = lastChunk.span > 1 ? tick(lastChunk.id, lastChunk.span - 1) : lastChunk.id; | ||
const end = txt.point(lastId, Anchor.After); | ||
return end; | ||
} | ||
|
||
public all(): Range | undefined { | ||
const start = this.start(); | ||
const end = this.end(); | ||
if (!start || !end) return; | ||
return this.txt.range(start, end); | ||
} | ||
|
||
public selectAll(): void { | ||
const range = this.all(); | ||
if (range) this.cursor.setRange(range); | ||
} | ||
|
||
public insertSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { | ||
return this.txt.insSlice(this.cursor, SliceBehavior.Stack, type, data); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import {Point} from '../point/Point'; | ||
import {Anchor, SliceBehavior, Tags} from '../constants'; | ||
import {Range} from './Range'; | ||
import {printTree} from '../../../util/print/printTree'; | ||
import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; | ||
import type {Peritext} from '../Peritext'; | ||
import type {Slice} from './types'; | ||
|
||
export class Cursor extends Range implements Slice { | ||
public readonly behavior = SliceBehavior.Overwrite; | ||
public readonly type = Tags.Cursor; | ||
|
||
/** | ||
* Specifies whether the start or the end of the cursor is the "anchor", e.g. | ||
* the end which does not move when user changes selection. The other | ||
* end is free to move, the moving end of the cursor is "focus". By default | ||
* "anchor" is the start of the cursor. | ||
*/ | ||
public base: Anchor = Anchor.Before; | ||
|
||
constructor( | ||
public readonly id: ITimestampStruct, | ||
protected readonly txt: Peritext, | ||
public start: Point, | ||
public end: Point, | ||
) { | ||
super(txt, start, end); | ||
} | ||
|
||
public anchor(): Point { | ||
return this.base === Anchor.Before ? this.start : this.end; | ||
} | ||
|
||
public focus(): Point { | ||
return this.base === Anchor.Before ? this.end : this.start; | ||
} | ||
|
||
public set(start: Point, end?: Point, anchor: Anchor = Anchor.Before): void { | ||
if (!end || end === start) end = start.clone(); | ||
super.set(start, end); | ||
this.base = anchor; | ||
} | ||
|
||
public setAt(start: number, length: number = 0): void { | ||
let at = start; | ||
let len = length; | ||
if (len < 0) { | ||
at += len; | ||
len = -len; | ||
} | ||
super.setAt(at, len); | ||
this.base = length < 0 ? Anchor.After : Anchor.Before; | ||
} | ||
|
||
/** | ||
* Move one of the edges of the cursor to a new point. | ||
* | ||
* @param point Point to set the edge to. | ||
* @param edge 0 for "focus", 1 for "anchor." | ||
*/ | ||
public setEdge(point: Point, edge: 0 | 1 = 0): void { | ||
if (this.start === this.end) this.end = this.end.clone(); | ||
let anchor = this.anchor(); | ||
let focus = this.focus(); | ||
if (edge === 0) focus = point; | ||
else anchor = point; | ||
if (focus.compareSpatial(anchor) < 0) { | ||
this.base = Anchor.After; | ||
this.start = focus; | ||
this.end = anchor; | ||
} else { | ||
this.base = Anchor.Before; | ||
this.start = anchor; | ||
this.end = focus; | ||
} | ||
} | ||
|
||
/** @deprecated What is this method for? */ | ||
public del(): boolean { | ||
return false; | ||
} | ||
|
||
public data(): unknown { | ||
return 1; | ||
} | ||
|
||
public move(move: number): void { | ||
const {start, end} = this; | ||
start.move(move); | ||
if (start === end) return; | ||
end.move(move); | ||
} | ||
|
||
public toString(tab: string = ''): string { | ||
const text = JSON.stringify(this.text()); | ||
const focusIcon = this.base === Anchor.Before ? '.⇨|' : '|⇦.'; | ||
const main = `${this.constructor.name} ${super.toString(tab + ' ', true)} ${focusIcon}`; | ||
return main + printTree(tab, [() => text]); | ||
} | ||
|
||
// ----------------------------------------------------------------- Stateful | ||
|
||
public hash: number = 0; | ||
|
||
public refresh(): number { | ||
// TODO: implement this ... | ||
return this.hash; | ||
} | ||
} |
Oops, something went wrong.