| @@ -5,6 +5,12 @@ import Foundation |
| 5 | 5 | // features beyond `Calc`, Lisp beyond `Elisp`, remote tables found by ID and relative field |
| 6 | 6 | // targets throw `Calc.Unsupported`; the caller then recalculates the table in Emacs. |
| 7 | 7 | |
| 8 | /// Where `remote()` finds tables by ID in other files. |
| 9 | public enum RemoteTables { |
| 10 | /// The text of the file holding the entry with an ID; the app sets it, from its index. |
| 11 | nonisolated(unsafe) public static var fileWithID: ((String) -> String?)? |
| 12 | } |
| 13 | |
| 8 | 14 | extension EmacsBuffer { |
| 9 | 15 | struct TableAnalysis { |
| 10 | 16 | /// Table-relative line index of each data line, from 1 (index 0 unused). |
| @@ -452,21 +458,40 @@ extension EmacsBuffer { |
| 452 | 458 | return new |
| 453 | 459 | } |
| 454 | 460 | |
| 455 | | /// `org-table-get-remote-range`: the table after `#+NAME: name` in this file. |
| 461 | /// `org-table-get-remote-range`: the table after `#+NAME: name` in this file, or the first |
| 462 | /// table in the entry with that ID, in this file or another. |
| 456 | 463 | func remoteRange(_ name: String, _ form: String) throws -> TableRangeValue { |
| 457 | | let ns = string as NSString |
| 458 | | let pattern = "^[ \\t]*#\\+(tbl)?name:[ \\t]*" + NSRegularExpression.escapedPattern(for: name) + "[ \\t]*$" |
| 459 | | let keyword = try! NSRegularExpression(pattern: pattern, options: [.anchorsMatchLines, .caseInsensitive]) |
| 460 | | guard let found = keyword.firstMatch(in: string, range: NSRange(location: 0, length: ns.length)) else { |
| 461 | | throw Calc.Unsupported("remote table \(name) by ID") |
| 464 | let escaped = NSRegularExpression.escapedPattern(for: name) |
| 465 | let keyword = try! NSRegularExpression(pattern: "^[ \\t]*#\\+(tbl)?name:[ \\t]*" + escaped + "[ \\t]*$", options: [.anchorsMatchLines, .caseInsensitive]) |
| 466 | let idLine = try! NSRegularExpression(pattern: "^[ \\t]*:ID:[ \\t]+" + escaped + "[ \\t]*$", options: [.anchorsMatchLines, .caseInsensitive]) |
| 467 | func first(_ regex: NSRegularExpression, in text: String) -> Int? { |
| 468 | regex.firstMatch(in: text, range: NSRange(location: 0, length: (text as NSString).length))?.range.location |
| 469 | } |
| 470 | // `org-id-find`: the entry's heading, here or in the file holding the ID. |
| 471 | func heading(_ text: String, _ offset: Int) -> Int { |
| 472 | let before = try! NSRegularExpression(pattern: "^\\*+ ", options: .anchorsMatchLines) |
| 473 | return before.matches(in: text, range: NSRange(location: 0, length: offset)).last?.range.location ?? 0 |
| 474 | } |
| 475 | var text = string |
| 476 | let location: Int |
| 477 | if let found = first(keyword, in: string) { |
| 478 | location = found |
| 479 | } else if let found = first(idLine, in: string) { |
| 480 | location = heading(string, found) |
| 481 | } else if let other = RemoteTables.fileWithID?(name), let found = first(idLine, in: other) { |
| 482 | text = other |
| 483 | location = heading(other, found) |
| 484 | } else { |
| 485 | throw UserError(message: "Can't find remote table \"\(name)\"") |
| 462 | 486 | } |
| 463 | | let after = min(ns.length, found.range.location + 1) |
| 487 | let ns = text as NSString |
| 488 | let after = min(ns.length, location + 1) |
| 464 | 489 | let next = try! NSRegularExpression(pattern: "^(\\*+ )|^[ \\t]*\\|", options: .anchorsMatchLines) |
| 465 | | guard let m = next.firstMatch(in: string, range: NSRange(location: after, length: ns.length - after)), |
| 490 | guard let m = next.firstMatch(in: text, range: NSRange(location: after, length: ns.length - after)), |
| 466 | 491 | m.range(at: 1).location == NSNotFound else { |
| 467 | 492 | throw UserError(message: "Cannot find a table at NAME or ID \(name)") |
| 468 | 493 | } |
| 469 | | let remote = EmacsBuffer(string, point: m.range.location, settings: settings, options: options) |
| 494 | let remote = EmacsBuffer(text, point: m.range.location, settings: settings, options: options) |
| 470 | 495 | let analysis = try remote.tableAnalyze() |
| 471 | 496 | let resolved = try remote.substituteNames(try Self.handleFirstLast(Self.refsToRC(form), analysis: analysis), analysis: analysis) |
| 472 | 497 | let range = try! NSRegularExpression(pattern: Self.rangePattern) |
| @@ -745,7 +770,8 @@ extension EmacsBuffer { |
| 745 | 770 | } |
| 746 | 771 | fields.append((row, location.column, rhs)) |
| 747 | 772 | } else if lhs.range(of: "^@[-+0-9]+\\$-?[0-9]+$", options: .regularExpression) != nil { |
| 748 | | throw Calc.Unsupported("relative field formula") |
| 773 | // `org-table-goto-field` knows only absolute fields and names. |
| 774 | throw UserError(message: "Unknown field: \(lhs)") |
| 749 | 775 | } else if let m = lhs.firstMatch(of: /^@([0-9]+)$/) { |
| 750 | 776 | let row = try Self.integer(String(m.1)) |
| 751 | 777 | for c in 1...analysis.ncol { fields.append((row, c, rhs)) } |