krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 745599e8e9

745599e8e905766da00da4d49c9b867ba4d6efb5

parent: 4c75f0eea5

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-09 01:09 UTC

Highlight src blocks in the iOS editor

OrgstarMobile depends on OrgHighlight; the tree-sitter targets build for
iOS as they are. The syntax category to theme key mapping moves to
OrgPresentation for both editors.

Layout: unified · split

Package.swift +1 −1
@@ -84,7 +84,7 @@ let package = Package(
84 .target(name: "OrgApp", dependencies: ["OrgWorkspace", "OrgDocument", "OrgPresentation"]), 84 .target(name: "OrgApp", dependencies: ["OrgWorkspace", "OrgDocument", "OrgPresentation"]),
85 .executableTarget(name: "Orgstar", dependencies: ["OrgApp", "OrgEditorAppKit", "OrgHighlight"]), 85 .executableTarget(name: "Orgstar", dependencies: ["OrgApp", "OrgEditorAppKit", "OrgHighlight"]),
86 // The iOS app, bundled by scripts/build-ios-app.sh. 86 // The iOS app, bundled by scripts/build-ios-app.sh.
87 .executableTarget(name: "OrgstarMobile", dependencies: ["OrgApp"]), 87 .executableTarget(name: "OrgstarMobile", dependencies: ["OrgApp", "OrgHighlight"]),
88 // App extensions, bundled into Orgstar.app by scripts/build-app.sh. 88 // App extensions, bundled into Orgstar.app by scripts/build-app.sh.
89 .executableTarget(name: "OrgstarQuickLook", dependencies: ["OrgCore"], linkerSettings: extensionEntry), 89 .executableTarget(name: "OrgstarQuickLook", dependencies: ["OrgCore"], linkerSettings: extensionEntry),
90 .executableTarget(name: "OrgstarSpotlight", dependencies: ["OrgCore"], linkerSettings: extensionEntry), 90 .executableTarget(name: "OrgstarSpotlight", dependencies: ["OrgCore"], linkerSettings: extensionEntry),
Sources/OrgEditorAppKit/Theme.swift +1 −15
@@ -118,21 +118,7 @@ struct Theme {
118 case .block: 118 case .block:
119 break 119 break
120 case .syntax(let category): 120 case .syntax(let category):
121 if let key = Self.slot(category) { fg(key) } 121 if let key = category.themeKey { fg(key) }
122 }
123 }
124
125 static func slot(_ category: SyntaxCategory) -> String? {
126 switch category {
127 case .keyword: "syntax-keyword"
128 case .string: "syntax-string"
129 case .comment: "syntax-comment"
130 case .function: "syntax-function"
131 case .type: "syntax-type"
132 case .number, .constant, .escape: "syntax-number"
133 case .property, .attribute, .tag: "syntax-property"
134 case .label: "syntax-label"
135 case .variable, .operator, .punctuation: nil
136 } 122 }
137 } 123 }
138 124
Sources/OrgPresentation/Presentation.swift +18 −1
@@ -51,7 +51,24 @@ public enum SyntaxCategory: String, Hashable, Sendable, CaseIterable {
51 } 51 }
52} 52}
53 53
54/// Highlights source code; the platform supplies one (tree-sitter on macOS; iOS supplies none). 54extension SyntaxCategory {
55 /// The theme color for this kind of token; nil for those drawn in the text color.
56 public var themeKey: String? {
57 switch self {
58 case .keyword: "syntax-keyword"
59 case .string: "syntax-string"
60 case .comment: "syntax-comment"
61 case .function: "syntax-function"
62 case .type: "syntax-type"
63 case .number, .constant, .escape: "syntax-number"
64 case .property, .attribute, .tag: "syntax-property"
65 case .label: "syntax-label"
66 case .variable, .operator, .punctuation: nil
67 }
68 }
69}
70
71/// Highlights source code; the apps supply tree-sitter's (OrgHighlight).
55public protocol CodeHighlighter: Sendable { 72public protocol CodeHighlighter: Sendable {
56 /// Categories for ranges of `code`, in UTF-16 offsets; later runs win over earlier ones. 73 /// Categories for ranges of `code`, in UTF-16 offsets; later runs win over earlier ones.
57 /// Empty for languages it doesn't know. 74 /// Empty for languages it doesn't know.
Sources/OrgstarMobile/MobileEditor.swift +4 −1
@@ -1,6 +1,7 @@
1#if os(iOS) 1#if os(iOS)
2import OrgCore 2import OrgCore
3import OrgDocument 3import OrgDocument
4import OrgHighlight
4import OrgPresentation 5import OrgPresentation
5import UIKit 6import UIKit
6 7
@@ -96,6 +97,8 @@ final class KeyedTextView: UITextView {
96/// fold as TAB cycles them. 97/// fold as TAB cycles them.
97@MainActor 98@MainActor
98final class MobileEditor: NSObject, DocumentBuffer { 99final class MobileEditor: NSObject, DocumentBuffer {
100 /// One highlighter for every editor and the reader: it caches compiled grammars and results.
101 static let highlighter = TreeSitterHighlighter()
99 private(set) var document: DocumentState 102 private(set) var document: DocumentState
100 let textView: UITextView 103 let textView: UITextView
101 var options: EditingOptions = .org { 104 var options: EditingOptions = .org {
@@ -555,7 +558,7 @@ final class MobileEditor: NSObject, DocumentBuffer {
555 } 558 }
556 let revealed = caretLine 559 let revealed = caretLine
557 var scriptExclusions: [Range<Int>] = [] 560 var scriptExclusions: [Range<Int>] = []
558 for run in Presentation.styles(document.tree, in: whole) { 561 for run in Presentation.styles(document.tree, in: whole, highlighter: Self.highlighter) {
559 let clipped = NSIntersectionRange(NSRange(run.range), lines) 562 let clipped = NSIntersectionRange(NSRange(run.range), lines)
560 guard clipped.length > 0 else { continue } 563 guard clipped.length > 0 else { continue }
561 if Presentation.skipsLeadingStars(run.role, indented: indented, hideStars: hideStars) { continue } 564 if Presentation.skipsLeadingStars(run.role, indented: indented, hideStars: hideStars) { continue }
Sources/OrgstarMobile/MobileTheme.swift +2 −1
@@ -115,7 +115,8 @@ struct MobileTheme {
115 case .footnote, .cookie, .target, .macro, .latex: fg("special") 115 case .footnote, .cookie, .target, .macro, .latex: fg("special")
116 case .table, .rule: fg("table") 116 case .table, .rule: fg("table")
117 case .block: break 117 case .block: break
118 case .syntax: break 118 case .syntax(let category):
119 if let key = category.themeKey { fg(key) }
119 } 120 }
120 } 121 }
121 122