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
This commit is contained in:
Neil
2026-07-20 21:25:17 -07:00
committed by GitHub
parent 8847f615ac
commit 91ee29a808
23 changed files with 361 additions and 58 deletions
@@ -36,7 +36,7 @@ type DiffSectionBodyProps = {
isEditable: boolean
diffEditorFontSize: number
diffWordWrap?: boolean
terminalFontFamily?: string
editorFontFamily?: string
onCancelComment: () => void
onSubmitComment: (body: string) => Promise<void>
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,
@@ -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}
@@ -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,
@@ -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({
<div
ref={setRootRef}
className="h-full min-h-0 overflow-auto bg-editor-surface scrollbar-editor"
style={{ fontSize, fontFamily: settings?.terminalFontFamily || undefined }}
style={{ fontSize, fontFamily: resolveEditorFontFamilyOrInherit(settings) }}
onKeyDownCapture={handleNotebookKeyDownCapture}
onPointerDownCapture={handleNotebookPointerDownCapture}
>
@@ -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<string[]>(() => lines.map(() => ''))
@@ -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<string, unknown> | null }))
const storeState = vi.hoisted(() => ({
current: {
theme: 'dark',
terminalFontSize: 13,
terminalFontFamily: 'D2Coding Nerd Font Mono',
editorFontFamily: ''
} as Record<string, unknown>
}))
vi.mock('@monaco-editor/react', () => ({
default: (props: Record<string, unknown>) => {
editorProps.current = props
return null
},
loader: { config: vi.fn() }
}))
vi.mock('@/store', () => ({
useAppStore: (selector: (state: Record<string, unknown>) => 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(
<MonacoEditor
fileId="file"
filePath="/repo/file.py"
viewStateKey="pane:file"
relativePath="file.py"
content="# 한글 주석"
language="python"
onContentChange={vi.fn()}
onSave={vi.fn()}
readOnly
/>
)
}
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<string, unknown> | 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<string, unknown> | undefined
expect(options?.fontFamily).toBe('D2Coding Nerd Font')
})
})
@@ -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) {
@@ -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<GlobalSettings>) => void
fontSuggestions: string[]
onRequestFontSuggestions?: () => void
}
export function EditorFontFamilySetting({
settings,
updateSettings,
fontSuggestions,
onRequestFontSuggestions
}: EditorFontFamilySettingProps): React.JSX.Element {
return (
<SearchableSetting
title={translate(
'auto.components.settings.EditorFontFamilySetting.title',
'Editor Font Family'
)}
description={translate(
'auto.components.settings.EditorFontFamilySetting.description',
'Font used by file editors and diff views. Leave empty to follow the terminal font.'
)}
keywords={['editor', 'font', 'typography', 'family', 'code', 'cjk']}
>
<SettingsRow
label={translate(
'auto.components.settings.EditorFontFamilySetting.title',
'Editor Font Family'
)}
description={translate(
'auto.components.settings.EditorFontFamilySetting.description',
'Font used by file editors and diff views. Leave empty to follow the terminal font.'
)}
control={
<FontAutocomplete
value={settings.editorFontFamily ?? ''}
suggestions={fontSuggestions}
onRequestSuggestions={onRequestFontSuggestions}
placeholder={translate(
'auto.components.settings.EditorFontFamilySetting.placeholder',
'Same as terminal font'
)}
onChange={(value) => updateSettings({ editorFontFamily: value })}
/>
}
/>
</SearchableSetting>
)
}
@@ -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<GlobalSettings>) => 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({
/>
</SearchableSetting>
<EditorFontFamilySetting
settings={settings}
updateSettings={updateSettings}
fontSuggestions={fontSuggestions}
onRequestFontSuggestions={onRequestFontSuggestions}
/>
<EditorWordWrapSetting settings={settings} updateSettings={updateSettings} />
<SearchableSetting
@@ -28,7 +28,7 @@ export {
createAutoSaveDelayDraftState,
updateAutoSaveDelayDraftState,
type AutoSaveDelayDraftState
} from './GeneralEditorSettingsSection'
} from './auto-save-delay-draft'
export { shouldCommitOpenInApplicationsDraft } from './OpenInMenuSetting'
type GeneralSearchEntry = ReturnType<typeof getGeneralNavigationSearchEntries>[number]
@@ -79,6 +79,8 @@ const EMPTY_WSL_DISTROS: string[] = []
type GeneralPaneProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => 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()) ? (
@@ -1290,6 +1290,8 @@ function Settings(): React.JSX.Element {
<GeneralPane
settings={settings}
updateSettings={updateSettings}
fontSuggestions={terminalFontSuggestions}
onRequestFontSuggestions={requestFontSuggestions}
wslSupportedPlatform={wslSupportedPlatform}
wslAvailable={windowsTerminalCapabilities.wslAvailable}
wslDistros={windowsTerminalCapabilities.wslDistros}
@@ -0,0 +1,35 @@
export type AutoSaveDelayDraftState = {
sourceDelayMs: number
draft: string
}
export function createAutoSaveDelayDraftState(
editorAutoSaveDelayMs: number
): AutoSaveDelayDraftState {
return {
sourceDelayMs: editorAutoSaveDelayMs,
draft: String(editorAutoSaveDelayMs)
}
}
export 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
}
}
@@ -29,6 +29,21 @@ export const getGeneralEditorSearchEntries = createLocalizedCatalog(() => [
)
]
},
{
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(
@@ -3,6 +3,7 @@ import type { GlobalSettings } from '../../../../shared/types'
export const SETTING_LABELS: Partial<Record<keyof GlobalSettings, string>> = {
terminalFontSize: 'Font Size',
terminalFontFamily: 'Font Family',
editorFontFamily: 'Editor Font Family',
terminalFontWeight: 'Font Weight',
terminalLineHeight: 'Line Height',
terminalScrollSensitivity: 'Normal Scroll Speed',
+8 -1
View File
@@ -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": {
+8 -1
View File
@@ -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": {
+8 -1
View File
@@ -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": {
+8 -1
View File
@@ -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": {
+8 -1
View File
@@ -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": {
+54 -1
View File
@@ -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')
})
})
+20
View File
@@ -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
}
+2
View File
@@ -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,
+2
View File
@@ -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. */