Fix quoted header values, shell values and setup-file staleness !141
13 files changed, +95 −20
Layout: unified · split
Sources/OrgApp/AgendaModel.swift +2 −1
| @@ -102,7 +102,8 @@ public final class AgendaModel { | |||
| 102 | let attributes = try? FileManager.default.attributesOfItem(atPath: path) | 102 | let attributes = try? FileManager.default.attributesOfItem(atPath: path) |
| 103 | let mtime = attributes?[.modificationDate] as? Date ?? .distantPast | 103 | let mtime = attributes?[.modificationDate] as? Date ?? .distantPast |
| 104 | let size = (attributes?[.size] as? NSNumber)?.intValue ?? -1 | 104 | let size = (attributes?[.size] as? NSNumber)?.intValue ?? -1 |
| 105 | let cached = cache[path].flatMap { $0.mtime == mtime && $0.size == size ? $0.source : nil } | 105 | // A file reading setup files is read again: they may have changed. |
| 106 | let cached = cache[path].flatMap { $0.mtime == mtime && $0.size == size && !$0.source.readsSetupFiles ? $0.source : nil } | ||
| 106 | reads.append((path, cached, mtime, size, nil)) | 107 | reads.append((path, cached, mtime, size, nil)) |
| 107 | } | 108 | } |
| 108 | let loaded = await Task.detached { | 109 | let loaded = await Task.detached { |
Sources/OrgApp/DocumentSession.swift +11 −4
| @@ -90,12 +90,17 @@ public final class DocumentSession { | |||
| 90 | /// C-c C-c on a keyword line: the open file's setup files read again (`org-mode-restart`). | 90 | /// C-c C-c on a keyword line: the open file's setup files read again (`org-mode-restart`). |
| 91 | public func refreshSetup() { | 91 | public func refreshSetup() { |
| 92 | guard let entry = current else { return } | 92 | guard let entry = current else { return } |
| 93 | let settings = defaults.resolvingSetupFiles(in: entry.buffer.document.text, path: entry.path, read: readText) | 93 | resolveSetup(entry) |
| 94 | entry.buffer.update { $0.setDefaults(settings) } | ||
| 95 | refresh() | 94 | refresh() |
| 96 | editVersion += 1 | 95 | editVersion += 1 |
| 97 | } | 96 | } |
| 98 | 97 | ||
| 98 | /// The entry's setup files read again, as its text names them now. | ||
| 99 | private func resolveSetup(_ entry: Entry) { | ||
| 100 | let settings = defaults.resolvingSetupFiles(in: entry.buffer.document.text, path: entry.path, read: readText) | ||
| 101 | if settings != entry.buffer.document.defaults { entry.buffer.update { $0.setDefaults(settings) } } | ||
| 102 | } | ||
| 103 | |||
| 99 | /// One open file. | 104 | /// One open file. |
| 100 | @MainActor | 105 | @MainActor |
| 101 | final class Entry { | 106 | final class Entry { |
| @@ -358,7 +363,8 @@ public final class DocumentSession { | |||
| 358 | do { | 363 | do { |
| 359 | guard let bytes = try fileSystem.read(url) else { return } | 364 | guard let bytes = try fileSystem.read(url) else { return } |
| 360 | if isDirty { try recovery.keep(try buffer.document.encodedText(), for: url, label: "local") } | 365 | if isDirty { try recovery.keep(try buffer.document.encodedText(), for: url, label: "local") } |
| 361 | buffer.update { $0 = DocumentState(bytes: bytes, defaults: $0.defaults) } | 366 | let path = url.path |
| 367 | buffer.update { $0 = loaded(bytes, path: path) } | ||
| 362 | current?.conflict = nil | 368 | current?.conflict = nil |
| 363 | } catch { | 369 | } catch { |
| 364 | lastError = "Can't read \(name): \(error.localizedDescription)" | 370 | lastError = "Can't read \(name): \(error.localizedDescription)" |
| @@ -383,7 +389,7 @@ public final class DocumentSession { | |||
| 383 | try recovery.keep(try buffer.document.encodedText(), for: url, label: "local") | 389 | try recovery.keep(try buffer.document.encodedText(), for: url, label: "local") |
| 384 | let merged = mergeWithMarkers(base: versions.base, ours: versions.mine, theirs: versions.disk, oursLabel: "yours", theirsLabel: "disk") | 390 | let merged = mergeWithMarkers(base: versions.base, ours: versions.mine, theirs: versions.disk, oursLabel: "yours", theirsLabel: "disk") |
| 385 | try buffer.update { state in | 391 | try buffer.update { state in |
| 386 | state = DocumentState(bytes: disk, defaults: state.defaults) | 392 | state = loaded(disk, path: url.path) |
| 387 | try state.apply(lineEdits(from: versions.disk, to: merged), baseRevision: state.revision) | 393 | try state.apply(lineEdits(from: versions.disk, to: merged), baseRevision: state.revision) |
| 388 | } | 394 | } |
| 389 | current?.conflict = nil | 395 | current?.conflict = nil |
| @@ -835,6 +841,7 @@ public final class DocumentSession { | |||
| 835 | entry.conflict = "\(name) changed on disk, and the changes conflict with yours." | 841 | entry.conflict = "\(name) changed on disk, and the changes conflict with yours." |
| 836 | case .merged, .reloaded: | 842 | case .merged, .reloaded: |
| 837 | entry.conflict = nil | 843 | entry.conflict = nil |
| 844 | resolveSetup(entry) | ||
| 838 | // Edits merged with the disk's still need saving. | 845 | // Edits merged with the disk's still need saving. |
| 839 | scheduleAutosave(entry) | 846 | scheduleAutosave(entry) |
| 840 | case .unchanged: | 847 | case .unchanged: |
Sources/OrgCore/Agenda/AgendaSource.swift +3
| @@ -4,6 +4,8 @@ import Foundation | |||
| 4 | /// and the active timestamps and date ranges org's agenda would consider. Positions are UTF-16. | 4 | /// and the active timestamps and date ranges org's agenda would consider. Positions are UTF-16. |
| 5 | public struct AgendaSource: Sendable { | 5 | public struct AgendaSource: Sendable { |
| 6 | public let path: String | 6 | public let path: String |
| 7 | /// The file names `#+SETUPFILE`s. | ||
| 8 | public let readsSetupFiles: Bool | ||
| 7 | 9 | ||
| 8 | struct Heading: Sendable { | 10 | struct Heading: Sendable { |
| 9 | /// Start of the heading line. | 11 | /// Start of the heading line. |
| @@ -96,6 +98,7 @@ public struct AgendaSource: Sendable { | |||
| 96 | 98 | ||
| 97 | public init(path: String, text: String, defaults: OrgSettings = .default) { | 99 | public init(path: String, text: String, defaults: OrgSettings = .default) { |
| 98 | self.path = path | 100 | self.path = path |
| 101 | readsSetupFiles = text.range(of: "#+setupfile:", options: .caseInsensitive) != nil | ||
| 99 | let tree = OrgParser.parse(text, defaults: defaults) | 102 | let tree = OrgParser.parse(text, defaults: defaults) |
| 100 | let model = DocumentModel(tree: tree) | 103 | let model = DocumentModel(tree: tree) |
| 101 | let ns = text as NSString | 104 | let ns = text as NSString |
Sources/OrgCore/Compute/Babel.swift +11 −7
| @@ -337,7 +337,7 @@ public enum Babel { | |||
| 337 | // command line after it and `:stdin` as its input; the value is its last line. | 337 | // command line after it and `:stdin` as its input; the value is its last line. |
| 338 | var script = (shebang ?? "#!/usr/bin/env \(executable)") + "\n" | 338 | var script = (shebang ?? "#!/usr/bin/env \(executable)") + "\n" |
| 339 | if params.single[":padline"] != "no" { script += "\n" } | 339 | if params.single[":padline"] != "no" { script += "\n" } |
| 340 | script += (lines + [body]).joined(separator: "\n") | 340 | script += (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "") |
| 341 | let command = Babel.scriptFileToken + (stdin != nil || cmdline != nil ? " " + (cmdline ?? "") : "") | 341 | let command = Babel.scriptFileToken + (stdin != nil || cmdline != nil ? " " + (cmdline ?? "") : "") |
| 342 | var job = BabelJob( | 342 | var job = BabelJob( |
| 343 | language: language, runner: .process(executable: executable, arguments: ["-c", command]), | 343 | language: language, runner: .process(executable: executable, arguments: ["-c", command]), |
| @@ -562,6 +562,8 @@ public enum Babel { | |||
| 562 | var single: [String: String] = [:] | 562 | var single: [String: String] = [:] |
| 563 | var vars: [(name: String, value: String)] = [] | 563 | var vars: [(name: String, value: String)] = [] |
| 564 | var wrap: String? | 564 | var wrap: String? |
| 565 | /// Keys whose value is Lisp to evaluate (`org-babel-read`): unquoted, starting with `(`. | ||
| 566 | var lisp: Set<String> = [] | ||
| 565 | 567 | ||
| 566 | static let resultGroups: [Set<String>] = [ | 568 | static let resultGroups: [Set<String>] = [ |
| 567 | ["file", "list", "vector", "table", "scalar", "verbatim"], | 569 | ["file", "list", "vector", "table", "scalar", "verbatim"], |
| @@ -610,7 +612,12 @@ public enum Babel { | |||
| 610 | layers += extra | 612 | layers += extra |
| 611 | 613 | ||
| 612 | for layer in layers { | 614 | for layer in layers { |
| 613 | for (key, value) in layer { | 615 | for (key, raw) in layer { |
| 616 | let quoted = key != ":var" && raw.count >= 2 && raw.hasPrefix("\"") && raw.hasSuffix("\"") | ||
| 617 | let value = quoted ? Params.unescape(String(raw.dropFirst().dropLast())) : raw | ||
| 618 | if key != ":var" { | ||
| 619 | if !quoted, value.hasPrefix("(") { lisp.insert(key) } else { lisp.remove(key) } | ||
| 620 | } | ||
| 614 | switch key { | 621 | switch key { |
| 615 | case ":results": | 622 | case ":results": |
| 616 | for word in value.split(whereSeparator: \.isWhitespace).map(String.init) { | 623 | for word in value.split(whereSeparator: \.isWhitespace).map(String.init) { |
| @@ -715,11 +722,8 @@ public enum Babel { | |||
| 715 | let trimmed = part.trimmingCharacters(in: .whitespaces) | 722 | let trimmed = part.trimmingCharacters(in: .whitespaces) |
| 716 | guard trimmed.hasPrefix(":") else { continue } | 723 | guard trimmed.hasPrefix(":") else { continue } |
| 717 | let key = String(trimmed.prefix { !$0.isWhitespace }).lowercased() | 724 | let key = String(trimmed.prefix { !$0.isWhitespace }).lowercased() |
| 718 | var value = trimmed.dropFirst(key.count).trimmingCharacters(in: .whitespaces) | 725 | // Quotes stay until the value is used, so a quoted string isn't taken for Lisp. |
| 719 | if key != ":var", value.count >= 2, value.hasPrefix("\""), value.hasSuffix("\"") { | 726 | pairs.append((key, trimmed.dropFirst(key.count).trimmingCharacters(in: .whitespaces))) |
| 720 | value = Params.unescape(String(value.dropFirst().dropLast())) | ||
| 721 | } | ||
| 722 | pairs.append((key, value)) | ||
| 723 | } | 727 | } |
| 724 | return pairs | 728 | return pairs |
| 725 | } | 729 | } |
Sources/OrgCore/Compute/Tangle.swift +2 −1
| @@ -128,7 +128,8 @@ public enum Tangle { | |||
| 128 | 128 | ||
| 129 | static func params(_ block: SrcBlockInfo, model: DocumentModel, text: NSString, path: String) throws -> Babel.Params { | 129 | static func params(_ block: SrcBlockInfo, model: DocumentModel, text: NSString, path: String) throws -> Babel.Params { |
| 130 | var params = try Babel.Params(block: block, model: model, text: text) | 130 | var params = try Babel.Params(block: block, model: model, text: text) |
| 131 | for (key, value) in params.single where value.hasPrefix("(") && key != ":tangle-mode" { | 131 | for key in params.lisp where key != ":tangle-mode" { |
| 132 | guard let value = params.single[key] else { continue } | ||
| 132 | params.single[key] = try evaluate(value, key: key, path: path) | 133 | params.single[key] = try evaluate(value, key: key, path: path) |
| 133 | } | 134 | } |
| 134 | return params | 135 | return params |
Sources/OrgCore/Parser/SetupFile.swift +5 −4
| @@ -5,9 +5,10 @@ import Foundation | |||
| 5 | /// naming file's folder; URLs aren't fetched. | 5 | /// naming file's folder; URLs aren't fetched. |
| 6 | public enum SetupFile { | 6 | public enum SetupFile { |
| 7 | /// The keyword lines the setup files `text` names bring, in order, as `#+KEY: value` lines. | 7 | /// The keyword lines the setup files `text` names bring, in order, as `#+KEY: value` lines. |
| 8 | /// `read` gives a file's text, or nil when it can't be read. | 8 | /// `read` gives a file's text, or nil when it can't be read. `path`, the naming file's, |
| 9 | public static func keywordLines(in text: String, directory: String, read: (String) -> String?) -> String { | 9 | /// counts as read already, as `buffer-file-name` does in Org. |
| 10 | lines(in: text, directory: directory, read: read, own: false, seen: []) | 10 | public static func keywordLines(in text: String, directory: String, path: String? = nil, read: (String) -> String?) -> String { |
| 11 | lines(in: text, directory: directory, read: read, own: false, seen: path.map { [($0 as NSString).standardizingPath] } ?? []) | ||
| 11 | } | 12 | } |
| 12 | 13 | ||
| 13 | private static func lines(in text: String, directory: String, read: (String) -> String?, own: Bool, seen: Set<String>) -> String { | 14 | private static func lines(in text: String, directory: String, read: (String) -> String?, own: Bool, seen: Set<String>) -> String { |
| @@ -45,7 +46,7 @@ extension OrgSettings { | |||
| 45 | /// These settings with the keywords of the setup files `text` names, for a file at `path`. | 46 | /// These settings with the keywords of the setup files `text` names, for a file at `path`. |
| 46 | public func resolvingSetupFiles(in text: String, path: String, read: (String) -> String?) -> OrgSettings { | 47 | public func resolvingSetupFiles(in text: String, path: String, read: (String) -> String?) -> OrgSettings { |
| 47 | var copy = self | 48 | var copy = self |
| 48 | copy.setupKeywords = SetupFile.keywordLines(in: text, directory: (path as NSString).deletingLastPathComponent, read: read) | 49 | copy.setupKeywords = SetupFile.keywordLines(in: text, directory: (path as NSString).deletingLastPathComponent, path: path, read: read) |
| 49 | return copy | 50 | return copy |
| 50 | } | 51 | } |
| 51 | 52 | ||
Sources/OrgIndex/FileRecord.swift +21 −1
| @@ -120,13 +120,33 @@ public struct FileRecord: Sendable, Equatable { | |||
| 120 | self.size = bytes.count | 120 | self.size = bytes.count |
| 121 | self.mtime = mtime | 121 | self.mtime = mtime |
| 122 | self.hash = Self.hash(bytes) | 122 | self.hash = Self.hash(bytes) |
| 123 | self.settingsVersion = settings.version | ||
| 124 | let source = SourceText(bytes: bytes) | 123 | let source = SourceText(bytes: bytes) |
| 125 | var resolved = settings | 124 | var resolved = settings |
| 126 | resolved.org = settings.org.resolvingSetupFiles(in: source.text, path: path, read: OrgSettings.readFile) | 125 | resolved.org = settings.org.resolvingSetupFiles(in: source.text, path: path, read: OrgSettings.readFile) |
| 126 | self.settingsVersion = Self.settingsVersion(settings, text: source.text, setupKeywords: resolved.org.setupKeywords) | ||
| 127 | headings = kind == .conflict ? [] : Self.headings(source.text, settings: resolved) | 127 | headings = kind == .conflict ? [] : Self.headings(source.text, settings: resolved) |
| 128 | } | 128 | } |
| 129 | 129 | ||
| 130 | /// The settings version a file's rows are built under: the index's, with the keywords of | ||
| 131 | /// the setup files it names mixed in, so a changed setup file indexes it again. | ||
| 132 | public static func settingsVersion(_ settings: IndexSettings, text: String, setupKeywords: String) -> Int { | ||
| 133 | guard text.range(of: "#+setupfile:", options: .caseInsensitive) != nil else { return settings.version } | ||
| 134 | var hash: UInt64 = 0xcbf29ce484222325 | ||
| 135 | for byte in setupKeywords.utf8 { | ||
| 136 | hash ^= UInt64(byte) | ||
| 137 | hash = hash &* 0x100000001b3 | ||
| 138 | } | ||
| 139 | return settings.version + 1_000_000 * (1 + Int(hash % 1_000_000)) | ||
| 140 | } | ||
| 141 | |||
| 142 | /// `settingsVersion` for a file's bytes, reading the setup files it names. | ||
| 143 | public static func settingsVersion(_ settings: IndexSettings, bytes: [UInt8], path: String) -> Int { | ||
| 144 | let text = SourceText(bytes: bytes).text | ||
| 145 | guard text.range(of: "#+setupfile:", options: .caseInsensitive) != nil else { return settings.version } | ||
| 146 | let keywords = settings.org.resolvingSetupFiles(in: text, path: path, read: OrgSettings.readFile).setupKeywords | ||
| 147 | return settingsVersion(settings, text: text, setupKeywords: keywords) | ||
| 148 | } | ||
| 149 | |||
| 130 | /// A file the index lists without reading: `hash` stands for its size and mtime. | 150 | /// A file the index lists without reading: `hash` stands for its size and mtime. |
| 131 | public init(path: String, root: String, unreadSize size: Int, mtime: Double, settings: IndexSettings) { | 151 | public init(path: String, root: String, unreadSize size: Int, mtime: Double, settings: IndexSettings) { |
| 132 | self.path = path | 152 | self.path = path |
Sources/OrgWorkspace/Reconciler.swift +4 −2
| @@ -90,7 +90,8 @@ public struct Reconciler: Sendable { | |||
| 90 | guard let data = try? Data(contentsOf: file.url) else { continue } | 90 | guard let data = try? Data(contentsOf: file.url) else { continue } |
| 91 | let bytes = [UInt8](data) | 91 | let bytes = [UInt8](data) |
| 92 | let hash = FileRecord.hash(bytes) | 92 | let hash = FileRecord.hash(bytes) |
| 93 | if let state = known[path], state.hash == hash, state.settingsVersion == settings.version, state.kind == file.kind { | 93 | if let state = known[path], state.hash == hash, state.settingsVersion == FileRecord.settingsVersion(settings, bytes: bytes, path: path), |
| 94 | state.kind == file.kind { | ||
| 94 | change.touches.append((path, file.mtime)) | 95 | change.touches.append((path, file.mtime)) |
| 95 | report.touched.append(path) | 96 | report.touched.append(path) |
| 96 | } else { | 97 | } else { |
| @@ -105,7 +106,8 @@ public struct Reconciler: Sendable { | |||
| 105 | let path = candidate.file.url.path | 106 | let path = candidate.file.url.path |
| 106 | if known[path] == nil, | 107 | if known[path] == nil, |
| 107 | let (oldPath, _) = missing.first(where: { | 108 | let (oldPath, _) = missing.first(where: { |
| 108 | $0.value.hash == candidate.hash && $0.value.settingsVersion == settings.version && $0.value.kind == candidate.file.kind | 109 | $0.value.hash == candidate.hash && $0.value.kind == candidate.file.kind |
| 110 | && $0.value.settingsVersion == (candidate.file.kind == .other ? settings.version : FileRecord.settingsVersion(settings, bytes: candidate.bytes, path: path)) | ||
| 109 | }) { | 111 | }) { |
| 110 | missing[oldPath] = nil | 112 | missing[oldPath] = nil |
| 111 | change.moves.append((oldPath, path, candidate.file.mtime)) | 113 | change.moves.append((oldPath, path, candidate.file.mtime)) |
Tests/OrgAppTests/AppTests.swift +12
| @@ -216,6 +216,18 @@ struct DocumentSessionTests { | |||
| 216 | #expect(session.document?.isDirty == false) | 216 | #expect(session.document?.isDirty == false) |
| 217 | } | 217 | } |
| 218 | 218 | ||
| 219 | @Test func reloadingFromDiskReadsSetupFilesAgain() throws { | ||
| 220 | let notes = try Folder() | ||
| 221 | try notes.write("setup.org", "#+TODO: NEXT | DONE\n") | ||
| 222 | let file = try notes.write("a.org", "* WAIT One\n") | ||
| 223 | let session = DocumentSession(recovery: FileRecoveryStore(directory: notes.url.appendingPathComponent(".r"))) | ||
| 224 | session.open(file.path) | ||
| 225 | try notes.write("a.org", "#+SETUPFILE: setup.org\n* WAIT One\n") | ||
| 226 | try notes.write("setup.org", "#+TODO: WAIT | DONE\n") | ||
| 227 | session.fileChanged(file.path) | ||
| 228 | #expect(session.document?.tree.settings.todoKeywordNames.contains("WAIT") == true) | ||
| 229 | } | ||
| 230 | |||
| 219 | @Test func missingFileReportsAnError() { | 231 | @Test func missingFileReportsAnError() { |
| 220 | let session = DocumentSession() | 232 | let session = DocumentSession() |
| 221 | session.open("/nonexistent/a.org") | 233 | session.open("/nonexistent/a.org") |
Tests/OrgCoreTests/BabelTests.swift +3
| @@ -145,6 +145,9 @@ struct BabelTests { | |||
| 145 | "#+NAME: items\n- x\n- y\n\n#+begin_src sh :stdin items\nwc -l | tr -d ' '\n#+end_src\n", | 145 | "#+NAME: items\n- x\n- y\n\n#+begin_src sh :stdin items\nwc -l | tr -d ' '\n#+end_src\n", |
| 146 | "#+begin_src bash :shebang #!/bin/bash :results output\necho ${BASH_VERSION:+bash}\n#+end_src\n", | 146 | "#+begin_src bash :shebang #!/bin/bash :results output\necho ${BASH_VERSION:+bash}\n#+end_src\n", |
| 147 | "#+begin_src sh :cmdline a b :padline no :var x=5 :results output\necho $x $#\n#+end_src\n", | 147 | "#+begin_src sh :cmdline a b :padline no :var x=5 :results output\necho $x $#\n#+end_src\n", |
| 148 | "#+NAME: in\n| hello |\n\n#+begin_src sh :results value :stdin in\ncat\nfalse\n#+end_src\n", | ||
| 149 | "#+begin_src sh :results value :cmdline x\necho one\n#+end_src\n", | ||
| 150 | "#+begin_src bash :shebang #!/bin/bash :cmdline \"a b\" c :results output\necho $#\n#+end_src\n", | ||
| 148 | "#+begin_src sh :stdin src :results output\ntr a-z A-Z\n#+end_src\n\n#+NAME: src\n#+begin_src sh\necho hello\n#+end_src\n", | 151 | "#+begin_src sh :stdin src :results output\ntr a-z A-Z\n#+end_src\n\n#+NAME: src\n#+begin_src sh\necho hello\n#+end_src\n", |
| 149 | // `:var` from other blocks' results. | 152 | // `:var` from other blocks' results. |
| 150 | "#+begin_src sh :var x=five\necho $((x+1))\n#+end_src\n\n#+NAME: five\n#+begin_src sh\necho 5\n#+end_src\n", | 153 | "#+begin_src sh :var x=five\necho $((x+1))\n#+end_src\n\n#+NAME: five\n#+begin_src sh\necho 5\n#+end_src\n", |
Tests/OrgCoreTests/SetupFileTests.swift +5
| @@ -36,6 +36,11 @@ struct SetupFileTests { | |||
| 36 | #expect(lines.contains("#+TYP_TODO: BUG | FIXED\n")) | 36 | #expect(lines.contains("#+TYP_TODO: BUG | FIXED\n")) |
| 37 | #expect(lines.components(separatedBy: "#+TODO:").count == 2) | 37 | #expect(lines.components(separatedBy: "#+TODO:").count == 2) |
| 38 | #expect(SetupFile.keywordLines(in: "* x\n", directory: "/w") { files[$0] } == "") | 38 | #expect(SetupFile.keywordLines(in: "* x\n", directory: "/w") { files[$0] } == "") |
| 39 | // A file naming itself adds nothing; a setup file naming it back adds only its own. | ||
| 40 | let own = "#+SETUPFILE: a.org\n#+TODO: A B | C\n" | ||
| 41 | #expect(SetupFile.keywordLines(in: own, directory: "/w", path: "/w/a.org") { $0 == "/w/a.org" ? own : nil } == "") | ||
| 42 | let back = ["/w/a.org": "#+SETUPFILE: b.org\n#+TODO: A | B\n", "/w/b.org": "#+SETUPFILE: a.org\n#+TODO: X | Y\n"] | ||
| 43 | #expect(SetupFile.keywordLines(in: back["/w/a.org"]!, directory: "/w", path: "/w/a.org") { back[$0] } == "#+TODO: X | Y\n") | ||
| 39 | #expect(SetupFile.keywordLines(in: "#+SETUPFILE: https://example.com/a.org\n", directory: "/w") { _ in "#+TODO: X\n" } == "") | 44 | #expect(SetupFile.keywordLines(in: "#+SETUPFILE: https://example.com/a.org\n", directory: "/w") { _ in "#+TODO: X\n" } == "") |
| 40 | } | 45 | } |
| 41 | 46 | ||
Tests/OrgCoreTests/TangleTests.swift +3
| @@ -223,6 +223,9 @@ struct TangleTests { | |||
| 223 | #+begin_src sh :tangle (file-name-sans-extension (file-name-nondirectory buffer-file-name)) | 223 | #+begin_src sh :tangle (file-name-sans-extension (file-name-nondirectory buffer-file-name)) |
| 224 | echo named after the file | 224 | echo named after the file |
| 225 | #+end_src | 225 | #+end_src |
| 226 | #+begin_src sh :tangle quoted.sh :prologue "(cd /tmp && ls)" | ||
| 227 | echo quoted | ||
| 228 | #+end_src | ||
| 226 | #+begin_src python -r -l "#(ref:%s)" :tangle refs.py | 229 | #+begin_src python -r -l "#(ref:%s)" :tangle refs.py |
| 227 | x = 1 #(ref:one) | 230 | x = 1 #(ref:one) |
| 228 | y = 2 (ref:kept) | 231 | y = 2 (ref:kept) |
Tests/OrgWorkspaceTests/ReconcileTests.swift +13
| @@ -42,6 +42,19 @@ struct ReconcileTests { | |||
| 42 | #expect(try reconciler.reconcile(root: folder.url).touched == [a.path]) | 42 | #expect(try reconciler.reconcile(root: folder.url).touched == [a.path]) |
| 43 | } | 43 | } |
| 44 | 44 | ||
| 45 | @Test func changedSetupFileReindexesTheFilesNamingIt() throws { | ||
| 46 | let folder = try Folder() | ||
| 47 | let index = try IndexStore() | ||
| 48 | let reconciler = Reconciler(index: index, settings: IndexSettings()) | ||
| 49 | try folder.write("setup.txt", "#+TODO: TODO | DONE\n") | ||
| 50 | let a = try folder.write("a.org", "#+SETUPFILE: setup.txt\n* NEXT Alpha\n") | ||
| 51 | _ = try reconciler.reconcile(root: folder.url) | ||
| 52 | #expect(try reconciler.reconcile(root: folder.url).indexed.isEmpty) | ||
| 53 | try folder.write("setup.txt", "#+TODO: NEXT | DONE\n") | ||
| 54 | #expect(try reconciler.reconcile(root: folder.url).indexed.contains(a.path)) | ||
| 55 | #expect(try reconciler.reconcile(root: folder.url).indexed.isEmpty) | ||
| 56 | } | ||
| 57 | |||
| 45 | @Test func newSettingsVersionReindexes() throws { | 58 | @Test func newSettingsVersionReindexes() throws { |
| 46 | let folder = try Folder() | 59 | let folder = try Folder() |
| 47 | let index = try IndexStore() | 60 | let index = try IndexStore() |