| @@ -5,6 +5,12 @@ import Foundation |
| 5 | // features beyond `Calc`, Lisp beyond `Elisp`, remote tables found by ID and relative field |
5 | // features beyond `Calc`, Lisp beyond `Elisp`, remote tables found by ID and relative field |
| 6 | // targets throw `Calc.Unsupported`; the caller then recalculates the table in Emacs. |
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 | extension EmacsBuffer { |
14 | extension EmacsBuffer { |
| 9 | struct TableAnalysis { |
15 | struct TableAnalysis { |
| 10 | /// Table-relative line index of each data line, from 1 (index 0 unused). |
16 | /// Table-relative line index of each data line, from 1 (index 0 unused). |
| @@ -452,21 +458,40 @@ extension EmacsBuffer { |
| 452 | return new |
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 | func remoteRange(_ name: String, _ form: String) throws -> TableRangeValue { |
463 | func remoteRange(_ name: String, _ form: String) throws -> TableRangeValue { |
| 457 | let ns = string as NSString |
464 | let escaped = NSRegularExpression.escapedPattern(for: name) |
| 458 | let pattern = "^[ \\t]*#\\+(tbl)?name:[ \\t]*" + NSRegularExpression.escapedPattern(for: name) + "[ \\t]*$" |
465 | let keyword = try! NSRegularExpression(pattern: "^[ \\t]*#\\+(tbl)?name:[ \\t]*" + escaped + "[ \\t]*$", options: [.anchorsMatchLines, .caseInsensitive]) |
| 459 | let keyword = try! NSRegularExpression(pattern: pattern, options: [.anchorsMatchLines, .caseInsensitive]) |
466 | let idLine = try! NSRegularExpression(pattern: "^[ \\t]*:ID:[ \\t]+" + escaped + "[ \\t]*$", options: [.anchorsMatchLines, .caseInsensitive]) |
| 460 | guard let found = keyword.firstMatch(in: string, range: NSRange(location: 0, length: ns.length)) else { |
467 | func first(_ regex: NSRegularExpression, in text: String) -> Int? { |
| 461 | throw Calc.Unsupported("remote table \(name) by ID") |
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 | let next = try! NSRegularExpression(pattern: "^(\\*+ )|^[ \\t]*\\|", options: .anchorsMatchLines) |
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 | m.range(at: 1).location == NSNotFound else { |
491 | m.range(at: 1).location == NSNotFound else { |
| 467 | throw UserError(message: "Cannot find a table at NAME or ID \(name)") |
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 | let analysis = try remote.tableAnalyze() |
495 | let analysis = try remote.tableAnalyze() |
| 471 | let resolved = try remote.substituteNames(try Self.handleFirstLast(Self.refsToRC(form), analysis: analysis), analysis: analysis) |
496 | let resolved = try remote.substituteNames(try Self.handleFirstLast(Self.refsToRC(form), analysis: analysis), analysis: analysis) |
| 472 | let range = try! NSRegularExpression(pattern: Self.rangePattern) |
497 | let range = try! NSRegularExpression(pattern: Self.rangePattern) |
| @@ -745,7 +770,8 @@ extension EmacsBuffer { |
| 745 | } |
770 | } |
| 746 | fields.append((row, location.column, rhs)) |
771 | fields.append((row, location.column, rhs)) |
| 747 | } else if lhs.range(of: "^@[-+0-9]+\\$-?[0-9]+$", options: .regularExpression) != nil { |
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 | } else if let m = lhs.firstMatch(of: /^@([0-9]+)$/) { |
775 | } else if let m = lhs.firstMatch(of: /^@([0-9]+)$/) { |
| 750 | let row = try Self.integer(String(m.1)) |
776 | let row = try Self.integer(String(m.1)) |
| 751 | for c in 1...analysis.ncol { fields.append((row, c, rhs)) } |
777 | for c in 1...analysis.ncol { fields.append((row, c, rhs)) } |