Add API for propagating cursor locations #61
4 changed files with 234 additions and 2 deletions
Add dummy cursor
commit
5ce6143838
|
|
@ -16,7 +16,10 @@ import { ObsidianFileSystemOperations } from "./obsidian-file-system";
|
||||||
import { SyncSettingsTab } from "./views/settings/settings-tab";
|
import { SyncSettingsTab } from "./views/settings/settings-tab";
|
||||||
import { registerConsoleForLogging } from "./utils/register-console-for-logging";
|
import { registerConsoleForLogging } from "./utils/register-console-for-logging";
|
||||||
import { updateEditorStatusDisplay } from "./views/editor-sync-line/editor-sync-line";
|
import { updateEditorStatusDisplay } from "./views/editor-sync-line/editor-sync-line";
|
||||||
|
import { remoteCursorsTheme } from "./views/remote-cursors/remote-cursor-theme";
|
||||||
|
import { remoteCursorsPlugin } from "./views/remote-cursors/remote-cursors-plugin";
|
||||||
|
|
||||||
|
const MIN_WAIT_BETWEEN_UPDATES_IN_MS = 250;
|
||||||
export default class VaultLinkPlugin extends Plugin {
|
export default class VaultLinkPlugin extends Plugin {
|
||||||
private readonly disposables: (() => unknown)[] = [];
|
private readonly disposables: (() => unknown)[] = [];
|
||||||
private settingsTab: SyncSettingsTab | undefined;
|
private settingsTab: SyncSettingsTab | undefined;
|
||||||
|
|
@ -61,18 +64,23 @@ export default class VaultLinkPlugin extends Plugin {
|
||||||
|
|
||||||
this.registerView(
|
this.registerView(
|
||||||
HistoryView.TYPE,
|
HistoryView.TYPE,
|
||||||
(leaf) => new HistoryView(leaf, this.client)
|
(leaf) => new HistoryView(this.client, leaf)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.registerView(
|
this.registerView(
|
||||||
LogsView.TYPE,
|
LogsView.TYPE,
|
||||||
(leaf) => new LogsView(this.client, leaf)
|
(leaf) => new LogsView(this.client, leaf)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.registerEditorExtension([remoteCursorsTheme, remoteCursorsPlugin]);
|
||||||
|
this.app.workspace.updateOptions();
|
||||||
|
|
||||||
this.addRibbonIcon(
|
this.addRibbonIcon(
|
||||||
HistoryView.ICON,
|
HistoryView.ICON,
|
||||||
"Open VaultLink events",
|
"Open VaultLink events",
|
||||||
async (_: MouseEvent) => this.activateView(HistoryView.TYPE)
|
async (_: MouseEvent) => this.activateView(HistoryView.TYPE)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addRibbonIcon(
|
this.addRibbonIcon(
|
||||||
LogsView.ICON,
|
LogsView.ICON,
|
||||||
"Open VaultLink logs",
|
"Open VaultLink logs",
|
||||||
|
|
@ -181,7 +189,7 @@ export default class VaultLinkPlugin extends Plugin {
|
||||||
this.client.syncLocallyUpdatedFile({
|
this.client.syncLocallyUpdatedFile({
|
||||||
relativePath: path
|
relativePath: path
|
||||||
}),
|
}),
|
||||||
250
|
MIN_WAIT_BETWEEN_UPDATES_IN_MS
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { AnnotationType, Annotation, RangeSet, Range } from "@codemirror/state";
|
||||||
|
import {
|
||||||
|
EditorView,
|
||||||
|
ViewUpdate,
|
||||||
|
ViewPlugin,
|
||||||
|
Decoration,
|
||||||
|
WidgetType
|
||||||
|
} from "@codemirror/view";
|
||||||
|
|
||||||
|
import type { PluginValue, DecorationSet } from "@codemirror/view";
|
||||||
|
|
||||||
|
export const remoteCursorsTheme = EditorView.baseTheme({
|
||||||
|
".Selection": {},
|
||||||
|
".LineSelection": {
|
||||||
|
padding: 0,
|
||||||
|
margin: "0px 2px 0px 4px"
|
||||||
|
},
|
||||||
|
".SelectionCaret": {
|
||||||
|
position: "relative",
|
||||||
|
borderLeft: "1px solid black",
|
||||||
|
borderRight: "1px solid black",
|
||||||
|
marginLeft: "-1px",
|
||||||
|
marginRight: "-1px",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
display: "inline"
|
||||||
|
},
|
||||||
|
".SelectionCaretDot": {
|
||||||
|
borderRadius: "50%",
|
||||||
|
position: "absolute",
|
||||||
|
width: ".4em",
|
||||||
|
height: ".4em",
|
||||||
|
top: "-.2em",
|
||||||
|
left: "-.2em",
|
||||||
|
backgroundColor: "inherit",
|
||||||
|
transition: "transform .3s ease-in-out",
|
||||||
|
boxSizing: "border-box"
|
||||||
|
},
|
||||||
|
".SelectionCaret:hover > .SelectionCaretDot": {
|
||||||
|
transformOrigin: "bottom center",
|
||||||
|
transform: "scale(0)"
|
||||||
|
},
|
||||||
|
".SelectionInfo": {
|
||||||
|
position: "absolute",
|
||||||
|
top: "-1.05em",
|
||||||
|
left: "-1px",
|
||||||
|
fontSize: ".75em",
|
||||||
|
fontFamily: "serif",
|
||||||
|
fontStyle: "normal",
|
||||||
|
fontWeight: "normal",
|
||||||
|
lineHeight: "normal",
|
||||||
|
userSelect: "none",
|
||||||
|
color: "white",
|
||||||
|
paddingLeft: "2px",
|
||||||
|
paddingRight: "2px",
|
||||||
|
zIndex: 101,
|
||||||
|
transition: "opacity .3s ease-in-out",
|
||||||
|
backgroundColor: "inherit",
|
||||||
|
// these should be separate
|
||||||
|
opacity: 0,
|
||||||
|
transitionDelay: "0s",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
},
|
||||||
|
".SelectionCaret:hover > .SelectionInfo": {
|
||||||
|
opacity: 1,
|
||||||
|
transitionDelay: "0s"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { AnnotationType, Annotation, RangeSet, Range } from "@codemirror/state";
|
||||||
|
import {
|
||||||
|
EditorView,
|
||||||
|
ViewUpdate,
|
||||||
|
ViewPlugin,
|
||||||
|
Decoration,
|
||||||
|
WidgetType
|
||||||
|
} from "@codemirror/view";
|
||||||
|
|
||||||
|
import type { PluginValue, DecorationSet } from "@codemirror/view";
|
||||||
|
|
||||||
|
export class RemoteCursorWidget extends WidgetType {
|
||||||
|
public constructor(
|
||||||
|
private readonly color: string,
|
||||||
|
private readonly name: string
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toDOM(editor: EditorView): HTMLElement {
|
||||||
|
return editor.contentDOM.createEl(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
cls: "SelectionCaret",
|
||||||
|
attr: {
|
||||||
|
style: `background-color: ${this.color}; border-color: ${this.color}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(span) => {
|
||||||
|
span.appendText("\u2060");
|
||||||
|
span.createEl("div", {
|
||||||
|
cls: "SelectionCaretDot"
|
||||||
|
});
|
||||||
|
span.appendText("\u2060");
|
||||||
|
span.createEl("div", {
|
||||||
|
cls: "SelectionInfo",
|
||||||
|
text: this.name
|
||||||
|
});
|
||||||
|
span.appendText("\u2060");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public eq(other: RemoteCursorWidget) {
|
||||||
|
return other.color === this.color && other.name === this.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { RangeSet, Range } from "@codemirror/state";
|
||||||
|
import {
|
||||||
|
EditorView,
|
||||||
|
ViewUpdate,
|
||||||
|
ViewPlugin,
|
||||||
|
Decoration,
|
||||||
|
WidgetType
|
||||||
|
} from "@codemirror/view";
|
||||||
|
|
||||||
|
import type { PluginValue, DecorationSet } from "@codemirror/view";
|
||||||
|
import { RemoteCursorWidget } from "./remote-cursor-widget";
|
||||||
|
|
||||||
|
export class RemoteCursorsPluginValue implements PluginValue {
|
||||||
|
public decorations: DecorationSet = RangeSet.of([]);
|
||||||
|
|
||||||
|
public constructor(private readonly _editor: EditorView) {}
|
||||||
|
|
||||||
|
public update(update: ViewUpdate) {
|
||||||
|
const decorations: Array<Range<Decoration>> = [];
|
||||||
|
|
||||||
|
const cursors: {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
anchor: { index: number };
|
||||||
|
head: { index: number };
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: "Alice",
|
||||||
|
color: "#ff6b6b",
|
||||||
|
anchor: { index: 10 },
|
||||||
|
head: { index: 20 }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
cursors.forEach(({ name, color, anchor, head }) => {
|
||||||
|
const start = Math.min(anchor.index, head.index);
|
||||||
|
const end = Math.max(anchor.index, head.index);
|
||||||
|
const startLine = update.view.state.doc.lineAt(start);
|
||||||
|
const endLine = update.view.state.doc.lineAt(end);
|
||||||
|
|
||||||
|
if (startLine.number === endLine.number) {
|
||||||
|
// selected content in a single line.
|
||||||
|
decorations.push({
|
||||||
|
from: start,
|
||||||
|
to: end,
|
||||||
|
value: Decoration.mark({
|
||||||
|
attributes: {
|
||||||
|
style: `background-color: ${color}`
|
||||||
|
},
|
||||||
|
class: "Selection"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// selected content in multiple lines
|
||||||
|
// first, render text-selection in the first line
|
||||||
|
decorations.push({
|
||||||
|
from: start,
|
||||||
|
to: startLine.from + startLine.length,
|
||||||
|
value: Decoration.mark({
|
||||||
|
attributes: {
|
||||||
|
style: `background-color: ${color}`
|
||||||
|
},
|
||||||
|
class: "Selection"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
// render text-selection in the last line
|
||||||
|
decorations.push({
|
||||||
|
from: endLine.from,
|
||||||
|
to: end,
|
||||||
|
value: Decoration.mark({
|
||||||
|
attributes: {
|
||||||
|
style: `background-color: ${color}`
|
||||||
|
},
|
||||||
|
class: "Selection"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
for (let i = startLine.number + 1; i < endLine.number; i++) {
|
||||||
|
const linePos = update.view.state.doc.line(i).from;
|
||||||
|
decorations.push({
|
||||||
|
from: linePos,
|
||||||
|
to: linePos,
|
||||||
|
value: Decoration.line({
|
||||||
|
attributes: {
|
||||||
|
style: `background-color: ${color}`,
|
||||||
|
class: "LineSelection"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decorations.push({
|
||||||
|
from: head.index,
|
||||||
|
to: head.index,
|
||||||
|
value: Decoration.widget({
|
||||||
|
side: head.index - anchor.index > 0 ? -1 : 1, // the local cursor should be rendered outside the remote selection
|
||||||
|
block: false,
|
||||||
|
widget: new RemoteCursorWidget(color, name)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.decorations = Decoration.set(decorations, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remoteCursorsPlugin = ViewPlugin.fromClass(
|
||||||
|
RemoteCursorsPluginValue,
|
||||||
|
{
|
||||||
|
decorations: (v) => v.decorations
|
||||||
|
}
|
||||||
|
);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue