Most plugins end up implementing some kind of mapping between their internal data structures and in which lines they got rendered.
Once a user wants to trigger an action on a line, you look up what entry is supposed to be at that line and then execute the associated logic.
That approach works fairly well if you control all the rendering and if it’s not possible for users to insert other content to the buffer.
Whether you re-render everything or only changed parts shouldn’t matter too much if you’re not drawing thousands of lines. I’d opt for whatever is easier in your particular use-case, both is feasible.
If content could be inserted outside of your control and you still want to keep track of where your structures got rendered you could utilize extmarks. extmarks allow you to get a mark_id for a region in a buffer. This “marked” region will move with text changes and you can either get back the original mark_id based on the region or you can find the region based on the mark_id. This allows you to create some kind of mapping between your structures and the rendered text - and compared to tracking via line numbers it has the advantage that it is resilient against changes made to the buffer.
(And therefore makes a partial re-render a lot easier)
See :help nvim_buf_set_extmark() and :help nvim_buf_get_extmarks()
In nvim-dap I make use of that and built an abstraction on top that is basically some kind of buf_set_lines, except that it works with arbitrary lua tables and a render function and lets you attach some arbitrary context to each rendered line. This context could include lua functions for example
If you’re curious and want to take a look at the code:
Some test cases illustrate how it’s used:
Maybe it helps to get some inspiration
There is also a tree abstraction built on top that allows to render hierarchical structures and provides expand and collapse functionality