Reread setup files on C-c C-c on a keyword line !137
9 files changed, +55 −6
Layout: unified · split
Sources/OrgApp/DocumentSession.swift +15 −4
| @@ -79,10 +79,21 @@ public final class DocumentSession { | |||
| 79 | 79 | ||
| 80 | /// A buffer for `bytes` read from `path`, with the keywords of the setup files it names. | 80 | /// A buffer for `bytes` read from `path`, with the keywords of the setup files it names. |
| 81 | func loaded(_ bytes: [UInt8], path: String) -> DocumentState { | 81 | func loaded(_ bytes: [UInt8], path: String) -> DocumentState { |
| 82 | let settings = defaults.resolvingSetupFiles(in: SourceText(bytes: bytes).text, path: path) { file in | 82 | DocumentState(bytes: bytes, defaults: defaults.resolvingSetupFiles(in: SourceText(bytes: bytes).text, path: path, read: readText)) |
| 83 | (try? self.fileSystem.read(URL(fileURLWithPath: file))).flatMap { $0 }.flatMap { String(bytes: $0, encoding: .utf8) } | 83 | } |
| 84 | } | 84 | |
| 85 | return DocumentState(bytes: bytes, defaults: settings) | 85 | /// A file's text, for setup files. |
| 86 | func readText(_ file: String) -> String? { | ||
| 87 | (try? fileSystem.read(URL(fileURLWithPath: file))).flatMap { $0 }.flatMap { String(bytes: $0, encoding: .utf8) } | ||
| 88 | } | ||
| 89 | |||
| 90 | /// C-c C-c on a keyword line: the open file's setup files read again (`org-mode-restart`). | ||
| 91 | public func refreshSetup() { | ||
| 92 | guard let entry = current else { return } | ||
| 93 | let settings = defaults.resolvingSetupFiles(in: entry.buffer.document.text, path: entry.path, read: readText) | ||
| 94 | entry.buffer.update { $0.setDefaults(settings) } | ||
| 95 | refresh() | ||
| 96 | editVersion += 1 | ||
| 86 | } | 97 | } |
| 87 | 98 | ||
| 88 | /// One open file. | 99 | /// One open file. |
Sources/OrgCore/Commands/Command.swift +2
| @@ -194,6 +194,8 @@ public enum Effect: Sendable, Equatable { | |||
| 194 | case sparseTree(show: [Int], highlights: [Range<Int>]) | 194 | case sparseTree(show: [Int], highlights: [Range<Int>]) |
| 195 | /// Fold the subtree of the heading starting here (in the new text). | 195 | /// Fold the subtree of the heading starting here (in the new text). |
| 196 | case fold(Int) | 196 | case fold(Int) |
| 197 | /// Read the file's `#+SETUPFILE`s again (`org-mode-restart`). | ||
| 198 | case refreshSetup | ||
| 197 | } | 199 | } |
| 198 | 200 | ||
| 199 | public struct EditResult: Sendable, Equatable { | 201 | public struct EditResult: Sendable, Equatable { |
Sources/OrgCore/Commands/CtrlCCtrlC.swift +1 −1
| @@ -62,7 +62,7 @@ public struct CtrlCCtrlC: OrgCommand { | |||
| 62 | } | 62 | } |
| 63 | } | 63 | } |
| 64 | if buffer.lookingAtLine("^[ \\t]*#\\+[A-Za-z_]+:") { | 64 | if buffer.lookingAtLine("^[ \\t]*#\\+[A-Za-z_]+:") { |
| 65 | return .commit(EditResult(baseRevision: context.revision, edits: [], effects: [.message("Local setup has been refreshed")])) | 65 | return .commit(EditResult(baseRevision: context.revision, edits: [], effects: [.refreshSetup, .message("Local setup has been refreshed")])) |
| 66 | } | 66 | } |
| 67 | if buffer.propertyAtPoint() != nil || buffer.lookingAtLine("^[ \\t]*:(?:PROPERTIES|END):[ \\t]*$") && buffer.existingPropertyBlock() != nil { | 67 | if buffer.propertyAtPoint() != nil || buffer.lookingAtLine("^[ \\t]*:(?:PROPERTIES|END):[ \\t]*$") && buffer.existingPropertyBlock() != nil { |
| 68 | return PropertyAction().run(in: context) | 68 | return PropertyAction().run(in: context) |
Sources/OrgDocument/DocumentState.swift +8 −1
| @@ -27,7 +27,7 @@ public struct DocumentState: Sendable { | |||
| 27 | public private(set) var tree: OrgTree | 27 | public private(set) var tree: OrgTree |
| 28 | /// Increases on every change to `text`. | 28 | /// Increases on every change to `text`. |
| 29 | public private(set) var revision = 0 | 29 | public private(set) var revision = 0 |
| 30 | public let defaults: OrgSettings | 30 | public private(set) var defaults: OrgSettings |
| 31 | private var undoStack: [[TextEdit]] = [] | 31 | private var undoStack: [[TextEdit]] = [] |
| 32 | private var redoStack: [[TextEdit]] = [] | 32 | private var redoStack: [[TextEdit]] = [] |
| 33 | 33 | ||
| @@ -38,6 +38,13 @@ public struct DocumentState: Sendable { | |||
| 38 | self.defaults = defaults | 38 | self.defaults = defaults |
| 39 | } | 39 | } |
| 40 | 40 | ||
| 41 | /// New settings for the same text, as when setup files are read again; undo history stays. | ||
| 42 | public mutating func setDefaults(_ defaults: OrgSettings) { | ||
| 43 | self.defaults = defaults | ||
| 44 | tree = OrgParser.parse(text, defaults: defaults) | ||
| 45 | revision += 1 | ||
| 46 | } | ||
| 47 | |||
| 41 | public var mergeBase: [UInt8] { source.originalBytes } | 48 | public var mergeBase: [UInt8] { source.originalBytes } |
| 42 | public var isDirty: Bool { text != source.text } | 49 | public var isDirty: Bool { text != source.text } |
| 43 | public var isEditable: Bool { source.isEditable } | 50 | public var isEditable: Bool { source.isEditable } |
Sources/OrgEditorAppKit/OrgEditor.swift +4
| @@ -1350,6 +1350,7 @@ extension OrgEditor: DocumentBuffer { | |||
| 1350 | /// mapped through the change. Text-view undo refers to the old text, so it is cleared. | 1350 | /// mapped through the change. Text-view undo refers to the old text, so it is cleared. |
| 1351 | public func update<T>(_ change: (inout DocumentState) throws -> T) rethrows -> T { | 1351 | public func update<T>(_ change: (inout DocumentState) throws -> T) rethrows -> T { |
| 1352 | let before = document.text | 1352 | let before = document.text |
| 1353 | let defaultsBefore = document.defaults | ||
| 1353 | let selected = textView.selectedRange() | 1354 | let selected = textView.selectedRange() |
| 1354 | let result = try change(&document) | 1355 | let result = try change(&document) |
| 1355 | if document.text != before { | 1356 | if document.text != before { |
| @@ -1364,6 +1365,9 @@ extension OrgEditor: DocumentBuffer { | |||
| 1364 | if !selection.isEmpty { textView.setSelectedRange(NSRange(selection)) } | 1365 | if !selection.isEmpty { textView.setSelectedRange(NSRange(selection)) } |
| 1365 | textView.undoManager?.removeAllActions(withTarget: textView.textStorage!) | 1366 | textView.undoManager?.removeAllActions(withTarget: textView.textStorage!) |
| 1366 | textView.undoManager?.removeAllActions(withTarget: textView) | 1367 | textView.undoManager?.removeAllActions(withTarget: textView) |
| 1368 | } else if document.defaults != defaultsBefore { | ||
| 1369 | // New settings for the same text: TODO keywords may color differently. | ||
| 1370 | restyleEverything() | ||
| 1367 | } | 1371 | } |
| 1368 | onChange?() | 1372 | onChange?() |
| 1369 | return result | 1373 | return result |
Sources/Orgstar/EditorView.swift +1
| @@ -114,6 +114,7 @@ struct EditorView: NSViewRepresentable { | |||
| 114 | case .copy(let text): | 114 | case .copy(let text): |
| 115 | NSPasteboard.general.clearContents() | 115 | NSPasteboard.general.clearContents() |
| 116 | NSPasteboard.general.setString(text, forType: .string) | 116 | NSPasteboard.general.setString(text, forType: .string) |
| 117 | case .refreshSetup: session.refreshSetup() | ||
| 117 | case .message, .sparseTree, .fold: break | 118 | case .message, .sparseTree, .fold: break |
| 118 | } | 119 | } |
| 119 | } | 120 | } |
Sources/OrgstarMobile/EditorScreen.swift +1
| @@ -162,6 +162,7 @@ struct EditorScreen: View { | |||
| 162 | editor.onEditBlock = { edit in sheet = .block(edit) } | 162 | editor.onEditBlock = { edit in sheet = .block(edit) } |
| 163 | editor.onTangle = { scope in message = session.tangle(scope) } | 163 | editor.onTangle = { scope in message = session.tangle(scope) } |
| 164 | editor.onBabel = { job in run(job) } | 164 | editor.onBabel = { job in run(job) } |
| 165 | editor.onRefreshSetup = { session.refreshSetup() } | ||
| 165 | editor.onAgenda = { start, count in model.agendaRequest = .init(start: start, count: count) } | 166 | editor.onAgenda = { start, count in model.agendaRequest = .init(start: start, count: count) } |
| 166 | editor.onAppCommand = { id in | 167 | editor.onAppCommand = { id in |
| 167 | switch id { | 168 | switch id { |
Sources/OrgstarMobile/MobileEditor.swift +6
| @@ -66,6 +66,8 @@ final class MobileEditor: NSObject, DocumentBuffer { | |||
| 66 | var onTangle: ((Tangle.Scope) -> Void)? | 66 | var onTangle: ((Tangle.Scope) -> Void)? |
| 67 | /// A source block to run (C-c C-c). | 67 | /// A source block to run (C-c C-c). |
| 68 | var onBabel: ((BabelJob) -> Void)? | 68 | var onBabel: ((BabelJob) -> Void)? |
| 69 | /// The file's setup files to read again (C-c C-c on a keyword line). | ||
| 70 | var onRefreshSetup: (() -> Void)? | ||
| 69 | /// A block, a table's formulas or a field to edit apart (C-c ', C-c `). | 71 | /// A block, a table's formulas or a field to edit apart (C-c ', C-c `). |
| 70 | var onEditBlock: ((BlockEdit) -> Void)? | 72 | var onEditBlock: ((BlockEdit) -> Void)? |
| 71 | /// A followed timestamp's agenda days: the first (a `Days` number) and how many. | 73 | /// A followed timestamp's agenda days: the first (a `Days` number) and how many. |
| @@ -119,6 +121,7 @@ final class MobileEditor: NSObject, DocumentBuffer { | |||
| 119 | 121 | ||
| 120 | func update<T>(_ change: (inout DocumentState) throws -> T) rethrows -> T { | 122 | func update<T>(_ change: (inout DocumentState) throws -> T) rethrows -> T { |
| 121 | let before = document.text | 123 | let before = document.text |
| 124 | let defaultsBefore = document.defaults | ||
| 122 | let selected = textView.selectedRange | 125 | let selected = textView.selectedRange |
| 123 | let result = try change(&document) | 126 | let result = try change(&document) |
| 124 | if document.text != before { | 127 | if document.text != before { |
| @@ -128,6 +131,8 @@ final class MobileEditor: NSObject, DocumentBuffer { | |||
| 128 | textView.selectedRange = NSRange(location: min(selected.location, length), length: 0) | 131 | textView.selectedRange = NSRange(location: min(selected.location, length), length: 0) |
| 129 | textView.undoManager?.removeAllActions() | 132 | textView.undoManager?.removeAllActions() |
| 130 | setFolds(fold) | 133 | setFolds(fold) |
| 134 | } else if document.defaults != defaultsBefore { | ||
| 135 | restyle(0..<(document.text as NSString).length) | ||
| 131 | } | 136 | } |
| 132 | return result | 137 | return result |
| 133 | } | 138 | } |
| @@ -157,6 +162,7 @@ final class MobileEditor: NSObject, DocumentBuffer { | |||
| 157 | var state = fold | 162 | var state = fold |
| 158 | state.folds.insert(start) | 163 | state.folds.insert(start) |
| 159 | setFolds(state) | 164 | setFolds(state) |
| 165 | case .refreshSetup: onRefreshSetup?() | ||
| 160 | default: break | 166 | default: break |
| 161 | } | 167 | } |
| 162 | } | 168 | } |
Tests/OrgAppTests/AppTests.swift +17
| @@ -199,6 +199,23 @@ struct DocumentSessionTests { | |||
| 199 | #expect(session.document?.tree.settings.todoKeywordNames.contains("NEXT") == true) | 199 | #expect(session.document?.tree.settings.todoKeywordNames.contains("NEXT") == true) |
| 200 | } | 200 | } |
| 201 | 201 | ||
| 202 | @Test func keywordCtrlCCtrlCRereadsSetupFiles() throws { | ||
| 203 | let notes = try Folder() | ||
| 204 | try notes.write("setup.org", "#+TODO: NEXT | DONE\n") | ||
| 205 | let file = try notes.write("a.org", "#+SETUPFILE: setup.org\n* WAIT One\n") | ||
| 206 | let session = DocumentSession(recovery: FileRecoveryStore(directory: notes.url.appendingPathComponent(".r"))) | ||
| 207 | session.open(file.path) | ||
| 208 | #expect(session.document?.tree.settings.todoKeywordNames.contains("WAIT") == false) | ||
| 209 | try notes.write("setup.org", "#+TODO: NEXT WAIT | DONE\n") | ||
| 210 | let text = session.document!.text | ||
| 211 | let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2]) | ||
| 212 | guard case .commit(let result) = CtrlCCtrlC().run(in: context) else { Issue.record("no commit"); return } | ||
| 213 | #expect(result.effects.contains(.refreshSetup)) | ||
| 214 | session.refreshSetup() | ||
| 215 | #expect(session.document?.tree.settings.todoKeywordNames.contains("WAIT") == true) | ||
| 216 | #expect(session.document?.isDirty == false) | ||
| 217 | } | ||
| 218 | |||
| 202 | @Test func missingFileReportsAnError() { | 219 | @Test func missingFileReportsAnError() { |
| 203 | let session = DocumentSession() | 220 | let session = DocumentSession() |
| 204 | session.open("/nonexistent/a.org") | 221 | session.open("/nonexistent/a.org") |