Add line number height measurement and synchronization in App.tsx
This commit is contained in:
52
src/App.tsx
52
src/App.tsx
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user