diff --git a/Packages/CmuxSettings/Sources/CmuxSettings/Keys/FileEditorCatalogSection.swift b/Packages/CmuxSettings/Sources/CmuxSettings/Keys/FileEditorCatalogSection.swift new file mode 100644 index 000000000000..af3af5b23228 --- /dev/null +++ b/Packages/CmuxSettings/Sources/CmuxSettings/Keys/FileEditorCatalogSection.swift @@ -0,0 +1,24 @@ +import Foundation + +/// Settings under the dotted-id prefix `fileEditor.*`. +/// +/// Controls the built-in plain-text file editor (the text preview that the +/// file explorer and `cmux` file routing open for editable text files). This +/// is distinct from the rendered markdown viewer, whose settings live in +/// ``MarkdownCatalogSection``. +public struct FileEditorCatalogSection: SettingCatalogSection { + /// Whether long lines soft-wrap at the editor's right edge. + /// + /// `false` (the default) preserves the established behavior: lines extend + /// past the viewport and a horizontal scroller appears. `true` wraps each + /// line to the visible width and hides the horizontal scroller, the way a + /// prose editor does. Changing this applies live to open editors. + public let wordWrap = DefaultsKey( + id: "fileEditor.wordWrap", + defaultValue: false, + userDefaultsKey: "fileEditor.wordWrap" + ) + + /// Creates the file editor settings section with its default keys. + public init() {} +} diff --git a/Packages/CmuxSettings/Sources/CmuxSettings/Keys/SettingCatalog.swift b/Packages/CmuxSettings/Sources/CmuxSettings/Keys/SettingCatalog.swift index 04b7497a840f..9e9c9e811e9e 100644 --- a/Packages/CmuxSettings/Sources/CmuxSettings/Keys/SettingCatalog.swift +++ b/Packages/CmuxSettings/Sources/CmuxSettings/Keys/SettingCatalog.swift @@ -32,6 +32,8 @@ public struct SettingCatalog: SettingCatalogSection { public let browser = BrowserCatalogSection() /// Settings for the built-in markdown viewer (the `markdown.*` keys). public let markdown = MarkdownCatalogSection() + /// Settings for the built-in plain-text file editor (the `fileEditor.*` keys). + public let fileEditor = FileEditorCatalogSection() public let betaFeatures = BetaFeaturesCatalogSection() public let shortcuts = KeyboardShortcutsCatalogSection() public let integrations = IntegrationsCatalogSection() diff --git a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift index a9e80ed13693..2bf682ec8de7 100644 --- a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift +++ b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift @@ -41,6 +41,7 @@ extension Array where Element == CuratedSettingEntry { .init(section: .app, id: "preferred-editor", title: "Open Files With", synonyms: "app.preferredEditor editor open file code vscode visual studio zed sublime subl cursor"), .init(section: .app, id: "supported-file-previews", title: "Open Supported Files in cmux", synonyms: "app.openSupportedFilesInCmux cmd click file preview pdf image video audio quicklook quick look editor external"), .init(section: .app, id: "markdown-viewer", title: "Open Markdown in cmux Viewer", synonyms: "app.openMarkdownInCmuxViewer md markdown mdx viewer preview readme"), + .init(section: .app, id: "file-editor-word-wrap", title: "File Editor Word Wrap", synonyms: "fileEditor.wordWrap file editor word wrap soft wrap reflow lines text horizontal scroll preview"), .init(section: .app, id: "terminal-config", title: "Terminal Config", synonyms: "ghostty config merged generated preview terminal configuration window open config"), .init(section: .app, id: "imessage-mode", title: "iMessage Mode", synonyms: "app.iMessageMode imessage message messages chat prompt prompts submitted texting reorder move workspace top agent send"), .init(section: .app, id: "reorder-notification", title: "Reorder on Notification", synonyms: "app.reorderOnNotification notification reorder move workspace top unread sort"), diff --git a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift index ca7185519606..ee1c91de0e42 100644 --- a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift +++ b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift @@ -38,6 +38,7 @@ public struct AppSection: View { @State private var markdownFontSize: DefaultsValueModel @State private var markdownFontFamily: DefaultsValueModel @State private var markdownMaxWidth: DefaultsValueModel + @State private var fileEditorWordWrap: DefaultsValueModel @State private var iMessage: DefaultsValueModel @State private var reorder: DefaultsValueModel @State private var dockBadge: DefaultsValueModel @@ -81,6 +82,7 @@ public struct AppSection: View { _markdownFontSize = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.markdown.fontSize)) _markdownFontFamily = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.markdown.fontFamily)) _markdownMaxWidth = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.markdown.maxWidth)) + _fileEditorWordWrap = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.fileEditor.wordWrap)) _iMessage = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.iMessageMode)) _reorder = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.reorderOnNotification)) _dockBadge = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.notifications.dockBadge)) @@ -380,6 +382,19 @@ public struct AppSection: View { } SettingsCardDivider() + // File Editor Word Wrap + SettingsCardRow( + configurationReview: .json("fileEditor.wordWrap"), + String(localized: "settings.app.fileEditorWordWrap", defaultValue: "File Editor Word Wrap"), + subtitle: String(localized: "settings.app.fileEditorWordWrap.subtitle", defaultValue: "Wrap long lines at the editor's right edge instead of scrolling horizontally. Applies to the plain-text file editor.") + ) { + Toggle("", isOn: Binding(get: { fileEditorWordWrap.current }, set: { fileEditorWordWrap.set($0) })) + .labelsHidden() + .controlSize(.small) + .accessibilityIdentifier("SettingsFileEditorWordWrapToggle") + } + SettingsCardDivider() + // iMessage Mode SettingsCardRow( configurationReview: .json("app.iMessageMode"), diff --git a/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift b/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift index 3d9b9dbea420..5333f0da3207 100644 --- a/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift +++ b/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift @@ -74,6 +74,7 @@ struct SettingsRowAnchorResolutionTests { "browser.showSearchSuggestions", "browser.theme", "browser.urlsToAlwaysOpenExternally", + "fileEditor.wordWrap", "notifications.command", "notifications.dockBadge", "notifications.paneFlash", diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 7c6141a0d212..6d6b34f52a1c 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -569,6 +569,40 @@ } } }, + "settings.app.fileEditorWordWrap": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "File Editor Word Wrap" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ファイルエディタの行折り返し" + } + } + } + }, + "settings.app.fileEditorWordWrap.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wrap long lines at the editor's right edge instead of scrolling horizontally. Applies to the plain-text file editor." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "長い行を横スクロールせずにエディタの右端で折り返します。プレーンテキストのファイルエディタに適用されます。" + } + } + } + }, "settings.search.alias.setting.app.markdown-font-size": { "extractionState": "manual", "localizations": { @@ -620,6 +654,23 @@ } } }, + "settings.search.alias.setting.app.file-editor-word-wrap": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "fileEditor.wordWrap file editor word wrap soft wrap reflow lines text horizontal scroll preview" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "fileEditor.wordWrap ファイル エディタ 行折り返し ワードラップ 折り返し 改行 テキスト 横スクロール プレビュー" + } + } + } + }, "cli.hooks.kiro.postInstallNote": { "localizations": { "en": { diff --git a/Sources/App/ShortcutRoutingSupport.swift b/Sources/App/ShortcutRoutingSupport.swift index 5e59b602c759..a8cb056cc764 100644 --- a/Sources/App/ShortcutRoutingSupport.swift +++ b/Sources/App/ShortcutRoutingSupport.swift @@ -170,13 +170,21 @@ func shouldDispatchCommandPaletteHorizontalArrowViaFirstResponderKeyDown( } } -func shouldDispatchTextBoxInputArrowViaFirstResponderKeyDown( +/// Whether an arrow keyDown belongs to a focused standalone editable text +/// responder (text-box input, file-preview editor, …) so it should be +/// forwarded to `firstResponder.keyDown` rather than swallowed by the original +/// `NSWindow.performKeyEquivalent`. +/// +/// Owns the four arrows (keyCodes 123–126) for the modifier combos a text +/// editor handles itself: plain (move), Shift (extend selection), Option +/// (word/paragraph), and Command (line/document boundary) plus their Shift +/// combos. Cmd+Option+Arrow is excluded so it still reaches cmux's pane-focus +/// shortcuts. Marked text (IME composition) is left to the input method. +private func standaloneTextResponderOwnsArrowKeyDown( keyCode: UInt16, - firstResponderIsTextBoxInput: Bool, - firstResponderHasMarkedText: Bool = false, + firstResponderHasMarkedText: Bool, flags: NSEvent.ModifierFlags ) -> Bool { - guard firstResponderIsTextBoxInput else { return false } guard !firstResponderHasMarkedText else { return false } guard (123...126).contains(keyCode) else { return false } @@ -191,6 +199,47 @@ func shouldDispatchTextBoxInputArrowViaFirstResponderKeyDown( } } +func shouldDispatchTextBoxInputArrowViaFirstResponderKeyDown( + keyCode: UInt16, + firstResponderIsTextBoxInput: Bool, + firstResponderHasMarkedText: Bool = false, + flags: NSEvent.ModifierFlags +) -> Bool { + guard firstResponderIsTextBoxInput else { return false } + return standaloneTextResponderOwnsArrowKeyDown( + keyCode: keyCode, + firstResponderHasMarkedText: firstResponderHasMarkedText, + flags: flags + ) +} + +/// Whether an arrow keyDown should be forwarded straight to the focused +/// standalone editable text view instead of falling through to the original +/// `NSWindow.performKeyEquivalent`, which swallows plain arrows before the +/// view's `keyDown` runs. +/// +/// This generalizes the per-surface arrow-forwarding seam (browser, omnibar, +/// command palette, text-box input) to cover the whole class of standalone +/// editable `NSTextView`s cmux hosts — the file-preview editor today, any +/// future one tomorrow. Field editors (the omnibar / command-palette / find +/// field editors) are excluded by the caller because they have their own +/// dedicated routing or work through the normal field-editor path. Shares the +/// keyCode/modifier policy with ``shouldDispatchTextBoxInputArrowViaFirstResponderKeyDown`` +/// via ``standaloneTextResponderOwnsArrowKeyDown(keyCode:firstResponderHasMarkedText:flags:)``. +func shouldDispatchEditableTextViewArrowViaFirstResponderKeyDown( + keyCode: UInt16, + firstResponderIsEditableTextView: Bool, + firstResponderHasMarkedText: Bool = false, + flags: NSEvent.ModifierFlags +) -> Bool { + guard firstResponderIsEditableTextView else { return false } + return standaloneTextResponderOwnsArrowKeyDown( + keyCode: keyCode, + firstResponderHasMarkedText: firstResponderHasMarkedText, + flags: flags + ) +} + func shouldToggleMainWindowFullScreenForCommandControlFShortcut( flags: NSEvent.ModifierFlags, chars: String, diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index b9ce024b7cfe..57b462fd0b69 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -16186,6 +16186,7 @@ private var cmuxBrowserArrowForwardingDepth = 0 private var cmuxBrowserOmnibarMarkedTextForwardingDepth = 0 private var cmuxCommandPaletteArrowForwardingDepth = 0 private var cmuxTextBoxInputArrowForwardingDepth = 0 +private var cmuxEditableTextViewArrowForwardingDepth = 0 private var cmuxWindowFirstResponderBypassDepth = 0 private var cmuxFieldEditorOwningWebViewAssociationKey: UInt8 = 0 @@ -16860,6 +16861,14 @@ private extension NSWindow { ) let firstResponderOmnibarPanelId = browserOmnibarPanelId(for: self.firstResponder) let firstResponderIsTextBoxInput = self.firstResponder is TextBoxInputTextView + // A standalone editable document text view (e.g. the file-preview + // editor's SavingTextView) owns arrow navigation through its own + // keyDown. Field editors (omnibar / command palette / find) are + // excluded — they route through their dedicated paths above. + let firstResponderIsStandaloneEditableTextView: Bool = { + guard let textView = self.firstResponder as? NSTextView else { return false } + return textView.isEditable && !textView.isFieldEditor + }() if ShortcutRecorderEventRouter.dispatchActiveRecordingEvent(event, preferredWindow: self) { return true } @@ -17010,6 +17019,25 @@ private extension NSWindow { return true } + // The file-preview editor and any other standalone editable NSTextView + // would otherwise lose plain/selection/word/line arrows to the original + // NSWindow.performKeyEquivalent. Route them to the text view's keyDown so + // arrow navigation works as in any text editor (manaflow-ai/cmux#5227). + if shouldDispatchEditableTextViewArrowViaFirstResponderKeyDown( + keyCode: event.keyCode, + firstResponderIsEditableTextView: firstResponderIsStandaloneEditableTextView, + firstResponderHasMarkedText: firstResponderHasMarkedText, + flags: event.modifierFlags + ) { + if cmuxEditableTextViewArrowForwardingDepth > 0 { + return false + } + cmuxEditableTextViewArrowForwardingDepth += 1 + defer { cmuxEditableTextViewArrowForwardingDepth = max(0, cmuxEditableTextViewArrowForwardingDepth - 1) } + self.firstResponder?.keyDown(with: event) + return true + } + // Web forms rely on Return/Enter flowing through keyDown. If the original // NSWindow.performKeyEquivalent consumes Enter first, submission never reaches // WebKit. Route Return/Enter directly to the current first responder and diff --git a/Sources/CmuxSettingsJSONPathSupport.swift b/Sources/CmuxSettingsJSONPathSupport.swift index 1e7bde8bdcf1..583935bbdf77 100644 --- a/Sources/CmuxSettingsJSONPathSupport.swift +++ b/Sources/CmuxSettingsJSONPathSupport.swift @@ -165,6 +165,7 @@ extension CmuxSettingsFileStore { "markdown.fontSize", "markdown.fontFamily", "markdown.maxWidth", + "fileEditor.wordWrap", "shortcuts.bindings", ] } diff --git a/Sources/CommandPalette/CommandPaletteSettingsToggle.swift b/Sources/CommandPalette/CommandPaletteSettingsToggle.swift index de928ebf41a8..8ac340850a7d 100644 --- a/Sources/CommandPalette/CommandPaletteSettingsToggle.swift +++ b/Sources/CommandPalette/CommandPaletteSettingsToggle.swift @@ -217,6 +217,17 @@ enum CommandPaletteSettingsToggleCommands { CmdClickMarkdownRouteSettings.notifyDidChange(notificationCenter: notificationCenter) } ), + CommandPaletteSettingToggleDescriptor( + commandId: commandIdPrefix + "fileEditorWordWrap", + settingsKey: "fileEditor.wordWrap", + title: { + String(localized: "settings.app.fileEditorWordWrap", defaultValue: "File Editor Word Wrap") + }, + sectionTitle: app, + keywords: ["fileEditor.wordWrap", "file", "editor", "word", "wrap", "soft", "reflow", "lines", "preview"], + defaultValue: FilePreviewWordWrapSettings.defaultEnabled, + defaultsKey: FilePreviewWordWrapSettings.key + ), CommandPaletteSettingToggleDescriptor( commandId: commandIdPrefix + "iMessageMode", settingsKey: "app.iMessageMode", diff --git a/Sources/KeyboardShortcutSettingsFileStore+Template.swift b/Sources/KeyboardShortcutSettingsFileStore+Template.swift index 81a1d81bcd9c..39c5c560d5ce 100644 --- a/Sources/KeyboardShortcutSettingsFileStore+Template.swift +++ b/Sources/KeyboardShortcutSettingsFileStore+Template.swift @@ -197,6 +197,11 @@ extension CmuxSettingsFileStore { "maxWidth": Int(MarkdownMaxWidthSettings.defaultCSSPixels), ], ], + [ + "fileEditor": [ + "wordWrap": FilePreviewWordWrapSettings.defaultEnabled, + ], + ], [ "shortcuts": [ "bindings": shortcutsBindings, diff --git a/Sources/KeyboardShortcutSettingsFileStore.swift b/Sources/KeyboardShortcutSettingsFileStore.swift index 66ac16a86472..c07927a54695 100644 --- a/Sources/KeyboardShortcutSettingsFileStore.swift +++ b/Sources/KeyboardShortcutSettingsFileStore.swift @@ -366,6 +366,9 @@ final class CmuxSettingsFileStore { if let markdownSection = root["markdown"] as? [String: Any] { parseMarkdownSection(markdownSection, sourcePath: sourcePath, snapshot: &snapshot) } + if let fileEditorSection = root["fileEditor"] as? [String: Any] { + parseFileEditorSection(fileEditorSection, sourcePath: sourcePath, snapshot: &snapshot) + } if let workspaceGroupsSection = root["workspaceGroups"] as? [String: Any] { parseWorkspaceGroupsSection(workspaceGroupsSection, sourcePath: sourcePath, snapshot: &snapshot) } @@ -629,6 +632,18 @@ final class CmuxSettingsFileStore { } } + private func parseFileEditorSection( + _ section: [String: Any], + sourcePath: String, + snapshot: inout ResolvedSettingsSnapshot + ) { + if let value = jsonBool(section["wordWrap"]) { + snapshot.managedUserDefaults[FilePreviewWordWrapSettings.key] = .bool(value) + } else if section.keys.contains("wordWrap") { + logInvalid("fileEditor.wordWrap", sourcePath: sourcePath) + } + } + private func parseSidebarSection( _ section: [String: Any], sourcePath: String, diff --git a/Sources/Panels/FilePreviewPanel.swift b/Sources/Panels/FilePreviewPanel.swift index eae320542a14..2a0c4f9ae367 100644 --- a/Sources/Panels/FilePreviewPanel.swift +++ b/Sources/Panels/FilePreviewPanel.swift @@ -1284,6 +1284,7 @@ struct FilePreviewPanelView: View { @State private var focusFlashOpacity = 0.0 @State private var focusFlashAnimationGeneration = 0 + @AppStorage(FilePreviewWordWrapSettings.key) private var fileEditorWordWrap = FilePreviewWordWrapSettings.defaultEnabled private var themeForegroundColor: NSColor { appearance.foregroundColor @@ -1354,7 +1355,8 @@ struct FilePreviewPanelView: View { isVisibleInUI: isVisibleInUI, themeBackgroundColor: contentBackgroundColor, themeForegroundColor: themeForegroundColor, - drawsBackground: appearance.drawsContentBackground + drawsBackground: appearance.drawsContentBackground, + wordWrap: fileEditorWordWrap ) case .pdf: FilePreviewPDFView( diff --git a/Sources/Panels/FilePreviewTextEditor.swift b/Sources/Panels/FilePreviewTextEditor.swift index 06e6b23f8df9..c9039f14ffd2 100644 --- a/Sources/Panels/FilePreviewTextEditor.swift +++ b/Sources/Panels/FilePreviewTextEditor.swift @@ -18,6 +18,9 @@ struct FilePreviewTextEditor: NSViewRepresentable where PanelModel: let themeBackgroundColor: NSColor let themeForegroundColor: NSColor let drawsBackground: Bool + /// Whether long lines soft-wrap at the editor's right edge. Sourced from + /// the persisted `fileEditor.wordWrap` setting; updates apply live. + let wordWrap: Bool func makeCoordinator() -> Coordinator { Coordinator(panel: panel) @@ -40,6 +43,7 @@ struct FilePreviewTextEditor: NSViewRepresentable where PanelModel: panel.attachTextView(textView) scrollView.documentView = textView + textView.applyFilePreviewWordWrap(wordWrap, scrollView: scrollView) Self.applyTheme( to: scrollView, backgroundColor: themeBackgroundColor, @@ -61,6 +65,7 @@ struct FilePreviewTextEditor: NSViewRepresentable where PanelModel: guard let textView = scrollView.documentView as? SavingTextView else { return } textView.panel = panel textView.applyFilePreviewTextEditorInsets() + textView.applyFilePreviewWordWrap(wordWrap, scrollView: scrollView) panel.attachTextView(textView) guard textView.string != panel.textContent else { return } context.coordinator.isApplyingPanelUpdate = true @@ -170,6 +175,36 @@ extension NSTextView { layoutManager.allowsNonContiguousLayout = true } + /// Configures the text view and its scroll view for soft line wrapping + /// (`wrap == true`) or the no-wrap baseline with a horizontal scroller + /// (`wrap == false`). Idempotent, so it is safe to call on every SwiftUI + /// update; toggling the `fileEditor.wordWrap` setting reflows open editors. + func applyFilePreviewWordWrap(_ wrap: Bool, scrollView: NSScrollView) { + guard let textContainer else { return } + scrollView.hasHorizontalScroller = !wrap + isHorizontallyResizable = !wrap + if wrap { + textContainer.widthTracksTextView = true + // `widthTracksTextView` keeps the container pinned to the text view + // width, so wrapping is correct even before the scroll view is laid + // out. Only snap the frame/container to a real measured width to + // avoid collapsing to a zero-width container during `makeNSView`, + // before the clip view has a size; `updateNSView` re-runs once laid + // out and reflows. + let visibleWidth = scrollView.contentSize.width + if visibleWidth > 0 { + textContainer.size = NSSize(width: visibleWidth, height: .greatestFiniteMagnitude) + setFrameSize(NSSize(width: visibleWidth, height: frame.height)) + } + } else { + textContainer.widthTracksTextView = false + textContainer.size = NSSize( + width: CGFloat.greatestFiniteMagnitude, + height: CGFloat.greatestFiniteMagnitude + ) + } + } + func applyFilePreviewTextEditorInsets() { let targetInset = FilePreviewTextEditorLayout.textContainerInset if textContainerInset.width != targetInset.width || textContainerInset.height != targetInset.height { diff --git a/Sources/Panels/FilePreviewWordWrapSettings.swift b/Sources/Panels/FilePreviewWordWrapSettings.swift new file mode 100644 index 000000000000..b0f2e79fef66 --- /dev/null +++ b/Sources/Panels/FilePreviewWordWrapSettings.swift @@ -0,0 +1,21 @@ +import Foundation + +/// Persistent toggle for soft line wrapping in the plain-text file editor. +/// +/// Backed by the `fileEditor.wordWrap` key, shared by the Settings window +/// (`CmuxSettings` catalog), the `~/.config/cmux/cmux.json` parser, and the +/// `FilePreviewTextEditor`. `false` preserves the established no-wrap behavior +/// (long lines extend past the viewport with a horizontal scroller). +enum FilePreviewWordWrapSettings { + /// UserDefaults / cmux.json key. + static let key = "fileEditor.wordWrap" + + /// Default state: wrapping off, matching the editor's prior behavior. + static let defaultEnabled = false + + /// Whether word wrap is currently enabled, honoring the stored override + /// and falling back to ``defaultEnabled``. + static func isEnabled(defaults: UserDefaults = .standard) -> Bool { + defaults.object(forKey: key) == nil ? defaultEnabled : defaults.bool(forKey: key) + } +} diff --git a/Sources/Panels/MarkdownPanelView.swift b/Sources/Panels/MarkdownPanelView.swift index fed0675ad081..35be58752eb1 100644 --- a/Sources/Panels/MarkdownPanelView.swift +++ b/Sources/Panels/MarkdownPanelView.swift @@ -28,6 +28,7 @@ struct MarkdownPanelView: View { @State private var focusFlashAnimationGeneration: Int = 0 @State private var copyConfirmation: CopyConfirmation? = nil @State private var copyConfirmationGeneration: Int = 0 + @AppStorage(FilePreviewWordWrapSettings.key) private var fileEditorWordWrap = FilePreviewWordWrapSettings.defaultEnabled private enum CopyConfirmation: Equatable { case markdown @@ -101,7 +102,8 @@ struct MarkdownPanelView: View { isVisibleInUI: isVisibleInUI, themeBackgroundColor: appearance.contentBackgroundColor, themeForegroundColor: themeForegroundColor, - drawsBackground: appearance.drawsContentBackground + drawsBackground: appearance.drawsContentBackground, + wordWrap: fileEditorWordWrap ) .frame(maxWidth: .infinity, maxHeight: .infinity) } diff --git a/Sources/SettingsNavigation.swift b/Sources/SettingsNavigation.swift index 3f1c1a9e3a84..729d99510ce6 100644 --- a/Sources/SettingsNavigation.swift +++ b/Sources/SettingsNavigation.swift @@ -314,6 +314,7 @@ enum SettingsSearchIndex { setting(.app, "markdown-font-size", String(localized: "settings.app.markdownFontSize", defaultValue: "Markdown Viewer Font Size"), "md markdown viewer font size points zoom scale text bigger smaller"), setting(.app, "markdown-font-family", String(localized: "settings.app.markdownFontFamily", defaultValue: "Markdown Viewer Font"), "markdown.fontFamily md markdown viewer font font-family family typeface system stack custom"), setting(.app, "markdown-max-width", String(localized: "settings.app.markdownMaxWidth", defaultValue: "Markdown Viewer Max Width"), "markdown.maxWidth md markdown viewer width column reading line length pixels px"), + setting(.app, "file-editor-word-wrap", String(localized: "settings.app.fileEditorWordWrap", defaultValue: "File Editor Word Wrap"), "fileEditor.wordWrap file editor word wrap soft wrap reflow lines text horizontal scroll preview"), setting(.app, "imessage-mode", String(localized: "settings.app.iMessageMode", defaultValue: "iMessage Mode"), "message messages imessage chat prompt prompts submitted message send agent workspace reorder move top"), setting(.app, "reorder-notification", String(localized: "settings.app.reorderOnNotification", defaultValue: "Reorder on Notification"), "workspace notification order"), setting(.app, "dock-badge", String(localized: "settings.app.dockBadge", defaultValue: "Dock Badge"), "unread count app icon"), @@ -441,6 +442,7 @@ enum SettingsSearchIndex { "markdown.fontSize": settingID(for: .app, idSuffix: "markdown-font-size"), "markdown.fontFamily": settingID(for: .app, idSuffix: "markdown-font-family"), "markdown.maxWidth": settingID(for: .app, idSuffix: "markdown-max-width"), + "fileEditor.wordWrap": settingID(for: .app, idSuffix: "file-editor-word-wrap"), "app.iMessageMode": settingID(for: .app, idSuffix: "imessage-mode"), "app.reorderOnNotification": settingID(for: .app, idSuffix: "reorder-notification"), "notifications.dockBadge": settingID(for: .app, idSuffix: "dock-badge"), diff --git a/Sources/SettingsSearchAliases.swift b/Sources/SettingsSearchAliases.swift index 6d211bc6f5f2..b6d7149b0cbe 100644 --- a/Sources/SettingsSearchAliases.swift +++ b/Sources/SettingsSearchAliases.swift @@ -60,6 +60,7 @@ enum SettingsSearchAliasIndex { "app:markdown-font-size": localized("settings.search.alias.setting.app.markdown-font-size", defaultValue: "markdown.fontSize md markdown viewer font size points zoom scale text bigger smaller larger default"), "app:markdown-font-family": localized("settings.search.alias.setting.app.markdown-font-family", defaultValue: "markdown.fontFamily md markdown viewer font font-family family typeface system stack custom"), "app:markdown-max-width": localized("settings.search.alias.setting.app.markdown-max-width", defaultValue: "markdown.maxWidth md markdown viewer max width column reading line length pixels px narrow wide"), + "app:file-editor-word-wrap": localized("settings.search.alias.setting.app.file-editor-word-wrap", defaultValue: "fileEditor.wordWrap file editor word wrap soft wrap reflow lines text horizontal scroll preview"), "app:imessage-mode": localized("settings.search.alias.setting.app.imessage-mode", defaultValue: "app.iMessageMode imessage message messages chat prompt prompts submitted message texting reorder move workspace top agent send"), "app:reorder-notification": localized("settings.search.alias.setting.app.reorder-notification", defaultValue: "app.reorderOnNotification notification reorder move workspace top unread sort"), "app:dock-badge": localized("settings.search.alias.setting.app.dock-badge", defaultValue: "notifications.dockBadge badge dock unread count icon notifications red bubble"), diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 5accfc93b1d4..4f5ed0b70c9d 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -208,6 +208,7 @@ D0C0D0C0D0C0D0C0D0C0D003 /* DockEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0D0C0D0C0D0C0D0C0D004 /* DockEmptyView.swift */; }; D0C0D0C0D0C0D0C0D0C0D001 /* DockPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0D0C0D0C0D0C0D0C0D002 /* DockPanelView.swift */; }; D0B10014A1B2C3D4E5F60001 /* DragOverlayRoutingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10015A1B2C3D4E5F60001 /* DragOverlayRoutingPolicy.swift */; }; + D3622100A1B2C3D4E5F60718 /* EditableTextViewArrowKeyForwardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3622101A1B2C3D4E5F60718 /* EditableTextViewArrowKeyForwardingTests.swift */; }; E4C001000000000000000001 /* ExtensionSidebarWorkspaceRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C001000000000000000002 /* ExtensionSidebarWorkspaceRowView.swift */; }; E4C001000000000000000003 /* ExtensionWorktreePrototype.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C001000000000000000004 /* ExtensionWorktreePrototype.swift */; }; E50320010000000000000001 /* ExtensionWorktreeSpawnArgsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E50320010000000000000002 /* ExtensionWorktreeSpawnArgsTests.swift */; }; @@ -254,6 +255,7 @@ DD62AFF1D95A1D5D1850AC10 /* FilePreviewQuickLookSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A02623202562490B3E88372 /* FilePreviewQuickLookSession.swift */; }; C0DE31390000000000000103 /* FilePreviewReviewFeedbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE31390000000000000104 /* FilePreviewReviewFeedbackTests.swift */; }; D0B10016A1B2C3D4E5F60001 /* FilePreviewTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10017A1B2C3D4E5F60001 /* FilePreviewTextEditor.swift */; }; + F11E52270000000000005227 /* FilePreviewWordWrapSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11E52270000000000005228 /* FilePreviewWordWrapSettings.swift */; }; A5001445A5001445A5001445 /* FilePreviewWorkspaceOpenSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001444A5001444A5001444 /* FilePreviewWorkspaceOpenSupport.swift */; }; FE002103 /* FileSearchRipgrepParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE002003 /* FileSearchRipgrepParserTests.swift */; }; C35610000000000000000001 /* FinderFileDropRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35610000000000000000002 /* FinderFileDropRegressionTests.swift */; }; @@ -832,6 +834,7 @@ D0C0D0C0D0C0D0C0D0C0D004 /* DockEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DockEmptyView.swift; sourceTree = ""; }; D0C0D0C0D0C0D0C0D0C0D002 /* DockPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DockPanelView.swift; sourceTree = ""; }; D0B10015A1B2C3D4E5F60001 /* DragOverlayRoutingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragOverlayRoutingPolicy.swift; sourceTree = ""; }; + D3622101A1B2C3D4E5F60718 /* EditableTextViewArrowKeyForwardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextViewArrowKeyForwardingTests.swift; sourceTree = ""; }; E4C001000000000000000002 /* ExtensionSidebarWorkspaceRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionSidebarWorkspaceRowView.swift; sourceTree = ""; }; E4C001000000000000000004 /* ExtensionWorktreePrototype.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionWorktreePrototype.swift; sourceTree = ""; }; E50320010000000000000002 /* ExtensionWorktreeSpawnArgsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionWorktreeSpawnArgsTests.swift; sourceTree = ""; }; @@ -877,6 +880,7 @@ 0A02623202562490B3E88372 /* FilePreviewQuickLookSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/FilePreviewQuickLookSession.swift; sourceTree = ""; }; C0DE31390000000000000104 /* FilePreviewReviewFeedbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewReviewFeedbackTests.swift; sourceTree = ""; }; D0B10017A1B2C3D4E5F60001 /* FilePreviewTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/FilePreviewTextEditor.swift; sourceTree = ""; }; + F11E52270000000000005228 /* FilePreviewWordWrapSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/FilePreviewWordWrapSettings.swift; sourceTree = ""; }; A5001444A5001444A5001444 /* FilePreviewWorkspaceOpenSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/FilePreviewWorkspaceOpenSupport.swift; sourceTree = ""; }; FE002003 /* FileSearchRipgrepParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSearchRipgrepParserTests.swift; sourceTree = ""; }; C35610000000000000000002 /* FinderFileDropRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinderFileDropRegressionTests.swift; sourceTree = ""; }; @@ -1608,6 +1612,7 @@ F5168A040000000000000002 /* MarkdownFontFamily.swift */, F5168A070000000000000002 /* MarkdownFontFamilyCache.swift */, F5168A050000000000000002 /* MarkdownMaxWidthSettings.swift */, + F11E52270000000000005228 /* FilePreviewWordWrapSettings.swift */, F5168A060000000000000002 /* MarkdownTypographyDefaults.swift */, F5168A020000000000000002 /* MarkdownTypographyControl.swift */, A5001419 /* MarkdownPanelView.swift */, @@ -1792,6 +1797,7 @@ 970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */, 43F90FAF3FD44F11BF547BE9 /* CmuxWebViewMouseNavigationButtonTests.swift */, D3622001A1B2C3D4E5F60718 /* BrowserArrowKeyForwardingTests.swift */, + D3622101A1B2C3D4E5F60718 /* EditableTextViewArrowKeyForwardingTests.swift */, 58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */, D0B1000DA1B2C3D4E5F60001 /* CmuxWebViewDragRoutingTests.swift */, D0B1000FA1B2C3D4E5F60001 /* BrowserPaneDropRoutingTests.swift */, @@ -2349,6 +2355,7 @@ A5001429 /* FilePreviewPDFViewport.swift in Sources */, DD62AFF1D95A1D5D1850AC10 /* FilePreviewQuickLookSession.swift in Sources */, D0B10016A1B2C3D4E5F60001 /* FilePreviewTextEditor.swift in Sources */, + F11E52270000000000005227 /* FilePreviewWordWrapSettings.swift in Sources */, A5001445A5001445A5001445 /* FilePreviewWorkspaceOpenSupport.swift in Sources */, F1A0C0DE0000000000000002 /* FindTextFieldSupport.swift in Sources */, F0C05170000000000000002 /* FocusHistory.swift in Sources */, @@ -2681,6 +2688,7 @@ A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */, C0DEF4120000000000000001 /* CommandPaletteSettingsToggleTests.swift in Sources */, C1713006C1713006C1713006 /* CommandPaletteShortcutCustomizationTests.swift in Sources */, + D3622100A1B2C3D4E5F60718 /* EditableTextViewArrowKeyForwardingTests.swift in Sources */, E50320010000000000000001 /* ExtensionWorktreeSpawnArgsTests.swift in Sources */, FEEDC0DEC0DEC0DEC0DE0001 /* FeedCoordinatorTests.swift in Sources */, FEED49850000000000000001 /* FeedEventClassificationTests.swift in Sources */, diff --git a/cmuxTests/EditableTextViewArrowKeyForwardingTests.swift b/cmuxTests/EditableTextViewArrowKeyForwardingTests.swift new file mode 100644 index 000000000000..1d5efadf38e1 --- /dev/null +++ b/cmuxTests/EditableTextViewArrowKeyForwardingTests.swift @@ -0,0 +1,100 @@ +import AppKit +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +/// Regression coverage for https://github.com/manaflow-ai/cmux/issues/5227. +/// +/// In a cmux window every keyDown is funneled through the swizzled +/// `NSWindow.performKeyEquivalent`, where the original AppKit implementation +/// swallows plain arrow keys (keyCodes 123-126) before they reach the focused +/// view's `keyDown`. The window swizzle re-routes arrows to +/// `firstResponder.keyDown(with:)` only for an enumerated set of responder +/// types. The file-editor text view (`SavingTextView`, a standalone editable +/// `NSTextView`) was missing from that set, so arrow keys never moved the +/// cursor. These tests pin the generalized routing decision that fixes the +/// whole class: any standalone editable `NSTextView` owns arrow navigation. +@Suite("EditableTextViewArrowKeyForwarding") +struct EditableTextViewArrowKeyForwardingTests { + @Test(arguments: [123, 124, 125, 126] as [UInt16]) + func routesPlainArrowWhenEditableTextViewFirstResponder(keyCode: UInt16) { + #expect( + shouldDispatchEditableTextViewArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsEditableTextView: true, + flags: [] + ), + "Expected editable text view to own plain arrow keyCode \(keyCode)" + ) + } + + @Test + func routesSelectionWordAndLineArrows() { + let flagSets: [NSEvent.ModifierFlags] = [ + [.shift], [.option], [.option, .shift], [.command], [.command, .shift], + ] + for flags in flagSets { + for keyCode in [123, 124, 125, 126] as [UInt16] { + #expect( + shouldDispatchEditableTextViewArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsEditableTextView: true, + flags: flags + ), + "Expected editable text view to own modified arrow keyCode \(keyCode) flags \(flags.rawValue)" + ) + } + } + } + + @Test(arguments: [123, 124, 125, 126] as [UInt16]) + func doesNotForwardWhenResponderIsNotEditableTextView(keyCode: UInt16) { + #expect( + !shouldDispatchEditableTextViewArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsEditableTextView: false, + flags: [] + ) + ) + } + + @Test + func doesNotForwardDuringMarkedTextComposition() { + #expect( + !shouldDispatchEditableTextViewArrowViaFirstResponderKeyDown( + keyCode: 125, + firstResponderIsEditableTextView: true, + firstResponderHasMarkedText: true, + flags: [] + ) + ) + } + + @Test + func doesNotForwardNonArrowKeys() { + #expect( + !shouldDispatchEditableTextViewArrowViaFirstResponderKeyDown( + keyCode: 0, + firstResponderIsEditableTextView: true, + flags: [] + ) + ) + } + + @Test(arguments: [123, 124, 125, 126] as [UInt16]) + func doesNotStealCommandOptionArrowFromPaneFocusShortcut(keyCode: UInt16) { + // Cmd+Option+Arrow is reserved for cmux pane-focus shortcuts and must + // not be claimed by the text view. + #expect( + !shouldDispatchEditableTextViewArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsEditableTextView: true, + flags: [.command, .option] + ) + ) + } +} diff --git a/cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift b/cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift index c4217a86cf23..b7ea57331708 100644 --- a/cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift +++ b/cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift @@ -482,6 +482,50 @@ final class KeyboardShortcutSettingsFileStoreStartupTests: XCTestCase { } } + func testSettingsFileParsesFileEditorWordWrap() throws { + let defaults = UserDefaults.standard + + try preservingDefaults(keys: [ + FilePreviewWordWrapSettings.key, + settingsFileBackupsDefaultsKey, + importedManagedDefaultsKey + ]) { + defaults.removeObject(forKey: FilePreviewWordWrapSettings.key) + defaults.removeObject(forKey: settingsFileBackupsDefaultsKey) + defaults.removeObject(forKey: importedManagedDefaultsKey) + + // Defaults to off until the config opts in. + XCTAssertFalse(FilePreviewWordWrapSettings.isEnabled(defaults: defaults)) + + let directoryURL = try makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: directoryURL) } + + let settingsFileURL = directoryURL.appendingPathComponent("cmux.json", isDirectory: false) + try writeSettingsFile( + """ + { + "fileEditor": { + "wordWrap": true + } + } + """, + to: settingsFileURL + ) + + let store = KeyboardShortcutSettingsFileStore( + primaryPath: settingsFileURL.path, + fallbackPath: nil, + additionalFallbackPaths: [], + startWatching: false + ) + + withExtendedLifetime(store) { + XCTAssertTrue(defaults.bool(forKey: FilePreviewWordWrapSettings.key)) + XCTAssertTrue(FilePreviewWordWrapSettings.isEnabled(defaults: defaults)) + } + } + } + func testManagedAppearanceUserDefaultSurvivesSettingsFileReapplyUntilFileChanges() throws { let defaults = UserDefaults.standard let key = AppearanceSettings.appearanceModeKey diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index 7209b4900946..91394aa3d08f 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -49,6 +49,7 @@ const sectionOrder = [ "commands", "browser", "markdown", + "fileEditor", "shortcuts", ] as const; @@ -102,6 +103,11 @@ function buildSettingsFileExample(t: ConfigurationTranslation) { // "maxWidth": 980 // }, + // "fileEditor": { + // // ${t("exampleFileEditorWordWrap")} + // "wordWrap": false + // }, + // "automation": { // "suppressSubagentNotifications": true // }, diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index 5a25955aa48f..23ea65db3df7 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -1041,6 +1041,20 @@ } } }, + "fileEditor": { + "title": "fileEditor", + "description": "Built-in plain-text file editor settings.", + "type": "object", + "additionalProperties": false, + "properties": { + "wordWrap": { + "type": "boolean", + "default": false, + "description": "Wrap long lines at the editor's right edge instead of scrolling horizontally.", + "descriptionKey": "schemaDescriptions.fileEditor.wordWrap" + } + } + }, "shortcuts": { "title": "shortcuts", "description": "Keyboard shortcut settings from Settings > Keyboard Shortcuts.", diff --git a/web/messages/en.json b/web/messages/en.json index 78218fa805c8..67574fc69d5c 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -639,12 +639,16 @@ "fontSize": "Default body font size, in points, for newly opened markdown viewers. Zoom a viewer live with Cmd-+ / Cmd-- / Cmd-0.", "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." + }, + "fileEditor": { + "wordWrap": "Wrap long lines at the editor's right edge instead of scrolling horizontally." } }, "exampleMarkdownFontSize": "Default body font size (points) for newly opened markdown viewers.", "exampleMarkdownFontSizeZoom": "Zoom a viewer live with Cmd-+ / Cmd-- / Cmd-0.", "exampleMarkdownFontFamily": "Default body font family. Empty keeps the system markdown font stack.", - "exampleMarkdownMaxWidth": "Default maximum reading column width, in CSS pixels." + "exampleMarkdownMaxWidth": "Default maximum reading column width, in CSS pixels.", + "exampleFileEditorWordWrap": "Wrap long lines at the editor's right edge instead of scrolling horizontally." }, "customCommands": { "title": "Custom Commands", diff --git a/web/messages/ja.json b/web/messages/ja.json index 777bf7203a1a..a162589bd640 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -598,12 +598,16 @@ "fontSize": "新しく開く Markdown ビューアの本文フォントサイズの既定値(ポイント単位)です。Cmd-+ / Cmd-- / Cmd-0 でその場で拡大・縮小できます。", "fontFamily": "新しく開く Markdown ビューアの本文フォントファミリーの既定値です。空のままにするとシステムの Markdown フォントスタックを使います。", "maxWidth": "新しく開く Markdown ビューアの読み取りカラム最大幅の既定値(CSS ピクセル単位)です。" + }, + "fileEditor": { + "wordWrap": "横スクロールする代わりに、エディタの右端で長い行を折り返します。" } }, "exampleMarkdownFontSize": "新しく開く Markdown ビューアの本文フォントサイズ(ポイント単位)。", "exampleMarkdownFontSizeZoom": "Cmd-+ / Cmd-- / Cmd-0 でその場で拡大・縮小できます。", "exampleMarkdownFontFamily": "本文フォントファミリーの既定値です。空のままにするとシステムの Markdown フォントスタックを使います。", - "exampleMarkdownMaxWidth": "読み取りカラム最大幅の既定値(CSS ピクセル単位)です。" + "exampleMarkdownMaxWidth": "読み取りカラム最大幅の既定値(CSS ピクセル単位)です。", + "exampleFileEditorWordWrap": "横スクロールする代わりに、エディタの右端で長い行を折り返します。" }, "customCommands": { "title": "カスタムコマンド",