search.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package diff
  3. import (
  4. "fmt"
  5. "regexp"
  6. "strings"
  7. "sync"
  8. "kitty/tools/tui"
  9. "kitty/tools/utils"
  10. "kitty/tools/utils/images"
  11. "kitty/tools/wcswidth"
  12. "golang.org/x/exp/slices"
  13. )
  14. var _ = fmt.Print
  15. type Search struct {
  16. pat *regexp.Regexp
  17. matches map[ScrollPos][]Span
  18. }
  19. func (self *Search) Len() int { return len(self.matches) }
  20. func (self *Search) find_matches_in_lines(clean_lines []string, origin int, send_result func(screen_line, offset, size int)) {
  21. lengths := utils.Map(func(x string) int { return len(x) }, clean_lines)
  22. offsets := make([]int, len(clean_lines))
  23. cell_lengths := utils.Map(wcswidth.Stringwidth, clean_lines)
  24. cell_offsets := make([]int, len(clean_lines))
  25. for i := range clean_lines {
  26. if i > 0 {
  27. offsets[i] = offsets[i-1] + lengths[i-1]
  28. cell_offsets[i] = cell_offsets[i-1] + cell_lengths[i-1]
  29. }
  30. }
  31. joined_text := strings.Join(clean_lines, "")
  32. matches := self.pat.FindAllStringIndex(joined_text, -1)
  33. pos := 0
  34. find_pos := func(start int) int {
  35. for i := pos; i < len(clean_lines); i++ {
  36. if start < offsets[i]+lengths[i] {
  37. pos = i
  38. return pos
  39. }
  40. }
  41. return -1
  42. }
  43. for _, m := range matches {
  44. start, end := m[0], m[1]
  45. total_size := end - start
  46. if total_size < 1 {
  47. continue
  48. }
  49. start_line := find_pos(start)
  50. if start_line > -1 {
  51. end_line := find_pos(end)
  52. if end_line > -1 {
  53. for i := start_line; i <= end_line; i++ {
  54. cell_start := 0
  55. if i == start_line {
  56. byte_offset := start - offsets[i]
  57. cell_start = wcswidth.Stringwidth(clean_lines[i][:byte_offset])
  58. }
  59. cell_end := cell_lengths[i]
  60. if i == end_line {
  61. byte_offset := end - offsets[i]
  62. cell_end = wcswidth.Stringwidth(clean_lines[i][:byte_offset])
  63. }
  64. send_result(i, origin+cell_start, cell_end-cell_start)
  65. }
  66. }
  67. }
  68. }
  69. }
  70. func (self *Search) find_matches_in_line(line *LogicalLine, margin_size, cols int, send_result func(screen_line, offset, size int)) {
  71. half_width := cols / 2
  72. right_offset := half_width + margin_size
  73. left_clean_lines, right_clean_lines := make([]string, len(line.screen_lines)), make([]string, len(line.screen_lines))
  74. for i, sl := range line.screen_lines {
  75. if line.is_full_width {
  76. left_clean_lines[i] = wcswidth.StripEscapeCodes(sl.left.marked_up_text)
  77. } else {
  78. left_clean_lines[i] = wcswidth.StripEscapeCodes(sl.left.marked_up_text)
  79. right_clean_lines[i] = wcswidth.StripEscapeCodes(sl.right.marked_up_text)
  80. }
  81. }
  82. self.find_matches_in_lines(left_clean_lines, margin_size, send_result)
  83. self.find_matches_in_lines(right_clean_lines, right_offset, send_result)
  84. }
  85. func (self *Search) Has(pos ScrollPos) bool {
  86. return len(self.matches[pos]) > 0
  87. }
  88. type Span struct{ start, end int }
  89. func (self *Search) search(logical_lines *LogicalLines) {
  90. margin_size := logical_lines.margin_size
  91. cols := logical_lines.columns
  92. self.matches = make(map[ScrollPos][]Span)
  93. ctx := images.Context{}
  94. mutex := sync.Mutex{}
  95. ctx.Parallel(0, logical_lines.Len(), func(nums <-chan int) {
  96. for i := range nums {
  97. line := logical_lines.At(i)
  98. if line.line_type == EMPTY_LINE || line.line_type == IMAGE_LINE {
  99. continue
  100. }
  101. self.find_matches_in_line(line, margin_size, cols, func(screen_line, offset, size int) {
  102. if size > 0 {
  103. mutex.Lock()
  104. defer mutex.Unlock()
  105. pos := ScrollPos{i, screen_line}
  106. self.matches[pos] = append(self.matches[pos], Span{offset, offset + size - 1})
  107. }
  108. })
  109. }
  110. })
  111. for _, spans := range self.matches {
  112. slices.SortFunc(spans, func(a, b Span) int { return a.start - b.start })
  113. }
  114. }
  115. func (self *Search) markup_line(pos ScrollPos, y int) string {
  116. spans := self.matches[pos]
  117. if spans == nil {
  118. return ""
  119. }
  120. sgr := format_as_sgr.search[2:]
  121. sgr = sgr[:len(sgr)-1]
  122. ans := make([]byte, 0, 32)
  123. for _, span := range spans {
  124. ans = append(ans, tui.FormatPartOfLine(sgr, span.start, span.end, y)...)
  125. }
  126. return utils.UnsafeBytesToString(ans)
  127. }
  128. func do_search(pat *regexp.Regexp, logical_lines *LogicalLines) *Search {
  129. ans := &Search{pat: pat, matches: make(map[ScrollPos][]Span)}
  130. ans.search(logical_lines)
  131. return ans
  132. }