feat: wire smart text replacements, autocomplete, Writing Tools toggles
Adds TextSubstitutionPolicy/TextCompletionPolicy/WritingToolsPolicy to MarkdownEditorConfiguration (previously hardcoded AppKit calls with no config surface). Outline's existing synced smartText preference now actually drives smart quotes/dashes. Autocomplete and Writing Tools get new local-only Settings -> Editor toggles (writingToolsBehavior was already on unconditionally with no way to turn it off).
This commit is contained in:
@@ -15,6 +15,8 @@ struct SettingsView: View {
|
|||||||
@Environment(SessionStore.self) private var session
|
@Environment(SessionStore.self) private var session
|
||||||
@AppStorage("outpost.appearance") private var appearance: AppAppearance = .system
|
@AppStorage("outpost.appearance") private var appearance: AppAppearance = .system
|
||||||
@AppStorage("outpost.splitViewEnabled") private var isSplitViewEnabled = false
|
@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.commandPaletteEnabled") private var isCommandPaletteEnabled = true
|
||||||
@AppStorage("outpost.commandPaletteFullWorkspaceSearch") private var isCommandPaletteFullWorkspaceSearch = false
|
@AppStorage("outpost.commandPaletteFullWorkspaceSearch") private var isCommandPaletteFullWorkspaceSearch = false
|
||||||
@AppStorage(CachingOutlineAPIClient.offlineModeDefaultsKey) private var isOfflineModeEnabled = false
|
@AppStorage(CachingOutlineAPIClient.offlineModeDefaultsKey) private var isOfflineModeEnabled = false
|
||||||
@@ -166,6 +168,26 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Divider().frame(maxWidth: 480)
|
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) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Toggle("Command Palette", isOn: $isCommandPaletteEnabled)
|
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.")
|
Text("Press ⌘K to quickly jump to a document or collection. Always searches locally on your device — never a network request while typing.")
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ struct DocumentReaderView: View {
|
|||||||
/// Local-only Outpost setting (Settings → Editor), not synced to
|
/// Local-only Outpost setting (Settings → Editor), not synced to
|
||||||
/// Outline — see `SettingsView.editorDetail`.
|
/// Outline — see `SettingsView.editorDetail`.
|
||||||
@AppStorage("outpost.splitViewEnabled") private var isSplitViewEnabled = false
|
@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
|
/// Outline's own "Show line numbers" preference (synced, read via
|
||||||
/// `session.userPreferences`, not `@AppStorage` — this one's the
|
/// `session.userPreferences`, not `@AppStorage` — this one's the
|
||||||
@@ -32,6 +34,20 @@ struct DocumentReaderView: View {
|
|||||||
private var editorCodeBlockStyle: CodeBlockStyle {
|
private var editorCodeBlockStyle: CodeBlockStyle {
|
||||||
showCodeBlockLineNumbers ? .init(horizontalIndent: Self.lineNumberGutterWidth) : .default
|
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
|
@State private var viewModel: DocumentReaderViewModel
|
||||||
let apiClient: OutlineAPIClient
|
let apiClient: OutlineAPIClient
|
||||||
@@ -337,6 +353,9 @@ struct DocumentReaderView: View {
|
|||||||
configuration: .init(
|
configuration: .init(
|
||||||
services: .init(syntaxHighlighter: CodeSyntaxHighlighting.shared),
|
services: .init(syntaxHighlighter: CodeSyntaxHighlighting.shared),
|
||||||
codeBlock: editorCodeBlockStyle,
|
codeBlock: editorCodeBlockStyle,
|
||||||
|
textSubstitution: editorTextSubstitution,
|
||||||
|
textCompletion: editorTextCompletion,
|
||||||
|
writingTools: editorWritingTools,
|
||||||
heightBehavior: .fitsContent
|
heightBehavior: .fitsContent
|
||||||
),
|
),
|
||||||
documentId: viewModel.documentId,
|
documentId: viewModel.documentId,
|
||||||
|
|||||||
Vendored
+69
@@ -48,6 +48,16 @@ public struct MarkdownEditorConfiguration: Sendable {
|
|||||||
/// Centered reading-column width; wide tables break out to full width. nil = full width (default).
|
/// Centered reading-column width; wide tables break out to full width. nil = full width (default).
|
||||||
public var readingWidth: CGFloat?
|
public var readingWidth: CGFloat?
|
||||||
public var spellChecking: SpellCheckingPolicy
|
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.
|
/// How the editor resolves its own height.
|
||||||
///
|
///
|
||||||
/// - `.scrolls` (default): the editor scrolls internally within whatever
|
/// - `.scrolls` (default): the editor scrolls internally within whatever
|
||||||
@@ -107,6 +117,9 @@ public struct MarkdownEditorConfiguration: Sendable {
|
|||||||
textInsets: TextInsets = .default,
|
textInsets: TextInsets = .default,
|
||||||
readingWidth: CGFloat? = nil,
|
readingWidth: CGFloat? = nil,
|
||||||
spellChecking: SpellCheckingPolicy = .default,
|
spellChecking: SpellCheckingPolicy = .default,
|
||||||
|
textSubstitution: TextSubstitutionPolicy = .default,
|
||||||
|
textCompletion: TextCompletionPolicy = .default,
|
||||||
|
writingTools: WritingToolsPolicy = .default,
|
||||||
heightBehavior: HeightBehavior = .scrolls,
|
heightBehavior: HeightBehavior = .scrolls,
|
||||||
rawSourceMode: Bool = false,
|
rawSourceMode: Bool = false,
|
||||||
extensions: [any MarkdownExtension] = [],
|
extensions: [any MarkdownExtension] = [],
|
||||||
@@ -133,6 +146,9 @@ public struct MarkdownEditorConfiguration: Sendable {
|
|||||||
self.textInsets = textInsets
|
self.textInsets = textInsets
|
||||||
self.readingWidth = readingWidth
|
self.readingWidth = readingWidth
|
||||||
self.spellChecking = spellChecking
|
self.spellChecking = spellChecking
|
||||||
|
self.textSubstitution = textSubstitution
|
||||||
|
self.textCompletion = textCompletion
|
||||||
|
self.writingTools = writingTools
|
||||||
self.heightBehavior = heightBehavior
|
self.heightBehavior = heightBehavior
|
||||||
self.rawSourceMode = rawSourceMode
|
self.rawSourceMode = rawSourceMode
|
||||||
self.extensions = extensions
|
self.extensions = extensions
|
||||||
@@ -168,6 +184,59 @@ public struct SpellCheckingPolicy: Sendable {
|
|||||||
public static let `default` = SpellCheckingPolicy()
|
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
|
// MARK: - Scroll bars
|
||||||
|
|
||||||
/// Scroll bar visibility. Default: vertical only, autohide on.
|
/// Scroll bar visibility. Default: vertical only, autohide on.
|
||||||
|
|||||||
+6
-2
@@ -62,8 +62,12 @@ extension NativeTextViewCoordinator {
|
|||||||
textView.isGrammarCheckingEnabled = shouldDisableSpelling
|
textView.isGrammarCheckingEnabled = shouldDisableSpelling
|
||||||
? false
|
? false
|
||||||
: userPrefersGrammarChecking
|
: userPrefersGrammarChecking
|
||||||
textView.isAutomaticQuoteSubstitutionEnabled = !shouldDisableSpelling
|
textView.isAutomaticQuoteSubstitutionEnabled = shouldDisableSpelling
|
||||||
textView.isAutomaticDashSubstitutionEnabled = false
|
? false
|
||||||
|
: configuration.textSubstitution.quoteSubstitution
|
||||||
|
textView.isAutomaticDashSubstitutionEnabled = shouldDisableSpelling
|
||||||
|
? false
|
||||||
|
: configuration.textSubstitution.dashSubstitution
|
||||||
}
|
}
|
||||||
|
|
||||||
func isInsideCode(range: NSRange, in text: String) -> Bool {
|
func isInsideCode(range: NSRange, in text: String) -> Bool {
|
||||||
|
|||||||
+4
-3
@@ -280,16 +280,17 @@ public struct NativeTextViewWrapper: NSViewRepresentable {
|
|||||||
textView.isAutomaticSpellingCorrectionEnabled = configuration.spellChecking.automaticSpellingCorrection
|
textView.isAutomaticSpellingCorrectionEnabled = configuration.spellChecking.automaticSpellingCorrection
|
||||||
textView.isContinuousSpellCheckingEnabled = configuration.spellChecking.continuousSpellChecking
|
textView.isContinuousSpellCheckingEnabled = configuration.spellChecking.continuousSpellChecking
|
||||||
textView.isGrammarCheckingEnabled = configuration.spellChecking.grammarChecking
|
textView.isGrammarCheckingEnabled = configuration.spellChecking.grammarChecking
|
||||||
textView.isAutomaticQuoteSubstitutionEnabled = true
|
textView.isAutomaticQuoteSubstitutionEnabled = configuration.textSubstitution.quoteSubstitution
|
||||||
textView.isAutomaticDataDetectionEnabled = true
|
textView.isAutomaticDataDetectionEnabled = true
|
||||||
textView.isAutomaticDashSubstitutionEnabled = false
|
textView.isAutomaticDashSubstitutionEnabled = configuration.textSubstitution.dashSubstitution
|
||||||
textView.onPasteImage = onPasteImage
|
textView.onPasteImage = onPasteImage
|
||||||
|
textView.isAutomaticTextCompletionEnabled = configuration.textCompletion.isEnabled
|
||||||
if #available(macOS 15.1, *) {
|
if #available(macOS 15.1, *) {
|
||||||
// `.limited` = the Writing Tools popover panel; `.complete` = the inline
|
// `.limited` = the Writing Tools popover panel; `.complete` = the inline
|
||||||
// experience that morphs the text with an animation. We use `.limited` so
|
// experience that morphs the text with an animation. We use `.limited` so
|
||||||
// rewrites/proofread land in the popover (no in-text animation) — it also
|
// rewrites/proofread land in the popover (no in-text animation) — it also
|
||||||
// sidesteps the inline-rewrite flicker that dims text below the selection.
|
// 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
|
// Create TextKit 2 layout bridge
|
||||||
let bridge = LayoutBridge(textLayoutManager)
|
let bridge = LayoutBridge(textLayoutManager)
|
||||||
|
|||||||
Reference in New Issue
Block a user