mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-19 01:51:15 -05:00
refac
This commit is contained in:
1 parent
5c9865a1f1
commit
aa8ce52c7a
3 files changed
+140
-108
No files matched your search
@@ -245,6 +245,7 @@
|
|||||||
if (!merge || !chunkCount) return;
|
if (!merge || !chunkCount) return;
|
||||||
activeChunk = Math.max(0, Math.min(index, chunkCount - 1));
|
activeChunk = Math.max(0, Math.min(index, chunkCount - 1));
|
||||||
const chunk = visibleChunks[activeChunk];
|
const chunk = visibleChunks[activeChunk];
|
||||||
|
merge.scrollSource = chunk.toB > chunk.fromB ? merge.b : merge.a;
|
||||||
for (const [view, start] of [
|
for (const [view, start] of [
|
||||||
[merge.a, chunk.fromA],
|
[merge.a, chunk.fromA],
|
||||||
[merge.b, chunk.fromB]
|
[merge.b, chunk.fromB]
|
||||||
@@ -334,32 +335,6 @@
|
|||||||
let observer: MutationObserver | undefined;
|
let observer: MutationObserver | undefined;
|
||||||
let alignmentFrame = 0;
|
let alignmentFrame = 0;
|
||||||
let presentationFrame = 0;
|
let presentationFrame = 0;
|
||||||
let scrollAnchor: { editor: EditorView; position: number; top: number } | null = null;
|
|
||||||
const captureScrollAnchor = () => {
|
|
||||||
if (!view) return null;
|
|
||||||
const y = view.dom.getBoundingClientRect().top + 1;
|
|
||||||
for (const editor of [view.a, view.b]) {
|
|
||||||
const position = editor.posAtCoords({
|
|
||||||
x: editor.contentDOM.getBoundingClientRect().left + 4,
|
|
||||||
y
|
|
||||||
});
|
|
||||||
const coords = position === null ? null : editor.coordsAtPos(position);
|
|
||||||
if (position !== null && coords) return { editor, position, top: coords.top };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
const cancelScrollAnchor = () => {
|
|
||||||
scrollAnchor = null;
|
|
||||||
};
|
|
||||||
for (const event of ['touchmove', 'pointerdown'])
|
|
||||||
node.addEventListener(event, cancelScrollAnchor, { passive: true });
|
|
||||||
const advanceScrollAnchor = (event: WheelEvent) => {
|
|
||||||
if (event.deltaMode !== 0) return cancelScrollAnchor();
|
|
||||||
scrollAnchor ??= captureScrollAnchor();
|
|
||||||
if (scrollAnchor) scrollAnchor.top -= event.deltaY;
|
|
||||||
scheduleAlignment();
|
|
||||||
};
|
|
||||||
node.addEventListener('wheel', advanceScrollAnchor, { passive: true });
|
|
||||||
let expanded: CollapsedPair[] = [];
|
let expanded: CollapsedPair[] = [];
|
||||||
const schedulePresentation = (reset = false) => {
|
const schedulePresentation = (reset = false) => {
|
||||||
if (reset) expanded = [];
|
if (reset) expanded = [];
|
||||||
@@ -403,14 +378,6 @@
|
|||||||
alignmentFrame = requestAnimationFrame(() => {
|
alignmentFrame = requestAnimationFrame(() => {
|
||||||
alignmentFrame = 0;
|
alignmentFrame = 0;
|
||||||
if (!disposed && view) {
|
if (!disposed && view) {
|
||||||
// Restore a visual row after both panes measure new spacers. Anchoring
|
|
||||||
// to the paragraph start jumps backwards inside long wrapped lines.
|
|
||||||
if (scrollAnchor) {
|
|
||||||
const { editor, position, top } = scrollAnchor;
|
|
||||||
scrollAnchor = null;
|
|
||||||
const coords = editor.coordsAtPos(position);
|
|
||||||
if (coords) view.dom.scrollTop += coords.top - top;
|
|
||||||
}
|
|
||||||
const documents = [view.a.state.doc, view.b.state.doc] as const;
|
const documents = [view.a.state.doc, view.b.state.doc] as const;
|
||||||
const geometry = [
|
const geometry = [
|
||||||
view.a.contentDOM.clientWidth,
|
view.a.contentDOM.clientWidth,
|
||||||
@@ -427,10 +394,9 @@
|
|||||||
resized);
|
resized);
|
||||||
alignmentDocuments = documents;
|
alignmentDocuments = documents;
|
||||||
if (!alignmentGeometry || resized) alignmentGeometry = geometry;
|
if (!alignmentGeometry || resized) alignmentGeometry = geometry;
|
||||||
scrollAnchor = captureScrollAnchor();
|
|
||||||
if (!reset || !clearLineAlignment(view.a, view.b))
|
if (!reset || !clearLineAlignment(view.a, view.b))
|
||||||
alignChangedLines(view.a, view.b, view.chunks);
|
alignChangedLines(view.a, view.b, view.linePairs);
|
||||||
if (!alignmentFrame) scrollAnchor = null;
|
view.syncScroll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -458,7 +424,6 @@
|
|||||||
themes[index].of(document.documentElement.classList.contains('dark') ? oneDark : []),
|
themes[index].of(document.documentElement.classList.contains('dark') ? oneDark : []),
|
||||||
language[index].of([]),
|
language[index].of([]),
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (update.docChanged || update.selectionSet) scrollAnchor = null;
|
|
||||||
for (const transaction of update.transactions) {
|
for (const transaction of update.transactions) {
|
||||||
if (transaction.docChanged) {
|
if (transaction.docChanged) {
|
||||||
for (const pair of expanded) {
|
for (const pair of expanded) {
|
||||||
@@ -480,8 +445,8 @@
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
EditorView.theme({
|
EditorView.theme({
|
||||||
'&': { fontSize: '0.75rem' },
|
'&': { fontSize: '0.75rem', height: '100%' },
|
||||||
'&, .cm-scroller': { height: 'auto', overflowY: 'visible' },
|
'.cm-scroller': { overflowY: 'auto' },
|
||||||
'.cm-content': { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace' },
|
'.cm-content': { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace' },
|
||||||
'&.cm-focused': { outline: 'none' }
|
'&.cm-focused': { outline: 'none' }
|
||||||
})
|
})
|
||||||
@@ -527,9 +492,6 @@
|
|||||||
return {
|
return {
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
for (const event of ['touchmove', 'pointerdown'])
|
|
||||||
node.removeEventListener(event, cancelScrollAnchor);
|
|
||||||
node.removeEventListener('wheel', advanceScrollAnchor);
|
|
||||||
cancelAnimationFrame(alignmentFrame);
|
cancelAnimationFrame(alignmentFrame);
|
||||||
cancelAnimationFrame(presentationFrame);
|
cancelAnimationFrame(presentationFrame);
|
||||||
if (updatePresentation === schedulePresentation) updatePresentation = null;
|
if (updatePresentation === schedulePresentation) updatePresentation = null;
|
||||||
@@ -1000,7 +962,7 @@
|
|||||||
}
|
}
|
||||||
.merge-container :global(.cm-mergeView) {
|
.merge-container :global(.cm-mergeView) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.merge-container :global(.cm-mergeViewEditor) {
|
.merge-container :global(.cm-mergeViewEditor) {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
@@ -1065,6 +1027,7 @@
|
|||||||
background: #101413 !important;
|
background: #101413 !important;
|
||||||
}
|
}
|
||||||
.merge-container :global(.cm-mergeViewEditors) {
|
.merge-container :global(.cm-mergeViewEditors) {
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '@codemirror/state';
|
} from '@codemirror/state';
|
||||||
import {
|
import {
|
||||||
Decoration,
|
Decoration,
|
||||||
|
BlockType,
|
||||||
EditorView,
|
EditorView,
|
||||||
GutterMarker,
|
GutterMarker,
|
||||||
gutter,
|
gutter,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
type DecorationSet
|
type DecorationSet
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { Chunk, type DiffConfig } from '@codemirror/merge';
|
import { Chunk, type DiffConfig } from '@codemirror/merge';
|
||||||
|
import { lineTextTop } from './diffEditorAlignment';
|
||||||
|
|
||||||
// A scanLimit also gives up based on document length, even for sparse, tiny edits.
|
// A scanLimit also gives up based on document length, even for sparse, tiny edits.
|
||||||
export const diffEditorConfig: DiffConfig = { timeout: 100 };
|
export const diffEditorConfig: DiffConfig = { timeout: 100 };
|
||||||
@@ -28,6 +30,9 @@ export class DiffEditorView {
|
|||||||
readonly b: EditorView;
|
readonly b: EditorView;
|
||||||
readonly dom: HTMLDivElement;
|
readonly dom: HTMLDivElement;
|
||||||
chunks: readonly Chunk[];
|
chunks: readonly Chunk[];
|
||||||
|
linePairs: [number, number][];
|
||||||
|
scrollSource: EditorView;
|
||||||
|
private scrollFrame = 0;
|
||||||
constructor(config: { a: EditorStateConfig; b: EditorStateConfig; parent: HTMLElement }) {
|
constructor(config: { a: EditorStateConfig; b: EditorStateConfig; parent: HTMLElement }) {
|
||||||
const states = (['a', 'b'] as const).map((side) =>
|
const states = (['a', 'b'] as const).map((side) =>
|
||||||
EditorState.create({
|
EditorState.create({
|
||||||
@@ -39,6 +44,7 @@ export class DiffEditorView {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.chunks = Chunk.build(states[0].doc, states[1].doc, diffEditorConfig);
|
this.chunks = Chunk.build(states[0].doc, states[1].doc, diffEditorConfig);
|
||||||
|
this.linePairs = matchingLinePairs(states[0].doc, states[1].doc, this.chunks);
|
||||||
this.dom = document.createElement('div');
|
this.dom = document.createElement('div');
|
||||||
this.dom.className = 'cm-mergeView';
|
this.dom.className = 'cm-mergeView';
|
||||||
const editors = this.dom.appendChild(document.createElement('div'));
|
const editors = this.dom.appendChild(document.createElement('div'));
|
||||||
@@ -55,6 +61,51 @@ export class DiffEditorView {
|
|||||||
this.a = create('a', 0);
|
this.a = create('a', 0);
|
||||||
this.b = create('b', 1);
|
this.b = create('b', 1);
|
||||||
config.parent.appendChild(this.dom);
|
config.parent.appendChild(this.dom);
|
||||||
|
// Only the pane receiving input drives scrolling; the other pane's height corrections must not feed back.
|
||||||
|
this.scrollSource = this.a;
|
||||||
|
for (const editor of [this.a, this.b]) {
|
||||||
|
for (const event of ['wheel', 'touchstart', 'pointerdown', 'keydown', 'focusin'])
|
||||||
|
editor.dom.addEventListener(event, () => (this.scrollSource = editor), { passive: true });
|
||||||
|
editor.scrollDOM.addEventListener('scroll', () => this.syncScroll());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncScroll() {
|
||||||
|
if (this.scrollFrame) return;
|
||||||
|
this.scrollFrame = requestAnimationFrame(() => {
|
||||||
|
this.scrollFrame = 0;
|
||||||
|
const index = this.scrollSource === this.a ? 0 : 1;
|
||||||
|
const other = index === 0 ? this.b : this.a;
|
||||||
|
const block = this.scrollSource.lineBlockAtHeight(this.scrollSource.scrollDOM.scrollTop);
|
||||||
|
const line = block.from;
|
||||||
|
const text = Array.isArray(block.type)
|
||||||
|
? block.type.find((part) => part.type === BlockType.Text)
|
||||||
|
: block.type === BlockType.Text
|
||||||
|
? block
|
||||||
|
: null;
|
||||||
|
let pair = this.linePairs[0];
|
||||||
|
for (const candidate of this.linePairs) {
|
||||||
|
if (candidate[index] > line) break;
|
||||||
|
pair = candidate;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
pair &&
|
||||||
|
text &&
|
||||||
|
pair[index] === text.from &&
|
||||||
|
this.scrollSource.scrollDOM.scrollTop < text.bottom &&
|
||||||
|
(pair[1 - index] < other.viewport.from || pair[1 - index] > other.viewport.to)
|
||||||
|
)
|
||||||
|
other.dispatch({
|
||||||
|
effects: EditorView.scrollIntoView(pair[1 - index], { y: 'nearest', yMargin: 0 })
|
||||||
|
});
|
||||||
|
const sourceTop = pair
|
||||||
|
? (lineTextTop(this.scrollSource, pair[index]) ??
|
||||||
|
this.scrollSource.lineBlockAt(pair[index]).top)
|
||||||
|
: 0;
|
||||||
|
const targetTop = pair
|
||||||
|
? (lineTextTop(other, pair[1 - index]) ?? other.lineBlockAt(pair[1 - index]).top)
|
||||||
|
: 0;
|
||||||
|
other.scrollDOM.scrollTop = targetTop + this.scrollSource.scrollDOM.scrollTop - sourceTop;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
private dispatch(transactions: readonly Transaction[], side: 'a' | 'b') {
|
private dispatch(transactions: readonly Transaction[], side: 'a' | 'b') {
|
||||||
if (transactions.some((transaction) => transaction.docChanged)) {
|
if (transactions.some((transaction) => transaction.docChanged)) {
|
||||||
@@ -67,16 +118,40 @@ export class DiffEditorView {
|
|||||||
side === 'a'
|
side === 'a'
|
||||||
? Chunk.updateA(this.chunks, document, this.b.state.doc, changes, diffEditorConfig)
|
? Chunk.updateA(this.chunks, document, this.b.state.doc, changes, diffEditorConfig)
|
||||||
: Chunk.updateB(this.chunks, this.a.state.doc, document, changes, diffEditorConfig);
|
: Chunk.updateB(this.chunks, this.a.state.doc, document, changes, diffEditorConfig);
|
||||||
|
this.linePairs = matchingLinePairs(
|
||||||
|
side === 'a' ? document : this.a.state.doc,
|
||||||
|
side === 'b' ? document : this.b.state.doc,
|
||||||
|
this.chunks
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this[side].update(transactions);
|
this[side].update(transactions);
|
||||||
}
|
}
|
||||||
destroy() {
|
destroy() {
|
||||||
|
cancelAnimationFrame(this.scrollFrame);
|
||||||
this.a.destroy();
|
this.a.destroy();
|
||||||
this.b.destroy();
|
this.b.destroy();
|
||||||
this.dom.remove();
|
this.dom.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pair unchanged rows and the rows whose text survives within changed chunks.
|
||||||
|
function matchingLinePairs(a: Text, b: Text, chunks: readonly Chunk[]): [number, number][] {
|
||||||
|
const pairs: [number, number][] = [];
|
||||||
|
let fromA = 0,
|
||||||
|
fromB = 0;
|
||||||
|
for (let index = 0; index <= chunks.length; index++) {
|
||||||
|
const chunk = chunks[index];
|
||||||
|
const end = chunk ? chunk.fromA : a.length + 1;
|
||||||
|
for (let pos = fromA; pos < end; pos = a.lineAt(pos).to + 1)
|
||||||
|
pairs.push([pos, fromB + pos - fromA]);
|
||||||
|
if (!chunk) break;
|
||||||
|
for (const pair of changedLinePairs(a, b, chunk)) pairs.push(pair);
|
||||||
|
fromA = chunk.toA;
|
||||||
|
fromB = chunk.toB;
|
||||||
|
}
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
export type MergeDirection = 'to-working' | 'to-proposal';
|
export type MergeDirection = 'to-working' | 'to-proposal';
|
||||||
|
|
||||||
// Match rows by the text preserved inside a chunk, not their ordinal position.
|
// Match rows by the text preserved inside a chunk, not their ordinal position.
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
WidgetType,
|
WidgetType,
|
||||||
type DecorationSet
|
type DecorationSet
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import type { Chunk } from '@codemirror/merge';
|
|
||||||
import { changedLinePairs } from './diffEditor';
|
|
||||||
|
|
||||||
// Font measurements can differ by fractions of a pixel between virtualized lines.
|
// Font measurements can differ by fractions of a pixel between virtualized lines.
|
||||||
export function alignmentGeometryChanged(previous: readonly number[], next: readonly number[]) {
|
export function alignmentGeometryChanged(previous: readonly number[], next: readonly number[]) {
|
||||||
@@ -52,77 +50,68 @@ export function clearLineAlignment(a: EditorView, b: EditorView) {
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep spacers outside both viewports fixed. Include pairs visible on either side
|
export function lineTextTop(view: EditorView, pos: number) {
|
||||||
// so resizing or a large insertion cannot leave the two viewports disconnected.
|
const block = view.lineBlockAt(pos);
|
||||||
export function alignChangedLines(a: EditorView, b: EditorView, chunks: readonly Chunk[]) {
|
const text = Array.isArray(block.type)
|
||||||
|
? block.type.find((part) => part.type === BlockType.Text && part.from === pos)
|
||||||
|
: block.type === BlockType.Text
|
||||||
|
? block
|
||||||
|
: null;
|
||||||
|
return text?.from === pos ? text.top : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure matching rows locally; estimate unmatched ranges so large insertions remain scrollable.
|
||||||
|
export function alignChangedLines(
|
||||||
|
a: EditorView,
|
||||||
|
b: EditorView,
|
||||||
|
pairs: readonly [number, number][]
|
||||||
|
) {
|
||||||
const previous = (view: EditorView) => {
|
const previous = (view: EditorView) => {
|
||||||
const result = new Map<number, number>();
|
const result = new Map<number, number>();
|
||||||
for (let cursor = view.state.field(lineAlignment).iter(); cursor.value; cursor.next())
|
for (let cursor = view.state.field(lineAlignment).iter(); cursor.value; cursor.next())
|
||||||
result.set(cursor.from, cursor.value.spec.widget.height);
|
result.set(cursor.value.spec.row, cursor.value.spec.widget.height);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const oldA = previous(a),
|
const oldA = previous(a),
|
||||||
oldB = previous(b);
|
oldB = previous(b);
|
||||||
const nextA = new Map(oldA),
|
const nextA = new Map(oldA),
|
||||||
nextB = new Map(oldB);
|
nextB = new Map(oldB);
|
||||||
let offsetA = 0,
|
// Compare heights between adjacent matching rows, never estimated offsets from document start.
|
||||||
offsetB = 0;
|
for (let index = 0; index <= pairs.length; index++) {
|
||||||
const textTop = (view: EditorView, pos: number) => {
|
const start = index ? pairs[index - 1] : null;
|
||||||
const block = view.lineBlockAt(pos);
|
const end =
|
||||||
const text = Array.isArray(block.type)
|
index < pairs.length ? pairs[index] : [a.state.doc.length + 1, b.state.doc.length + 1];
|
||||||
? block.type.find((part) => part.type === BlockType.Text && part.from === pos)
|
const from = start ?? [0, 0];
|
||||||
: block.type === BlockType.Text
|
const to = [Math.min(end[0], a.state.doc.length), Math.min(end[1], b.state.doc.length)];
|
||||||
? block
|
const measured =
|
||||||
: null;
|
from[0] >= a.viewport.from &&
|
||||||
return text?.from === pos ? text.top : null;
|
from[1] >= b.viewport.from &&
|
||||||
};
|
to[0] <= a.viewport.to &&
|
||||||
const align = (posA: number, posB: number) => {
|
to[1] <= b.viewport.to;
|
||||||
if (
|
if (!measured) {
|
||||||
(posA < a.viewport.from || posA > a.viewport.to) &&
|
// Keep estimates fixed until the range is rendered, otherwise they feed back into the height map.
|
||||||
(posB < b.viewport.from || posB > b.viewport.to)
|
if (oldA.has(end[0]) || oldB.has(end[1])) continue;
|
||||||
)
|
const matchedRows = start && index < pairs.length ? 1 : 0;
|
||||||
return;
|
const unmatched =
|
||||||
const topA = textTop(a, posA),
|
a.state.doc.lineAt(to[0]).number - a.state.doc.lineAt(from[0]).number > matchedRows ||
|
||||||
topB = textTop(b, posB);
|
b.state.doc.lineAt(to[1]).number - b.state.doc.lineAt(from[1]).number > matchedRows;
|
||||||
if (topA === null || topB === null) return;
|
if (!unmatched) continue;
|
||||||
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, Math.min(a.viewport.from, fromA + b.viewport.from - fromB))
|
|
||||||
)
|
|
||||||
).from
|
|
||||||
);
|
|
||||||
for (
|
|
||||||
;
|
|
||||||
posA < endA && posA <= Math.max(a.viewport.to, fromA + b.viewport.to - fromB);
|
|
||||||
posA = a.state.doc.lineAt(posA).to + 1
|
|
||||||
) {
|
|
||||||
const posB = fromB + posA - fromA;
|
|
||||||
align(posA, posB);
|
|
||||||
}
|
}
|
||||||
if (!chunk) break;
|
const heights = [a, b].map((view, side) => {
|
||||||
for (const [posA, posB] of changedLinePairs(a.state.doc, b.state.doc, chunk)) align(posA, posB);
|
const from = start ? (measured ? start[side] : view.state.doc.lineAt(start[side]).to + 1) : 0,
|
||||||
fromA = chunk.toA;
|
to = Math.min(end[side], view.state.doc.length);
|
||||||
fromB = chunk.toB;
|
if (!measured && from >= end[side]) return 0;
|
||||||
|
const top = start ? lineTextTop(view, from) : 0;
|
||||||
|
const bottom =
|
||||||
|
end[side] > view.state.doc.length ? view.lineBlockAt(to).bottom : lineTextTop(view, to);
|
||||||
|
return top === null || bottom === null ? null : bottom - top;
|
||||||
|
});
|
||||||
|
if (heights[0] === null || heights[1] === null) continue;
|
||||||
|
const difference = heights[0] - (oldA.get(end[0]) ?? 0) - heights[1] + (oldB.get(end[1]) ?? 0);
|
||||||
|
nextA.delete(end[0]);
|
||||||
|
nextB.delete(end[1]);
|
||||||
|
if (difference < -1) nextA.set(end[0], -difference);
|
||||||
|
if (difference > 1) nextB.set(end[1], difference);
|
||||||
}
|
}
|
||||||
const apply = (view: EditorView, old: Map<number, number>, next: Map<number, number>) => {
|
const apply = (view: EditorView, old: Map<number, number>, next: Map<number, number>) => {
|
||||||
if (
|
if (
|
||||||
@@ -134,7 +123,12 @@ export function alignChangedLines(a: EditorView, b: EditorView, chunks: readonly
|
|||||||
effects: setSpacers.of(
|
effects: setSpacers.of(
|
||||||
Decoration.set(
|
Decoration.set(
|
||||||
[...next].map(([pos, height]) =>
|
[...next].map(([pos, height]) =>
|
||||||
Decoration.widget({ widget: new RowSpacer(height), block: true, side: -1 }).range(pos)
|
Decoration.widget({
|
||||||
|
widget: new RowSpacer(height),
|
||||||
|
block: true,
|
||||||
|
side: pos ? 1 : -1,
|
||||||
|
row: pos
|
||||||
|
}).range(Math.max(0, pos - 1))
|
||||||
),
|
),
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user