feat(appearance): apply UI zoom level to terminal and Monaco editors
Extend the existing appearance zoom setting so it also scales xterm.js terminals and Monaco editors (diff viewer, three-pane merge editor, file workspace editor), which previously rendered at a hard-coded 13px regardless of zoom. - Terminals read zoom at init and update term.options.fontSize live on zoom change, refitting after a double rAF so xterm's renderer has recomputed cell metrics. Font size is rounded to an integer to avoid subpixel blur in the canvas renderer. - Monaco editors derive fontSize from zoomLevel; three-pane merge editor memoizes its options object to avoid redundant updateOptions calls on unrelated re-renders.
This commit is contained in:
@@ -6,8 +6,11 @@ import { ChevronLeft, ChevronRight } from "lucide-react"
|
|||||||
import type { DiffOnMount } from "@monaco-editor/react"
|
import type { DiffOnMount } from "@monaco-editor/react"
|
||||||
import type { editor as MonacoEditorNs } from "monaco-editor"
|
import type { editor as MonacoEditorNs } from "monaco-editor"
|
||||||
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
|
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
|
||||||
|
import { useZoomLevel } from "@/hooks/use-appearance"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const EDITOR_BASE_FONT_SIZE = 13
|
||||||
|
|
||||||
import "@/lib/monaco-local"
|
import "@/lib/monaco-local"
|
||||||
|
|
||||||
const MonacoDiffEditor = dynamic(
|
const MonacoDiffEditor = dynamic(
|
||||||
@@ -36,6 +39,7 @@ export function DiffViewer({
|
|||||||
className,
|
className,
|
||||||
}: DiffViewerProps) {
|
}: DiffViewerProps) {
|
||||||
const editorTheme = useMonacoThemeSync()
|
const editorTheme = useMonacoThemeSync()
|
||||||
|
const { zoomLevel } = useZoomLevel()
|
||||||
const diffEditorRef = useRef<MonacoEditorNs.IStandaloneDiffEditor | null>(
|
const diffEditorRef = useRef<MonacoEditorNs.IStandaloneDiffEditor | null>(
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
@@ -187,7 +191,7 @@ export function DiffViewer({
|
|||||||
renderSideBySide: true,
|
renderSideBySide: true,
|
||||||
renderSideBySideInlineBreakpoint: 0,
|
renderSideBySideInlineBreakpoint: 0,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
fontSize: 13,
|
fontSize: (EDITOR_BASE_FONT_SIZE * zoomLevel) / 100,
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
renderOverviewRuler: false,
|
renderOverviewRuler: false,
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ import { Streamdown } from "streamdown"
|
|||||||
import { readFileBase64 } from "@/lib/api"
|
import { readFileBase64 } from "@/lib/api"
|
||||||
import { normalizeMathDelimiters } from "@/components/ai-elements/message"
|
import { normalizeMathDelimiters } from "@/components/ai-elements/message"
|
||||||
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
|
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
|
||||||
|
import { useZoomLevel } from "@/hooks/use-appearance"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
|
||||||
|
const EDITOR_BASE_FONT_SIZE = 13
|
||||||
import "@/lib/monaco-local"
|
import "@/lib/monaco-local"
|
||||||
|
|
||||||
const math = createMathPlugin({ singleDollarTextMath: true })
|
const math = createMathPlugin({ singleDollarTextMath: true })
|
||||||
@@ -770,6 +773,7 @@ export function FileWorkspacePanel() {
|
|||||||
const cursorListenerRef = useRef<{ dispose: () => void } | null>(null)
|
const cursorListenerRef = useRef<{ dispose: () => void } | null>(null)
|
||||||
const gitChangeDecorationsRef = useRef<string[]>([])
|
const gitChangeDecorationsRef = useRef<string[]>([])
|
||||||
const editorTheme = useMonacoThemeSync()
|
const editorTheme = useMonacoThemeSync()
|
||||||
|
const { zoomLevel } = useZoomLevel()
|
||||||
const [editorMountVersion, setEditorMountVersion] = useState(0)
|
const [editorMountVersion, setEditorMountVersion] = useState(0)
|
||||||
const [cursorLine, setCursorLine] = useState(1)
|
const [cursorLine, setCursorLine] = useState(1)
|
||||||
const [collapsedFiles, setCollapsedFiles] = useState<Record<string, boolean>>(
|
const [collapsedFiles, setCollapsedFiles] = useState<Record<string, boolean>>(
|
||||||
@@ -1610,7 +1614,7 @@ export function FileWorkspacePanel() {
|
|||||||
readOnly: !canEdit,
|
readOnly: !canEdit,
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
fontSize: 13,
|
fontSize: (EDITOR_BASE_FONT_SIZE * zoomLevel) / 100,
|
||||||
lineNumbersMinChars,
|
lineNumbersMinChars,
|
||||||
lineDecorationsWidth: 10,
|
lineDecorationsWidth: 10,
|
||||||
wordWrap: "off",
|
wordWrap: "off",
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import type { editor as MonacoEditorNs, IRange } from "monaco-editor"
|
|||||||
import { ArrowLeft, ArrowRight, CheckCheck } from "lucide-react"
|
import { ArrowLeft, ArrowRight, CheckCheck } from "lucide-react"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
|
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
|
||||||
|
import { useZoomLevel } from "@/hooks/use-appearance"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const EDITOR_BASE_FONT_SIZE = 13
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
@@ -55,6 +58,7 @@ export function ThreePaneMergeEditor({
|
|||||||
}: ThreePaneMergeEditorProps) {
|
}: ThreePaneMergeEditorProps) {
|
||||||
const t = useTranslations("MergePage")
|
const t = useTranslations("MergePage")
|
||||||
const editorTheme = useMonacoThemeSync()
|
const editorTheme = useMonacoThemeSync()
|
||||||
|
const { zoomLevel } = useZoomLevel()
|
||||||
const { registerEditor } = useSyncScroll()
|
const { registerEditor } = useSyncScroll()
|
||||||
|
|
||||||
const leftEditorRef = useRef<MonacoEditorNs.IStandaloneCodeEditor | null>(
|
const leftEditorRef = useRef<MonacoEditorNs.IStandaloneCodeEditor | null>(
|
||||||
@@ -497,23 +501,30 @@ export function ThreePaneMergeEditor({
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Editor options
|
// Editor options
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const editorOptions: MonacoEditorNs.IStandaloneEditorConstructionOptions = {
|
const editorOptions =
|
||||||
fontSize: 13,
|
useMemo<MonacoEditorNs.IStandaloneEditorConstructionOptions>(
|
||||||
minimap: { enabled: false },
|
() => ({
|
||||||
scrollBeyondLastLine: false,
|
fontSize: (EDITOR_BASE_FONT_SIZE * zoomLevel) / 100,
|
||||||
automaticLayout: true,
|
minimap: { enabled: false },
|
||||||
lineNumbers: "on",
|
scrollBeyondLastLine: false,
|
||||||
glyphMargin: true,
|
automaticLayout: true,
|
||||||
folding: false,
|
lineNumbers: "on",
|
||||||
wordWrap: "off",
|
glyphMargin: true,
|
||||||
overviewRulerLanes: 0,
|
folding: false,
|
||||||
}
|
wordWrap: "off",
|
||||||
|
overviewRulerLanes: 0,
|
||||||
|
}),
|
||||||
|
[zoomLevel]
|
||||||
|
)
|
||||||
|
|
||||||
const readonlyOptions = {
|
const readonlyOptions = useMemo(
|
||||||
...editorOptions,
|
() => ({
|
||||||
readOnly: true,
|
...editorOptions,
|
||||||
domReadOnly: true,
|
readOnly: true,
|
||||||
}
|
domReadOnly: true,
|
||||||
|
}),
|
||||||
|
[editorOptions]
|
||||||
|
)
|
||||||
|
|
||||||
const loadingEl = (
|
const loadingEl = (
|
||||||
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">
|
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -8,8 +8,15 @@ import {
|
|||||||
terminalResize,
|
terminalResize,
|
||||||
terminalKill,
|
terminalKill,
|
||||||
} from "@/lib/api"
|
} from "@/lib/api"
|
||||||
|
import { useZoomLevel } from "@/hooks/use-appearance"
|
||||||
import type { TerminalEvent } from "@/lib/types"
|
import type { TerminalEvent } from "@/lib/types"
|
||||||
import type { ITheme } from "@xterm/xterm"
|
import type { ITheme, Terminal as XTermTerminal } from "@xterm/xterm"
|
||||||
|
|
||||||
|
const TERMINAL_BASE_FONT_SIZE = 13
|
||||||
|
|
||||||
|
function computeTerminalFontSize(zoomLevel: number): number {
|
||||||
|
return Math.round((TERMINAL_BASE_FONT_SIZE * zoomLevel) / 100)
|
||||||
|
}
|
||||||
|
|
||||||
const DARK_THEME: ITheme = {
|
const DARK_THEME: ITheme = {
|
||||||
background: "#1a1a1a",
|
background: "#1a1a1a",
|
||||||
@@ -108,11 +115,13 @@ export function TerminalView({
|
|||||||
}: TerminalViewProps) {
|
}: TerminalViewProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const fitAddonRef = useRef<{ fit: () => void } | null>(null)
|
const fitAddonRef = useRef<{ fit: () => void } | null>(null)
|
||||||
const termRef = useRef<{ focus: () => void } | null>(null)
|
const termRef = useRef<XTermTerminal | null>(null)
|
||||||
const lastResizeRef = useRef<{ cols: number; rows: number } | null>(null)
|
const lastResizeRef = useRef<{ cols: number; rows: number } | null>(null)
|
||||||
const isActiveRef = useRef(isActive)
|
const isActiveRef = useRef(isActive)
|
||||||
const isVisibleRef = useRef(isVisible)
|
const isVisibleRef = useRef(isVisible)
|
||||||
const onProcessExitedRef = useRef(onProcessExited)
|
const onProcessExitedRef = useRef(onProcessExited)
|
||||||
|
const { zoomLevel } = useZoomLevel()
|
||||||
|
const zoomLevelRef = useRef(zoomLevel)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -140,7 +149,7 @@ export function TerminalView({
|
|||||||
|
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
fontSize: 13,
|
fontSize: computeTerminalFontSize(zoomLevelRef.current),
|
||||||
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
|
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
|
||||||
theme: getTerminalTheme(containerRef.current),
|
theme: getTerminalTheme(containerRef.current),
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
@@ -292,6 +301,25 @@ export function TerminalView({
|
|||||||
}
|
}
|
||||||
}, [isActive, isVisible])
|
}, [isActive, isVisible])
|
||||||
|
|
||||||
|
// React to zoom level changes. Updates the ref synchronously so async init()
|
||||||
|
// always reads the latest zoom, and pushes the new font size to already-mounted
|
||||||
|
// terminals. Double rAF ensures xterm's renderer has recomputed cell metrics
|
||||||
|
// before we refit.
|
||||||
|
useEffect(() => {
|
||||||
|
zoomLevelRef.current = zoomLevel
|
||||||
|
const term = termRef.current
|
||||||
|
if (!term) return
|
||||||
|
term.options.fontSize = computeTerminalFontSize(zoomLevel)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const el = containerRef.current
|
||||||
|
if (el && el.clientWidth > 0 && el.clientHeight > 0) {
|
||||||
|
fitAddonRef.current?.fit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [zoomLevel])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 h-full w-full p-2"
|
className="absolute inset-0 h-full w-full p-2"
|
||||||
|
|||||||
Reference in New Issue
Block a user