Apply editorconfig
This commit is contained in:
parent
ad3191957a
commit
b05e415acf
1 changed files with 17155 additions and 14368 deletions
|
|
@ -3,17 +3,17 @@ import { RangeSet } from "@codemirror/state";
|
|||
import { ViewPlugin, Decoration } from "@codemirror/view";
|
||||
|
||||
import type {
|
||||
PluginValue,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewUpdate
|
||||
PluginValue,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewUpdate
|
||||
} from "@codemirror/view";
|
||||
import { RemoteCursorWidget } from "./remote-cursor-widget";
|
||||
import type { RelativePath } from "sync-client";
|
||||
import {
|
||||
utils,
|
||||
type CursorSpan,
|
||||
type MaybeOutdatedClientCursors
|
||||
utils,
|
||||
type CursorSpan,
|
||||
type MaybeOutdatedClientCursors
|
||||
} from "sync-client";
|
||||
import type { App } from "obsidian";
|
||||
import { MarkdownView } from "obsidian";
|
||||
|
|
@ -25,241 +25,241 @@ import { reconcileWithHistory } from "reconcile-text";
|
|||
const forceUpdate = StateEffect.define();
|
||||
|
||||
export class RemoteCursorsPluginValue implements PluginValue {
|
||||
private static cursors: {
|
||||
name: string;
|
||||
path: string;
|
||||
span: CursorSpan;
|
||||
deviceId: string;
|
||||
isOutdated: boolean;
|
||||
}[] = [];
|
||||
private static cursors: {
|
||||
name: string;
|
||||
path: string;
|
||||
span: CursorSpan;
|
||||
deviceId: string;
|
||||
isOutdated: boolean;
|
||||
}[] = [];
|
||||
|
||||
private static app?: App;
|
||||
public decorations: DecorationSet = RangeSet.of([]);
|
||||
private static app?: App;
|
||||
public decorations: DecorationSet = RangeSet.of([]);
|
||||
|
||||
public static setCursors(
|
||||
clients: MaybeOutdatedClientCursors[],
|
||||
app: App
|
||||
): void {
|
||||
RemoteCursorsPluginValue.app = app;
|
||||
RemoteCursorsPluginValue.cursors = [
|
||||
...RemoteCursorsPluginValue.cursors.filter(({ deviceId }) =>
|
||||
clients.some(
|
||||
(client) =>
|
||||
client.deviceId === deviceId && client.isOutdated
|
||||
)
|
||||
),
|
||||
...clients
|
||||
.filter(
|
||||
({ isOutdated, deviceId }) =>
|
||||
!isOutdated ||
|
||||
RemoteCursorsPluginValue.cursors.every(
|
||||
(c) => deviceId !== c.deviceId
|
||||
)
|
||||
)
|
||||
.flatMap((client) => {
|
||||
const clientCursors = client.documentsWithCursors;
|
||||
return clientCursors.flatMap((cursor) =>
|
||||
cursor.cursors.map((span) => ({
|
||||
name: client.userName,
|
||||
path: cursor.relative_path,
|
||||
deviceId: client.deviceId,
|
||||
isOutdated: client.isOutdated,
|
||||
span: { ...span }
|
||||
}))
|
||||
);
|
||||
})
|
||||
];
|
||||
public static setCursors(
|
||||
clients: MaybeOutdatedClientCursors[],
|
||||
app: App
|
||||
): void {
|
||||
RemoteCursorsPluginValue.app = app;
|
||||
RemoteCursorsPluginValue.cursors = [
|
||||
...RemoteCursorsPluginValue.cursors.filter(({ deviceId }) =>
|
||||
clients.some(
|
||||
(client) =>
|
||||
client.deviceId === deviceId && client.isOutdated
|
||||
)
|
||||
),
|
||||
...clients
|
||||
.filter(
|
||||
({ isOutdated, deviceId }) =>
|
||||
!isOutdated ||
|
||||
RemoteCursorsPluginValue.cursors.every(
|
||||
(c) => deviceId !== c.deviceId
|
||||
)
|
||||
)
|
||||
.flatMap((client) => {
|
||||
const clientCursors = client.documentsWithCursors;
|
||||
return clientCursors.flatMap((cursor) =>
|
||||
cursor.cursors.map((span) => ({
|
||||
name: client.userName,
|
||||
path: cursor.relative_path,
|
||||
deviceId: client.deviceId,
|
||||
isOutdated: client.isOutdated,
|
||||
span: { ...span }
|
||||
}))
|
||||
);
|
||||
})
|
||||
];
|
||||
|
||||
app.workspace
|
||||
.getLeavesOfType("markdown")
|
||||
.map((leaf) => leaf.view)
|
||||
.filter((view) => view instanceof MarkdownView)
|
||||
.forEach((view) => {
|
||||
// @ts-expect-error, not typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const editor = view.editor.cm as EditorView;
|
||||
app.workspace
|
||||
.getLeavesOfType("markdown")
|
||||
.map((leaf) => leaf.view)
|
||||
.filter((view) => view instanceof MarkdownView)
|
||||
.forEach((view) => {
|
||||
// @ts-expect-error, not typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const editor = view.editor.cm as EditorView;
|
||||
|
||||
editor.dispatch({
|
||||
effects: [forceUpdate.of(null)]
|
||||
});
|
||||
});
|
||||
}
|
||||
editor.dispatch({
|
||||
effects: [forceUpdate.of(null)]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static findFileForEditor(
|
||||
editor: EditorView
|
||||
): RelativePath | undefined {
|
||||
return RemoteCursorsPluginValue.app?.workspace
|
||||
.getLeavesOfType("markdown")
|
||||
.map((leaf) => leaf.view)
|
||||
.filter((view) => view instanceof MarkdownView)
|
||||
.flatMap((view) => {
|
||||
// @ts-expect-error, not typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
if ((view.editor.cm as EditorView) !== editor) {
|
||||
return [];
|
||||
}
|
||||
private static findFileForEditor(
|
||||
editor: EditorView
|
||||
): RelativePath | undefined {
|
||||
return RemoteCursorsPluginValue.app?.workspace
|
||||
.getLeavesOfType("markdown")
|
||||
.map((leaf) => leaf.view)
|
||||
.filter((view) => view instanceof MarkdownView)
|
||||
.flatMap((view) => {
|
||||
// @ts-expect-error, not typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
if ((view.editor.cm as EditorView) !== editor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { file } = view;
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
const { file } = view;
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [file.path];
|
||||
})
|
||||
.first();
|
||||
}
|
||||
return [file.path];
|
||||
})
|
||||
.first();
|
||||
}
|
||||
|
||||
private static interpolateRemoteCursorPositions(
|
||||
original: string,
|
||||
edited: string
|
||||
): void {
|
||||
if (
|
||||
original === edited ||
|
||||
RemoteCursorsPluginValue.cursors.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
private static interpolateRemoteCursorPositions(
|
||||
original: string,
|
||||
edited: string
|
||||
): void {
|
||||
if (
|
||||
original === edited ||
|
||||
RemoteCursorsPluginValue.cursors.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedPositions: number[] = [];
|
||||
const reconciled = reconcileWithHistory(
|
||||
original,
|
||||
{
|
||||
text: original,
|
||||
cursors: RemoteCursorsPluginValue.cursors.flatMap(
|
||||
({ span }, i) => [
|
||||
{ id: i * 2, position: span.start },
|
||||
{ id: i * 2 + 1, position: span.end }
|
||||
]
|
||||
)
|
||||
},
|
||||
edited
|
||||
);
|
||||
const updatedPositions: number[] = [];
|
||||
const reconciled = reconcileWithHistory(
|
||||
original,
|
||||
{
|
||||
text: original,
|
||||
cursors: RemoteCursorsPluginValue.cursors.flatMap(
|
||||
({ span }, i) => [
|
||||
{ id: i * 2, position: span.start },
|
||||
{ id: i * 2 + 1, position: span.end }
|
||||
]
|
||||
)
|
||||
},
|
||||
edited
|
||||
);
|
||||
|
||||
reconciled.cursors.forEach(({ id, position }) => {
|
||||
const whereToJump = RemoteCursorsPluginValue.findWhereToMoveCursor(
|
||||
position,
|
||||
reconciled.history
|
||||
);
|
||||
if (whereToJump !== null) {
|
||||
updatedPositions[id] = whereToJump;
|
||||
} else {
|
||||
updatedPositions[id] = position;
|
||||
}
|
||||
});
|
||||
reconciled.cursors.forEach(({ id, position }) => {
|
||||
const whereToJump = RemoteCursorsPluginValue.findWhereToMoveCursor(
|
||||
position,
|
||||
reconciled.history
|
||||
);
|
||||
if (whereToJump !== null) {
|
||||
updatedPositions[id] = whereToJump;
|
||||
} else {
|
||||
updatedPositions[id] = position;
|
||||
}
|
||||
});
|
||||
|
||||
RemoteCursorsPluginValue.cursors.forEach(({ span }, i) => {
|
||||
span.start = updatedPositions[i * 2];
|
||||
span.end = updatedPositions[i * 2 + 1];
|
||||
});
|
||||
}
|
||||
RemoteCursorsPluginValue.cursors.forEach(({ span }, i) => {
|
||||
span.start = updatedPositions[i * 2];
|
||||
span.end = updatedPositions[i * 2 + 1];
|
||||
});
|
||||
}
|
||||
|
||||
private static findWhereToMoveCursor(
|
||||
cursor: number,
|
||||
spans: SpanWithHistory[]
|
||||
): number | null {
|
||||
let position = 0;
|
||||
for (const span of spans) {
|
||||
// left and origin are the same
|
||||
if (position === cursor && span.history === "AddedFromRight") {
|
||||
return position + span.text.length;
|
||||
}
|
||||
position += span.text.length;
|
||||
if (position === cursor && span.history === "RemovedFromRight") {
|
||||
return position - span.text.length;
|
||||
}
|
||||
}
|
||||
private static findWhereToMoveCursor(
|
||||
cursor: number,
|
||||
spans: SpanWithHistory[]
|
||||
): number | null {
|
||||
let position = 0;
|
||||
for (const span of spans) {
|
||||
// left and origin are the same
|
||||
if (position === cursor && span.history === "AddedFromRight") {
|
||||
return position + span.text.length;
|
||||
}
|
||||
position += span.text.length;
|
||||
if (position === cursor && span.history === "RemovedFromRight") {
|
||||
return position - span.text.length;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public update(update: ViewUpdate): void {
|
||||
const original = update.startState.doc.toString();
|
||||
const edited = update.state.doc.toString();
|
||||
public update(update: ViewUpdate): void {
|
||||
const original = update.startState.doc.toString();
|
||||
const edited = update.state.doc.toString();
|
||||
|
||||
RemoteCursorsPluginValue.interpolateRemoteCursorPositions(
|
||||
original,
|
||||
edited
|
||||
);
|
||||
RemoteCursorsPluginValue.interpolateRemoteCursorPositions(
|
||||
original,
|
||||
edited
|
||||
);
|
||||
|
||||
const decorations: Range<Decoration>[] = [];
|
||||
const relative_path = RemoteCursorsPluginValue.findFileForEditor(
|
||||
update.view
|
||||
);
|
||||
RemoteCursorsPluginValue.cursors
|
||||
.filter(({ path }) => path == relative_path)
|
||||
.forEach(({ name, span: { start, end } }) => {
|
||||
const color = utils.getRandomColor(name);
|
||||
const startLine = update.view.state.doc.lineAt(start);
|
||||
const endLine = update.view.state.doc.lineAt(end);
|
||||
const decorations: Range<Decoration>[] = [];
|
||||
const relative_path = RemoteCursorsPluginValue.findFileForEditor(
|
||||
update.view
|
||||
);
|
||||
RemoteCursorsPluginValue.cursors
|
||||
.filter(({ path }) => path == relative_path)
|
||||
.forEach(({ name, span: { start, end } }) => {
|
||||
const color = utils.getRandomColor(name);
|
||||
const startLine = update.view.state.doc.lineAt(start);
|
||||
const endLine = update.view.state.doc.lineAt(end);
|
||||
|
||||
const attributes = {
|
||||
style: `background-color: ${color};`
|
||||
};
|
||||
const attributes = {
|
||||
style: `background-color: ${color};`
|
||||
};
|
||||
|
||||
if (startLine.number === endLine.number) {
|
||||
// selected content in a single line.
|
||||
decorations.push({
|
||||
from: start,
|
||||
to: end,
|
||||
value: Decoration.mark({
|
||||
attributes
|
||||
})
|
||||
});
|
||||
} 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
|
||||
})
|
||||
});
|
||||
if (startLine.number === endLine.number) {
|
||||
// selected content in a single line.
|
||||
decorations.push({
|
||||
from: start,
|
||||
to: end,
|
||||
value: Decoration.mark({
|
||||
attributes
|
||||
})
|
||||
});
|
||||
} 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
|
||||
})
|
||||
});
|
||||
|
||||
// render text-selection in the lines between the first and last line
|
||||
for (
|
||||
let i = startLine.number + 1;
|
||||
i < endLine.number;
|
||||
i++
|
||||
) {
|
||||
const currentLine = update.view.state.doc.line(i);
|
||||
decorations.push({
|
||||
from: currentLine.from,
|
||||
to: currentLine.to,
|
||||
value: Decoration.mark({
|
||||
attributes
|
||||
})
|
||||
});
|
||||
}
|
||||
// render text-selection in the lines between the first and last line
|
||||
for (
|
||||
let i = startLine.number + 1;
|
||||
i < endLine.number;
|
||||
i++
|
||||
) {
|
||||
const currentLine = update.view.state.doc.line(i);
|
||||
decorations.push({
|
||||
from: currentLine.from,
|
||||
to: currentLine.to,
|
||||
value: Decoration.mark({
|
||||
attributes
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// render text-selection in the last line
|
||||
decorations.push({
|
||||
from: endLine.from,
|
||||
to: end,
|
||||
value: Decoration.mark({
|
||||
attributes
|
||||
})
|
||||
});
|
||||
}
|
||||
// render text-selection in the last line
|
||||
decorations.push({
|
||||
from: endLine.from,
|
||||
to: end,
|
||||
value: Decoration.mark({
|
||||
attributes
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
decorations.push({
|
||||
from: end,
|
||||
to: end,
|
||||
value: Decoration.widget({
|
||||
side: end - start > 0 ? -1 : 1, // the local cursor should be rendered outside the remote selection
|
||||
block: false,
|
||||
widget: new RemoteCursorWidget(color, name)
|
||||
})
|
||||
});
|
||||
});
|
||||
decorations.push({
|
||||
from: end,
|
||||
to: end,
|
||||
value: Decoration.widget({
|
||||
side: end - start > 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);
|
||||
}
|
||||
this.decorations = Decoration.set(decorations, true);
|
||||
}
|
||||
}
|
||||
|
||||
export const remoteCursorsPlugin = ViewPlugin.fromClass(
|
||||
RemoteCursorsPluginValue,
|
||||
{
|
||||
decorations: (v) => v.decorations
|
||||
}
|
||||
RemoteCursorsPluginValue,
|
||||
{
|
||||
decorations: (v) => v.decorations
|
||||
}
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue