mirror of
https://github.com/kean/Pulse.git
synced 2026-05-30 21:07:33 +00:00
When opening RichTextView, show found results
This commit is contained in:
@@ -6,13 +6,15 @@ import SwiftUI
|
||||
import Pulse
|
||||
import PulseUI
|
||||
|
||||
#warning("TODO: update")
|
||||
@main
|
||||
struct Pulse_Demo_iOSApp: App {
|
||||
var body: some Scene {
|
||||
// let _ = testProxy()
|
||||
WindowGroup {
|
||||
NavigationView {
|
||||
ConsoleView(store: .demo)
|
||||
_SearchView()
|
||||
// ConsoleView(store: .demo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,19 @@ import Pulse
|
||||
import CoreData
|
||||
import Combine
|
||||
|
||||
#warning("TODO: remove public")
|
||||
|
||||
public struct _SearchView: View {
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
if #available(iOS 15, *) {
|
||||
ConsoleSearchView(viewModel: .init(entities: try! LoggerStore.mock.allMessages()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use custom search bar?
|
||||
@available(iOS 15, tvOS 15, *)
|
||||
struct ConsoleSearchView: View {
|
||||
@ObservedObject var viewModel: ConsoleSearchViewModel
|
||||
@@ -20,7 +33,7 @@ struct ConsoleSearchView: View {
|
||||
// TODO: when open body, start with a search term immediatelly
|
||||
let occurences = Array(result.occurences.enumerated())
|
||||
ForEach(occurences.prefix(3), id: \.offset) { item in
|
||||
NavigationLink(destination: makeDestination(for: item.element.kind, entity: result.entity)) {
|
||||
NavigationLink(destination: makeDestination(for: item.element, entity: result.entity)) {
|
||||
makeCell(for: item.element)
|
||||
}
|
||||
}
|
||||
@@ -38,23 +51,22 @@ struct ConsoleSearchView: View {
|
||||
|
||||
// TODO: add occurence IDs instead of indices
|
||||
func makeCell(for occurence: ConsoleSearchOccurence) -> some View {
|
||||
NavigationLink(destination: Text("Placeholder")) {
|
||||
// TODO: handle errors
|
||||
let attr = try! AttributedString(occurence.occurrence, including: \.uiKit)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(occurence.kind.title + " (Line: \(occurence.line):\(occurence.range.lowerBound))")
|
||||
.font(ConsoleConstants.fontTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text(attr)
|
||||
.lineLimit(3)
|
||||
}
|
||||
// TODO: handle errors
|
||||
let attr = try! AttributedString(occurence.occurrence, including: \.uiKit)
|
||||
return VStack(alignment: .leading, spacing: 4) {
|
||||
Text(occurence.kind.title + " (Line: \(occurence.line):\(occurence.range.lowerBound))")
|
||||
.font(ConsoleConstants.fontTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text(attr)
|
||||
.lineLimit(3)
|
||||
}
|
||||
}
|
||||
|
||||
func makeDestination(for kind: ConsoleSearchOccurence.Kind, entity: NSManagedObject) -> some View {
|
||||
switch kind {
|
||||
func makeDestination(for occurence: ConsoleSearchOccurence, entity: NSManagedObject) -> some View {
|
||||
switch occurence.kind {
|
||||
case .responseBody:
|
||||
return NetworkInspectorResponseBodyView(viewModel: .init(task: entity as! NetworkTaskEntity))
|
||||
.environment(\.textViewSearchContext, occurence.searchContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +92,11 @@ final class ConsoleSearchViewModel: ObservableObject {
|
||||
$searchText.dropFirst().sink { [weak self] in
|
||||
self?.search($0)
|
||||
}.store(in: &cancellables)
|
||||
|
||||
#warning("TEPM")
|
||||
DispatchQueue.main.async {
|
||||
self.searchText = "Nuke"
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: perform in background
|
||||
@@ -110,7 +127,7 @@ final class ConsoleSearchViewModel: ObservableObject {
|
||||
var occurences: [ConsoleSearchOccurence] = []
|
||||
if let responseBody = task.responseBody?.data, let string = String(data: responseBody, encoding: .utf8) {
|
||||
let matches = regex.matches(in: string)
|
||||
for match in matches {
|
||||
for (index, match) in matches.enumerated() {
|
||||
let range = match.fullMatch.startIndex..<match.fullMatch.endIndex
|
||||
// TODO: check range limits
|
||||
// TODO: find next and current line + adjust to fit current line on screen
|
||||
@@ -124,11 +141,14 @@ final class ConsoleSearchViewModel: ObservableObject {
|
||||
|
||||
// TODO: optimize + show line number
|
||||
let range2 = Int.random(in: 1...50)..<200
|
||||
// TODO: pass options
|
||||
let context = RichTextViewModel.SearchContext(searchTerm: searchText, options: .default, matchIndex: index)
|
||||
let occurence = ConsoleSearchOccurence(
|
||||
kind: .responseBody,
|
||||
line: .random(in: 1...100),
|
||||
range: range2,
|
||||
occurrence: substring
|
||||
occurrence: substring,
|
||||
searchContext: context
|
||||
)
|
||||
occurences.append(occurence)
|
||||
}
|
||||
@@ -157,6 +177,7 @@ struct ConsoleSearchOccurence {
|
||||
let line: Int
|
||||
let range: Range<Int>
|
||||
let occurrence: NSAttributedString
|
||||
let searchContext: RichTextViewModel.SearchContext
|
||||
}
|
||||
|
||||
struct ConsoleSearchResultViewModel: Identifiable {
|
||||
|
||||
@@ -14,6 +14,7 @@ struct RichTextView: View {
|
||||
let viewModel: RichTextViewModel
|
||||
|
||||
private var isTextViewBarItemsHidden = false
|
||||
@Environment(\.textViewSearchContext) private var searchContext
|
||||
|
||||
init(viewModel: RichTextViewModel) {
|
||||
self.viewModel = viewModel
|
||||
@@ -26,6 +27,12 @@ struct RichTextView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
contents
|
||||
.onAppear { viewModel.prepare(searchContext) }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var contents: some View {
|
||||
if #available(iOS 15, *) {
|
||||
_RichTextView(viewModel: viewModel, isTextViewBarItemsHidden: isTextViewBarItemsHidden)
|
||||
} else {
|
||||
@@ -72,7 +79,7 @@ struct _RichTextView: View {
|
||||
WrappedTextView(viewModel: viewModel)
|
||||
.edgesIgnoringSafeArea([.bottom])
|
||||
.overlay {
|
||||
if isSearching {
|
||||
if isSearching || !viewModel.matches.isEmpty {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
@@ -306,3 +313,14 @@ struct RichTextView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private struct TextViewSearchContextKey: EnvironmentKey {
|
||||
static var defaultValue: RichTextViewModel.SearchContext?
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var textViewSearchContext: RichTextViewModel.SearchContext? {
|
||||
get { self[TextViewSearchContextKey.self] }
|
||||
set { self[TextViewSearchContextKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,22 @@ final class RichTextViewModel: ObservableObject {
|
||||
}.store(in: &bag)
|
||||
}
|
||||
|
||||
func prepare(_ context: SearchContext?) {
|
||||
guard let context = context else { return }
|
||||
|
||||
// Not updated self.searchTerm because searchable doesn't like that
|
||||
let matches = search(searchTerm: context.searchTerm, in: textStorage.string as NSString, options: context.options)
|
||||
self.didUpdateMatches(matches)
|
||||
if context.matchIndex < matches.count {
|
||||
DispatchQueue.main.async {
|
||||
self.textView?.layoutManager.allowsNonContiguousLayout = false // Remove this workaround
|
||||
UIView.performWithoutAnimation {
|
||||
self.updateMatchIndex(context.matchIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func display(_ text: NSAttributedString) {
|
||||
self.text = text
|
||||
self.matches.removeAll()
|
||||
@@ -127,7 +143,7 @@ final class RichTextViewModel: ObservableObject {
|
||||
updateMatchIndex(selectedMatchIndex - 1 < 0 ? matches.count - 1 : selectedMatchIndex - 1)
|
||||
}
|
||||
|
||||
private func updateMatchIndex(_ newIndex: Int) {
|
||||
func updateMatchIndex(_ newIndex: Int) {
|
||||
let previousIndex = selectedMatchIndex
|
||||
selectedMatchIndex = newIndex
|
||||
didUpdateCurrentSelectedMatch(previousMatch: previousIndex)
|
||||
@@ -137,9 +153,20 @@ final class RichTextViewModel: ObservableObject {
|
||||
guard !matches.isEmpty else { return }
|
||||
|
||||
// Scroll to visible range
|
||||
// Make sure it's somewhere in the middle (find newlines)
|
||||
var range = matches[selectedMatchIndex]
|
||||
if range.length + 50 < textStorage.length {
|
||||
range.length += 50
|
||||
var index = range.upperBound
|
||||
var newlines = 0
|
||||
let string = textStorage.string as NSString
|
||||
while index < textStorage.length {
|
||||
if let character = UnicodeScalar(string.character(at: index)).map(Character.init), character.isNewline {
|
||||
newlines += 1
|
||||
range.length += index - range.upperBound
|
||||
if newlines == 8 {
|
||||
break
|
||||
}
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
if let textView = textView {
|
||||
textView.scrollRangeToVisible(range)
|
||||
@@ -167,6 +194,12 @@ final class RichTextViewModel: ObservableObject {
|
||||
.foregroundColor: UXColor.white
|
||||
], range: range)
|
||||
}
|
||||
|
||||
struct SearchContext {
|
||||
let searchTerm: String
|
||||
let options: StringSearchOptions
|
||||
let matchIndex: Int
|
||||
}
|
||||
}
|
||||
|
||||
private func search(searchTerm: String, in string: NSString, options: StringSearchOptions) -> [NSRange] {
|
||||
|
||||
Reference in New Issue
Block a user