projectsearch.lua 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. local core = require "core"
  2. local common = require "core.common"
  3. local keymap = require "core.keymap"
  4. local command = require "core.command"
  5. local style = require "core.style"
  6. local View = require "core.view"
  7. local ResultsView = View:extend()
  8. function ResultsView:new(text, fn)
  9. ResultsView.super.new(self)
  10. self.scrollable = true
  11. self:begin_search(text, fn)
  12. end
  13. function ResultsView:get_name()
  14. return "Search Results"
  15. end
  16. local function find_all_matches_in_file(t, filename, fn)
  17. local fp = io.open(filename)
  18. if not fp then return t end
  19. local n = 1
  20. for line in fp:lines() do
  21. local s = fn(line)
  22. if s then
  23. table.insert(t, { file = filename, text = line, line = n, col = s })
  24. core.redraw = true
  25. end
  26. if n % 100 == 0 then coroutine.yield() end
  27. n = n + 1
  28. core.redraw = true
  29. end
  30. fp:close()
  31. end
  32. function ResultsView:begin_search(text, fn)
  33. self.results = {}
  34. self.last_file_idx = 1
  35. self.query = text
  36. self.searching = true
  37. self.selected_idx = 0
  38. core.add_thread(function()
  39. for i, file in ipairs(core.project_files) do
  40. if file.type == "file" then
  41. find_all_matches_in_file(self.results, file.filename, fn)
  42. end
  43. self.last_file_idx = i
  44. end
  45. self.searching = false
  46. core.redraw = true
  47. end, self)
  48. end
  49. function ResultsView:on_mouse_moved(mx, my, ...)
  50. ResultsView.super.on_mouse_moved(self, mx, my, ...)
  51. self.selected_idx = 0
  52. for i, item, x,y,w,h in self:each_visible_result() do
  53. if mx >= x and my >= y and mx < x + w and my < y + h then
  54. self.selected_idx = i
  55. break
  56. end
  57. end
  58. end
  59. function ResultsView:on_mouse_pressed(...)
  60. local caught = ResultsView.super.on_mouse_pressed(self, ...)
  61. if not caught then
  62. self:open_selected_result()
  63. end
  64. end
  65. function ResultsView:open_selected_result()
  66. local res = self.results[self.selected_idx]
  67. if not res then
  68. return
  69. end
  70. core.try(function()
  71. local dv = core.root_view:open_doc(core.open_doc(res.file))
  72. core.root_view.root_node:update_layout()
  73. dv.doc:set_selection(res.line, res.col)
  74. dv:scroll_to_line(res.line, false, true)
  75. end)
  76. end
  77. function ResultsView:update()
  78. self.scroll.to.y = math.max(0, self.scroll.to.y)
  79. ResultsView.super.update(self)
  80. end
  81. function ResultsView:get_results_yoffset()
  82. return style.font:get_height() + style.padding.y * 3
  83. end
  84. function ResultsView:get_line_height()
  85. return style.padding.y + style.font:get_height()
  86. end
  87. function ResultsView:get_scrollable_size()
  88. local rh = style.padding.y + style.font:get_height()
  89. return self:get_results_yoffset() + #self.results * self:get_line_height()
  90. end
  91. function ResultsView:get_visible_results_range()
  92. local lh = self:get_line_height()
  93. local oy = self:get_results_yoffset()
  94. local min = math.max(1, math.floor((self.scroll.y - oy) / lh))
  95. return min, min + math.floor(self.size.y / lh) + 1
  96. end
  97. function ResultsView:each_visible_result()
  98. return coroutine.wrap(function()
  99. local lh = self:get_line_height()
  100. local x, y = self:get_content_offset()
  101. local min, max = self:get_visible_results_range()
  102. y = y + self:get_results_yoffset() + lh * (min - 1)
  103. for i = min, max do
  104. local item = self.results[i]
  105. if not item then break end
  106. coroutine.yield(i, item, x, y, self.size.x, lh)
  107. y = y + lh
  108. end
  109. end)
  110. end
  111. function ResultsView:draw()
  112. self:draw_background(style.background)
  113. -- status
  114. local ox, oy = self:get_content_offset()
  115. local x, y = ox + style.padding.x, oy + style.padding.y
  116. local per = self.last_file_idx / #core.project_files
  117. local text
  118. if self.searching then
  119. text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...",
  120. per * 100, self.last_file_idx, #core.project_files,
  121. #self.results, self.query)
  122. else
  123. text = string.format("Found %d matches for %q",
  124. #self.results, self.query)
  125. end
  126. renderer.draw_text(style.font, text, x, y, style.text)
  127. -- horizontal line
  128. local yoffset = self:get_results_yoffset()
  129. local x = ox + style.padding.x
  130. local w = self.size.x - style.padding.x * 2
  131. local h = style.divider_size
  132. renderer.draw_rect(x, oy + yoffset - style.padding.y, w, h, style.dim)
  133. if self.searching then
  134. renderer.draw_rect(x, oy + yoffset - style.padding.y, w * per, h, style.text)
  135. end
  136. -- results
  137. local y1, y2 = self.position.y, self.position.y + self.size.y
  138. for i, item, x,y,w,h in self:each_visible_result() do
  139. local color = style.text
  140. if i == self.selected_idx then
  141. color = style.accent
  142. renderer.draw_rect(x, y, w, h, style.line_highlight)
  143. end
  144. x = x + style.padding.x
  145. local text = string.format("%s at line %d (col %d): ", item.file, item.line, item.col)
  146. x = common.draw_text(style.font, style.dim, text, "left", x, y, w, h)
  147. x = common.draw_text(style.code_font, color, item.text, "left", x, y, w, h)
  148. end
  149. self:draw_scrollbar()
  150. end
  151. local function begin_search(text, fn)
  152. if text == "" then
  153. core.error("Expected non-empty string")
  154. return
  155. end
  156. local rv = ResultsView(text, fn)
  157. core.root_view:get_active_node():add_view(rv)
  158. end
  159. command.add(nil, {
  160. ["project-search:find"] = function()
  161. core.command_view:enter("Find Text In Project", function(text)
  162. text = text:lower()
  163. begin_search(text, function(line_text)
  164. return line_text:lower():find(text, nil, true)
  165. end)
  166. end)
  167. end,
  168. ["project-search:find-pattern"] = function()
  169. core.command_view:enter("Find Pattern In Project", function(text)
  170. begin_search(text, function(line_text) return line_text:find(text) end)
  171. end)
  172. end,
  173. ["project-search:fuzzy-find"] = function()
  174. core.command_view:enter("Fuzzy Find Text In Project", function(text)
  175. begin_search(text, function(line_text)
  176. return common.fuzzy_match(line_text, text) and 1
  177. end)
  178. end)
  179. end,
  180. })
  181. command.add(ResultsView, {
  182. ["project-search:select-previous"] = function()
  183. local view = core.active_view
  184. view.selected_idx = math.max(view.selected_idx - 1, 1)
  185. end,
  186. ["project-search:select-next"] = function()
  187. local view = core.active_view
  188. view.selected_idx = math.min(view.selected_idx + 1, #view.results)
  189. end,
  190. ["project-search:open-selected"] = function()
  191. core.active_view:open_selected_result()
  192. end,
  193. })
  194. keymap.add {
  195. ["ctrl+shift+f"] = "project-search:find",
  196. ["up"] = "project-search:select-previous",
  197. ["down"] = "project-search:select-next",
  198. ["return"] = "project-search:open-selected",
  199. }