krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/Orgstar/BacklinksPane.swift

778058aafc7ddacee4a613b1da87c60aa0c8e113
orgstar/Sources/Orgstar/BacklinksPane.swift history · blame · raw

72 lines · 2696 bytes

4 symbols in this file
 1import OrgApp
 2import OrgCore
 3import OrgIndex
 4import OrgPresentation
 5import SwiftUI
 6
 7/// Headings that link to the heading at the caret, and to the file.
 8struct BacklinksPane: View {
 9    let workspace: WorkspaceModel
10    let session: DocumentSession
11    let commands: AppCommands
12    let open: (HeadingLocation) -> Void
13    @State private var toHeading: [HeadingLocation] = []
14    @State private var toFile: [HeadingLocation] = []
15    @Environment(\.orgTheme) private var theme
16
17    /// The heading at the caret.
18    private var heading: OutlineEntry? { session.outline.last { $0.start <= commands.selection.location } }
19
20    private struct Key: Equatable {
21        var path: String?
22        var heading: Int?
23        var version: Int
24    }
25
26    var body: some View {
27        List {
28            if let heading {
29                Section("Links to \(heading.title.isEmpty ? "(untitled)" : heading.title)") { rows(toHeading) }
30            }
31            Section("Links to this file") { rows(toFile) }
32        }
33        .themedListBackground(theme.color("sidebar-background"))
34        .task(id: Key(path: session.path, heading: heading?.start, version: session.editVersion)) { refresh() }
35    }
36
37    @ViewBuilder
38    private func rows(_ locations: [HeadingLocation]) -> some View {
39        if locations.isEmpty {
40            Text("None").foregroundStyle(.secondary)
41        }
42        ForEach(locations, id: \.self) { location in
43            Button {
44                open(location)
45            } label: {
46                VStack(alignment: .leading, spacing: 2) {
47                    Text(location.title.isEmpty ? "(untitled)" : location.title).lineLimit(1)
48                    Text((location.path as NSString).lastPathComponent).font(.caption).foregroundStyle(.secondary)
49                }
50            }
51            .buttonStyle(.plain)
52        }
53    }
54
55    private func refresh() {
56        guard let path = session.path else {
57            toHeading = []
58            toFile = []
59            return
60        }
61        let overlay = workspace.overlay(session.openDocuments)
62        toFile = workspace.backlinks(to: Backlinks.Target(path: path), overlay: overlay)
63        guard let heading, let tree = session.document?.tree,
64              let info = DocumentModel(tree: tree).headings.first(where: { $0.headingRange.lowerBound == heading.start }) else {
65            toHeading = []
66            return
67        }
68        let customID = info.properties.last { $0.key.uppercased() == "CUSTOM_ID" }?.value
69        let target = Backlinks.Target(path: path, title: info.title, id: info.id, customID: customID)
70        toHeading = workspace.backlinks(to: target, excluding: heading.start, overlay: overlay)
71    }
72}