mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-15 04:18:18 +00:00
refac
This commit is contained in:
parent
14cdf39afb
commit
d49bdc5257
2 changed files with 154 additions and 2 deletions
|
|
@ -14,6 +14,7 @@
|
|||
import ConfirmDialog from './ConfirmDialog.svelte';
|
||||
import FileCodeEditor from '$lib/components/chat/FileNav/FileCodeEditor.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { lineAlignment, alignChangedLines } from './diffEditorAlignment';
|
||||
import {
|
||||
copyChunk,
|
||||
selectedChunk,
|
||||
|
|
@ -186,6 +187,7 @@
|
|||
loading = true;
|
||||
const extensions = (side: 'a' | 'b', index: number) => [
|
||||
basicSetup,
|
||||
lineAlignment,
|
||||
keymap.of([indentWithTab, { key: 'Mod-s', run: () => true }]),
|
||||
indentUnit.of(' '),
|
||||
EditorView.lineWrapping,
|
||||
|
|
@ -196,11 +198,19 @@
|
|||
language[index].of([]),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged || update.selectionSet) refresh(update, side);
|
||||
if ((update.heightChanged || update.viewportChanged) && !alignmentFrame) {
|
||||
if (
|
||||
(update.geometryChanged ||
|
||||
update.viewportChanged ||
|
||||
update.transactions.some((tr) => tr.effects.length)) &&
|
||||
!alignmentFrame
|
||||
) {
|
||||
// Align after both virtualized editors have measured their visible lines.
|
||||
alignmentFrame = requestAnimationFrame(() => {
|
||||
alignmentFrame = 0;
|
||||
if (!disposed) view?.reconfigure({});
|
||||
if (!disposed && view) {
|
||||
alignChangedLines(view.a, view.b, view.chunks);
|
||||
view.reconfigure({});
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
142
src/lib/components/common/diffEditorAlignment.ts
Normal file
142
src/lib/components/common/diffEditorAlignment.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
import {
|
||||
BlockType,
|
||||
Decoration,
|
||||
EditorView,
|
||||
WidgetType,
|
||||
type DecorationSet
|
||||
} from '@codemirror/view';
|
||||
import type { Chunk } from '@codemirror/merge';
|
||||
|
||||
class RowSpacer extends WidgetType {
|
||||
constructor(readonly height: number) {
|
||||
super();
|
||||
}
|
||||
eq(other: RowSpacer) {
|
||||
return Math.abs(this.height - other.height) <= 1;
|
||||
}
|
||||
toDOM() {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'cm-mergeSpacer cm-diff-rowSpacer';
|
||||
element.style.height = `${this.height}px`;
|
||||
return element;
|
||||
}
|
||||
get estimatedHeight() {
|
||||
return this.height;
|
||||
}
|
||||
}
|
||||
const setSpacers = StateEffect.define<DecorationSet>();
|
||||
export const lineAlignment = StateField.define<DecorationSet>({
|
||||
create: () => Decoration.none,
|
||||
update: (value, transaction) => {
|
||||
for (const effect of transaction.effects) if (effect.is(setSpacers)) return effect.value;
|
||||
return value.map(transaction.changes);
|
||||
},
|
||||
provide: (field) => EditorView.decorations.from(field)
|
||||
});
|
||||
|
||||
// Only remeasure paired rows present in both virtualized viewports. Recomputing
|
||||
// offscreen spacers from estimated heights would move already aligned content.
|
||||
export function alignChangedLines(a: EditorView, b: EditorView, chunks: readonly Chunk[]) {
|
||||
const previous = (view: EditorView) => {
|
||||
const result = new Map<number, number>();
|
||||
for (let cursor = view.state.field(lineAlignment).iter(); cursor.value; cursor.next())
|
||||
result.set(cursor.from, cursor.value.spec.widget.height);
|
||||
return result;
|
||||
};
|
||||
const oldA = previous(a),
|
||||
oldB = previous(b);
|
||||
const nextA = new Map(oldA),
|
||||
nextB = new Map(oldB);
|
||||
let offsetA = 0,
|
||||
offsetB = 0;
|
||||
const textTop = (view: EditorView, pos: number) => {
|
||||
const block = view.lineBlockAt(pos);
|
||||
const text = Array.isArray(block.type)
|
||||
? block.type.find((part) => part.type === BlockType.Text && part.from === pos)
|
||||
: block;
|
||||
return text?.from === pos ? text.top : null;
|
||||
};
|
||||
const align = (posA: number, posB: number) => {
|
||||
if (
|
||||
posA < a.viewport.from ||
|
||||
posB < b.viewport.from ||
|
||||
posA > a.viewport.to ||
|
||||
posB > b.viewport.to
|
||||
)
|
||||
return;
|
||||
const topA = textTop(a, posA),
|
||||
topB = textTop(b, posB);
|
||||
if (topA === null || topB === null) return;
|
||||
const previousA = oldA.get(posA) ?? 0,
|
||||
previousB = oldB.get(posB) ?? 0;
|
||||
const difference = topA - previousA + offsetA - topB + previousB - offsetB;
|
||||
const sizeA = difference < -1 ? -difference : 0,
|
||||
sizeB = difference > 1 ? difference : 0;
|
||||
nextA.delete(posA);
|
||||
nextB.delete(posB);
|
||||
if (sizeA) nextA.set(posA, sizeA);
|
||||
if (sizeB) nextB.set(posB, sizeB);
|
||||
offsetA += sizeA - previousA;
|
||||
offsetB += sizeB - previousB;
|
||||
};
|
||||
let fromA = 0,
|
||||
fromB = 0;
|
||||
for (let index = 0; index <= chunks.length; index++) {
|
||||
const chunk = chunks[index];
|
||||
const endA = chunk ? chunk.fromA : a.state.doc.length + 1;
|
||||
let posA = Math.max(
|
||||
fromA,
|
||||
a.state.doc.lineAt(
|
||||
Math.min(
|
||||
a.state.doc.length,
|
||||
Math.max(fromA, a.viewport.from, fromA + b.viewport.from - fromB)
|
||||
)
|
||||
).from
|
||||
);
|
||||
for (; posA < endA && posA <= a.viewport.to; posA = a.state.doc.lineAt(posA).to + 1) {
|
||||
const posB = fromB + posA - fromA;
|
||||
if (posB > b.viewport.to) break;
|
||||
align(posA, posB);
|
||||
}
|
||||
if (!chunk) break;
|
||||
if (chunk.fromA < chunk.toA && chunk.fromB < chunk.toB) {
|
||||
const lineA = a.state.doc.lineAt(chunk.fromA).number,
|
||||
lineB = b.state.doc.lineAt(chunk.fromB).number;
|
||||
let row = Math.max(
|
||||
0,
|
||||
a.state.doc.lineAt(a.viewport.from).number - lineA,
|
||||
b.state.doc.lineAt(b.viewport.from).number - lineB
|
||||
);
|
||||
while (lineA + row <= a.state.doc.lines && lineB + row <= b.state.doc.lines) {
|
||||
const posA = a.state.doc.line(lineA + row).from,
|
||||
posB = b.state.doc.line(lineB + row).from;
|
||||
if (posA >= chunk.toA || posB >= chunk.toB || posA > a.viewport.to || posB > b.viewport.to)
|
||||
break;
|
||||
align(posA, posB);
|
||||
row++;
|
||||
}
|
||||
}
|
||||
fromA = chunk.toA;
|
||||
fromB = chunk.toB;
|
||||
}
|
||||
const apply = (view: EditorView, old: Map<number, number>, next: Map<number, number>) => {
|
||||
if (
|
||||
old.size === next.size &&
|
||||
[...next].every(([pos, size]) => Math.abs((old.get(pos) ?? 0) - size) <= 1)
|
||||
)
|
||||
return;
|
||||
view.dispatch({
|
||||
effects: setSpacers.of(
|
||||
Decoration.set(
|
||||
[...next].map(([pos, height]) =>
|
||||
Decoration.widget({ widget: new RowSpacer(height), block: true, side: -1 }).range(pos)
|
||||
),
|
||||
true
|
||||
)
|
||||
)
|
||||
});
|
||||
};
|
||||
apply(a, oldA, nextA);
|
||||
apply(b, oldB, nextB);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue