Path: blob/master/src/packages/frontend/editors/slate/cursors/other-users.ts
1697 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// Support display of other user's cursors67import { useMemo } from "react";8import { Map } from "immutable";9import { Editor, Node, Point, Text } from "slate";10import { getProfile } from "@cocalc/frontend/jupyter/cursors";11import { redux } from "@cocalc/frontend/app-framework";12import { markdownPositionToSlatePoint } from "../sync";13import { SearchHook } from "../search";14import { SlateEditor } from "../editable-markdown";1516interface OtherCursor {17offset: number;18name: string;19color: string;20}2122export const useCursorDecorate = ({23editor,24value,25cursors,26search, // passed in since can only have one decorate function.27}: {28editor: SlateEditor;29value: string;30cursors?: Map<string, any>;31search: SearchHook;32// NOTE: Passing search in is really ugly but we are doing it because slate33// can only have one decorate function at once and both search and cursors34// use decorate at once. **NOTE/TODO:** if a text node has a search result in35// it **and** a cursor, then only the search is shown.36}) => {37// NOTE: It is VERY important to only update this decorate function38// when things really change. Otherwise every Text node in the slate editor39// will get re-rendereded (forced by the decorate function changing identity).40return useMemo(() => {41const nodeToCursors: WeakMap<Node, OtherCursor[]> = new WeakMap();4243const cursors0 = cursors?.toJS();44if (cursors0 != null) {45const user_map = redux.getStore("users").get("user_map");46for (const account_id in cursors0) {47for (const cursor of cursors0[account_id] ?? []) {48// TODO -- insanely inefficient!49const loc = markdownPositionToSlatePoint({50markdown: value,51pos: { line: cursor.y, ch: cursor.x },52editor,53});54if (loc == null) continue;55const { path, offset } = loc;56// TODO: for now we're ONLY implementing cursors for leafs,57// and ignoring everything else.58let leaf;59try {60leaf = Editor.leaf(editor, { path, offset })[0];61} catch (_err) {62// failing is expected since the document can change from63// when the cursor was reported.64// TODO: find nearest valid leaf?65continue;66}67const { name, color } = getProfile(account_id, user_map);68nodeToCursors.set(69leaf,70(nodeToCursors.get(leaf) ?? []).concat([{ offset, name, color }])71);72}73}74}7576return ([node, path]) => {77// We do the search decorate and if there is no search,78// then we do the cursor. TODO: maybe combine, though if79// you are searching, seeing cursors blocking search results80// could be annoying.81const s = search.decorate([node, path]);82if (s.length > 0) return s;83const ranges: {84anchor: Point;85focus: Point;86cursor: { name: string; color: string; paddingText?: string };87}[] = [];88if (!Text.isText(node)) return ranges;89const c = nodeToCursors.get(node);90if (c == null) return ranges;91for (const cur of c) {92const { offset, name, color } = cur;93if (offset < node.text.length - 1) {94ranges.push({95anchor: { path, offset },96focus: { path, offset: offset + 1 },97cursor: { name, color },98});99} else {100// You can't make an *empty* decorated block, since101// it just gets discarded.... or does it?102ranges.push({103anchor: { path, offset: offset - 1 },104focus: { path, offset: offset },105cursor: {106name,107color,108paddingText: node.text[node.text.length - 1],109},110});111}112// TODO: We are just showing the first user cursor for now, even if113// they have multiple cursors (only happens if they are using source view)114// or there are multiple users editing that text node.115break;116}117118return ranges;119};120}, [cursors, value, search.search]);121};122123124