Add line number height measurement and synchronization in App.tsx

This commit is contained in:
2026-01-31 13:04:57 +01:00
parent d7be202720
commit fb183223b1

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { open, save } from "@tauri-apps/plugin-dialog"; import { open, save } from "@tauri-apps/plugin-dialog";
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs"; import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
@@ -91,6 +91,8 @@ export default function App() {
const [showLineNumbers, setShowLineNumbers] = useState(() => { const [showLineNumbers, setShowLineNumbers] = useState(() => {
return localStorage.getItem("textdb.lineNumbers") === "true"; return localStorage.getItem("textdb.lineNumbers") === "true";
}); });
const [lineHeights, setLineHeights] = useState<number[]>([]);
const [measureTick, setMeasureTick] = useState(0);
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
return localStorage.getItem("textdb.sidebarCollapsed") === "true"; return localStorage.getItem("textdb.sidebarCollapsed") === "true";
}); });
@@ -98,6 +100,7 @@ export default function App() {
const bodyRef = useRef(body); const bodyRef = useRef(body);
const textareaRef = useRef<HTMLTextAreaElement | null>(null); const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const lineNumbersRef = useRef<HTMLDivElement | null>(null); const lineNumbersRef = useRef<HTMLDivElement | null>(null);
const measureRef = useRef<HTMLDivElement | null>(null);
const historySnapshotRef = useRef<HistorySnapshot | null>(null); const historySnapshotRef = useRef<HistorySnapshot | null>(null);
const recentOpenRef = useRef(new Map<string, number>()); const recentOpenRef = useRef(new Map<string, number>());
@@ -159,8 +162,8 @@ export default function App() {
const historyIconSrc = theme === "light" ? historyIconBright : historyIcon; const historyIconSrc = theme === "light" ? historyIconBright : historyIcon;
const lineNumbers = useMemo(() => { const lines = useMemo(() => body.split("\n"), [body]);
const count = Math.max(body.split("\n").length, 1);
return Array.from({ length: count }, (_, index) => index + 1); return Array.from({ length: count }, (_, index) => index + 1);
}, [body]); }, [body]);
@@ -171,6 +174,32 @@ export default function App() {
} }
}, [showLineNumbers]); }, [showLineNumbers]);
useEffect(() => {
if (!showLineNumbers) return;
const textarea = textareaRef.current;
if (!textarea || typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(() => {
setMeasureTick((tick) => tick + 1);
});
observer.observe(textarea);
return () => observer.disconnect();
}, [showLineNumbers]);
useLayoutEffect(() => {
if (!showLineNumbers) return;
const textarea = textareaRef.current;
const measure = measureRef.current;
if (!textarea || !measure) return;
measure.style.width = `${textarea.clientWidth}px`;
const heights = Array.from(measure.children).map((child) =>
Math.ceil((child as HTMLElement).getBoundingClientRect().height)
);
setLineHeights(heights);
if (lineNumbersRef.current) {
lineNumbersRef.current.scrollTop = textarea.scrollTop;
}
}, [lines, showLineNumbers, textSize, measureTick]);
useEffect(() => { useEffect(() => {
if (showLineNumbers && textareaRef.current && lineNumbersRef.current) { if (showLineNumbers && textareaRef.current && lineNumbersRef.current) {
lineNumbersRef.current.scrollTop = textareaRef.current.scrollTop; lineNumbersRef.current.scrollTop = textareaRef.current.scrollTop;
@@ -844,10 +873,23 @@ export default function App() {
</div> </div>
<div className="editor__textarea-wrap"> <div className="editor__textarea-wrap">
{showLineNumbers ? (
<div className="line-measure" ref={measureRef} aria-hidden="true">
{lines.map((line, index) => (
<div key={index} className="line-measure__line">
{line.length > 0 ? line : " "}
</div>
))}
</div>
) : null}
{showLineNumbers ? ( {showLineNumbers ? (
<div className="line-numbers" ref={lineNumbersRef}> <div className="line-numbers" ref={lineNumbersRef}>
{lineNumbers.map((line) => ( {lineNumbers.map((line, index) => (
<div key={line} className="line-numbers__line"> <div
key={line}
className="line-numbers__line"
style={{ height: lineHeights[index] ? `${lineHeights[index]}px` : undefined }}
>
{line} {line}
</div> </div>
))} ))}