highlighter.lua 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. local a = vim.api
  2. local query = require"vim.treesitter.query"
  3. -- support reload for quick experimentation
  4. local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {}
  5. TSHighlighter.__index = TSHighlighter
  6. TSHighlighter.active = TSHighlighter.active or {}
  7. local TSHighlighterQuery = {}
  8. TSHighlighterQuery.__index = TSHighlighterQuery
  9. local ns = a.nvim_create_namespace("treesitter/highlighter")
  10. local _default_highlights = {}
  11. local _link_default_highlight_once = function(from, to)
  12. if not _default_highlights[from] then
  13. _default_highlights[from] = true
  14. vim.cmd(string.format("highlight default link %s %s", from, to))
  15. end
  16. return from
  17. end
  18. TSHighlighter.hl_map = {
  19. ["error"] = "Error",
  20. -- Miscs
  21. ["comment"] = "Comment",
  22. ["punctuation.delimiter"] = "Delimiter",
  23. ["punctuation.bracket"] = "Delimiter",
  24. ["punctuation.special"] = "Delimiter",
  25. -- Constants
  26. ["constant"] = "Constant",
  27. ["constant.builtin"] = "Special",
  28. ["constant.macro"] = "Define",
  29. ["string"] = "String",
  30. ["string.regex"] = "String",
  31. ["string.escape"] = "SpecialChar",
  32. ["character"] = "Character",
  33. ["number"] = "Number",
  34. ["boolean"] = "Boolean",
  35. ["float"] = "Float",
  36. -- Functions
  37. ["function"] = "Function",
  38. ["function.special"] = "Function",
  39. ["function.builtin"] = "Special",
  40. ["function.macro"] = "Macro",
  41. ["parameter"] = "Identifier",
  42. ["method"] = "Function",
  43. ["field"] = "Identifier",
  44. ["property"] = "Identifier",
  45. ["constructor"] = "Special",
  46. -- Keywords
  47. ["conditional"] = "Conditional",
  48. ["repeat"] = "Repeat",
  49. ["label"] = "Label",
  50. ["operator"] = "Operator",
  51. ["keyword"] = "Keyword",
  52. ["exception"] = "Exception",
  53. ["type"] = "Type",
  54. ["type.builtin"] = "Type",
  55. ["structure"] = "Structure",
  56. ["include"] = "Include",
  57. }
  58. ---@private
  59. local function is_highlight_name(capture_name)
  60. local firstc = string.sub(capture_name, 1, 1)
  61. return firstc ~= string.lower(firstc)
  62. end
  63. ---@private
  64. function TSHighlighterQuery.new(lang, query_string)
  65. local self = setmetatable({}, { __index = TSHighlighterQuery })
  66. self.hl_cache = setmetatable({}, {
  67. __index = function(table, capture)
  68. local hl, is_vim_highlight = self:_get_hl_from_capture(capture)
  69. if not is_vim_highlight then
  70. hl = _link_default_highlight_once(lang .. hl, hl)
  71. end
  72. local id = a.nvim_get_hl_id_by_name(hl)
  73. rawset(table, capture, id)
  74. return id
  75. end
  76. })
  77. if query_string then
  78. self._query = query.parse_query(lang, query_string)
  79. else
  80. self._query = query.get_query(lang, "highlights")
  81. end
  82. return self
  83. end
  84. ---@private
  85. function TSHighlighterQuery:query()
  86. return self._query
  87. end
  88. ---@private
  89. --- Get the hl from capture.
  90. --- Returns a tuple { highlight_name: string, is_builtin: bool }
  91. function TSHighlighterQuery:_get_hl_from_capture(capture)
  92. local name = self._query.captures[capture]
  93. if is_highlight_name(name) then
  94. -- From "Normal.left" only keep "Normal"
  95. return vim.split(name, '.', true)[1], true
  96. else
  97. return TSHighlighter.hl_map[name] or 0, false
  98. end
  99. end
  100. --- Creates a new highlighter using @param tree
  101. ---
  102. ---@param tree The language tree to use for highlighting
  103. ---@param opts Table used to configure the highlighter
  104. --- - queries: Table to overwrite queries used by the highlighter
  105. function TSHighlighter.new(tree, opts)
  106. local self = setmetatable({}, TSHighlighter)
  107. if type(tree:source()) ~= "number" then
  108. error("TSHighlighter can not be used with a string parser source.")
  109. end
  110. opts = opts or {}
  111. self.tree = tree
  112. tree:register_cbs {
  113. on_changedtree = function(...) self:on_changedtree(...) end;
  114. on_bytes = function(...) self:on_bytes(...) end;
  115. on_detach = function(...) self:on_detach(...) end;
  116. }
  117. self.bufnr = tree:source()
  118. self.edit_count = 0
  119. self.redraw_count = 0
  120. self.line_count = {}
  121. -- A map of highlight states.
  122. -- This state is kept during rendering across each line update.
  123. self._highlight_states = {}
  124. self._queries = {}
  125. -- Queries for a specific language can be overridden by a custom
  126. -- string query... if one is not provided it will be looked up by file.
  127. if opts.queries then
  128. for lang, query_string in pairs(opts.queries) do
  129. self._queries[lang] = TSHighlighterQuery.new(lang, query_string)
  130. end
  131. end
  132. a.nvim_buf_set_option(self.bufnr, "syntax", "")
  133. TSHighlighter.active[self.bufnr] = self
  134. -- Tricky: if syntax hasn't been enabled, we need to reload color scheme
  135. -- but use synload.vim rather than syntax.vim to not enable
  136. -- syntax FileType autocmds. Later on we should integrate with the
  137. -- `:syntax` and `set syntax=...` machinery properly.
  138. if vim.g.syntax_on ~= 1 then
  139. vim.api.nvim_command("runtime! syntax/synload.vim")
  140. end
  141. self.tree:parse()
  142. return self
  143. end
  144. --- Removes all internal references to the highlighter
  145. function TSHighlighter:destroy()
  146. if TSHighlighter.active[self.bufnr] then
  147. TSHighlighter.active[self.bufnr] = nil
  148. end
  149. end
  150. ---@private
  151. function TSHighlighter:get_highlight_state(tstree)
  152. if not self._highlight_states[tstree] then
  153. self._highlight_states[tstree] = {
  154. next_row = 0,
  155. iter = nil
  156. }
  157. end
  158. return self._highlight_states[tstree]
  159. end
  160. ---@private
  161. function TSHighlighter:reset_highlight_state()
  162. self._highlight_states = {}
  163. end
  164. ---@private
  165. function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end)
  166. a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1)
  167. end
  168. ---@private
  169. function TSHighlighter:on_detach()
  170. self:destroy()
  171. end
  172. ---@private
  173. function TSHighlighter:on_changedtree(changes)
  174. for _, ch in ipairs(changes or {}) do
  175. a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3]+1)
  176. end
  177. end
  178. --- Gets the query used for @param lang
  179. ---
  180. ---@param lang A language used by the highlighter.
  181. function TSHighlighter:get_query(lang)
  182. if not self._queries[lang] then
  183. self._queries[lang] = TSHighlighterQuery.new(lang)
  184. end
  185. return self._queries[lang]
  186. end
  187. ---@private
  188. local function on_line_impl(self, buf, line)
  189. self.tree:for_each_tree(function(tstree, tree)
  190. if not tstree then return end
  191. local root_node = tstree:root()
  192. local root_start_row, _, root_end_row, _ = root_node:range()
  193. -- Only worry about trees within the line range
  194. if root_start_row > line or root_end_row < line then return end
  195. local state = self:get_highlight_state(tstree)
  196. local highlighter_query = self:get_query(tree:lang())
  197. -- Some injected languages may not have highlight queries.
  198. if not highlighter_query:query() then return end
  199. if state.iter == nil then
  200. state.iter = highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1)
  201. end
  202. while line >= state.next_row do
  203. local capture, node, metadata = state.iter()
  204. if capture == nil then break end
  205. local start_row, start_col, end_row, end_col = node:range()
  206. local hl = highlighter_query.hl_cache[capture]
  207. if hl and end_row >= line then
  208. a.nvim_buf_set_extmark(buf, ns, start_row, start_col,
  209. { end_line = end_row, end_col = end_col,
  210. hl_group = hl,
  211. ephemeral = true,
  212. priority = tonumber(metadata.priority) or 100 -- Low but leaves room below
  213. })
  214. end
  215. if start_row > line then
  216. state.next_row = start_row
  217. end
  218. end
  219. end, true)
  220. end
  221. ---@private
  222. function TSHighlighter._on_line(_, _win, buf, line, _)
  223. local self = TSHighlighter.active[buf]
  224. if not self then return end
  225. on_line_impl(self, buf, line)
  226. end
  227. ---@private
  228. function TSHighlighter._on_buf(_, buf)
  229. local self = TSHighlighter.active[buf]
  230. if self then
  231. self.tree:parse()
  232. end
  233. end
  234. ---@private
  235. function TSHighlighter._on_win(_, _win, buf, _topline)
  236. local self = TSHighlighter.active[buf]
  237. if not self then
  238. return false
  239. end
  240. self:reset_highlight_state()
  241. self.redraw_count = self.redraw_count + 1
  242. return true
  243. end
  244. a.nvim_set_decoration_provider(ns, {
  245. on_buf = TSHighlighter._on_buf;
  246. on_win = TSHighlighter._on_win;
  247. on_line = TSHighlighter._on_line;
  248. })
  249. return TSHighlighter