iOS fixes from the manual review !157
8 files changed, +66 −16
Layout: unified · split
Sources/OrgApp/DocumentSession.swift +8
| @@ -82,6 +82,14 @@ public final class DocumentSession { | ||
| 82 | 82 | DocumentState(bytes: bytes, defaults: defaults.resolvingSetupFiles(in: SourceText(bytes: bytes).text, path: path, read: readText)) |
| 83 | 83 | } |
| 84 | 84 | |
| 85 | /// The settings of the file at `path` as the editor sees them: its open buffer's, or its | |
| 86 | /// text on disk with the defaults and the setup files it names. | |
| 87 | public func settings(for path: String) -> OrgSettings? { | |
| 88 | if let entry = entry(path) { return entry.buffer.document.tree.settings } | |
| 89 | guard let bytes = (try? fileSystem.read(URL(fileURLWithPath: path))).flatMap({ $0 }) else { return nil } | |
| 90 | return loaded(bytes, path: path).tree.settings | |
| 91 | } | |
| 92 | ||
| 85 | 93 | /// A file's text, for setup files. |
| 86 | 94 | func readText(_ file: String) -> String? { |
| 87 | 95 | (try? fileSystem.read(URL(fileURLWithPath: file))).flatMap { $0 }.flatMap { String(bytes: $0, encoding: .utf8) } |
Sources/OrgApp/OrgPreferences.swift +9 −2
| @@ -14,13 +14,20 @@ public enum OrgPreferences { | ||
| 14 | 14 | return settings |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | /// What commands run from the agenda and capture follow: logging and tags. | |
| 17 | /// The editing options, as the Mac's Settings apply them. | |
| 18 | 18 | public static func options(_ defaults: UserDefaults = .standard) -> EditingOptions { |
| 19 | 19 | var options = EditingOptions( |
| 20 | 20 | tagsColumn: integer("tagsColumn", defaults), insertHeadingRespectContent: bool("headingAfterSubtree", defaults), |
| 21 | metaReturnMaySplitLine: bool("splitLine", defaults), | |
| 21 | 22 | listAllowAlphabetical: bool("alphabeticalLists", defaults), fillColumn: integer("fillColumn", defaults), |
| 23 | hideEmphasisMarkers: bool("hiddenMarkers", defaults), prettyEntities: bool("prettyEntities", defaults), subSuperscriptsNeedBraces: true, | |
| 22 | 24 | logDone: LogHow(rawValue: string("logDone", defaults)), logIntoDrawer: string("logIntoDrawer", defaults).nonEmpty, |
| 23 | logReschedule: LogHow(rawValue: string("logReschedule", defaults)), logRedeadline: LogHow(rawValue: string("logRedeadline", defaults)) | |
| 25 | hideDrawerStartup: bool("hideDrawerStartup", defaults), hideBlockStartup: bool("hideBlockStartup", defaults), | |
| 26 | logReschedule: LogHow(rawValue: string("logReschedule", defaults)), logRedeadline: LogHow(rawValue: string("logRedeadline", defaults)), | |
| 27 | useSpeedCommands: bool("useSpeedCommands", defaults), startupWithInlineImages: bool("startupWithInlineImages", defaults), | |
| 28 | startupIndented: bool("startupIndented", defaults), hideLeadingStars: bool("hideLeadingStars", defaults), | |
| 29 | startupAlignAllTables: bool("startupAlignAllTables", defaults), startupTruncated: bool("startupTruncated", defaults), | |
| 30 | spellCheck: bool("spellCheck", defaults) | |
| 24 | 31 | ) |
| 25 | 32 | options.electricPair = bool("electricPair", defaults) |
| 26 | 33 | return options |
Sources/OrgstarMobile/AgendaScreen.swift +3 −5
| @@ -250,8 +250,8 @@ struct AgendaScreen: View { | ||
| 250 | 250 | } |
| 251 | 251 | case .failed(let text): |
| 252 | 252 | message = text |
| 253 | case .external: | |
| 254 | message = "\(command.title) needs Emacs, which runs on the Mac." | |
| 253 | case .external(let request): | |
| 254 | message = MobileEditor.unavailable(request, title: command.title) | |
| 255 | 255 | case .commit: |
| 256 | 256 | message = nil |
| 257 | 257 | } |
| @@ -260,9 +260,7 @@ struct AgendaScreen: View { | ||
| 260 | 260 | |
| 261 | 261 | /// The first done keyword of the item's TODO sequence. |
| 262 | 262 | private func doneKeyword(for item: AgendaItem) -> String? { |
| 263 | guard let todo = item.todo, let path = item.path else { return nil } | |
| 264 | let text = session.openTexts[path] ?? (try? String(contentsOfFile: path, encoding: .utf8)) ?? "" | |
| 265 | let settings = OrgParser.parse(text).settings | |
| 263 | guard let todo = item.todo, let path = item.path, let settings = session.settings(for: path) else { return nil } | |
| 266 | 264 | return settings.todoSequences.first { ($0.active + $0.done).contains { $0.name == todo } }?.done.first?.name |
| 267 | 265 | } |
| 268 | 266 | |
Sources/OrgstarMobile/CaptureScreen.swift +1
| @@ -65,6 +65,7 @@ struct CaptureScreen: View { | ||
| 65 | 65 | templates = loaded.templates |
| 66 | 66 | problem = loaded.problems.first |
| 67 | 67 | key = request.template.flatMap { wanted in templates.first { $0.key == wanted }?.key } ?? templates.first?.key |
| 68 | if let wanted = request.template, !templates.contains(where: { $0.key == wanted }) { problem = "No capture template \"\(wanted)\"" } | |
| 68 | 69 | } |
| 69 | 70 | .onChange(of: key, initial: true) { choose() } |
| 70 | 71 | } |
Sources/OrgstarMobile/MobileEditor.swift +15 −8
| @@ -54,7 +54,9 @@ final class KeyedTextView: UITextView { | ||
| 54 | 54 | final class MobileEditor: NSObject, DocumentBuffer { |
| 55 | 55 | private(set) var document: DocumentState |
| 56 | 56 | let textView: UITextView |
| 57 | var options: EditingOptions = .org | |
| 57 | var options: EditingOptions = .org { | |
| 58 | didSet { textView.spellCheckingType = options.spellCheck ? .yes : .no } | |
| 59 | } | |
| 58 | 60 | var keymap = KeymapPreset.emacs.keymap |
| 59 | 61 | var onChange: (() -> Void)? |
| 60 | 62 | var onMessage: ((String) -> Void)? |
| @@ -96,7 +98,7 @@ final class MobileEditor: NSObject, DocumentBuffer { | ||
| 96 | 98 | textView.smartQuotesType = .no |
| 97 | 99 | textView.smartDashesType = .no |
| 98 | 100 | textView.smartInsertDeleteType = .no |
| 99 | textView.spellCheckingType = .yes | |
| 101 | textView.spellCheckingType = options.spellCheck ? .yes : .no | |
| 100 | 102 | textView.keyboardDismissMode = .interactive |
| 101 | 103 | textView.alwaysBounceVertical = true |
| 102 | 104 | textView.textContainerInset = UIEdgeInsets(top: 12, left: 8, bottom: 24, right: 8) |
| @@ -182,14 +184,18 @@ final class MobileEditor: NSObject, DocumentBuffer { | ||
| 182 | 184 | onTangle?(scope) |
| 183 | 185 | case .external(.agenda(let start, let count)) where onAgenda != nil: |
| 184 | 186 | onAgenda?(start, count) |
| 185 | case .external(.emacsTable(_, _, _, let reason, _)): | |
| 186 | onMessage?("This table needs Emacs, which runs on the Mac: \(reason).") | |
| 187 | case .external: | |
| 188 | onMessage?("\(command.title) needs Emacs, which runs on the Mac.") | |
| 187 | case .external(let request): | |
| 188 | onMessage?(Self.unavailable(request, title: command.title)) | |
| 189 | 189 | } |
| 190 | 190 | return step |
| 191 | 191 | } |
| 192 | 192 | |
| 193 | /// Why a request the app doesn't handle did nothing: only table recalculation needs Emacs. | |
| 194 | static func unavailable(_ request: ExternalRequest, title: String) -> String { | |
| 195 | if case .emacsTable(_, _, _, let reason, _) = request { return "This table needs Emacs, which runs on the Mac: \(reason)." } | |
| 196 | return "\(title) isn't available on iPhone and iPad." | |
| 197 | } | |
| 198 | ||
| 193 | 199 | // MARK: - Commands by name and key |
| 194 | 200 | |
| 195 | 201 | /// Runs what `keys` (as `C-c C-t`) is bound to in the Emacs keymap, for the caret's context. |
| @@ -286,8 +292,9 @@ final class MobileEditor: NSObject, DocumentBuffer { | ||
| 286 | 292 | let runnable = candidates.first { binding in |
| 287 | 293 | (binding.when.map { KeyContext.holds($0, in: context) } ?? true) && applies(binding.command, in: context) |
| 288 | 294 | } |
| 289 | guard let binding = runnable else { return keys.count > 1 } | |
| 290 | if binding.command.hasPrefix("edit."), !["edit.undo", "edit.redo"].contains(binding.command) { return keys.count > 1 } | |
| 295 | // A sequence runs its command even where it doesn't apply, which says why, as on the Mac. | |
| 296 | guard let binding = runnable ?? (keys.count > 1 ? candidates.first : nil) else { return false } | |
| 297 | if keys.count == 1, binding.command.hasPrefix("edit."), !["edit.undo", "edit.redo"].contains(binding.command) { return false } | |
| 291 | 298 | run(binding.command) |
| 292 | 299 | return true |
| 293 | 300 | } |
Sources/OrgstarMobile/SettingsScreen.swift +9 −1
| @@ -26,7 +26,7 @@ struct SettingsScreen: View { | ||
| 26 | 26 | Section("In use") { |
| 27 | 27 | let _ = model.settingsVersion |
| 28 | 28 | LabeledContent("TODO keywords", value: todoKeywords) |
| 29 | LabeledContent("Agenda", value: "\(OrgPreferences.agendaSpan()) days from \(OrgPreferences.agendaStartOffset())d") | |
| 29 | LabeledContent("Agenda", value: agenda) | |
| 30 | 30 | LabeledContent("Reminders", value: OrgPreferences.reminders() ? "\(OrgPreferences.reminderLead()) min before" : "Off") |
| 31 | 31 | LabeledContent("Capture templates", value: CaptureTemplates.load().templates.map(\.key).joined(separator: " ")) |
| 32 | 32 | } |
| @@ -37,6 +37,14 @@ struct SettingsScreen: View { | ||
| 37 | 37 | } |
| 38 | 38 | } |
| 39 | 39 | |
| 40 | /// The span and start day, worded as the Mac's Settings word the start. | |
| 41 | private var agenda: String { | |
| 42 | func days(_ count: Int) -> String { count == 1 ? "1 day" : "\(count) days" } | |
| 43 | let offset = OrgPreferences.agendaStartOffset() | |
| 44 | let start = offset == 0 ? "today" : offset < 0 ? "\(days(-offset)) before today" : "\(days(offset)) after today" | |
| 45 | return "\(days(OrgPreferences.agendaSpan())), starting \(start)" | |
| 46 | } | |
| 47 | ||
| 40 | 48 | private var todoKeywords: String { |
| 41 | 49 | OrgPreferences.orgSettings().todoSequences.map { ($0.active.map(\.name) + ["|"] + $0.done.map(\.name)).joined(separator: " ") } |
| 42 | 50 | .joined(separator: "; ") |
Tests/OrgAppTests/AppTests.swift +14
| @@ -226,6 +226,20 @@ struct DocumentSessionTests { | ||
| 226 | 226 | #expect(session.document?.tree.settings.todoKeywordNames.contains("NEXT") == true) |
| 227 | 227 | } |
| 228 | 228 | |
| 229 | @Test func settingsForAFileUseDefaultsAndSetupFiles() throws { | |
| 230 | let notes = try Folder() | |
| 231 | try notes.write("setup.org", "#+TODO: NEXT | FINISHED\n") | |
| 232 | let viaSetup = try notes.write("a.org", "#+SETUPFILE: setup.org\n* NEXT One\n") | |
| 233 | let plain = try notes.write("b.org", "* WAIT Two\n") | |
| 234 | let session = DocumentSession(recovery: FileRecoveryStore(directory: notes.url.appendingPathComponent(".r"))) | |
| 235 | session.defaults = try #require(OrgSettings.default.withTodoKeywords("WAIT | OK")) | |
| 236 | #expect(session.settings(for: viaSetup.path)?.todoSequences.first?.done.first?.name == "FINISHED") | |
| 237 | #expect(session.settings(for: plain.path)?.todoSequences.first?.done.first?.name == "OK") | |
| 238 | session.open(plain.path) | |
| 239 | #expect(session.settings(for: plain.path)?.todoSequences.first?.done.first?.name == "OK") | |
| 240 | #expect(session.settings(for: notes.url.appendingPathComponent("missing.org").path) == nil) | |
| 241 | } | |
| 242 | ||
| 229 | 243 | @Test func keywordCtrlCCtrlCRereadsSetupFiles() throws { |
| 230 | 244 | let notes = try Folder() |
| 231 | 245 | try notes.write("setup.org", "#+TODO: NEXT | DONE\n") |
Tests/OrgAppTests/OrgPreferencesTests.swift +7
| @@ -11,6 +11,9 @@ struct OrgPreferencesTests { | ||
| 11 | 11 | #expect(OrgPreferences.options(defaults).logDone == nil && OrgPreferences.options(defaults).tagsColumn == -77) |
| 12 | 12 | #expect(OrgPreferences.agendaSpan(defaults) == 10 && OrgPreferences.agendaStartOffset(defaults) == -3) |
| 13 | 13 | #expect(OrgPreferences.reminders(defaults) && OrgPreferences.reminderLead(defaults) == 12) |
| 14 | let fallbacks = OrgPreferences.options(defaults) | |
| 15 | #expect(!fallbacks.metaReturnMaySplitLine && fallbacks.hideEmphasisMarkers && fallbacks.prettyEntities && fallbacks.subSuperscriptsNeedBraces) | |
| 16 | #expect(fallbacks.startupIndented && !fallbacks.spellCheck && fallbacks.hideDrawerStartup && !fallbacks.hideBlockStartup) | |
| 14 | 17 | |
| 15 | 18 | let problems = UserConfig.apply(""" |
| 16 | 19 | org-todo-keywords = "TODO NEXT | DONE" |
| @@ -19,6 +22,9 @@ struct OrgPreferencesTests { | ||
| 19 | 22 | org-agenda-span = 7 |
| 20 | 23 | org-agenda-start-day = "-1d" |
| 21 | 24 | appt-message-warning-time = 30 |
| 25 | org-M-RET-may-split-line = true | |
| 26 | org-hide-emphasis-markers = false | |
| 27 | org-pretty-entities = false | |
| 22 | 28 | [orgstar] |
| 23 | 29 | reminders = false |
| 24 | 30 | """, to: defaults) |
| @@ -26,6 +32,7 @@ struct OrgPreferencesTests { | ||
| 26 | 32 | #expect(OrgPreferences.orgSettings(defaults).todoSequences.first?.active.map(\.name) == ["TODO", "NEXT"]) |
| 27 | 33 | let options = OrgPreferences.options(defaults) |
| 28 | 34 | #expect(options.logDone == .time && options.logIntoDrawer == "LOGBOOK") |
| 35 | #expect(options.metaReturnMaySplitLine && !options.hideEmphasisMarkers && !options.prettyEntities) | |
| 29 | 36 | #expect(OrgPreferences.agendaSpan(defaults) == 7 && OrgPreferences.agendaStartOffset(defaults) == -1) |
| 30 | 37 | #expect(!OrgPreferences.reminders(defaults) && OrgPreferences.reminderLead(defaults) == 30) |
| 31 | 38 | } |