import SwiftUI import Foundation struct FindInFileView: View { let content: String let onNavigate: (NSRange) -> Void @Environment(\.dismiss) var dismiss @State private var query = ""; @State private var isRegex = false; @State private var isCaseSensitive = false @State private var results: [Result] = []; @State private var selected = 0 @FocusState private var focused: Bool struct Result: Identifiable { let id = UUID(); let range: NSRange; let lineNumber: Int; let lineText: String } var body: some View { NavigationStack { VStack(spacing: 0) { HStack(spacing: 8) { Image(systemName: "magnifyingglass").foregroundStyle(.secondary) TextField("Find…", text: $query).textFieldStyle(.plain).font(.system(.body, design: .monospaced)) .autocorrectionDisabled().autocapitalization(.none).focused($focused).onSubmit { navigateNext() } .onChange(of: query) { _, _ in runSearch() } if !results.isEmpty { Text("\(selected+1)/\(results.count)").font(.system(.caption, design: .monospaced)).foregroundStyle(.secondary) Button { navigatePrev() } label: { Image(systemName: "chevron.up") }.buttonStyle(.bordered).controlSize(.small) Button { navigateNext() } label: { Image(systemName: "chevron.down") }.buttonStyle(.bordered).controlSize(.small) } } .padding(.horizontal, 14).padding(.vertical, 10).background(Color(.secondarySystemBackground)) Divider() if results.isEmpty { ContentUnavailableView(query.isEmpty ? "Type to search" : "No Results", systemImage: "magnifyingglass") } else { List(Array(results.enumerated()), id: \.element.id) { idx, r in HStack(alignment: .top, spacing: 10) { Text("\(r.lineNumber)").font(.system(.caption, design: .monospaced)).foregroundStyle(.secondary).frame(width: 36, alignment: .trailing) Text(r.lineText).font(.system(.body, design: .monospaced)).lineLimit(1) } .background(idx == selected ? Color.accentColor.opacity(0.1) : .clear) .onTapGesture { selected = idx; onNavigate(r.range); dismiss() } } .listStyle(.plain) } } .navigationTitle("Find in File").navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Done") { dismiss() } } } .onAppear { focused = true } } .presentationDetents([.medium, .large]) } private func runSearch() { guard !query.isEmpty else { results = []; return } let lines = content.components(separatedBy: "\n") var found: [Result] = []; var offset = 0 let pattern = isRegex ? query : NSRegularExpression.escapedPattern(for: query) let options: NSRegularExpression.Options = isCaseSensitive ? [] : .caseInsensitive guard let regex = try? NSRegularExpression(pattern: pattern, options: options) else { results = []; return } for (i, line) in lines.enumerated() { let r = NSRange(line.startIndex..., in: line) for m in regex.matches(in: line, range: r) { found.append(Result(range: NSRange(location: offset + m.range.location, length: m.range.length), lineNumber: i + 1, lineText: line.trimmingCharacters(in: .whitespaces))) } offset += line.count + 1 } results = found; if !found.isEmpty { selected = 0; onNavigate(found[0].range) } } private func navigateNext() { guard !results.isEmpty else { return }; selected = (selected+1) % results.count; onNavigate(results[selected].range) } private func navigatePrev() { guard !results.isEmpty else { return }; selected = (selected-1+results.count) % results.count; onNavigate(results[selected].range) } }