|
297 | 297 | local Ctx = {} |
298 | 298 | Ctx.__index = Ctx |
299 | 299 |
|
300 | | ---- @param document? morph.Morph |
301 | 300 | --- @param bufnr? integer |
| 301 | +--- @param document? morph.Morph |
302 | 302 | --- @param props TProps |
303 | 303 | --- @param state? TState |
304 | 304 | --- @param children morph.Tree |
|
354 | 354 | --- @field private text_content { old: morph.MorphTextState, curr: morph.MorphTextState } |
355 | 355 | --- @field private component_tree { old: morph.Tree } |
356 | 356 | --- @field private cleanup_hooks function[] |
| 357 | +--- @field private buf_watcher morph.BufWatcher |
357 | 358 | local Morph = {} |
358 | 359 | Morph.__index = Morph |
359 | 360 |
|
@@ -437,10 +438,7 @@ function Morph.markup_to_lines(opts) |
437 | 438 | return lines |
438 | 439 | end |
439 | 440 |
|
440 | | ---- @param opts { |
441 | | ---- tree: morph.Tree, |
442 | | ---- format_tag?: fun(tag: morph.Tag): string |
443 | | ---- } |
| 441 | +--- @param opts { tree: morph.Tree } |
444 | 442 | function Morph.markup_to_string(opts) return table.concat(Morph.markup_to_lines(opts), '\n') end |
445 | 443 |
|
446 | 444 | --- @param bufnr integer |
@@ -528,22 +526,15 @@ function Morph.new(bufnr) |
528 | 526 | old = { lines = {}, extmarks = {}, tags_to_extmark_ids = {}, extmark_ids_to_tag = {} }, |
529 | 527 | curr = { lines = {}, extmarks = {}, tags_to_extmark_ids = {}, extmark_ids_to_tag = {} }, |
530 | 528 | }, |
531 | | - component_tree = { |
532 | | - old = nil, |
533 | | - curr = nil, |
534 | | - ctx_by_node = {}, |
535 | | - }, |
| 529 | + component_tree = { old = nil }, |
536 | 530 | cleanup_hooks = {}, |
537 | 531 | }, Morph) |
538 | 532 |
|
539 | | - local text_changed_id = vim.api.nvim_create_autocmd( |
540 | | - { 'TextChanged', 'TextChangedI', 'TextChangedP' }, |
541 | | - { |
542 | | - buffer = bufnr, |
543 | | - callback = function() self:_on_text_changed() end, |
544 | | - } |
| 533 | + self.buf_watcher = H.buf_attach_after_autocmd( |
| 534 | + bufnr, |
| 535 | + function(...) self:_on_bytes_after_autocmd(...) end |
545 | 536 | ) |
546 | | - table.insert(self.cleanup_hooks, function() vim.api.nvim_del_autocmd(text_changed_id) end) |
| 537 | + table.insert(self.cleanup_hooks, self.buf_watcher.cleanup) |
547 | 538 |
|
548 | 539 | local wipeout_id = vim.api.nvim_create_autocmd('BufWipeout', { |
549 | 540 | buffer = self.bufnr, |
@@ -974,22 +965,46 @@ function Morph:_expr_map_callback(mode, lhs) |
974 | 965 | return keypress_cancel and '' or lhs |
975 | 966 | end |
976 | 967 |
|
977 | | ---- @private |
978 | | -function Morph:_on_text_changed() |
| 968 | +function Morph:_on_bytes_after_autocmd( |
| 969 | + _, |
| 970 | + _, |
| 971 | + _, |
| 972 | + start_row0, |
| 973 | + start_col0, |
| 974 | + _, |
| 975 | + _, |
| 976 | + _, |
| 977 | + _, |
| 978 | + new_end_row_off, |
| 979 | + new_end_col_off, |
| 980 | + _ |
| 981 | +) |
| 982 | + if self.changing then return end |
| 983 | + |
| 984 | + local end_row0 = start_row0 + new_end_row_off |
| 985 | + local end_col0 = start_col0 + new_end_col_off |
| 986 | + |
| 987 | + local max_end_row0 = vim.api.nvim_buf_line_count(self.bufnr) - 1 |
| 988 | + if end_row0 > max_end_row0 then end_row0 = max_end_row0 end |
| 989 | + |
| 990 | + local max_end_col0 = #(vim.api.nvim_buf_get_lines(self.bufnr, -2, -1, true)[1] or '') |
| 991 | + if end_col0 > max_end_col0 then end_col0 = max_end_col0 end |
| 992 | + |
| 993 | + local live_extmarks = Extmark._get_near_overshoot( |
| 994 | + self.bufnr, |
| 995 | + self.ns, |
| 996 | + Pos00.new(start_row0, start_col0), |
| 997 | + Pos00.new(end_row0, end_col0) |
| 998 | + ) |
| 999 | + |
979 | 1000 | --- @type { extmark: morph.Extmark, text: string }[] |
980 | 1001 | local changed = {} |
981 | | - for _, cached_extmark in ipairs(self.text_content.curr.extmarks) do |
982 | | - local live_extmark = assert(Extmark.by_id(self.bufnr, self.ns, cached_extmark.id)) |
983 | | - if live_extmark.start ~= cached_extmark or live_extmark.stop ~= cached_extmark.stop then |
984 | | - -- Just because extmarks have shifted, doesn't mean their text has changed: |
985 | | - -- Lookup the old value vs the new on and check: |
986 | | - local cached_tag = assert(self.text_content.curr.extmark_ids_to_tag[cached_extmark.id]) |
987 | | - local curr_text = live_extmark:_text() |
988 | | - |
989 | | - if cached_tag.curr_text ~= curr_text then |
990 | | - cached_tag.curr_text = curr_text |
991 | | - table.insert(changed, { extmark = live_extmark, text = curr_text }) |
992 | | - end |
| 1002 | + for _, live_extmark in ipairs(live_extmarks) do |
| 1003 | + local curr_text = live_extmark:_text() |
| 1004 | + local cached_tag = self.text_content.curr.extmark_ids_to_tag[live_extmark.id] |
| 1005 | + if cached_tag and cached_tag.curr_text ~= curr_text then |
| 1006 | + cached_tag.curr_text = curr_text |
| 1007 | + table.insert(changed, { extmark = live_extmark, text = curr_text }) |
993 | 1008 | end |
994 | 1009 | end |
995 | 1010 |
|
@@ -1113,7 +1128,12 @@ function H.is_textlock() |
1113 | 1128 |
|
1114 | 1129 | -- Try to change the window: if textlock is active, an error will be raised: |
1115 | 1130 | local tmp_buf = vim.api.nvim_create_buf(false, true) |
1116 | | - local ok, tmp_win = pcall(vim.api.nvim_open_win, tmp_buf, true, {}) |
| 1131 | + local ok, tmp_win = pcall( |
| 1132 | + vim.api.nvim_open_win, |
| 1133 | + tmp_buf, |
| 1134 | + true, |
| 1135 | + { relative = 'editor', width = 1, height = 1, row = 1, col = 1 } |
| 1136 | + ) |
1117 | 1137 | if |
1118 | 1138 | not ok |
1119 | 1139 | and type(tmp_win) == 'string' |
@@ -1232,6 +1252,41 @@ function H.levenshtein(opts) |
1232 | 1252 | return changes |
1233 | 1253 | end |
1234 | 1254 |
|
| 1255 | +--- @class morph.BufWatcher |
| 1256 | +--- @field last_on_bytes_args unknown |
| 1257 | +--- @field text_changed_autocmd_id integer |
| 1258 | +--- @field fire function |
| 1259 | +--- @field cleanup function |
| 1260 | + |
| 1261 | +--- This is a helper that waits to notify about changes until _after_ the |
| 1262 | +--- TextChanged{,I,P} autocmd has fired. This is because, at the time |
| 1263 | +--- nvim_buf_attach notifies the callback of changes, the buffer seems to be in |
| 1264 | +--- a strange state. In my testing I saw extra blank lines during the on_bytes |
| 1265 | +--- callback, as opposed to when the autocmd fires. |
| 1266 | +--- |
| 1267 | +--- @param bufnr integer |
| 1268 | +--- @param callback function |
| 1269 | +--- @return morph.BufWatcher |
| 1270 | +function H.buf_attach_after_autocmd(bufnr, callback) |
| 1271 | + local state = {} |
| 1272 | + |
| 1273 | + vim.api.nvim_buf_attach( |
| 1274 | + bufnr, |
| 1275 | + false, |
| 1276 | + { on_bytes = function(...) state.last_on_bytes_args = { ... } end } |
| 1277 | + ) |
| 1278 | + |
| 1279 | + state.text_changed_autocmd_id = vim.api.nvim_create_autocmd( |
| 1280 | + { 'TextChanged', 'TextChangedI', 'TextChangedP' }, |
| 1281 | + { buffer = bufnr, callback = function() state.fire() end } |
| 1282 | + ) |
| 1283 | + |
| 1284 | + function state.fire() callback(unpack(state.last_on_bytes_args)) end |
| 1285 | + function state.cleanup() vim.api.nvim_del_autocmd(state.text_changed_autocmd_id) end |
| 1286 | + |
| 1287 | + return state |
| 1288 | +end |
| 1289 | + |
1235 | 1290 | -------------------------------------------------------------------------------- |
1236 | 1291 | -- Exports |
1237 | 1292 | -------------------------------------------------------------------------------- |
|
0 commit comments