gitbay/Views/Repos/SymbolSearchView.swift
84 lines · 3411 bytes
1import SwiftUI
2
3/// Where a name is defined, from the repository's symbol index. A result
4/// opens the file at its line on the default branch, the one indexed.
5struct SymbolSearchView: View {
6
7 @State private var model: SymbolSearchViewModel
8 @State private var query: String
9 private let initialQuery: String?
10
11 init(client: GitbayClient, repo: String, query: String? = nil) {
12 _model = State(initialValue: SymbolSearchViewModel(client: client, repoPath: repo))
13 _query = State(initialValue: query ?? "")
14 initialQuery = query
15 }
16
17 var body: some View {
18 List {
19 ForEach(model.state?.value ?? []) { symbol in
20 NavigationLink(value: RepoRoute.file(
21 repo: model.repoPath, path: symbol.path, ref: nil, line: symbol.line
22 )) {
23 VStack(alignment: .leading, spacing: 3) {
24 HStack(spacing: 6) {
25 Text(symbol.name)
26 .font(.gbMono(.subheadline).weight(.medium))
27 .lineLimit(1)
28 GBChip(symbol.kind, .secondary)
29 }
30 Text("\(symbol.path):\(symbol.line)")
31 .font(.gbMono(.caption))
32 .foregroundStyle(.secondary)
33 .lineLimit(1)
34 .truncationMode(.head)
35 }
36 .accessibilityElement(children: .combine)
37 }
38 .accessibilityIdentifier("symbol-\(symbol.name)")
39 }
40 }
41 .overlay {
42 if let state = model.state {
43 LoadStateOverlay(state: state)
44 } else {
45 ContentUnavailableView {
46 Label("Find a definition", systemImage: "function")
47 } description: {
48 Text("A name or its start, on the default branch.")
49 }
50 }
51 }
52 .searchable(text: $query, prompt: "Find a definition")
53 .onSubmit(of: .search) {
54 Task { await model.search(query) }
55 }
56 .toolbar {
57 ToolbarItem(placement: .topBarTrailing) {
58 Menu {
59 Picker("Kind", selection: Binding(get: { model.kind }, set: { model.kind = $0 })) {
60 Text("Any kind").tag(String?.none)
61 ForEach(SymbolSearchViewModel.kinds, id: \.self) { kind in
62 Text(kind).tag(String?.some(kind))
63 }
64 }
65 } label: {
66 Image(systemName: model.kind == nil
67 ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill")
68 }
69 .accessibilityLabel(model.kind.map { "Kind: \($0)" } ?? "Kind: any")
70 .accessibilityIdentifier("symbols-kind")
71 }
72 }
73 .onChange(of: model.kind) {
74 if !query.trimmingCharacters(in: .whitespaces).isEmpty {
75 Task { await model.search(query) }
76 }
77 }
78 .task {
79 if let initialQuery, model.state == nil { await model.search(initialQuery) }
80 }
81 .navigationTitle("Definitions")
82 .navigationBarTitleDisplayMode(.inline)
83 }
84}