@@ -391,16 +391,24 @@ Pos00.__index = Pos00
391391
392392--- @param row integer 0-based row
393393--- @param col integer 0-based column
394+ --- @return morph.Pos00
394395function Pos00 .new (row , col ) return setmetatable ({ row , col }, Pos00 ) end
395396
396- function Pos00 :__eq (other ) return self [1 ] == other [1 ] and self [2 ] == other [2 ] end
397+ --- @param other unknown
398+ function Pos00 :__eq (other )
399+ return type (other ) == ' table' and self [1 ] == other [1 ] and self [2 ] == other [2 ]
400+ end
397401
402+ --- @param other unknown
398403function Pos00 :__lt (other )
404+ if type (other ) ~= ' table' then return false end
399405 if self [1 ] ~= other [1 ] then return self [1 ] < other [1 ] end
400406 return self [2 ] < other [2 ]
401407end
402408
409+ --- @param other unknown
403410function Pos00 :__gt (other )
411+ if type (other ) ~= ' table' then return false end
404412 if self [1 ] ~= other [1 ] then return self [1 ] > other [1 ] end
405413 return self [2 ] > other [2 ]
406414end
@@ -461,6 +469,12 @@ function Extmark.by_id(bufnr, ns, id)
461469end
462470
463471--- @private
472+ --- @param bufnr integer
473+ --- @param ns integer
474+ --- @param id integer
475+ --- @param start_row0 integer
476+ --- @param start_col0 integer
477+ --- @param details vim.api.keyset.extmark_details
464478--- Construct an Extmark from raw API data, normalizing bounds that extend past buffer end.
465479function Extmark ._from_raw (bufnr , ns , id , start_row0 , start_col0 , details )
466480 local start = Pos00 .new (start_row0 , start_col0 )
488502
489503--- @private
490504--- Find all extmarks that overlap with the given region.
505+ --- @param bufnr integer
506+ --- @param ns integer
507+ --- @param start morph.Pos00
508+ --- @param stop morph.Pos00
491509--- @return morph.Extmark[]
492510function Extmark ._get_in_range (bufnr , ns , start , stop )
493511 local raw_extmarks = vim .api .nvim_buf_get_extmarks (
509527
510528--- @private
511529--- Extract the text content covered by this extmark.
530+ --- @return string
512531function Extmark :_text ()
513532 local start , stop = self .start , self .stop
514533 if start == stop then return ' ' end
@@ -664,6 +683,7 @@ function Morph.markup_to_lines(opts)
664683 -- so we can cache it for on_change handlers later
665684 local text_accumulators = {} --- @type { text : string[] } []
666685
686+ --- @param s string
667687 local function emit_text (s )
668688 lines [curr_line1 ] = (lines [curr_line1 ] or ' ' ) .. s
669689 curr_col1 = # lines [curr_line1 ] + 1
@@ -682,6 +702,7 @@ function Morph.markup_to_lines(opts)
682702 end
683703 end
684704
705+ --- @param node morph.Tree
685706 local function visit (node )
686707 local node_type = tree_type (node )
687708
@@ -942,6 +963,7 @@ function Morph:mount(tree)
942963 -- Callbacks scheduled via ctx:do_after_render() - run after each render
943964 local after_render_callbacks = {} --- @type function[]
944965
966+ --- @param cb function
945967 local function schedule_after_render (cb ) table.insert (after_render_callbacks , cb ) end
946968
947969 -- Forward declarations for mutual recursion
@@ -1029,7 +1051,11 @@ function Morph:mount(tree)
10291051
10301052 -- Pre-compute "identity keys" for each node so we can match them up
10311053 -- A key combines: type + component function (if any) + explicit key attribute
1032- local old_keys , new_keys = {}, {}
1054+ --- @type table<integer , string>
1055+ local old_keys = {}
1056+ --- @type table<integer , string>
1057+ local new_keys = {}
1058+ --- @type table<morph.Tree , string>
10331059 local node_key_cache = {}
10341060
10351061 for i , node in ipairs (old_nodes ) do
@@ -1092,7 +1118,7 @@ function Morph:mount(tree)
10921118 end
10931119
10941120 --- Reconcile a component node (mount, update, or reuse existing context).
1095- --- @param old_tree morph.Node[]
1121+ --- @param old_tree morph.Tree
10961122 --- @param new_tag morph.Tag
10971123 reconcile_component = function (old_tree , new_tag )
10981124 local Component = new_tag .name --[[ @as morph.Component]]
@@ -1177,7 +1203,7 @@ function Morph:get_elements_at(pos, mode)
11771203 local elements = {} --- @type morph.Element[]
11781204 for _ , extmark in ipairs (candidates ) do
11791205 local tag = self .text_content .curr .extmark_ids_to_tag [extmark .id ]
1180- if tag and self : _position_intersects_extmark (pos , extmark , mode ) then
1206+ if tag and self . _position_intersects_extmark (pos , extmark , mode ) then
11811207 table.insert (elements , vim .tbl_extend (' force' , {}, tag , { extmark = extmark }))
11821208 end
11831209 end
@@ -1194,8 +1220,10 @@ end
11941220
11951221--- @private
11961222--- Check if a position truly intersects an extmark (Neovim's API is over-inclusive).
1197- --- @diagnostic disable-next-line : unused
1198- function Morph :_position_intersects_extmark (pos , extmark , mode )
1223+ --- @param pos morph.Pos00
1224+ --- @param extmark morph.Extmark
1225+ --- @param mode ? string
1226+ function Morph ._position_intersects_extmark (pos , extmark , mode )
11991227 local start , stop = extmark .start , extmark .stop
12001228
12011229 -- Zero-width extmarks at cursor position are considered intersecting
@@ -1209,6 +1237,15 @@ function Morph:_position_intersects_extmark(pos, extmark, mode)
12091237
12101238 -- Check column bounds on stop row
12111239 if pos [1 ] == stop [1 ] then
1240+ -- Special case: on an empty line where extmark ends at column 0,
1241+ -- the cursor at column 0 should be considered inside. This happens when
1242+ -- an element ends with a newline - the cursor on the resulting empty line
1243+ -- has nowhere else to be, so it should still trigger handlers.
1244+ if pos [2 ] == 0 and stop [2 ] == 0 then
1245+ local line = vim .api .nvim_buf_get_lines (extmark .bufnr , pos [1 ], pos [1 ] + 1 , true )[1 ] or ' '
1246+ if # line == 0 then return true end
1247+ end
1248+
12121249 -- In insert mode the cursor is "thin" (between characters), so we include
12131250 -- the position if it's <= stop (cursor can sit "on" the boundary)
12141251 -- In normal mode the cursor is "wide" (occupies a character), so we only
@@ -1245,6 +1282,8 @@ end
12451282--- @private
12461283--- Handle a keypress by dispatching to element handlers (innermost first).
12471284--- Returns the key to execute, or '' to swallow the keypress.
1285+ --- @param mode string
1286+ --- @param lhs string
12481287function Morph :_dispatch_keypress (mode , lhs )
12491288 local cursor = vim .api .nvim_win_get_cursor (0 )
12501289 --- @diagnostic disable-next-line : need-check-nil , assign-type-mismatch
0 commit comments