Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion client/bin/app.ts
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,17 +1,46 @@
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning --experimental-transform-types

import { TermEditor } from '../editor.ts';
import { GumnutFloor } from '../lib/gumnut/floor.ts';
import { InoPosRef } from '../lib/gumnut/types.ts';
import { connectToPulse } from '../lib/pulse/conn.ts';
import type { InPacket, OutPacket } from '../lib/ymodel/types.ts';

const SEND_DELAY = 125;
const FIXED_STRING = 'fixed-string';
const FIXED_CURSORS = 'cursors';
const FIXED_NUM = 'num';

const c = new AbortController();
const e = new TermEditor(c.signal);

const floor = new GumnutFloor();
const model = floor.byIno(FIXED_STRING);
let cursorAt: InoPosRef | null = null;

function reconcileOtherCursors() {
// TODO: replace other user cursors
const cursors = floor.byIno(FIXED_CURSORS);
const selfCursorPos = cursorAt ? floor.resolveRef(cursorAt) : 0;

const cursorsToRender = new Map<string, number>();

for (let i = 0; i < cursors.length(); ++i) {
if (selfCursorPos - 1 === i) {
continue;
}
// TODO: someone else

const v = cursors.at(i);
if (!(v instanceof InoPosRef)) {
continue;
}

const pos = floor.resolveRef(v);
cursorsToRender.set(String(i), pos);
}

e.cursor = cursorsToRender;
}

e.addListener(
'modify',
Expand All @@ -22,6 +51,7 @@ e.addListener(
});

e.text = model.asString();
reconcileOtherCursors();

// readjust cursor and collapse selection
e.pos = m.lo + m.insert.length;
Expand All @@ -39,6 +69,27 @@ e.addListener(
c.signal,
);

e.addListener(
'cursor',
() => {
floor.perform(function* (byIno, resolveRef) {
const c = byIno(FIXED_CURSORS);
const text = byIno(FIXED_STRING);

let pos = cursorAt ? resolveRef(cursorAt) : 0;
if (pos === 0) {
// insert self
c.splice(0, 0, null);
cursorAt = c.refAt(1); // TODO: this is pos, not index
pos = 1;
}

c.set(pos - 1, text.refAt(e.pos));
});
},
c.signal,
);

e.addListener(
'submit',
() => {
Expand Down Expand Up @@ -120,6 +171,8 @@ for (;;) {

// TODO: we assume FIXED_STRING changes (not always true)
e.text = model.asString();
reconcileOtherCursors();

e.pos = floor.resolveRef(cursor);
e.anchor = floor.resolveRef(anchor);

Expand Down
48 changes: 37 additions & 11 deletions client/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface TermEditorEvents {
submit: void;
escape: void;
modify: { lo: number; hi: number; insert: string };
cursor: number; // TODO: just cursor now, not anchor
}

export class TermEditor {
Expand Down Expand Up @@ -51,25 +52,39 @@ export class TermEditor {

private moveCursor(newPos: number, extending: boolean) {
this._stickyCol = -1;
this._pos = Math.max(0, Math.min(newPos, this._text.length));
if (!extending) {
this._anchor = this._pos;

const updatePos = Math.max(0, Math.min(newPos, this._text.length));
const updateAnchor = extending ? this._anchor : updatePos;

if (updatePos !== this._pos || updateAnchor !== this._anchor) {
this._pos = updatePos;
this._anchor = updateAnchor;

this.dispatchCursor();
this.triggerRender();
}
this.triggerRender();
}

private moveCursorVertical(direction: -1 | 1, extending: boolean) {
const r = TermEditor.moveVertical(this._text, this._pos, direction, this._stickyCol);
this._stickyCol = r.stickyCol;
this._pos = r.pos;
if (!extending) {
this._anchor = this._pos;

const updatePos = r.pos;
const updateAnchor = extending ? this._anchor : updatePos;

if (updatePos !== this._pos || updateAnchor !== this._anchor) {
this._pos = updatePos;
this._anchor = updateAnchor;

this.dispatchCursor();
this.triggerRender();
}
this.triggerRender();
}

private copySelection(): void {
if (!this.hasSelection) return;
if (!this.hasSelection) {
return;
}
this._clipboardBuffer = this._text.slice(this.selectionStart, this.selectionEnd);
const encoded = Buffer.from(this._clipboardBuffer).toString('base64');
stdout.write(`\x1b]52;c;${encoded}\x07`);
Expand Down Expand Up @@ -216,6 +231,7 @@ export class TermEditor {
case '\x01':
this._anchor = 0;
this._pos = this._text.length;
this.dispatchCursor();
this.triggerRender();
return;

Expand Down Expand Up @@ -291,6 +307,10 @@ export class TermEditor {
});
}

private dispatchCursor() {
this.events.dispatch('cursor', this._pos);
}

public detach() {
const rowsDown = this.lastTotalRows - this.lastRenderRows;
if (rowsDown > 0) {
Expand Down Expand Up @@ -518,8 +538,13 @@ export class TermEditor {

set text(v: string) {
this._text = v;
this._pos = Math.min(this._pos, v.length);
this._anchor = Math.min(this._anchor, v.length);

if (this._pos > v.length || this._anchor > v.length) {
this._pos = Math.min(this._pos, v.length);
this._anchor = Math.min(this._anchor, v.length);
this.dispatchCursor();
}

this.triggerRender();
}

Expand All @@ -531,6 +556,7 @@ export class TermEditor {
const update = Math.max(0, Math.min(p, this._text.length));
if (this._pos !== update) {
this._pos = update;
this.dispatchCursor();
this.triggerRender();
}
}
Expand Down
12 changes: 12 additions & 0 deletions server/pkg/ylayer/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ func (d *docImpl) Apply(work *DocWork) (err error) {
out[ino] = res
}

// --- at this point we are going to apply or panic ---

// record last active version for this handle
if work.Handle > 0 {
handleVersion := work.TargetVersion
Expand Down Expand Up @@ -104,6 +106,16 @@ func (d *docImpl) Apply(work *DocWork) (err error) {
work.Update[ino] = res.entry.p
}

// xform sets we did here
// TODO: leaky abstraction, we poke in/out of node
for ino := range out {
nh := d.byIno[ino]
last := &nh.history[len(nh.history)-1]
last.p.Set.IterAll(func(x *GumnutData) {
d.moveForward(x, arg.targetVersion, arg.handle)
})
}

return nil
}

Expand Down
16 changes: 7 additions & 9 deletions server/pkg/ylayer/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,11 @@ func (nh *nodeHolder) read() (gd GumnutDataArray, err error) {
}

clone := entry.p.Set
for i := range clone {
for y := range clone[i].Body {
// we NEVER transformed this set against its own run, so we have to do -1 here
// TODO: to do this in prepare()/enact() is hard - we might target another inode!
// TODO: but we haven't checked if their -ve was invalid - we zero it inside moveForward
nh.doc.moveForward(&clone[i].Body[y], entry.version-1, entry.handle)
}
}

// xform sets from where they were valid to now
clone.IterAll(func(x *GumnutData) {
nh.doc.moveForward(x, entry.version, entry.handle)
})
ok := clone.ApplySlice(nh.data)
if !ok {
return nil, ErrSetOp
Expand Down Expand Up @@ -175,7 +172,8 @@ func (nh *nodeHolder) prepare(arg prepareArg, p *Patch) (res prepareResult, err
err = ErrSetOp
return
}
// nb. entry.Set might still contain -ve refs to own work

// nb. entry.Set might still contain -ve refs to own work, we fix "externally" (leaky abstraction)

res = prepareResult{
length: length,
Expand Down
10 changes: 10 additions & 0 deletions server/pkg/ymodel/sets.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ type Set[X any] struct {
// ManySet is a slice of Set.
type ManySet[X any] []Set[X]

// IterAll calls the provided function with a reference to every X here.
func (s ManySet[X]) IterAll(fn func(x *X)) {
for i := range s {
set := &s[i]
for i := range set.Body {
fn(&set.Body[i])
}
}
}

// ApplySlice applies an already-transformed ManySet to the target.
func (s ManySet[X]) ApplySlice(target []X) (ok bool) {
// check all parts first
Expand Down
Loading