From 91ee29a80839ea8ad6741949396bbdb0136f4b8b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:25:17 -0700 Subject: [PATCH] feat(editor): add an opt-in editor font family (default: follow terminal font) (#9658) The code editor was hard-wired to the terminal font, so Nerd Font 'Mono' CJK glyphs overlapped in the editor. Adds an opt-in editorFontFamily setting; default is empty so the resolved value is byte-identical to today for every existing user. Closes #9628 --- .../src/components/editor/DiffSectionBody.tsx | 6 +- .../src/components/editor/DiffSectionItem.tsx | 4 +- .../src/components/editor/DiffViewer.tsx | 4 +- .../src/components/editor/IpynbViewer.tsx | 10 +- .../components/editor/MonacoCodeExcerpt.tsx | 4 +- .../editor/MonacoEditor.font-family.test.tsx | 91 +++++++++++++++++++ .../src/components/editor/MonacoEditor.tsx | 4 +- .../settings/EditorFontFamilySetting.tsx | 56 ++++++++++++ .../settings/GeneralEditorSettingsSection.tsx | 55 ++++------- .../src/components/settings/GeneralPane.tsx | 8 +- .../src/components/settings/Settings.tsx | 2 + .../settings/auto-save-delay-draft.ts | 35 +++++++ .../settings/general-editor-search.ts | 15 +++ .../src/components/settings/setting-labels.ts | 1 + src/renderer/src/i18n/locales/en.json | 9 +- src/renderer/src/i18n/locales/es.json | 9 +- src/renderer/src/i18n/locales/ja.json | 9 +- src/renderer/src/i18n/locales/ko.json | 9 +- src/renderer/src/i18n/locales/zh.json | 9 +- src/renderer/src/lib/editor-font-zoom.test.ts | 55 ++++++++++- src/renderer/src/lib/editor-font-zoom.ts | 20 ++++ src/shared/constants.ts | 2 + src/shared/types.ts | 2 + 23 files changed, 361 insertions(+), 58 deletions(-) create mode 100644 src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx create mode 100644 src/renderer/src/components/settings/EditorFontFamilySetting.tsx create mode 100644 src/renderer/src/components/settings/auto-save-delay-draft.ts diff --git a/src/renderer/src/components/editor/DiffSectionBody.tsx b/src/renderer/src/components/editor/DiffSectionBody.tsx index dbea8ab50ef..8a1dc3cef4d 100644 --- a/src/renderer/src/components/editor/DiffSectionBody.tsx +++ b/src/renderer/src/components/editor/DiffSectionBody.tsx @@ -36,7 +36,7 @@ type DiffSectionBodyProps = { isEditable: boolean diffEditorFontSize: number diffWordWrap?: boolean - terminalFontFamily?: string + editorFontFamily?: string onCancelComment: () => void onSubmitComment: (body: string) => Promise onRetrySection: (index: number) => void @@ -61,7 +61,7 @@ export function DiffSectionBody({ isEditable, diffEditorFontSize, diffWordWrap, - terminalFontFamily, + editorFontFamily, onCancelComment, onSubmitComment, onRetrySection, @@ -191,7 +191,7 @@ export function DiffSectionBody({ minimap: { enabled: false }, scrollBeyondLastLine: false, fontSize: diffEditorFontSize, - fontFamily: terminalFontFamily || 'monospace', + fontFamily: editorFontFamily || 'monospace', lineNumbers: 'on', ...buildDiffEditorWordWrapOptions(diffWordWrap), automaticLayout: true, diff --git a/src/renderer/src/components/editor/DiffSectionItem.tsx b/src/renderer/src/components/editor/DiffSectionItem.tsx index a070c38cb66..8b2c143f660 100644 --- a/src/renderer/src/components/editor/DiffSectionItem.tsx +++ b/src/renderer/src/components/editor/DiffSectionItem.tsx @@ -12,7 +12,7 @@ import type { editor as monacoEditor } from 'monaco-editor' import { monaco } from '@/lib/monaco-setup' import { detectLanguage } from '@/lib/language-detect' import { useAppStore } from '@/store' -import { computeDiffEditorFontSize } from '@/lib/editor-font-zoom' +import { computeDiffEditorFontSize, resolveEditorFontFamily } from '@/lib/editor-font-zoom' import { selectWorktreeDiffComments } from '@/store/worktree-diff-comments-selector' import { useDiffCommentDecorator, @@ -431,7 +431,7 @@ export function DiffSectionItem({ isEditable={isEditable} diffEditorFontSize={diffEditorFontSize} diffWordWrap={settings?.diffWordWrap} - terminalFontFamily={settings?.terminalFontFamily} + editorFontFamily={resolveEditorFontFamily(settings)} onCancelComment={() => setPopover(null)} onSubmitComment={handleSubmitComment} onRetrySection={retrySection} diff --git a/src/renderer/src/components/editor/DiffViewer.tsx b/src/renderer/src/components/editor/DiffViewer.tsx index 8bf174afb21..5aed5c9670d 100644 --- a/src/renderer/src/components/editor/DiffViewer.tsx +++ b/src/renderer/src/components/editor/DiffViewer.tsx @@ -4,7 +4,7 @@ import type { editor } from 'monaco-editor' import { useAppStore } from '@/store' import { diffViewStateCache, setWithLRU } from '@/lib/scroll-cache' import { monaco } from '@/lib/monaco-setup' -import { computeDiffEditorFontSize } from '@/lib/editor-font-zoom' +import { computeDiffEditorFontSize, resolveEditorFontFamily } from '@/lib/editor-font-zoom' import { useContextualCopySetup } from './useContextualCopySetup' import { selectWorktreeDiffComments } from '@/store/worktree-diff-comments-selector' import { useDiffCommentDecorator } from '../diff-comments/useDiffCommentDecorator' @@ -422,7 +422,7 @@ export default function DiffViewer({ minimap: { enabled: false }, scrollBeyondLastLine: false, fontSize: diffEditorFontSize, - fontFamily: settings?.terminalFontFamily || 'monospace', + fontFamily: resolveEditorFontFamily(settings), lineNumbers: 'on', ...buildDiffEditorWordWrapOptions(settings?.diffWordWrap), automaticLayout: true, diff --git a/src/renderer/src/components/editor/IpynbViewer.tsx b/src/renderer/src/components/editor/IpynbViewer.tsx index b71afececb2..bbf7ded32ed 100644 --- a/src/renderer/src/components/editor/IpynbViewer.tsx +++ b/src/renderer/src/components/editor/IpynbViewer.tsx @@ -32,7 +32,11 @@ import { Trash2 } from 'lucide-react' import { monaco } from '@/lib/monaco-setup' -import { computeEditorFontSize } from '@/lib/editor-font-zoom' +import { + computeEditorFontSize, + resolveEditorFontFamily, + resolveEditorFontFamilyOrInherit +} from '@/lib/editor-font-zoom' import { getConnectionId } from '@/lib/connection-context' import { resolveDocumentTheme } from '@/lib/document-theme' import { useAppStore } from '@/store' @@ -409,7 +413,7 @@ function CodeCell({ onChange={(value) => onChange(value ?? '')} options={{ automaticLayout: true, - fontFamily: settings?.terminalFontFamily || 'monospace', + fontFamily: resolveEditorFontFamily(settings), fontSize, glyphMargin: false, lineNumbersMinChars: 3, @@ -877,7 +881,7 @@ export default function IpynbViewer({
diff --git a/src/renderer/src/components/editor/MonacoCodeExcerpt.tsx b/src/renderer/src/components/editor/MonacoCodeExcerpt.tsx index 9c868a61130..a13ed13e1b7 100644 --- a/src/renderer/src/components/editor/MonacoCodeExcerpt.tsx +++ b/src/renderer/src/components/editor/MonacoCodeExcerpt.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react' import { monaco } from '@/lib/monaco-setup' -import { computeEditorFontSize } from '@/lib/editor-font-zoom' +import { computeEditorFontSize, resolveEditorFontFamily } from '@/lib/editor-font-zoom' import { resolveDocumentTheme } from '@/lib/document-theme' import { useAppStore } from '@/store' import { cn } from '@/lib/utils' @@ -51,7 +51,7 @@ export default function MonacoCodeExcerpt({ settings?.terminalFontSize ?? 13, editorFontZoomLevel ) - const fontFamily = settings?.terminalFontFamily || 'monospace' + const fontFamily = resolveEditorFontFamily(settings) const isDark = resolveDocumentTheme(settings?.theme ?? 'system') const code = useMemo(() => lines.join('\n'), [lines]) const [htmlLines, setHtmlLines] = useState(() => lines.map(() => '')) diff --git a/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx b/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx new file mode 100644 index 00000000000..9e49a3d6ed3 --- /dev/null +++ b/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx @@ -0,0 +1,91 @@ +// @vitest-environment happy-dom +import { cleanup, render } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' + +const editorProps = vi.hoisted(() => ({ current: null as Record | null })) +const storeState = vi.hoisted(() => ({ + current: { + theme: 'dark', + terminalFontSize: 13, + terminalFontFamily: 'D2Coding Nerd Font Mono', + editorFontFamily: '' + } as Record +})) + +vi.mock('@monaco-editor/react', () => ({ + default: (props: Record) => { + editorProps.current = props + return null + }, + loader: { config: vi.fn() } +})) +vi.mock('@/store', () => ({ + useAppStore: (selector: (state: Record) => unknown) => + selector({ + settings: storeState.current, + editorFontZoomLevel: 0, + setPendingEditorReveal: vi.fn(), + setEditorCursorLine: vi.fn(), + addDiffComment: vi.fn(), + deleteDiffComment: vi.fn(), + updateDiffComment: vi.fn(), + scrollToDiffCommentId: null, + setScrollToDiffCommentId: vi.fn(), + worktreeDiffComments: {} + }) +})) +vi.mock('../diff-comments/useDiffCommentDecorator', () => ({ + useDiffCommentDecorator: vi.fn() +})) +vi.mock('./useContextualCopySetup', () => ({ + useContextualCopySetup: () => ({ setupCopy: vi.fn(), toastNode: null }) +})) + +import MonacoEditor from './MonacoEditor' + +function renderEditor(): void { + render( + + ) +} + +afterEach(() => { + cleanup() + editorProps.current = null +}) + +describe('MonacoEditor font family', () => { + it('follows the terminal font when no editor font override is set', () => { + storeState.current = { + theme: 'dark', + terminalFontSize: 13, + terminalFontFamily: 'D2Coding Nerd Font Mono', + editorFontFamily: '' + } + renderEditor() + const options = editorProps.current?.options as Record | undefined + expect(options?.fontFamily).toBe('D2Coding Nerd Font Mono') + }) + + it('uses the opt-in editor font override instead of the terminal font', () => { + storeState.current = { + theme: 'dark', + terminalFontSize: 13, + terminalFontFamily: 'D2Coding Nerd Font Mono', + editorFontFamily: 'D2Coding Nerd Font' + } + renderEditor() + const options = editorProps.current?.options as Record | undefined + expect(options?.fontFamily).toBe('D2Coding Nerd Font') + }) +}) diff --git a/src/renderer/src/components/editor/MonacoEditor.tsx b/src/renderer/src/components/editor/MonacoEditor.tsx index 8caad87f633..dbb5bd0b241 100644 --- a/src/renderer/src/components/editor/MonacoEditor.tsx +++ b/src/renderer/src/components/editor/MonacoEditor.tsx @@ -8,7 +8,7 @@ import type { MarkdownDocument } from '../../../../shared/types' import { useAppStore } from '@/store' import { scrollTopCache, cursorPositionCache, setWithLRU } from '@/lib/scroll-cache' import '@/lib/monaco-setup' -import { computeEditorFontSize } from '@/lib/editor-font-zoom' +import { computeEditorFontSize, resolveEditorFontFamily } from '@/lib/editor-font-zoom' import { registerFileSearchSelectedTextProvider } from '@/lib/file-search-selection' import { useContextualCopySetup } from './useContextualCopySetup' @@ -152,7 +152,7 @@ export default function MonacoEditor({ settings?.terminalFontSize ?? 13, editorFontZoomLevel ) - const editorFontFamily = settings?.terminalFontFamily || 'monospace' + const editorFontFamily = resolveEditorFontFamily(settings) const editorWordWrap = settings?.editorWordWrap const estimatedAutoHeight = useMemo(() => { if (!autoHeight) { diff --git a/src/renderer/src/components/settings/EditorFontFamilySetting.tsx b/src/renderer/src/components/settings/EditorFontFamilySetting.tsx new file mode 100644 index 00000000000..911a4e2fde1 --- /dev/null +++ b/src/renderer/src/components/settings/EditorFontFamilySetting.tsx @@ -0,0 +1,56 @@ +import type React from 'react' +import type { GlobalSettings } from '../../../../shared/types' +import { translate } from '@/i18n/i18n' +import { SearchableSetting } from './SearchableSetting' +import { FontAutocomplete, SettingsRow } from './SettingsFormControls' + +type EditorFontFamilySettingProps = { + settings: GlobalSettings + updateSettings: (updates: Partial) => void + fontSuggestions: string[] + onRequestFontSuggestions?: () => void +} + +export function EditorFontFamilySetting({ + settings, + updateSettings, + fontSuggestions, + onRequestFontSuggestions +}: EditorFontFamilySettingProps): React.JSX.Element { + return ( + + updateSettings({ editorFontFamily: value })} + /> + } + /> + + ) +} diff --git a/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx b/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx index 2a524600b07..b8d710b6886 100644 --- a/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx +++ b/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx @@ -18,51 +18,25 @@ import { import { translate } from '@/i18n/i18n' import { RichMarkdownSpellcheckSetting } from './RichMarkdownSpellcheckSetting' import { EditorWordWrapSetting } from './EditorWordWrapSetting' - -export type AutoSaveDelayDraftState = { - sourceDelayMs: number - draft: string -} - -export function createAutoSaveDelayDraftState( - editorAutoSaveDelayMs: number -): AutoSaveDelayDraftState { - return { - sourceDelayMs: editorAutoSaveDelayMs, - draft: String(editorAutoSaveDelayMs) - } -} - -function resolveAutoSaveDelayDraftState( - state: AutoSaveDelayDraftState, - editorAutoSaveDelayMs: number -): AutoSaveDelayDraftState { - return state.sourceDelayMs === editorAutoSaveDelayMs - ? state - : createAutoSaveDelayDraftState(editorAutoSaveDelayMs) -} - -export function updateAutoSaveDelayDraftState( - state: AutoSaveDelayDraftState, - editorAutoSaveDelayMs: number, - draft: string -): AutoSaveDelayDraftState { - return { - // Why: settings persistence is async, so a committed draft must stay tied - // to the current source until the persisted value reloads. - ...resolveAutoSaveDelayDraftState(state, editorAutoSaveDelayMs), - draft - } -} +import { EditorFontFamilySetting } from './EditorFontFamilySetting' +import { + createAutoSaveDelayDraftState, + resolveAutoSaveDelayDraftState, + updateAutoSaveDelayDraftState +} from './auto-save-delay-draft' type GeneralEditorSettingsSectionProps = { settings: GlobalSettings updateSettings: (updates: Partial) => void + fontSuggestions: string[] + onRequestFontSuggestions?: () => void } export function GeneralEditorSettingsSection({ settings, - updateSettings + updateSettings, + fontSuggestions, + onRequestFontSuggestions }: GeneralEditorSettingsSectionProps): React.JSX.Element { const [autoSaveDelayDraftState, setAutoSaveDelayDraftState] = useState(() => createAutoSaveDelayDraftState(settings.editorAutoSaveDelayMs) @@ -249,6 +223,13 @@ export function GeneralEditorSettingsSection({ /> + + [number] @@ -79,6 +79,8 @@ const EMPTY_WSL_DISTROS: string[] = [] type GeneralPaneProps = { settings: GlobalSettings updateSettings: (updates: Partial) => void + fontSuggestions: string[] + onRequestFontSuggestions?: () => void wslSupportedPlatform?: boolean wslAvailable?: boolean wslDistros?: string[] @@ -88,6 +90,8 @@ type GeneralPaneProps = { export function GeneralPane({ settings, updateSettings, + fontSuggestions, + onRequestFontSuggestions, wslSupportedPlatform, wslAvailable, wslDistros = EMPTY_WSL_DISTROS, @@ -177,6 +181,8 @@ export function GeneralPane({ key="editor" settings={settings} updateSettings={updateSettings} + fontSuggestions={fontSuggestions} + onRequestFontSuggestions={onRequestFontSuggestions} /> ) : null, matchesSettingsSearch(searchQuery, getGeneralCliSearchEntries()) ? ( diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx index 4c4f99ad4eb..d35ca40237a 100644 --- a/src/renderer/src/components/settings/Settings.tsx +++ b/src/renderer/src/components/settings/Settings.tsx @@ -1290,6 +1290,8 @@ function Settings(): React.JSX.Element { [ ) ] }, + { + title: translate( + 'auto.components.settings.general.search.editorFontFamily', + 'Editor Font Family' + ), + description: translate( + 'auto.components.settings.general.search.editorFontFamilyDesc', + 'Font used by file editors and diff views. Leave empty to follow the terminal font.' + ), + keywords: [ + ...translateSearchKeyword('auto.components.settings.general.search.e1ee631696', 'editor'), + ...translateSearchKeyword('auto.components.settings.general.search.editorFontKw', 'font'), + ...translateSearchKeyword('auto.components.settings.general.search.3ca5ab78a5', 'code') + ] + }, { title: translate('auto.components.settings.general.search.e61157e926', 'Editor Word Wrap'), description: translate( diff --git a/src/renderer/src/components/settings/setting-labels.ts b/src/renderer/src/components/settings/setting-labels.ts index 0f108fadeae..95eb3a26c7c 100644 --- a/src/renderer/src/components/settings/setting-labels.ts +++ b/src/renderer/src/components/settings/setting-labels.ts @@ -3,6 +3,7 @@ import type { GlobalSettings } from '../../../../shared/types' export const SETTING_LABELS: Partial> = { terminalFontSize: 'Font Size', terminalFontFamily: 'Font Family', + editorFontFamily: 'Editor Font Family', terminalFontWeight: 'Font Weight', terminalLineHeight: 'Line Height', terminalScrollSensitivity: 'Normal Scroll Speed', diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 3af70abb65f..95a9c72f64c 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -7879,7 +7879,9 @@ "d2d2d929c0": "Rich Markdown Spellcheck", "4497e2e2bb": "Show browser spelling underlines and suggestions while editing rich Markdown.", "e61157e926": "Editor Word Wrap", - "005be5c699": "Wrap long lines in file editors instead of requiring horizontal scrolling." + "005be5c699": "Wrap long lines in file editors instead of requiring horizontal scrolling.", + "editorFontFamily": "Editor Font Family", + "editorFontFamilyDesc": "Font used by file editors and diff views. Leave empty to follow the terminal font." } }, "git": { @@ -9271,6 +9273,11 @@ }, "MobileRelayBetaNotice": { "notice": "Orca Relay is in beta." + }, + "EditorFontFamilySetting": { + "title": "Editor Font Family", + "description": "Font used by file editors and diff views. Leave empty to follow the terminal font.", + "placeholder": "Same as terminal font" } }, "right": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 5932350ebeb..2f0ded12e1c 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -7819,7 +7819,9 @@ "d2d2d929c0": "Corrector ortográfico de Rich Markdown", "4497e2e2bb": "Muestra subrayados y sugerencias ortográficas del navegador al editar Rich Markdown.", "e61157e926": "Ajuste de línea en el editor", - "005be5c699": "Ajusta las líneas largas en editores de archivos en lugar de requerir desplazamiento horizontal." + "005be5c699": "Ajusta las líneas largas en editores de archivos en lugar de requerir desplazamiento horizontal.", + "editorFontFamily": "Editor Font Family", + "editorFontFamilyDesc": "Font used by file editors and diff views. Leave empty to follow the terminal font." } }, "git": { @@ -9248,6 +9250,11 @@ }, "MobileRelayBetaNotice": { "notice": "Orca Relay is in beta." + }, + "EditorFontFamilySetting": { + "title": "Editor Font Family", + "description": "Font used by file editors and diff views. Leave empty to follow the terminal font.", + "placeholder": "Same as terminal font" } }, "right": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index aa76a652aba..713a93f15f9 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -7841,7 +7841,9 @@ "d2d2d929c0": "Rich Markdown Spellcheck", "4497e2e2bb": "Show browser spelling underlines and suggestions while editing rich Markdown.", "e61157e926": "エディターのワードラップ", - "005be5c699": "水平スクロールを使わずに、ファイルエディターで長い行を折り返します。" + "005be5c699": "水平スクロールを使わずに、ファイルエディターで長い行を折り返します。", + "editorFontFamily": "Editor Font Family", + "editorFontFamilyDesc": "Font used by file editors and diff views. Leave empty to follow the terminal font." } }, "git": { @@ -9248,6 +9250,11 @@ }, "MobileRelayBetaNotice": { "notice": "Orca Relay is in beta." + }, + "EditorFontFamilySetting": { + "title": "Editor Font Family", + "description": "Font used by file editors and diff views. Leave empty to follow the terminal font.", + "placeholder": "Same as terminal font" } }, "right": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index d2a37623629..d1a8a8ac854 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -7804,7 +7804,9 @@ "d2d2d929c0": "리치 Markdown 맞춤법 검사", "4497e2e2bb": "리치 Markdown을 편집하는 동안 브라우저 맞춤법 밑줄과 제안을 표시합니다.", "e61157e926": "편집기 자동 줄 바꿈", - "005be5c699": "가로 스크롤 대신 파일 편집기에서 긴 줄을 자동으로 줄 바꿈합니다." + "005be5c699": "가로 스크롤 대신 파일 편집기에서 긴 줄을 자동으로 줄 바꿈합니다.", + "editorFontFamily": "Editor Font Family", + "editorFontFamilyDesc": "Font used by file editors and diff views. Leave empty to follow the terminal font." } }, "git": { @@ -9248,6 +9250,11 @@ }, "MobileRelayBetaNotice": { "notice": "Orca Relay is in beta." + }, + "EditorFontFamilySetting": { + "title": "Editor Font Family", + "description": "Font used by file editors and diff views. Leave empty to follow the terminal font.", + "placeholder": "Same as terminal font" } }, "right": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 8a38f538366..1a3caf2499a 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -7804,7 +7804,9 @@ "d2d2d929c0": "Rich Markdown Spellcheck", "4497e2e2bb": "在编辑富 Markdown 时显示浏览器拼写下划线和建议。", "e61157e926": "编辑器自动换行", - "005be5c699": "在文件编辑器中自动换行长行,而无需水平滚动。" + "005be5c699": "在文件编辑器中自动换行长行,而无需水平滚动。", + "editorFontFamily": "Editor Font Family", + "editorFontFamilyDesc": "Font used by file editors and diff views. Leave empty to follow the terminal font." } }, "git": { @@ -9248,6 +9250,11 @@ }, "MobileRelayBetaNotice": { "notice": "Orca Relay is in beta." + }, + "EditorFontFamilySetting": { + "title": "Editor Font Family", + "description": "Font used by file editors and diff views. Leave empty to follow the terminal font.", + "placeholder": "Same as terminal font" } }, "right": { diff --git a/src/renderer/src/lib/editor-font-zoom.test.ts b/src/renderer/src/lib/editor-font-zoom.test.ts index e59a4efc9f5..7ae12537621 100644 --- a/src/renderer/src/lib/editor-font-zoom.test.ts +++ b/src/renderer/src/lib/editor-font-zoom.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest' -import { computeDiffEditorFontSize, computeEditorFontSize } from './editor-font-zoom' +import { + computeDiffEditorFontSize, + computeEditorFontSize, + resolveEditorFontFamily, + resolveEditorFontFamilyOrInherit +} from './editor-font-zoom' describe('editor font zoom', () => { it('keeps diff editors smaller than regular editor surfaces', () => { @@ -13,3 +18,51 @@ describe('editor font zoom', () => { expect(computeDiffEditorFontSize(24, 18)).toBe(32) }) }) + +describe('resolveEditorFontFamily', () => { + it('follows the terminal font when no editor font is set (byte-identical to legacy behavior)', () => { + expect(resolveEditorFontFamily({ terminalFontFamily: 'D2Coding Nerd Font Mono' })).toBe( + 'D2Coding Nerd Font Mono' + ) + }) + + it('treats an empty/whitespace editor font as unset and follows the terminal font', () => { + expect(resolveEditorFontFamily({ editorFontFamily: '', terminalFontFamily: 'Menlo' })).toBe( + 'Menlo' + ) + expect(resolveEditorFontFamily({ editorFontFamily: ' ', terminalFontFamily: 'Menlo' })).toBe( + 'Menlo' + ) + }) + + it('uses the editor font override when the user opts in', () => { + expect( + resolveEditorFontFamily({ editorFontFamily: 'JetBrains Mono', terminalFontFamily: 'Menlo' }) + ).toBe('JetBrains Mono') + }) + + it('falls back to monospace when neither font is set', () => { + expect(resolveEditorFontFamily(undefined)).toBe('monospace') + expect(resolveEditorFontFamily({})).toBe('monospace') + }) +}) + +describe('resolveEditorFontFamilyOrInherit', () => { + it('returns undefined (inherit UI font) when neither font is set', () => { + expect(resolveEditorFontFamilyOrInherit({})).toBeUndefined() + expect(resolveEditorFontFamilyOrInherit(undefined)).toBeUndefined() + }) + + it('follows the terminal font when no editor override is set', () => { + expect(resolveEditorFontFamilyOrInherit({ terminalFontFamily: 'Menlo' })).toBe('Menlo') + }) + + it('uses the editor font override when set', () => { + expect( + resolveEditorFontFamilyOrInherit({ + editorFontFamily: 'Fira Code', + terminalFontFamily: 'Menlo' + }) + ).toBe('Fira Code') + }) +}) diff --git a/src/renderer/src/lib/editor-font-zoom.ts b/src/renderer/src/lib/editor-font-zoom.ts index 88c9d702f96..a06bd556263 100644 --- a/src/renderer/src/lib/editor-font-zoom.ts +++ b/src/renderer/src/lib/editor-font-zoom.ts @@ -30,3 +30,23 @@ export function computeDiffEditorFontSize(baseFontSize: number, zoomLevel: numbe // terminal font size makes review views feel oversized relative to app chrome. return computeEditorFontSize(baseFontSize - 0.5, zoomLevel) } + +export type EditorFontFamilySettings = { + editorFontFamily?: string + terminalFontFamily?: string +} + +/** + * Why: the editor font is opt-in and defaults to empty, so an unset value must + * keep falling back to the terminal font exactly as before the setting existed. + */ +export function resolveEditorFontFamily(settings?: EditorFontFamilySettings | null): string { + return settings?.editorFontFamily?.trim() || settings?.terminalFontFamily || 'monospace' +} + +/** Same resolution, but keeps the notebook shell's "no font set → inherit UI font" fallback. */ +export function resolveEditorFontFamilyOrInherit( + settings?: EditorFontFamilySettings | null +): string | undefined { + return settings?.editorFontFamily?.trim() || settings?.terminalFontFamily || undefined +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 1ccd9e4883f..41b290eb06f 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -191,6 +191,8 @@ export function getDefaultSettings(homedir: string): GlobalSettings { editorAutoSave: false, editorAutoSaveDelayMs: DEFAULT_EDITOR_AUTO_SAVE_DELAY_MS, editorMinimapEnabled: false, + // Why empty: the editor keeps following the terminal font unless the user opts in. + editorFontFamily: '', editorWordWrap: true, richMarkdownSpellcheckEnabled: true, markdownReviewToolsEnabled: true, diff --git a/src/shared/types.ts b/src/shared/types.ts index d9b597f4638..1e722b695f7 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -2602,6 +2602,8 @@ export type GlobalSettings = { editorAutoSave: boolean editorAutoSaveDelayMs: number editorMinimapEnabled: boolean + /** Opt-in code-editor font; empty (the default) keeps following `terminalFontFamily`. */ + editorFontFamily?: string /** Defaults on for profiles saved before file-editor wrapping became configurable. */ editorWordWrap?: boolean /** Persisted opt-out for browser spellcheck noise in rich Markdown editing surfaces. */