Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
Pangoraw committed Oct 1, 2023
1 parent 1cfe2f9 commit c0f069a
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 114 deletions.
4 changes: 4 additions & 0 deletions frontend/components/Cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export const Cell = ({
cell_dependencies,
cell_input_local,
notebook_id,
client_id,
users,
selected,
force_hide_input,
focus_after_creation,
Expand Down Expand Up @@ -331,6 +333,8 @@ export const Cell = ({
nbpkg=${nbpkg}
cell_id=${cell_id}
notebook_id=${notebook_id}
client_id=${client_id}
users=${users}
metadata=${metadata}
any_logs=${any_logs}
show_logs=${show_logs}
Expand Down
14 changes: 12 additions & 2 deletions frontend/components/CellInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
import { markdown, html as htmlLang, javascript, sqlLang, python, julia_mixed } from "./CellInput/mixedParsers.js"
import { julia_andrey } from "../imports/CodemirrorPlutoSetup.js"
import { pluto_autocomplete } from "./CellInput/pluto_autocomplete.js"
import { pluto_collab } from "./CellInput/pluto_collab.js"
import { UsersFacet, pluto_collab } from "./CellInput/pluto_collab.js"
import { NotebookpackagesFacet, pkgBubblePlugin } from "./CellInput/pkg_bubble_plugin.js"
import { awesome_line_wrapping } from "./CellInput/awesome_line_wrapping.js"
import { cell_movement_plugin, prevent_holding_a_key_from_doing_things_across_cells } from "./CellInput/cell_movement_plugin.js"
Expand Down Expand Up @@ -342,7 +342,7 @@ let line_and_ch_to_cm6_position = (/** @type {import("../imports/CodemirrorPluto
}

function eventEmitter() {
let events = {}
const events = {}
return {
subscribe: (/** @type {string} */ name, cb) => {
;(events[name] || (events[name] = [])).push(cb)
Expand Down Expand Up @@ -408,6 +408,8 @@ export const CellInput = ({
nbpkg,
cell_id,
notebook_id,
client_id,
users,
any_logs,
show_logs,
set_show_logs,
Expand All @@ -427,6 +429,8 @@ export const CellInput = ({
throw to_throw
}

console.log(users)

const notebook_id_ref = useRef(notebook_id)
notebook_id_ref.current = notebook_id

Expand All @@ -439,6 +443,7 @@ export const CellInput = ({
let highlighted_line_compartment = useCompartment(newcm_ref, HighlightLineFacet.of(cm_highlighted_line))
let highlighted_range_compartment = useCompartment(newcm_ref, HighlightRangeFacet.of(cm_highlighted_range))
let editable_compartment = useCompartment(newcm_ref, EditorState.readOnly.of(disable_input))
let users_compartment = useCompartment(newcm_ref, UsersFacet.of(users))

let on_change_compartment = useCompartment(
newcm_ref,
Expand Down Expand Up @@ -616,6 +621,7 @@ export const CellInput = ({
if (!update.view.hasFocus) {
return
}
pluto_actions.update_notebook((nb) => nb.users[client_id] && (nb.users[client_id].focused_cell = cell_id))

if (update.docChanged || update.selectionSet) {
let state = update.state
Expand Down Expand Up @@ -643,6 +649,7 @@ export const CellInput = ({
highlighted_range_compartment,
global_definitions_compartment,
editable_compartment,
users_compartment,
highlightLinePlugin(),
highlightRangePlugin(),

Expand Down Expand Up @@ -690,6 +697,7 @@ export const CellInput = ({
if (!caused_by_window_blur) {
// then it's caused by focusing something other than this cell in the editor.
// in this case, we want to collapse the selection into a single point, for aesthetic reasons.
pluto_actions.update_notebook((nb) => nb.users[client_id] && (nb.users[client_id].focused_cell = null))
setTimeout(() => {
view.dispatch({
selection: {
Expand Down Expand Up @@ -779,6 +787,8 @@ export const CellInput = ({
pluto_collab(start_version, {

Check failure on line 787 in frontend/components/CellInput.js

View workflow job for this annotation

GitHub Actions / test

Argument of type '{ push_updates: (data: any[]) => any; subscribe_to_updates: (cb: Function) => { unsubscribe: () => void; }; client_id: any; cell_id: any; }' is not assignable to parameter of type '{ get_notebook: () => Notebook; subscribe_to_updates: (cb: Function) => EventHandler; push_updates: (updates: any[]) => Promise<any>; client_id: string; cell_id: string; }'.
push_updates: (data) => pluto_actions.send("push_updates", { ...data, cell_id: cell_id }, { notebook_id }, false),
subscribe_to_updates: (cb) => updater.subscribe("updates", cb),
client_id,
cell_id,
}),

// This is my weird-ass extension that checks the AST and shows you where
Expand Down
120 changes: 77 additions & 43 deletions frontend/components/CellInput/pluto_collab.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import {
showTooltip,

Check failure on line 2 in frontend/components/CellInput/pluto_collab.js

View workflow job for this annotation

GitHub Actions / test

Module '"../../imports/CodemirrorPlutoSetup.js"' has no exported member 'showTooltip'.
tooltips,

Check failure on line 3 in frontend/components/CellInput/pluto_collab.js

View workflow job for this annotation

GitHub Actions / test

Module '"../../imports/CodemirrorPlutoSetup.js"' has no exported member 'tooltips'.
Facet,
ChangeSet,

Check failure on line 5 in frontend/components/CellInput/pluto_collab.js

View workflow job for this annotation

GitHub Actions / test

Module '"../../imports/CodemirrorPlutoSetup.js"' declares 'ChangeSet' locally, but it is not exported.
collab,
Decoration,
EditorSelection,
EditorView,
Facet,
getSyncedVersion,

Check failure on line 10 in frontend/components/CellInput/pluto_collab.js

View workflow job for this annotation

GitHub Actions / test

Module '"../../imports/CodemirrorPlutoSetup.js"' has no exported member 'getSyncedVersion'.
receiveUpdates,

Check failure on line 11 in frontend/components/CellInput/pluto_collab.js

View workflow job for this annotation

GitHub Actions / test

Module '"../../imports/CodemirrorPlutoSetup.js"' has no exported member 'receiveUpdates'.
SelectionRange,
sendableUpdates,

Check failure on line 12 in frontend/components/CellInput/pluto_collab.js

View workflow job for this annotation

GitHub Actions / test

Module '"../../imports/CodemirrorPlutoSetup.js"' has no exported member 'sendableUpdates'.
StateEffect,
StateField,
Expand Down Expand Up @@ -43,7 +42,7 @@ function pushUpdates(push_updates, version, fullUpdates) {
}

// Strip off transaction data
let updates = fullUpdates.map((u) => ({
const updates = fullUpdates.map((u) => ({
client_id: u.clientID,
document_length: u.changes.desc.length,
effects: u.effects.map((effect) => effect.value.selection.toJSON()),
Expand Down Expand Up @@ -77,72 +76,99 @@ const CaretEffect = StateEffect.define({
},
})

const CursorField = (client_id) =>
export const UsersFacet = Facet.define({
combine: (values) => values[0],
compare: (a, b) => a === b, // <-- TODO: not very performant
})

/** Shows the name of client on top of its cursor */
const CursorField = (client_id, cell_id) =>
StateField.define({
create: () => [],
create: () => [],
update(tooltips, tr) {
const newTooltips =
tr.effects
.filter((effect) => effect.is(CaretEffect) && effect.value.clientID != client_id)
.map(effect => ({
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) && clientID != client_id && !seen.has(clientID)) {
// TODO: still not in sync with caret
seen.add(clientID)
return true
}
return false
})
.map((effect) => ({
pos: effect.value.selection.main.head,
hover: true,
above: true,
strictSide: true,
arrow: false,
create: () => {
let dom = document.createElement("div")
const dom = document.createElement("div")
dom.className = "cm-tooltip-remoteClientID"
dom.textContent = effect.value.clientID
return {dom}
}
dom.textContent = users[effect.value.clientID]?.name || effect.value.clientID
return { dom }
},
}))
return newTooltips
},
provide: (f) => showTooltip.computeN([f], state => state.field(f))
provide: (f) => showTooltip.computeN([f, UsersFacet], (state) => state.field(f)),
})
const CaretField = (client_id) =>

/** Shows cursor and selection of user */
const CaretField = (client_id, cell_id) =>
StateField.define({
create() {
return {}
},
create: () => ({}),
update(value, tr) {
const new_value = { ...value }
const users = tr.state.facet(UsersFacet)
const new_value = {}

for (const clientID of Object.keys(value)) {
const client_cell = users[clientID]?.focused_cell
if (client_cell && client_cell == cell_id) {
new_value[clientID] = value[clientID]
}
}

/** @type {StateEffect<CarretEffectValue>[]} */
const caretEffects = tr.effects.filter((effect) => effect.is(CaretEffect))
for (const effect of caretEffects) {
if (effect.value.clientID == client_id) continue // don't show our own cursor
if (effect.value.clientID) new_value[effect.value.clientID] = effect.value.selection
const clientID = effect.value.clientID
if (clientID == client_id) continue // don't show our own cursor
const client_cell = users[clientID]?.focused_cell
if (!client_cell || client_cell != cell_id) continue // only show when focusing this cell
if (clientID) new_value[clientID] = { selection: effect.value.selection, color: users[clientID]?.color ?? "#ff00aa" }
}

return new_value
},
provide: (f) =>
EditorView.decorations.from(f, (/** @type {{[key: string]: EditorSelection}} */ value) => {
EditorView.decorations.compute([f, UsersFacet], (/** @type EditorState */ state) => {

Check failure on line 148 in frontend/components/CellInput/pluto_collab.js

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'EditorState'.
const value = state.field(f)
const decorations = []

for (const selection of Object.values(value)) {
for (const { selection, color } of Object.values(value)) {
decorations.push(
Decoration.widget({
widget: new ReactWidget(
html`<span class="cm-remoteCaret"></span>`
),
widget: new ReactWidget(html`<span style=${`border-color: ${color};`} class="cm-remoteCaret"></span>`),
}).range(selection.main.head) // Let's assume the remote cursor is here
)

for (const range of selection.ranges) {
if (range.from != range.to) {
decorations.push(Decoration.mark({ class: "cm-remoteSelection" }).range(range.from, range.to))
decorations.push(
Decoration.mark({ class: "cm-remoteSelection", attributes: { style: `background-color: ${color};` } }).range(
range.from,
range.to
)
)
}
}
}


let decs = Decoration.set(decorations, true)
console.log({decs})
return decs
// return showTooltip.computeN([f], state => state.field(f))
return Decoration.set(decorations, true)
}),
})

Expand All @@ -156,12 +182,15 @@ const CaretField = (client_id) =>
/**
* @param {number} startVersion
* @param {{
* get_notebook: () => Notebook,

Check failure on line 185 in frontend/components/CellInput/pluto_collab.js

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'Notebook'.
* subscribe_to_updates: (cb: Function) => EventHandler,
* push_updates: (updates: Array<any>) => Promise<any>
* client_id: string,
* cell_id: string,
* }} param1
* @returns
*/
export const pluto_collab = (startVersion, { subscribe_to_updates, push_updates }) => {
export const pluto_collab = (startVersion, { subscribe_to_updates, push_updates, client_id, cell_id }) => {
const plugin = ViewPlugin.fromClass(
class {
pushing = false
Expand All @@ -170,7 +199,6 @@ export const pluto_collab = (startVersion, { subscribe_to_updates, push_updates
* @param {EditorView} view
*/
constructor(view) {
console.log("BUILD", view)
this.view = view
this.handler = subscribe_to_updates((updates) => this.sync(updates))
}
Expand All @@ -180,13 +208,13 @@ export const pluto_collab = (startVersion, { subscribe_to_updates, push_updates
}

async push() {
let updates = sendableUpdates(this.view.state)
const updates = sendableUpdates(this.view.state)
if (this.pushing || !updates.length) {
return
}

this.pushing = true
let version = getSyncedVersion(this.view.state)
const version = getSyncedVersion(this.view.state)
await pushUpdates(push_updates, version, updates)
this.pushing = false

Expand All @@ -201,8 +229,15 @@ export const pluto_collab = (startVersion, { subscribe_to_updates, push_updates
* @param {Array<any>} updates
*/
sync(updates) {
let version = getSyncedVersion(this.view.state)
updates = updates.slice(version).map((u) => ({
const version = getSyncedVersion(this.view.state)
this.syncNewUpdates(updates.slice(version))
}

/**
* @param {Array<any>} updates

Check failure on line 237 in frontend/components/CellInput/pluto_collab.js

View workflow job for this annotation

GitHub Actions / test

JSDoc '@param' tag has name 'updates', but there is no parameter with that name.
*/
syncNewUpdates(newUpdates) {
const updates = newUpdates.map((u) => ({
changes: ChangeSet.of(u.specs, u.document_length, "\n"),
effects: u.effects.map((selection) => CaretEffect.of({ selection: EditorSelection.fromJSON(selection), clientID: u.client_id })),
clientID: u.client_id,
Expand All @@ -227,24 +262,23 @@ export const pluto_collab = (startVersion, { subscribe_to_updates, push_updates
}
)

const clientID = Math.random() + "_ok"
const cursorPlugin = EditorView.updateListener.of((update) => {
if (!update.selectionSet) {
return
}

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

return [
collab({ clientID, startVersion, sharedEffects: (tr) => tr.effects.filter((effect) => effect.is(CaretEffect) || effect.is(RunEffect)) }),
collab({ clientID: client_id, startVersion, sharedEffects: (tr) => tr.effects.filter((effect) => effect.is(CaretEffect) || effect.is(RunEffect)) }),
plugin,
cursorPlugin,
// tooltips(),
// CaretField(clientID),
// CursorField(clientID),
CaretField(client_id, cell_id),
CursorField(client_id, cell_id),
]
}
12 changes: 12 additions & 0 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { serialize_cells, deserialize_cells, detect_deserializer } from "../comm
import { FilePicker } from "./FilePicker.js"
import { Preamble } from "./Preamble.js"
import { NotebookMemo as Notebook } from "./Notebook.js"
import { MultiplayerPanel } from "./MultiplayerPanel.js"
import { BottomRightPanel } from "./BottomRightPanel.js"
import { DropRuler } from "./DropRuler.js"
import { SelectionArea } from "./SelectionArea.js"
Expand Down Expand Up @@ -682,6 +683,7 @@ export class Editor extends Component {
},
}
this.actions = { ...this.real_actions }
window.pluto_actions = this.actions

const apply_notebook_patches = (patches, /** @type {NotebookData?} */ old_state = null, get_reverse_patches = false) =>
new Promise((resolve) => {
Expand Down Expand Up @@ -866,6 +868,13 @@ patch: ${JSON.stringify(
this.client.send("complete", { query: "sq" }, { notebook_id: this.state.notebook.notebook_id })
this.client.send("complete", { query: "\\sq" }, { notebook_id: this.state.notebook.notebook_id })

const users = this.actions.get_notebook().users
const names = ["Mars", "Earth", "Moon", "Sun"]
const colors = ["#ffc09f", "#a0ced9", "#adf7b6", "#fcf5c7"]
this.actions.update_notebook((nb) => {
nb.users[this.client_id] = { name: names[Object.keys(users).length], color: colors[Object.keys(users).length] }
})

setTimeout(init_feedback, 2 * 1000) // 2 seconds - load feedback a little later for snappier UI
}

Expand Down Expand Up @@ -904,6 +913,7 @@ patch: ${JSON.stringify(
? `./${u}?id=${this.state.notebook.notebook_id}`
: `${this.state.binder_session_url}${u}?id=${this.state.notebook.notebook_id}&token=${this.state.binder_session_token}`

this.client_id = Math.random().toString(36).slice(2)
/** @type {import('../common/PlutoConnection').PlutoConnection} */
this.client = /** @type {import('../common/PlutoConnection').PlutoConnection} */ ({})

Expand Down Expand Up @@ -1572,8 +1582,10 @@ patch: ${JSON.stringify(
last_hot_reload_time=${notebook.last_hot_reload_time}
connected=${this.state.connected}
/>
<${MultiplayerPanel} users=${notebook.users}/>
<${Notebook}
notebook=${notebook}
client_id=${this.client_id}
cell_inputs_local=${this.state.cell_inputs_local}
disable_input=${this.state.disable_ui || !this.state.connected /* && this.state.backend_launch_phase == null*/}
last_created_cell=${this.state.last_created_cell}
Expand Down
Loading

0 comments on commit c0f069a

Please sign in to comment.