diff --git a/Outpost/Features/Account/SettingsView.swift b/Outpost/Features/Account/SettingsView.swift index 4fb94ff..e84f332 100644 --- a/Outpost/Features/Account/SettingsView.swift +++ b/Outpost/Features/Account/SettingsView.swift @@ -15,6 +15,8 @@ struct SettingsView: View { @Environment(SessionStore.self) private var session @AppStorage("outpost.appearance") private var appearance: AppAppearance = .system @AppStorage("outpost.splitViewEnabled") private var isSplitViewEnabled = false + @AppStorage("outpost.autocompleteEnabled") private var isAutocompleteEnabled = true + @AppStorage("outpost.writingToolsEnabled") private var isWritingToolsEnabled = true @AppStorage("outpost.commandPaletteEnabled") private var isCommandPaletteEnabled = true @AppStorage("outpost.commandPaletteFullWorkspaceSearch") private var isCommandPaletteFullWorkspaceSearch = false @AppStorage(CachingOutlineAPIClient.offlineModeDefaultsKey) private var isOfflineModeEnabled = false @@ -166,6 +168,26 @@ struct SettingsView: View { Divider().frame(maxWidth: 480) + VStack(alignment: .leading, spacing: 6) { + Toggle("Autocomplete", isOn: $isAutocompleteEnabled) + Text("Show inline predictive-text suggestions while typing, same as Notes and TextEdit. Accept with Tab or →.") + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(maxWidth: 480, alignment: .leading) + + Divider().frame(maxWidth: 480) + + VStack(alignment: .leading, spacing: 6) { + Toggle("Writing Tools", isOn: $isWritingToolsEnabled) + Text("Proofread, rewrite, summarize, or compose text using system-provided Apple Intelligence tools. Available on supported Macs and macOS versions.") + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(maxWidth: 480, alignment: .leading) + + Divider().frame(maxWidth: 480) + VStack(alignment: .leading, spacing: 6) { Toggle("Command Palette", isOn: $isCommandPaletteEnabled) Text("Press ⌘K to quickly jump to a document or collection. Always searches locally on your device — never a network request while typing.") diff --git a/Outpost/Features/Collections/DocumentReaderView.swift b/Outpost/Features/Collections/DocumentReaderView.swift index 7d4f36d..2de5c4e 100644 --- a/Outpost/Features/Collections/DocumentReaderView.swift +++ b/Outpost/Features/Collections/DocumentReaderView.swift @@ -17,6 +17,8 @@ struct DocumentReaderView: View { /// Local-only Outpost setting (Settings → Editor), not synced to /// Outline — see `SettingsView.editorDetail`. @AppStorage("outpost.splitViewEnabled") private var isSplitViewEnabled = false + @AppStorage("outpost.autocompleteEnabled") private var isAutocompleteEnabled = true + @AppStorage("outpost.writingToolsEnabled") private var isWritingToolsEnabled = true /// Outline's own "Show line numbers" preference (synced, read via /// `session.userPreferences`, not `@AppStorage` — this one's the @@ -32,6 +34,20 @@ struct DocumentReaderView: View { private var editorCodeBlockStyle: CodeBlockStyle { showCodeBlockLineNumbers ? .init(horizontalIndent: Self.lineNumberGutterWidth) : .default } + /// Outline's own "Smart text replacements" preference (synced) — smart + /// quotes/dashes while typing. Only meaningful on the editable pane. + private var editorTextSubstitution: TextSubstitutionPolicy { + let enabled = session.userPreferences?.smartText ?? false + return .init(quoteSubstitution: enabled, dashSubstitution: enabled) + } + /// Local-only Outpost settings (Settings → Editor) — not synced to + /// Outline, same as Split View above. + private var editorTextCompletion: TextCompletionPolicy { + .init(isEnabled: isAutocompleteEnabled) + } + private var editorWritingTools: WritingToolsPolicy { + .init(isEnabled: isWritingToolsEnabled) + } @State private var viewModel: DocumentReaderViewModel let apiClient: OutlineAPIClient @@ -337,6 +353,9 @@ struct DocumentReaderView: View { configuration: .init( services: .init(syntaxHighlighter: CodeSyntaxHighlighting.shared), codeBlock: editorCodeBlockStyle, + textSubstitution: editorTextSubstitution, + textCompletion: editorTextCompletion, + writingTools: editorWritingTools, heightBehavior: .fitsContent ), documentId: viewModel.documentId, diff --git a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/Configuration/MarkdownEditorConfiguration.swift b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/Configuration/MarkdownEditorConfiguration.swift index 871d144..87dd96d 100644 --- a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/Configuration/MarkdownEditorConfiguration.swift +++ b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/Configuration/MarkdownEditorConfiguration.swift @@ -48,6 +48,16 @@ public struct MarkdownEditorConfiguration: Sendable { /// Centered reading-column width; wide tables break out to full width. nil = full width (default). public var readingWidth: CGFloat? public var spellChecking: SpellCheckingPolicy + /// Smart quote/dash substitution while typing. Independent of `spellChecking` + /// — AppKit tracks these as separate `NSTextView` flags. + public var textSubstitution: TextSubstitutionPolicy + /// Inline predictive-text completion (the ghost-text suggestion AppKit + /// shows as you type, same feature as Notes/TextEdit). Mirrors + /// `NSTextView.isAutomaticTextCompletionEnabled`. + public var textCompletion: TextCompletionPolicy + /// System Writing Tools (proofread/rewrite/summarize). Mirrors + /// `NSTextView.writingToolsBehavior` — `.none` when disabled. + public var writingTools: WritingToolsPolicy /// How the editor resolves its own height. /// /// - `.scrolls` (default): the editor scrolls internally within whatever @@ -107,6 +117,9 @@ public struct MarkdownEditorConfiguration: Sendable { textInsets: TextInsets = .default, readingWidth: CGFloat? = nil, spellChecking: SpellCheckingPolicy = .default, + textSubstitution: TextSubstitutionPolicy = .default, + textCompletion: TextCompletionPolicy = .default, + writingTools: WritingToolsPolicy = .default, heightBehavior: HeightBehavior = .scrolls, rawSourceMode: Bool = false, extensions: [any MarkdownExtension] = [], @@ -133,6 +146,9 @@ public struct MarkdownEditorConfiguration: Sendable { self.textInsets = textInsets self.readingWidth = readingWidth self.spellChecking = spellChecking + self.textSubstitution = textSubstitution + self.textCompletion = textCompletion + self.writingTools = writingTools self.heightBehavior = heightBehavior self.rawSourceMode = rawSourceMode self.extensions = extensions @@ -168,6 +184,59 @@ public struct SpellCheckingPolicy: Sendable { public static let `default` = SpellCheckingPolicy() } +// MARK: - Text substitution + +/// Smart quote/dash substitution while typing. Mirrors the historical +/// hardcoded `NativeTextViewWrapper` behavior (quotes on, dashes off) as the +/// default, now exposed as a config knob instead of fixed AppKit calls. +public struct TextSubstitutionPolicy: Sendable { + /// Mirrors `NSTextView.isAutomaticQuoteSubstitutionEnabled`. + public var quoteSubstitution: Bool + /// Mirrors `NSTextView.isAutomaticDashSubstitutionEnabled`. + public var dashSubstitution: Bool + + public init( + quoteSubstitution: Bool = true, + dashSubstitution: Bool = false + ) { + self.quoteSubstitution = quoteSubstitution + self.dashSubstitution = dashSubstitution + } + + public static let `default` = TextSubstitutionPolicy() +} + +// MARK: - Text completion + +/// Inline predictive-text completion while typing (ghost-text suggestion, +/// accepted with Tab/→ — same feature as Notes/TextEdit on macOS 14+). +public struct TextCompletionPolicy: Sendable { + /// Mirrors `NSTextView.isAutomaticTextCompletionEnabled`. + public var isEnabled: Bool + + public init(isEnabled: Bool = true) { + self.isEnabled = isEnabled + } + + public static let `default` = TextCompletionPolicy() +} + +// MARK: - Writing Tools + +/// System Writing Tools (proofread / rewrite / summarize / compose), backed +/// by Apple Intelligence where available. +public struct WritingToolsPolicy: Sendable { + /// When `false`, `NSTextView.writingToolsBehavior` is set to `.none` + /// instead of `.limited` — hides Writing Tools entirely for this view. + public var isEnabled: Bool + + public init(isEnabled: Bool = true) { + self.isEnabled = isEnabled + } + + public static let `default` = WritingToolsPolicy() +} + // MARK: - Scroll bars /// Scroll bar visibility. Default: vertical only, autohide on. diff --git a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+Autocorrect.swift b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+Autocorrect.swift index 389ad52..6abfc31 100644 --- a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+Autocorrect.swift +++ b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+Autocorrect.swift @@ -62,8 +62,12 @@ extension NativeTextViewCoordinator { textView.isGrammarCheckingEnabled = shouldDisableSpelling ? false : userPrefersGrammarChecking - textView.isAutomaticQuoteSubstitutionEnabled = !shouldDisableSpelling - textView.isAutomaticDashSubstitutionEnabled = false + textView.isAutomaticQuoteSubstitutionEnabled = shouldDisableSpelling + ? false + : configuration.textSubstitution.quoteSubstitution + textView.isAutomaticDashSubstitutionEnabled = shouldDisableSpelling + ? false + : configuration.textSubstitution.dashSubstitution } func isInsideCode(range: NSRange, in text: String) -> Bool { diff --git a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/NativeTextViewWrapper.swift b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/NativeTextViewWrapper.swift index fbfbcc5..ebbd6ec 100644 --- a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/NativeTextViewWrapper.swift +++ b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/NativeTextViewWrapper.swift @@ -280,16 +280,17 @@ public struct NativeTextViewWrapper: NSViewRepresentable { textView.isAutomaticSpellingCorrectionEnabled = configuration.spellChecking.automaticSpellingCorrection textView.isContinuousSpellCheckingEnabled = configuration.spellChecking.continuousSpellChecking textView.isGrammarCheckingEnabled = configuration.spellChecking.grammarChecking - textView.isAutomaticQuoteSubstitutionEnabled = true + textView.isAutomaticQuoteSubstitutionEnabled = configuration.textSubstitution.quoteSubstitution textView.isAutomaticDataDetectionEnabled = true - textView.isAutomaticDashSubstitutionEnabled = false + textView.isAutomaticDashSubstitutionEnabled = configuration.textSubstitution.dashSubstitution textView.onPasteImage = onPasteImage + textView.isAutomaticTextCompletionEnabled = configuration.textCompletion.isEnabled if #available(macOS 15.1, *) { // `.limited` = the Writing Tools popover panel; `.complete` = the inline // experience that morphs the text with an animation. We use `.limited` so // rewrites/proofread land in the popover (no in-text animation) — it also // sidesteps the inline-rewrite flicker that dims text below the selection. - textView.writingToolsBehavior = .limited + textView.writingToolsBehavior = configuration.writingTools.isEnabled ? .limited : .none } // Create TextKit 2 layout bridge let bridge = LayoutBridge(textLayoutManager)