Skip to content

Commit

Permalink
share cursor stuff again and pass through transformations
Browse files Browse the repository at this point in the history
  • Loading branch information
Pangoraw committed Jul 11, 2024
1 parent e1f5eab commit a094d8e
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 52 deletions.
76 changes: 48 additions & 28 deletions frontend/components/CellInput/pluto_collab.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
StateEffect,
StateField,
ViewPlugin,
Transaction,
} from "../../imports/CodemirrorPlutoSetup.js"
import { html } from "../../imports/Preact.js"
import { ReactWidget } from "./ReactWidget.js"
Expand Down Expand Up @@ -87,7 +88,7 @@ function makeUpdates(cell_id, version, fullUpdates) {

const DEBUG_COLLAB = false
// set to true for cursor sharing (TODO: on the backend)
const ENABLE_EFFECTS = false
const ENABLE_EFFECTS = true

/**
* @typedef CarretEffectValue
Expand All @@ -97,8 +98,6 @@ const ENABLE_EFFECTS = false
* }}
*/

export const RunEffect = StateEffect.define()

/** @type {any} */
const CaretEffect = StateEffect.define({
/**
Expand All @@ -117,17 +116,27 @@ export const UsersFacet = Facet.define({
compare: (a, b) => a === b, // <-- TODO: not very performant
})

/** Shows the name of client on top of its cursor */
/** Shows the name of client on top of its cursor
* @param {string} client_id
* @param {string} cell_id
* @returns {StateField<import("../../imports/CodemirrorPlutoSetup.js").Tooltip[]>}
**/
const CursorField = (client_id, cell_id) =>
StateField.define({
StateField.define({
create: () => [],
/**
* @param {import("../../imports/CodemirrorPlutoSetup.js").Tooltip[]} tooltips
* @param {Transaction} tr
**/
update(tooltips, tr) {
const users = tr.state.facet(UsersFacet)
const seen = new Set()
const newTooltips = tr.effects
.filter((effect) => {
const clientID = effect.value.clientID
if (!users[clientID]?.focused_cell || users[clientID]?.focused_cell != cell_id) return false
if (effect.is(CaretEffect))
console.log("here", { effect })
// if (!users[clientID]?.focused_cell || users[clientID]?.focused_cell != cell_id) return false
if (effect.is(CaretEffect) && clientID != client_id && !seen.has(clientID)) {
// TODO: still not in sync with caret
seen.add(clientID)
Expand All @@ -153,10 +162,18 @@ const CursorField = (client_id, cell_id) =>
provide: (f) => showTooltip.computeN([f, UsersFacet], (state) => state.field(f)),
})

/** Shows cursor and selection of user */
/** Shows cursor and selection of user
* @param {string} client_id
* @param {string} cell_id
* @returns {StateField<{ [user: string] : Selection }>}
*/
const CaretField = (client_id, cell_id) =>
StateField.define({
create: () => ({}),
/**
* @param {{ [user: string] : Selection }} value
* @param {Transaction} tr
**/
update(value, tr) {
const users = tr.state.facet(UsersFacet)
const new_value = {}
Expand Down Expand Up @@ -239,7 +256,7 @@ export const pluto_collab = (startVersion, { pluto_actions, cell_id, client_id }
}

update(/** @type import("../../imports/CodemirrorPlutoSetup.d.ts").ViewUpdate */ update) {
if (update.docChanged) // NOTE: remove this to have cursor sync
if (ENABLE_EFFECTS || update.docChanged) // NOTE: remove this to have cursor sync
this.push()
}

Expand Down Expand Up @@ -281,11 +298,15 @@ export const pluto_collab = (startVersion, { pluto_actions, cell_id, client_id }
* @param {Array<any>} newUpdates
*/
syncNewUpdates(newUpdates) {
const updates = newUpdates.map((u) => ({
changes: ChangeSet.of(delta_to_specs(u.ops), u.document_length, "\n"),
effects:
ENABLE_EFFECTS ?
u.effects.map((selection) => CaretEffect.of({ selection: EditorSelection.fromJSON(selection), clientID: u.client_id })) : undefined,
console.log({newUpdates})
const updates = newUpdates.map((u) => ({
changes: ChangeSet.of(delta_to_specs(u.ops), u.document_length, "\n"),
effects:
ENABLE_EFFECTS ?
u.effects.map((selection) => {
console.log("creating caret effect", { selection })
return CaretEffect.of({ selection: EditorSelection.fromJSON(selection), clientID: u.client_id })
}) : undefined,
clientID: u.client_id,
}))

Expand All @@ -307,27 +328,26 @@ export const pluto_collab = (startVersion, { pluto_actions, cell_id, client_id }
}
)

// const cursorPlugin = EditorView.updateListener.of((update) => {
// if (!update.selectionSet) {
// return
// }
const cursorPlugin = EditorView.updateListener.of((update) => {
if (!update.selectionSet) {
return
}

// const effect = CaretEffect.of({ selection: update.view.state.selection, clientID: client_id })
// console.log({selection: effect.value.selection.toJSON()})
// update.view.dispatch({
// effects: [effect],
// })
// })
const effect = CaretEffect.of({ selection: update.view.state.selection, clientID: client_id })
console.log({selection: effect.value.selection.toJSON()})
update.view.dispatch({
effects: [effect],
})
})

return [
collab({
clientID: client_id, startVersion,
// sharedEffects: (tr) => tr.effects.filter((effect) => effect.is(CaretEffect) || effect.is(RunEffect)),
sharedEffects: (tr) => tr.effects.filter((effect) => effect.is(CaretEffect)),
}),
plugin,
// cursorPlugin,
// tooltips(),
// CaretField(client_id, cell_id),
// CursorField(client_id, cell_id),
cursorPlugin,
CaretField(client_id, cell_id),
CursorField(client_id, cell_id),
]
}
1 change: 1 addition & 0 deletions frontend/components/Notebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export const Notebook = ({
.map(
(cell_id, i) => html`<${CellMemo}
key=${cell_id}
users=${notebook.users}
cell_result=${notebook.cell_results[cell_id] ?? {
cell_id: cell_id,
queued: true,
Expand Down
47 changes: 23 additions & 24 deletions src/notebook/OperationalTransform.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,35 @@ module OperationalTransform
using Pinot: Pinot, Unicode
using Pinot: Range, retain, delete, insert

# struct SelectionRange
# anchor::UInt32
# head::UInt32
# end
# struct Selection
# ranges::Vector{SelectionRange}
# main::UInt32
# end
struct SelectionRange
head::UInt32
anchor::UInt32
end

struct Selection
main::UInt32 # zero-based index in `ranges`
ranges::Vector{SelectionRange}
end

const Effect = Selection

struct Update
client_id::Symbol
document_length::Int
ops::Vector{Pinot.Range}
# effects::Vector{Selection}
effects::Vector{Effect}
end

to_dict(u) = Dict{Symbol,Any}(:client_id => u.client_id,
:document_length => u.document_length,
:ops => Pinot.to_obj(u.ops)[:ops],
# :effects => map(e ->
# Dict{Symbol,Any}(:main => e.main, :ranges => map(r -> (; anchor=r.anchor, head=r.head), e.ranges)),
# u.effects)
# )
)
:effects => map(e -> Dict(:main => e.main, :ranges => map(r -> (; head=r.head, anchor=r.anchor), e.ranges)), u.effects))

from_dict(u) = Update(Symbol(u["client_id"]),
u["document_length"],
Pinot.from_obj(u),
# map(e -> Selection(e["main"],
# map(g -> SelectionRange(g["anchor"], g["head"]), e["ranges"])
# )), u["effects"]
)
haskey(u, "effects") ? map(e -> Selection(e["main"], map(r -> SelectionRange(r["head"], r["anchor"]), e["ranges"])), u["effects"]) :
Effect[])

function rebase(over, updates)
isempty(over) && return updates
Expand Down Expand Up @@ -68,14 +65,16 @@ function rebase(over, updates)
client_updates = Update[]
for u in old_client_updates
u_changes = Pinot.transform(changes, u.ops, Pinot.Left)

new_length = Pinot.transform_position(changes, u.document_length)
# if new_length != u.document_length
# @warn "ok" new_length u.document_length changes
# else
# @info "ok" changes u_changes
# end
changes = Pinot.transform(u.ops, changes, Pinot.Right)
push!(client_updates, Update(u.client_id, new_length, u_changes))

new_effects = map(u.effects) do effect
ranges = map(r -> SelectionRange(Pinot.transform_position(changes, r.head), Pinot.transform_position(changes, r.anchor)),
effect.ranges)
Selection(effect.main, ranges)
end
push!(client_updates, Update(u.client_id, new_length, u_changes, new_effects))
end

return client_updates
Expand Down

0 comments on commit a094d8e

Please sign in to comment.