feat(editor): add file editor word wrap preference (#8423)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
parent
2306f82113
commit
9dc1f253ad
|
|
@ -52,6 +52,7 @@ import {
|
|||
} from './monaco-markdown-selection-annotation'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { handleMonacoLargeTextPaste } from './monaco-large-text-paste'
|
||||
import { buildFileEditorWordWrapOptions } from './file-editor-word-wrap-options'
|
||||
import {
|
||||
clampMonacoAutoHeight,
|
||||
getMonacoAutoHeightForContent,
|
||||
|
|
@ -146,6 +147,8 @@ export default function MonacoEditor({
|
|||
settings?.terminalFontSize ?? 13,
|
||||
editorFontZoomLevel
|
||||
)
|
||||
const editorFontFamily = settings?.terminalFontFamily || 'monospace'
|
||||
const editorWordWrap = settings?.editorWordWrap
|
||||
const estimatedAutoHeight = useMemo(() => {
|
||||
if (!autoHeight) {
|
||||
return null
|
||||
|
|
@ -707,14 +710,15 @@ export default function MonacoEditor({
|
|||
|
||||
// Update editor options when settings change
|
||||
useEffect(() => {
|
||||
if (!editorRef.current || !settings) {
|
||||
if (!editorRef.current) {
|
||||
return
|
||||
}
|
||||
editorRef.current.updateOptions({
|
||||
fontSize: editorFontSize,
|
||||
fontFamily: settings.terminalFontFamily || 'monospace'
|
||||
fontFamily: editorFontFamily,
|
||||
...buildFileEditorWordWrapOptions(editorWordWrap)
|
||||
})
|
||||
}, [editorFontSize, settings])
|
||||
}, [editorFontFamily, editorFontSize, editorWordWrap])
|
||||
|
||||
useEffect(() => {
|
||||
markdownDocLinkDecorationsRef.current?.refresh()
|
||||
|
|
@ -833,9 +837,9 @@ export default function MonacoEditor({
|
|||
// setting into DiffViewer/DiffSectionItem would have no effect.
|
||||
minimap: { enabled: settings?.editorMinimapEnabled ?? false },
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
...buildFileEditorWordWrapOptions(editorWordWrap),
|
||||
fontSize: editorFontSize,
|
||||
fontFamily: settings?.terminalFontFamily || 'monospace',
|
||||
fontFamily: editorFontFamily,
|
||||
lineNumbers: 'on',
|
||||
renderLineHighlight: 'line',
|
||||
automaticLayout: true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { buildFileEditorWordWrapOptions } from './file-editor-word-wrap-options'
|
||||
|
||||
describe('buildFileEditorWordWrapOptions', () => {
|
||||
it('keeps wrapping enabled for existing profiles without the preference', () => {
|
||||
expect(buildFileEditorWordWrapOptions(undefined)).toEqual({ wordWrap: 'on' })
|
||||
expect(buildFileEditorWordWrapOptions(true)).toEqual({ wordWrap: 'on' })
|
||||
})
|
||||
|
||||
it('disables wrapping for horizontal file-editor scrolling', () => {
|
||||
expect(buildFileEditorWordWrapOptions(false)).toEqual({ wordWrap: 'off' })
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import type { editor } from 'monaco-editor'
|
||||
|
||||
export function buildFileEditorWordWrapOptions(
|
||||
editorWordWrap: boolean | undefined
|
||||
): Pick<editor.IStandaloneEditorConstructionOptions, 'wordWrap'> {
|
||||
// Why: profiles saved before this preference existed must retain Orca's previous wrapped default.
|
||||
return { wordWrap: editorWordWrap === false ? 'off' : 'on' }
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { join } from 'node:path'
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../../shared/constants'
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
useAppStore: (selector: (state: { settingsSearchQuery: string }) => unknown) =>
|
||||
selector({ settingsSearchQuery: '' })
|
||||
}))
|
||||
|
||||
import { EditorWordWrapSetting } from './EditorWordWrapSetting'
|
||||
|
||||
let root: Root | null = null
|
||||
let container: HTMLDivElement | null = null
|
||||
|
||||
afterEach(() => {
|
||||
if (root) {
|
||||
act(() => root?.unmount())
|
||||
}
|
||||
container?.remove()
|
||||
root = null
|
||||
container = null
|
||||
})
|
||||
|
||||
function renderSetting(editorWordWrap: boolean | undefined, updateSettings = vi.fn()) {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
root = createRoot(container)
|
||||
act(() => {
|
||||
root?.render(
|
||||
<EditorWordWrapSetting
|
||||
settings={{ ...getDefaultSettings(join('test', 'home')), editorWordWrap }}
|
||||
updateSettings={updateSettings}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return { container, updateSettings }
|
||||
}
|
||||
|
||||
describe('EditorWordWrapSetting', () => {
|
||||
it('shows wrapping as on for profiles saved before the preference existed', () => {
|
||||
const { container } = renderSetting(undefined)
|
||||
const on = [...container.querySelectorAll('[role="radio"]')].find(
|
||||
(button) => button.textContent === 'On'
|
||||
)
|
||||
|
||||
expect(on?.getAttribute('aria-checked')).toBe('true')
|
||||
})
|
||||
|
||||
it('shows wrapping as off when the preference is disabled', () => {
|
||||
const { container } = renderSetting(false)
|
||||
const off = [...container.querySelectorAll('[role="radio"]')].find(
|
||||
(button) => button.textContent === 'Off'
|
||||
)
|
||||
|
||||
expect(off?.getAttribute('aria-checked')).toBe('true')
|
||||
})
|
||||
|
||||
it('persists the off choice for horizontal scrolling', () => {
|
||||
const updateSettings = vi.fn()
|
||||
const { container } = renderSetting(true, updateSettings)
|
||||
const off = [...container.querySelectorAll<HTMLButtonElement>('[role="radio"]')].find(
|
||||
(button) => button.textContent === 'Off'
|
||||
)
|
||||
|
||||
act(() => off?.click())
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith({ editorWordWrap: false })
|
||||
})
|
||||
|
||||
it('persists the on choice without changing the diff preference', () => {
|
||||
const updateSettings = vi.fn()
|
||||
const { container } = renderSetting(false, updateSettings)
|
||||
const on = [...container.querySelectorAll<HTMLButtonElement>('[role="radio"]')].find(
|
||||
(button) => button.textContent === 'On'
|
||||
)
|
||||
|
||||
act(() => on?.click())
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith({ editorWordWrap: true })
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { Label } from '../ui/label'
|
||||
import { SettingsSegmentedControl } from './SettingsFormControls'
|
||||
|
||||
type EditorWordWrapSettingProps = {
|
||||
settings: GlobalSettings
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => void
|
||||
}
|
||||
|
||||
export function EditorWordWrapSetting({
|
||||
settings,
|
||||
updateSettings
|
||||
}: EditorWordWrapSettingProps): React.JSX.Element {
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.GeneralEditorSettingsSection.7ddd66fede',
|
||||
'Editor Word Wrap'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.GeneralEditorSettingsSection.9b18de6eea',
|
||||
'Wrap long lines in file editors instead of requiring horizontal scrolling.'
|
||||
)}
|
||||
keywords={['editor', 'code', 'word wrap', 'wrap', 'horizontal scroll', 'long lines']}
|
||||
className="flex items-center justify-between gap-4 py-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<Label>
|
||||
{translate(
|
||||
'auto.components.settings.GeneralEditorSettingsSection.7ddd66fede',
|
||||
'Editor Word Wrap'
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.GeneralEditorSettingsSection.9b18de6eea',
|
||||
'Wrap long lines in file editors instead of requiring horizontal scrolling.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<SettingsSegmentedControl
|
||||
ariaLabel={translate(
|
||||
'auto.components.settings.GeneralEditorSettingsSection.7ddd66fede',
|
||||
'Editor Word Wrap'
|
||||
)}
|
||||
value={settings.editorWordWrap === false ? 'off' : 'on'}
|
||||
onChange={(option) => updateSettings({ editorWordWrap: option === 'on' })}
|
||||
options={[
|
||||
{
|
||||
value: 'off',
|
||||
label: translate(
|
||||
'auto.components.settings.GeneralEditorSettingsSection.bf16ef0af2',
|
||||
'Off'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'on',
|
||||
label: translate(
|
||||
'auto.components.settings.GeneralEditorSettingsSection.3f6892f307',
|
||||
'On'
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
)
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from './SettingsFormControls'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { RichMarkdownSpellcheckSetting } from './RichMarkdownSpellcheckSetting'
|
||||
import { EditorWordWrapSetting } from './EditorWordWrapSetting'
|
||||
|
||||
export type AutoSaveDelayDraftState = {
|
||||
sourceDelayMs: number
|
||||
|
|
@ -248,6 +249,8 @@ export function GeneralEditorSettingsSection({
|
|||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<EditorWordWrapSetting settings={settings} updateSettings={updateSettings} />
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.GeneralEditorSettingsSection.8f1afdfbd8',
|
||||
|
|
|
|||
|
|
@ -95,6 +95,13 @@ describe('GeneralPane search entries', () => {
|
|||
expect(matchesSettingsSearch('wsl', entries)).toBe(true)
|
||||
})
|
||||
|
||||
it('includes file-editor word-wrap and horizontal-scroll keywords', () => {
|
||||
const entries = getGeneralPaneSearchEntries()
|
||||
|
||||
expect(matchesSettingsSearch('editor word wrap', entries)).toBe(true)
|
||||
expect(matchesSettingsSearch('horizontal scroll', entries)).toBe(true)
|
||||
})
|
||||
|
||||
it('includes rich Markdown spellcheck keywords', () => {
|
||||
const entries = getGeneralPaneSearchEntries()
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,17 @@ export const getGeneralEditorSearchEntries = createLocalizedCatalog(() => [
|
|||
)
|
||||
]
|
||||
},
|
||||
{
|
||||
title: translate('auto.components.settings.general.search.e61157e926', 'Editor Word Wrap'),
|
||||
description: translate(
|
||||
'auto.components.settings.general.search.005be5c699',
|
||||
'Wrap long lines in file editors instead of requiring horizontal scrolling.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.general.search.e1ee631696', 'editor'),
|
||||
...translateSearchKeyword('auto.components.settings.general.search.3ca5ab78a5', 'code')
|
||||
]
|
||||
},
|
||||
{
|
||||
title: translate('auto.components.settings.general.search.2760c9933f', 'Default Diff View'),
|
||||
description: translate(
|
||||
|
|
|
|||
|
|
@ -5401,7 +5401,9 @@
|
|||
"bf16ef0af2": "Off",
|
||||
"3f6892f307": "On",
|
||||
"b82f86d7d2": "Rich Markdown Spellcheck",
|
||||
"5195f0b9ef": "Show browser spelling underlines and suggestions while editing rich Markdown."
|
||||
"5195f0b9ef": "Show browser spelling underlines and suggestions while editing rich Markdown.",
|
||||
"7ddd66fede": "Editor Word Wrap",
|
||||
"9b18de6eea": "Wrap long lines in file editors instead of requiring horizontal scrolling."
|
||||
},
|
||||
"AdvancedNetworkSettingsSection": {
|
||||
"3e431564b5": "localhost, 127.0.0.1, *.internal",
|
||||
|
|
@ -7716,7 +7718,9 @@
|
|||
"defaultProjectRuntime": "Default Project Runtime",
|
||||
"defaultProjectRuntimeDescription": "Choose the runtime inherited by local Windows projects.",
|
||||
"d2d2d929c0": "Rich Markdown Spellcheck",
|
||||
"4497e2e2bb": "Show browser spelling underlines and suggestions while editing rich Markdown."
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
|
|
|
|||
|
|
@ -5401,7 +5401,9 @@
|
|||
"bf16ef0af2": "Desactivado",
|
||||
"3f6892f307": "Activado",
|
||||
"b82f86d7d2": "Corrector ortográfico de Rich Markdown",
|
||||
"5195f0b9ef": "Muestra subrayados y sugerencias ortográficas del navegador al editar Rich Markdown."
|
||||
"5195f0b9ef": "Muestra subrayados y sugerencias ortográficas del navegador al editar Rich Markdown.",
|
||||
"7ddd66fede": "Ajuste de línea en el editor",
|
||||
"9b18de6eea": "Ajusta las líneas largas en editores de archivos en lugar de requerir desplazamiento horizontal."
|
||||
},
|
||||
"AdvancedNetworkSettingsSection": {
|
||||
"3e431564b5": "localhost, 127.0.0.1, *.internal",
|
||||
|
|
@ -7679,7 +7681,9 @@
|
|||
"defaultProjectRuntime": "Runtime de proyecto predeterminado",
|
||||
"defaultProjectRuntimeDescription": "Elige el runtime que heredarán los proyectos locales de Windows.",
|
||||
"d2d2d929c0": "Corrector ortográfico de Rich Markdown",
|
||||
"4497e2e2bb": "Muestra subrayados y sugerencias ortográficas del navegador al editar 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."
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
|
|
|
|||
|
|
@ -5386,7 +5386,9 @@
|
|||
"bf16ef0af2": "オフ",
|
||||
"3f6892f307": "オン",
|
||||
"b82f86d7d2": "Rich Markdown Spellcheck",
|
||||
"5195f0b9ef": "Show browser spelling underlines and suggestions while editing rich Markdown."
|
||||
"5195f0b9ef": "Show browser spelling underlines and suggestions while editing rich Markdown.",
|
||||
"7ddd66fede": "エディターのワードラップ",
|
||||
"9b18de6eea": "水平スクロールを使わずに、ファイルエディターで長い行を折り返します。"
|
||||
},
|
||||
"AdvancedNetworkSettingsSection": {
|
||||
"3e431564b5": "ローカルホスト、127.0.0.1、*.internal",
|
||||
|
|
@ -7701,7 +7703,9 @@
|
|||
"defaultProjectRuntime": "Default Project Runtime",
|
||||
"defaultProjectRuntimeDescription": "Choose the runtime inherited by local Windows projects.",
|
||||
"d2d2d929c0": "Rich Markdown Spellcheck",
|
||||
"4497e2e2bb": "Show browser spelling underlines and suggestions while editing rich Markdown."
|
||||
"4497e2e2bb": "Show browser spelling underlines and suggestions while editing rich Markdown.",
|
||||
"e61157e926": "エディターのワードラップ",
|
||||
"005be5c699": "水平スクロールを使わずに、ファイルエディターで長い行を折り返します。"
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
|
|
|
|||
|
|
@ -5386,7 +5386,9 @@
|
|||
"bf16ef0af2": "끄다",
|
||||
"3f6892f307": "~에",
|
||||
"b82f86d7d2": "Rich Markdown Spellcheck",
|
||||
"5195f0b9ef": "Show browser spelling underlines and suggestions while editing rich Markdown."
|
||||
"5195f0b9ef": "Show browser spelling underlines and suggestions while editing rich Markdown.",
|
||||
"7ddd66fede": "편집기 자동 줄 바꿈",
|
||||
"9b18de6eea": "가로 스크롤 대신 파일 편집기에서 긴 줄을 자동으로 줄 바꿈합니다."
|
||||
},
|
||||
"AdvancedNetworkSettingsSection": {
|
||||
"3e431564b5": "로컬호스트, 127.0.0.1, *.internal",
|
||||
|
|
@ -7664,7 +7666,9 @@
|
|||
"defaultProjectRuntime": "기본 프로젝트 런타임",
|
||||
"defaultProjectRuntimeDescription": "로컬 Windows 프로젝트가 상속할 런타임을 선택합니다.",
|
||||
"d2d2d929c0": "Rich Markdown Spellcheck",
|
||||
"4497e2e2bb": "Show browser spelling underlines and suggestions while editing rich Markdown."
|
||||
"4497e2e2bb": "Show browser spelling underlines and suggestions while editing rich Markdown.",
|
||||
"e61157e926": "편집기 자동 줄 바꿈",
|
||||
"005be5c699": "가로 스크롤 대신 파일 편집기에서 긴 줄을 자동으로 줄 바꿈합니다."
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
|
|
|
|||
|
|
@ -5386,7 +5386,9 @@
|
|||
"bf16ef0af2": "关闭",
|
||||
"3f6892f307": "开启",
|
||||
"b82f86d7d2": "Rich Markdown Spellcheck",
|
||||
"5195f0b9ef": "在编辑富 Markdown 时显示浏览器拼写下划线和建议。"
|
||||
"5195f0b9ef": "在编辑富 Markdown 时显示浏览器拼写下划线和建议。",
|
||||
"7ddd66fede": "编辑器自动换行",
|
||||
"9b18de6eea": "在文件编辑器中自动换行长行,而无需水平滚动。"
|
||||
},
|
||||
"AdvancedNetworkSettingsSection": {
|
||||
"3e431564b5": "本地主机,127.0.0.1,*.internal",
|
||||
|
|
@ -7664,7 +7666,9 @@
|
|||
"defaultProjectRuntime": "默认项目运行时",
|
||||
"defaultProjectRuntimeDescription": "选择本地 Windows 项目继承的运行时。",
|
||||
"d2d2d929c0": "Rich Markdown Spellcheck",
|
||||
"4497e2e2bb": "在编辑富 Markdown 时显示浏览器拼写下划线和建议。"
|
||||
"4497e2e2bb": "在编辑富 Markdown 时显示浏览器拼写下划线和建议。",
|
||||
"e61157e926": "编辑器自动换行",
|
||||
"005be5c699": "在文件编辑器中自动换行长行,而无需水平滚动。"
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ describe('getDefaultSettings', () => {
|
|||
expect(getDefaultSettings('/tmp').confirmClosePinnedTab).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps file-editor word wrapping enabled by default', () => {
|
||||
expect(getDefaultSettings('/tmp').editorWordWrap).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps rich Markdown spellcheck enabled by default', () => {
|
||||
expect(getDefaultSettings('/tmp').richMarkdownSpellcheckEnabled).toBe(true)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
|
|||
editorAutoSave: false,
|
||||
editorAutoSaveDelayMs: DEFAULT_EDITOR_AUTO_SAVE_DELAY_MS,
|
||||
editorMinimapEnabled: false,
|
||||
editorWordWrap: true,
|
||||
richMarkdownSpellcheckEnabled: true,
|
||||
markdownReviewToolsEnabled: true,
|
||||
primarySelectionMiddleClickPaste: getDefaultPrimarySelectionMiddleClickPaste(),
|
||||
|
|
|
|||
|
|
@ -2512,6 +2512,8 @@ export type GlobalSettings = {
|
|||
editorAutoSave: boolean
|
||||
editorAutoSaveDelayMs: number
|
||||
editorMinimapEnabled: boolean
|
||||
/** 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. */
|
||||
richMarkdownSpellcheckEnabled?: boolean
|
||||
/** Whether local markdown review note controls and the review panel are shown. */
|
||||
|
|
|
|||
Loading…
Reference in New Issue