import RastrilloApple import SwiftUI @MainActor struct DocsJumpControls: View { @Binding var presented: Bool var body: some View { Button { presented = true } label: { Label { Text("docs.jumpto.label", bundle: DocsResources.bundle) } icon: { Image(systemName: "magnifyingglass") } } .keyboardShortcut("k", modifiers: .command) .accessibilityIdentifier("docs.jump") } } private struct DocumentSearchResult: Decodable, Identifiable { let title: String let href: String let when: String var id: String { href } var sqid: String? { guard href.hasPrefix("/d/") else { return nil } let value = String(href.dropFirst(3)) return !value.isEmpty && value.utf8.allSatisfy { (48...57).contains($0) || (65...90).contains($0) || (97...122).contains($0) } ? value : nil } } @MainActor struct DocsJumpSheet: View { let model: DocsModel let client: NativeHTTP @Environment(\.dismiss) private var dismiss @FocusState private var searchFocused: Bool @State private var query = "" @State private var results: [DocumentSearchResult] = [] @State private var resultsQuery = "" @State private var selected = 0 @State private var status: String? @State private var working = false var body: some View { NavigationStack { VStack(spacing: 0) { TextField(String(localized: "docs.jumpto.placeholder", bundle: DocsResources.bundle), text: $query) .textFieldStyle(.roundedBorder) .focused($searchFocused) .accessibilityIdentifier("docs.jump.query") .accessibilityHint(Text("docs.jumpto.hint", bundle: DocsResources.bundle)) .onSubmit { openSelected() } .onKeyPress(.return) { openSelected(); return .handled } .onKeyPress(.upArrow) { move(-1); return .handled } .onKeyPress(.downArrow) { move(1); return .handled } .padding() if let status { Text(LocalizedStringKey(status), bundle: DocsResources.bundle).foregroundStyle(.secondary).padding(.horizontal) } if working { ProgressView().padding() } ScrollViewReader { reader in List(Array(results.enumerated()), id: \.element.id) { index, result in Button { open(result) } label: { VStack(alignment: .leading) { Text(result.title) Text(result.when).font(.caption).foregroundStyle(.secondary) }.frame(maxWidth: .infinity, alignment: .leading).contentShape(Rectangle()) } .buttonStyle(.plain) .accessibilityAddTraits(index == selected ? .isSelected : []) .listRowBackground(index == selected ? Color.accentColor.opacity(0.12) : Color.clear) .id(result.id) } .onChange(of: selected) { if results.indices.contains(selected) { reader.scrollTo(results[selected].id) } } } } .disabled(working) .navigationTitle(Text("docs.jumpto.label", bundle: DocsResources.bundle)) .toolbar { ToolbarItem(placement: .cancellationAction) { Button { dismiss() } label: { Text("docs.jumpto.close", bundle: DocsResources.bundle) } .keyboardShortcut(.cancelAction) .disabled(working) } } } .task { searchFocused = true } .task(id: query) { await search() } .interactiveDismissDisabled(working) #if os(macOS) .frame(minWidth: 460, minHeight: 400) #else .presentationDetents([.medium, .large]) #endif } private func move(_ delta: Int) { guard !results.isEmpty else { return } selected = max(0, min(results.count - 1, selected + delta)) } private func search() async { guard !Task.isCancelled else { return } let searching = query results = []; selected = 0; status = nil guard !searching.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } do { try await Task.sleep(for: .milliseconds(120)) var components = URLComponents() components.queryItems = [URLQueryItem(name: "q", value: searching)] let encoded = (components.percentEncodedQuery ?? "").replacingOccurrences(of: "+", with: "%2B") let response = try await client.responseDetails("/documents/search?\(encoded)") guard response.status == 200 else { throw CRDTReplica.Failure.rejected } let rows = try JSONDecoder().decode([DocumentSearchResult].self, from: response.data) guard !Task.isCancelled, query == searching else { return } results = rows.filter { $0.sqid != nil } resultsQuery = searching status = response.header("Docs-Catalog-Partial") == "1" ? "docs.team.partial" : (results.isEmpty ? "docs.jumpto.empty" : nil) } catch { guard !Task.isCancelled, query == searching else { return } status = "docs.jumpto.failed" } } private func openSelected() { guard results.indices.contains(selected) else { return } open(results[selected]) } private func open(_ result: DocumentSearchResult) { guard !working, query == resultsQuery, let sqid = result.sqid else { return } working = true Task { defer { working = false } guard await model.saveAll() else { status = "native.pending"; return } await model.open(sqid) if model.detail?.sqid == sqid { dismiss() } else { status = "docs.error.generic" } } } }