neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

decoration.c (40759B)


      1 #include <assert.h>
      2 #include <limits.h>
      3 #include <stddef.h>
      4 #include <stdlib.h>
      5 #include <string.h>
      6 
      7 #include "nvim/api/extmark.h"
      8 #include "nvim/api/private/defs.h"
      9 #include "nvim/api/private/helpers.h"
     10 #include "nvim/ascii_defs.h"
     11 #include "nvim/buffer.h"
     12 #include "nvim/buffer_defs.h"
     13 #include "nvim/change.h"
     14 #include "nvim/decoration.h"
     15 #include "nvim/decoration_provider.h"
     16 #include "nvim/drawscreen.h"
     17 #include "nvim/extmark.h"
     18 #include "nvim/fold.h"
     19 #include "nvim/globals.h"
     20 #include "nvim/grid.h"
     21 #include "nvim/highlight.h"
     22 #include "nvim/highlight_group.h"
     23 #include "nvim/marktree.h"
     24 #include "nvim/memory.h"
     25 #include "nvim/memory_defs.h"
     26 #include "nvim/move.h"
     27 #include "nvim/option_vars.h"
     28 #include "nvim/pos_defs.h"
     29 #include "nvim/sign.h"
     30 
     31 #include "decoration.c.generated.h"
     32 
     33 uint32_t decor_freelist = UINT32_MAX;
     34 
     35 // Decorations might be requested to be deleted in a callback in the middle of redrawing.
     36 // In this case, there might still be live references to the memory allocated for the decoration.
     37 // Keep a "to free" list which can be safely processed when redrawing is done.
     38 DecorVirtText *to_free_virt = NULL;
     39 uint32_t to_free_sh = UINT32_MAX;
     40 
     41 /// Add highlighting to a buffer, bounded by two cursor positions,
     42 /// with an offset.
     43 ///
     44 /// TODO(bfredl): make decoration powerful enough so that this
     45 /// can be done with a single ephemeral decoration.
     46 ///
     47 /// @param buf Buffer to add highlights to
     48 /// @param src_id src_id to use or 0 to use a new src_id group,
     49 ///               or -1 for ungrouped highlight.
     50 /// @param hl_id Highlight group id
     51 /// @param pos_start Cursor position to start the highlighting at
     52 /// @param pos_end Cursor position to end the highlighting at
     53 /// @param offset Move the whole highlighting this many columns to the right
     54 void bufhl_add_hl_pos_offset(buf_T *buf, int src_id, int hl_id, lpos_T pos_start, lpos_T pos_end,
     55                             colnr_T offset)
     56 {
     57  colnr_T hl_start = 0;
     58  colnr_T hl_end = 0;
     59  DecorInline decor = DECOR_INLINE_INIT;
     60  decor.data.hl.hl_id = hl_id;
     61 
     62  // TODO(bfredl): if decoration had blocky mode, we could avoid this loop
     63  for (linenr_T lnum = pos_start.lnum; lnum <= pos_end.lnum; lnum++) {
     64    int end_off = 0;
     65    if (pos_start.lnum < lnum && lnum < pos_end.lnum) {
     66      // TODO(bfredl): This is quite ad-hoc, but the space between |num| and
     67      // text being highlighted is the indication of \n being part of the
     68      // substituted text. But it would be more consistent to highlight
     69      // a space _after_ the previous line instead (like highlight EOL list
     70      // char)
     71      hl_start = MAX(offset - 1, 0);
     72      end_off = 1;
     73      hl_end = 0;
     74    } else if (lnum == pos_start.lnum && lnum < pos_end.lnum) {
     75      hl_start = pos_start.col + offset;
     76      end_off = 1;
     77      hl_end = 0;
     78    } else if (pos_start.lnum < lnum && lnum == pos_end.lnum) {
     79      hl_start = MAX(offset - 1, 0);
     80      hl_end = pos_end.col + offset;
     81    } else if (pos_start.lnum == lnum && pos_end.lnum == lnum) {
     82      hl_start = pos_start.col + offset;
     83      hl_end = pos_end.col + offset;
     84    }
     85 
     86    extmark_set(buf, (uint32_t)src_id, NULL,
     87                (int)lnum - 1, hl_start, (int)lnum - 1 + end_off, hl_end,
     88                decor, MT_FLAG_DECOR_HL, true, false, true, false, NULL);
     89  }
     90 }
     91 
     92 void decor_redraw(buf_T *buf, int row1, int row2, int col1, DecorInline decor)
     93 {
     94  if (decor.ext) {
     95    DecorVirtText *vt = decor.data.ext.vt;
     96    while (vt) {
     97      bool below = (vt->flags & kVTIsLines) && !(vt->flags & kVTLinesAbove);
     98      linenr_T vt_lnum = row1 + 1 + below;
     99      redraw_buf_line_later(buf, vt_lnum, true);
    100      if (vt->flags & kVTIsLines || vt->pos == kVPosInline) {
    101        // changed_lines_redraw_buf(buf, vt_lnum, vt_lnum + 1, 0);
    102        colnr_T vt_col = vt->flags & kVTIsLines ? 0 : col1;
    103        changed_lines_invalidate_buf(buf, vt_lnum, vt_col, vt_lnum + 1, 0);
    104      }
    105      vt = vt->next;
    106    }
    107 
    108    uint32_t idx = decor.data.ext.sh_idx;
    109    while (idx != DECOR_ID_INVALID) {
    110      DecorSignHighlight *sh = &kv_A(decor_items, idx);
    111      decor_redraw_sh(buf, row1, row2, *sh);
    112      idx = sh->next;
    113    }
    114  } else {
    115    decor_redraw_sh(buf, row1, row2, decor_sh_from_inline(decor.data.hl));
    116  }
    117 }
    118 
    119 void decor_redraw_sh(buf_T *buf, int row1, int row2, DecorSignHighlight sh)
    120 {
    121  if (sh.hl_id || (sh.url != NULL)
    122      || (sh.flags & (kSHIsSign | kSHSpellOn | kSHSpellOff | kSHConceal))) {
    123    if (row2 >= row1) {
    124      redraw_buf_range_later(buf, row1 + 1, row2 + 1);
    125    }
    126  }
    127  if (sh.flags & kSHConcealLines) {
    128    FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    129      // TODO(luukvbaal): redraw only unconcealed lines, and scroll lines below
    130      // it up or down. Also when opening/closing a fold.
    131      if (wp->w_buffer == buf) {
    132        changed_window_setting(wp);
    133      }
    134    }
    135  }
    136  if (sh.flags & kSHUIWatched) {
    137    redraw_buf_line_later(buf, row1 + 1, false);
    138  }
    139 }
    140 
    141 uint32_t decor_put_sh(DecorSignHighlight item)
    142 {
    143  if (decor_freelist != UINT32_MAX) {
    144    uint32_t pos = decor_freelist;
    145    decor_freelist = kv_A(decor_items, decor_freelist).next;
    146    kv_A(decor_items, pos) = item;
    147    return pos;
    148  } else {
    149    uint32_t pos = (uint32_t)kv_size(decor_items);
    150    kv_push(decor_items, item);
    151    return pos;
    152  }
    153 }
    154 
    155 DecorVirtText *decor_put_vt(DecorVirtText vt, DecorVirtText *next)
    156 {
    157  DecorVirtText *decor_alloc = xmalloc(sizeof *decor_alloc);
    158  *decor_alloc = vt;
    159  decor_alloc->next = next;
    160  return decor_alloc;
    161 }
    162 
    163 DecorSignHighlight decor_sh_from_inline(DecorHighlightInline item)
    164 {
    165  // TODO(bfredl): Eventually simple signs will be inlinable as well
    166  assert(!(item.flags & kSHIsSign));
    167  DecorSignHighlight conv = {
    168    .flags = item.flags,
    169    .priority = item.priority,
    170    .text[0] = item.conceal_char,
    171    .hl_id = item.hl_id,
    172    .number_hl_id = 0,
    173    .line_hl_id = 0,
    174    .cursorline_hl_id = 0,
    175    .next = DECOR_ID_INVALID,
    176  };
    177 
    178  return conv;
    179 }
    180 
    181 void buf_put_decor(buf_T *buf, DecorInline decor, int row, int row2)
    182 {
    183  if (decor.ext && row < buf->b_ml.ml_line_count) {
    184    uint32_t idx = decor.data.ext.sh_idx;
    185    row2 = MIN(buf->b_ml.ml_line_count - 1, row2);
    186    while (idx != DECOR_ID_INVALID) {
    187      DecorSignHighlight *sh = &kv_A(decor_items, idx);
    188      buf_put_decor_sh(buf, sh, row, row2);
    189      idx = sh->next;
    190    }
    191  }
    192 }
    193 
    194 /// When displaying signs in the 'number' column, if the width of the number
    195 /// column is less than 2, then force recomputing the width after placing or
    196 /// unplacing the first sign in "buf".
    197 static void may_force_numberwidth_recompute(buf_T *buf, bool unplace)
    198 {
    199  FOR_ALL_TAB_WINDOWS(tp, wp) {
    200    if (wp->w_buffer == buf
    201        && wp->w_minscwidth == SCL_NUM
    202        && (wp->w_p_nu || wp->w_p_rnu)
    203        && (unplace || wp->w_nrwidth_width < 2)) {
    204      wp->w_nrwidth_line_count = 0;
    205    }
    206  }
    207 }
    208 
    209 static int sign_add_id = 0;
    210 void buf_put_decor_sh(buf_T *buf, DecorSignHighlight *sh, int row1, int row2)
    211 {
    212  if (sh->flags & kSHIsSign) {
    213    sh->sign_add_id = sign_add_id++;
    214    if (sh->text[0]) {
    215      buf_signcols_count_range(buf, row1, row2, 1, kFalse);
    216      may_force_numberwidth_recompute(buf, false);
    217    }
    218  }
    219 }
    220 
    221 void buf_decor_remove(buf_T *buf, int row1, int row2, int col1, DecorInline decor, bool free)
    222 {
    223  decor_redraw(buf, row1, row2, col1, decor);
    224  if (decor.ext && row1 < buf->b_ml.ml_line_count) {
    225    uint32_t idx = decor.data.ext.sh_idx;
    226    row2 = MIN(buf->b_ml.ml_line_count - 1, row2);
    227    while (idx != DECOR_ID_INVALID) {
    228      DecorSignHighlight *sh = &kv_A(decor_items, idx);
    229      buf_remove_decor_sh(buf, row1, row2, sh);
    230      idx = sh->next;
    231    }
    232  }
    233  if (free) {
    234    decor_free(decor);
    235  }
    236 }
    237 
    238 void buf_remove_decor_sh(buf_T *buf, int row1, int row2, DecorSignHighlight *sh)
    239 {
    240  if (sh->flags & kSHIsSign) {
    241    if (sh->text[0]) {
    242      if (buf_meta_total(buf, kMTMetaSignText)) {
    243        buf_signcols_count_range(buf, row1, row2, -1, kFalse);
    244      } else {
    245        may_force_numberwidth_recompute(buf, true);
    246        buf->b_signcols.count[0] = 0;
    247        buf->b_signcols.max = 0;
    248      }
    249    }
    250  }
    251 }
    252 
    253 void decor_free(DecorInline decor)
    254 {
    255  if (!decor.ext) {
    256    return;
    257  }
    258  DecorVirtText *vt = decor.data.ext.vt;
    259  uint32_t idx = decor.data.ext.sh_idx;
    260 
    261  if (decor_state.running_decor_provider) {
    262    while (vt) {
    263      if (vt->next == NULL) {
    264        vt->next = to_free_virt;
    265        to_free_virt = decor.data.ext.vt;
    266        break;
    267      }
    268      vt = vt->next;
    269    }
    270    while (idx != DECOR_ID_INVALID) {
    271      DecorSignHighlight *sh = &kv_A(decor_items, idx);
    272      if (sh->next == DECOR_ID_INVALID) {
    273        sh->next = to_free_sh;
    274        to_free_sh = decor.data.ext.sh_idx;
    275        break;
    276      }
    277      idx = sh->next;
    278    }
    279  } else {
    280    // safe to delete right now
    281    decor_free_inner(vt, idx);
    282  }
    283 }
    284 
    285 static void decor_free_inner(DecorVirtText *vt, uint32_t first_idx)
    286 {
    287  while (vt) {
    288    if (vt->flags & kVTIsLines) {
    289      clear_virtlines(&vt->data.virt_lines);
    290    } else {
    291      clear_virttext(&vt->data.virt_text);
    292    }
    293    DecorVirtText *tofree = vt;
    294    vt = vt->next;
    295    xfree(tofree);
    296  }
    297 
    298  uint32_t idx = first_idx;
    299  while (idx != DECOR_ID_INVALID) {
    300    DecorSignHighlight *sh = &kv_A(decor_items, idx);
    301    if (sh->flags & kSHIsSign) {
    302      XFREE_CLEAR(sh->sign_name);
    303    }
    304    sh->flags = 0;
    305    if (sh->url != NULL) {
    306      XFREE_CLEAR(sh->url);
    307    }
    308    if (sh->next == DECOR_ID_INVALID) {
    309      sh->next = decor_freelist;
    310      decor_freelist = first_idx;
    311      break;
    312    }
    313    idx = sh->next;
    314  }
    315 }
    316 
    317 /// Check if we are in a callback while drawing, which might invalidate the marktree iterator.
    318 ///
    319 /// This should be called whenever a structural modification has been done to a
    320 /// marktree in a public API function (i e any change which adds or deletes marks).
    321 void decor_state_invalidate(buf_T *buf)
    322 {
    323  if (decor_state.win && decor_state.win->w_buffer == buf) {
    324    decor_state.itr_valid = false;
    325  }
    326 }
    327 
    328 void decor_check_to_be_deleted(void)
    329 {
    330  assert(!decor_state.running_decor_provider);
    331  decor_free_inner(to_free_virt, to_free_sh);
    332  to_free_virt = NULL;
    333  to_free_sh = DECOR_ID_INVALID;
    334  decor_state.win = NULL;
    335 }
    336 
    337 void decor_state_free(DecorState *state)
    338 {
    339  kv_destroy(state->slots);
    340  kv_destroy(state->ranges_i);
    341 }
    342 
    343 void clear_virttext(VirtText *text)
    344 {
    345  for (size_t i = 0; i < kv_size(*text); i++) {
    346    xfree(kv_A(*text, i).text);
    347  }
    348  kv_destroy(*text);
    349  *text = (VirtText)KV_INITIAL_VALUE;
    350 }
    351 
    352 void clear_virtlines(VirtLines *lines)
    353 {
    354  for (size_t i = 0; i < kv_size(*lines); i++) {
    355    clear_virttext(&kv_A(*lines, i).line);
    356  }
    357  kv_destroy(*lines);
    358  *lines = (VirtLines)KV_INITIAL_VALUE;
    359 }
    360 
    361 void decor_check_invalid_glyphs(void)
    362 {
    363  for (size_t i = 0; i < kv_size(decor_items); i++) {
    364    DecorSignHighlight *it = &kv_A(decor_items, i);
    365    int width = (it->flags & kSHIsSign) ? SIGN_WIDTH : ((it->flags & kSHConceal) ? 1 : 0);
    366    for (int j = 0; j < width; j++) {
    367      if (schar_high(it->text[j])) {
    368        it->text[j] = schar_from_char(schar_get_first_codepoint(it->text[j]));
    369      }
    370    }
    371  }
    372 }
    373 
    374 /// Get the next chunk of a virtual text item.
    375 ///
    376 /// @param[in]     vt    The virtual text item
    377 /// @param[in,out] pos   Position in the virtual text item
    378 /// @param[in,out] attr  Highlight attribute
    379 ///
    380 /// @return  The text of the chunk, or NULL if there are no more chunks
    381 char *next_virt_text_chunk(VirtText vt, size_t *pos, int *attr)
    382 {
    383  char *text = NULL;
    384  for (; text == NULL && *pos < kv_size(vt); (*pos)++) {
    385    text = kv_A(vt, *pos).text;
    386    int hl_id = kv_A(vt, *pos).hl_id;
    387    if (hl_id >= 0) {
    388      *attr = MAX(*attr, 0);
    389      if (hl_id > 0) {
    390        *attr = hl_combine_attr(*attr, syn_id2attr(hl_id));
    391      }
    392    }
    393  }
    394  return text;
    395 }
    396 
    397 DecorVirtText *decor_find_virttext(buf_T *buf, int row, uint64_t ns_id)
    398 {
    399  MarkTreeIter itr[1] = { 0 };
    400  marktree_itr_get(buf->b_marktree, row, 0,  itr);
    401  while (true) {
    402    MTKey mark = marktree_itr_current(itr);
    403    if (mark.pos.row < 0 || mark.pos.row > row) {
    404      break;
    405    } else if (mt_invalid(mark)) {
    406      goto next_mark;
    407    }
    408    DecorVirtText *decor = mt_decor_virt(mark);
    409    while (decor && (decor->flags & kVTIsLines)) {
    410      decor = decor->next;
    411    }
    412    if ((ns_id == 0 || ns_id == mark.ns) && decor) {
    413      return decor;
    414    }
    415 next_mark:
    416    marktree_itr_next(buf->b_marktree, itr);
    417  }
    418  return NULL;
    419 }
    420 
    421 bool decor_redraw_reset(win_T *wp, DecorState *state)
    422 {
    423  state->row = -1;
    424  state->win = wp;
    425 
    426  int *const indices = state->ranges_i.items;
    427  DecorRangeSlot *const slots = state->slots.items;
    428 
    429  int const beg_pos[] = { 0, state->future_begin };
    430  int const end_pos[] = { state->current_end, (int)kv_size(state->ranges_i) };
    431 
    432  for (int pos_i = 0; pos_i < 2; pos_i++) {
    433    for (int i = beg_pos[pos_i]; i < end_pos[pos_i]; i++) {
    434      DecorRange *const r = &slots[indices[i]].range;
    435      if (r->owned && r->kind == kDecorKindVirtText) {
    436        clear_virttext(&r->data.vt->data.virt_text);
    437        xfree(r->data.vt);
    438      }
    439    }
    440  }
    441 
    442  kv_size(state->slots) = 0;
    443  kv_size(state->ranges_i) = 0;
    444  state->free_slot_i = -1;
    445  state->current_end = 0;
    446  state->future_begin = 0;
    447  state->new_range_ordering = 0;
    448 
    449  return wp->w_buffer->b_marktree->n_keys;
    450 }
    451 
    452 /// @return true if decor has a virtual position (virtual text or ui_watched)
    453 bool decor_virt_pos(const DecorRange *decor)
    454 {
    455  return (decor->kind == kDecorKindVirtText || decor->kind == kDecorKindUIWatched);
    456 }
    457 
    458 VirtTextPos decor_virt_pos_kind(const DecorRange *decor)
    459 {
    460  if (decor->kind == kDecorKindVirtText) {
    461    return decor->data.vt->pos;
    462  }
    463  if (decor->kind == kDecorKindUIWatched) {
    464    return decor->data.ui.pos;
    465  }
    466  return kVPosEndOfLine;  // not used; return whatever
    467 }
    468 
    469 bool decor_redraw_start(win_T *wp, int top_row, DecorState *state)
    470 {
    471  buf_T *buf = wp->w_buffer;
    472  state->top_row = top_row;
    473  state->itr_valid = true;
    474 
    475  if (!marktree_itr_get_overlap(buf->b_marktree, top_row, 0, state->itr)) {
    476    return false;
    477  }
    478  MTPair pair;
    479 
    480  while (marktree_itr_step_overlap(buf->b_marktree, state->itr, &pair)) {
    481    MTKey m = pair.start;
    482    if (mt_invalid(m) || !mt_decor_any(m)) {
    483      continue;
    484    }
    485 
    486    decor_range_add_from_inline(state, pair.start.pos.row, pair.start.pos.col, pair.end_pos.row,
    487                                pair.end_pos.col,
    488                                mt_decor(m), false, m.ns, m.id);
    489  }
    490 
    491  return true;  // TODO(bfredl): check if available in the region
    492 }
    493 
    494 static void decor_state_pack(DecorState *state)
    495 {
    496  int count = (int)kv_size(state->ranges_i);
    497  int const cur_end = state->current_end;
    498  int fut_beg = state->future_begin;
    499 
    500  // Move future ranges to start right after current ranges.
    501  // Otherwise future ranges will grow forward indefinitely.
    502  if (fut_beg == count) {
    503    fut_beg = count = cur_end;
    504  } else if (fut_beg != cur_end) {
    505    int *const indices = state->ranges_i.items;
    506    memmove(indices + cur_end, indices + fut_beg, (size_t)(count - fut_beg) * sizeof(indices[0]));
    507 
    508    count = cur_end + (count - fut_beg);
    509    fut_beg = cur_end;
    510  }
    511 
    512  kv_size(state->ranges_i) = (size_t)count;
    513  state->future_begin = fut_beg;
    514 }
    515 
    516 void decor_redraw_line(win_T *wp, int row, DecorState *state)
    517 {
    518  decor_state_pack(state);
    519 
    520  if (state->row == -1) {
    521    decor_redraw_start(wp, row, state);
    522  } else if (!state->itr_valid) {
    523    marktree_itr_get(wp->w_buffer->b_marktree, row, 0, state->itr);
    524    state->itr_valid = true;
    525  }
    526 
    527  state->row = row;
    528  state->col_until = -1;
    529  state->eol_col = -1;
    530 }
    531 
    532 // Checks if there are (likely) more decorations on the current line.
    533 bool decor_has_more_decorations(DecorState *state, int row)
    534 {
    535  if (state->current_end != 0 || state->future_begin != (int)kv_size(state->ranges_i)) {
    536    return true;
    537  }
    538 
    539  MTKey k = marktree_itr_current(state->itr);
    540  return (k.pos.row >= 0 && k.pos.row <= row);
    541 }
    542 
    543 static void decor_range_add_from_inline(DecorState *state, int start_row, int start_col,
    544                                        int end_row, int end_col, DecorInline decor, bool owned,
    545                                        uint32_t ns, uint32_t mark_id)
    546 {
    547  if (decor.ext) {
    548    DecorVirtText *vt = decor.data.ext.vt;
    549    while (vt) {
    550      decor_range_add_virt(state, start_row, start_col, end_row, end_col, vt, owned);
    551      vt = vt->next;
    552    }
    553    uint32_t idx = decor.data.ext.sh_idx;
    554    while (idx != DECOR_ID_INVALID) {
    555      DecorSignHighlight *sh = &kv_A(decor_items, idx);
    556      decor_range_add_sh(state, start_row, start_col, end_row, end_col, sh, owned, ns, mark_id, 0);
    557      idx = sh->next;
    558    }
    559  } else {
    560    DecorSignHighlight sh = decor_sh_from_inline(decor.data.hl);
    561    decor_range_add_sh(state, start_row, start_col, end_row, end_col, &sh, owned, ns, mark_id, 0);
    562  }
    563 }
    564 
    565 static void decor_range_insert(DecorState *state, DecorRange *range)
    566 {
    567  range->ordering = state->new_range_ordering++;
    568 
    569  int index;
    570  // Get space for a new `DecorRange` from the freelist or allocate.
    571  if (state->free_slot_i >= 0) {
    572    index = state->free_slot_i;
    573    DecorRangeSlot *slot = &kv_A(state->slots, index);
    574    state->free_slot_i = slot->next_free_i;
    575    slot->range = *range;
    576  } else {
    577    index = (int)kv_size(state->slots);
    578    kv_pushp(state->slots)->range = *range;
    579  }
    580 
    581  int const row = range->start_row;
    582  int const col = range->start_col;
    583 
    584  int const count = (int)kv_size(state->ranges_i);
    585  int *const indices = state->ranges_i.items;
    586  DecorRangeSlot *const slots = state->slots.items;
    587 
    588  int begin = state->future_begin;
    589  int end = count;
    590  while (begin < end) {
    591    int const mid = begin + ((end - begin) >> 1);
    592    DecorRange *const mr = &slots[indices[mid]].range;
    593 
    594    int const mrow = mr->start_row;
    595    int const mcol = mr->start_col;
    596    if (mrow < row || (mrow == row && mcol <= col)) {
    597      begin = mid + 1;
    598      if (mrow == row && mcol == col) {
    599        break;
    600      }
    601    } else {
    602      end = mid;
    603    }
    604  }
    605 
    606  kv_pushp(state->ranges_i);
    607  int *const item = &kv_A(state->ranges_i, begin);
    608  memmove(item + 1, item, (size_t)(count - begin) * sizeof(*item));
    609  *item = index;
    610 }
    611 
    612 void decor_range_add_virt(DecorState *state, int start_row, int start_col, int end_row, int end_col,
    613                          DecorVirtText *vt, bool owned)
    614 {
    615  bool is_lines = vt->flags & kVTIsLines;
    616  DecorRange range = {
    617    .start_row = start_row, .start_col = start_col, .end_row = end_row, .end_col = end_col,
    618    .kind = is_lines ? kDecorKindVirtLines : kDecorKindVirtText,
    619    .data.vt = vt,
    620    .attr_id = 0,
    621    .owned = owned,
    622    .priority_internal = ((DecorPriorityInternal)vt->priority << 16),
    623    .draw_col = -10,
    624  };
    625  decor_range_insert(state, &range);
    626 }
    627 
    628 void decor_range_add_sh(DecorState *state, int start_row, int start_col, int end_row, int end_col,
    629                        DecorSignHighlight *sh, bool owned, uint32_t ns, uint32_t mark_id,
    630                        DecorPriority subpriority)
    631 {
    632  if (sh->flags & kSHIsSign) {
    633    return;
    634  }
    635 
    636  DecorRange range = {
    637    .start_row = start_row, .start_col = start_col, .end_row = end_row, .end_col = end_col,
    638    .kind = kDecorKindHighlight,
    639    .data.sh = *sh,
    640    .attr_id = 0,
    641    .owned = owned,
    642    .priority_internal = ((DecorPriorityInternal)sh->priority << 16) + subpriority,
    643    .draw_col = -10,
    644  };
    645 
    646  if (sh->hl_id || (sh->url != NULL)
    647      || (sh->flags & (kSHConceal | kSHSpellOn | kSHSpellOff))) {
    648    if (sh->hl_id) {
    649      range.attr_id = syn_id2attr(sh->hl_id);
    650    }
    651    decor_range_insert(state, &range);
    652  }
    653 
    654  if (sh->flags & (kSHUIWatched)) {
    655    range.kind = kDecorKindUIWatched;
    656    range.data.ui.ns_id = ns;
    657    range.data.ui.mark_id = mark_id;
    658    range.data.ui.pos = (sh->flags & kSHUIWatchedOverlay) ? kVPosOverlay : kVPosEndOfLine;
    659    decor_range_insert(state, &range);
    660  }
    661 }
    662 
    663 /// Initialize the draw_col of a newly-added virtual text item.
    664 void decor_init_draw_col(int win_col, bool hidden, DecorRange *item)
    665 {
    666  DecorVirtText *vt = item->kind == kDecorKindVirtText ? item->data.vt : NULL;
    667  VirtTextPos pos = decor_virt_pos_kind(item);
    668  if (win_col < 0 && pos != kVPosInline) {
    669    item->draw_col = win_col;
    670  } else if (pos == kVPosOverlay) {
    671    item->draw_col = (vt && (vt->flags & kVTHide) && hidden) ? INT_MIN : win_col;
    672  } else {
    673    item->draw_col = -1;
    674  }
    675 }
    676 
    677 void decor_recheck_draw_col(int win_col, bool hidden, DecorState *state)
    678 {
    679  int const end = state->current_end;
    680  int *const indices = state->ranges_i.items;
    681  DecorRangeSlot *const slots = state->slots.items;
    682 
    683  for (int i = 0; i < end; i++) {
    684    DecorRange *const r = &slots[indices[i]].range;
    685    if (r->draw_col == -3) {
    686      decor_init_draw_col(win_col, hidden, r);
    687    }
    688  }
    689 }
    690 
    691 int decor_redraw_col_impl(win_T *wp, int col, int win_col, bool hidden, DecorState *state)
    692 {
    693  buf_T *const buf = wp->w_buffer;
    694  int const row = state->row;
    695  int col_until = MAXCOL;
    696 
    697  while (true) {
    698    // TODO(bfredl): check duplicate entry in "intersection"
    699    // branch
    700    MTKey mark = marktree_itr_current(state->itr);
    701    if (mark.pos.row < 0 || mark.pos.row > row) {
    702      break;
    703    } else if (mark.pos.row == row && mark.pos.col > col) {
    704      col_until = mark.pos.col - 1;
    705      break;
    706    }
    707 
    708    if (mt_invalid(mark) || mt_end(mark) || !mt_decor_any(mark) || !ns_in_win(mark.ns, wp)) {
    709      goto next_mark;
    710    }
    711 
    712    MTPos endpos = marktree_get_altpos(buf->b_marktree, mark, NULL);
    713    decor_range_add_from_inline(state, mark.pos.row, mark.pos.col, endpos.row, endpos.col,
    714                                mt_decor(mark), false, mark.ns, mark.id);
    715 
    716 next_mark:
    717    marktree_itr_next(buf->b_marktree, state->itr);
    718  }
    719 
    720  int *const indices = state->ranges_i.items;
    721  DecorRangeSlot *const slots = state->slots.items;
    722 
    723  int count = (int)kv_size(state->ranges_i);
    724  int cur_end = state->current_end;
    725  int fut_beg = state->future_begin;
    726 
    727  // Promote future ranges before the cursor to active.
    728  for (; fut_beg < count; fut_beg++) {
    729    int const index = indices[fut_beg];
    730    DecorRange *const r = &slots[index].range;
    731    if (r->start_row > row || (r->start_row == row && r->start_col > col)) {
    732      break;
    733    }
    734    int const ordering = r->ordering;
    735    DecorPriorityInternal const priority = r->priority_internal;
    736 
    737    int begin = 0;
    738    int end = cur_end;
    739    while (begin < end) {
    740      int mid = begin + ((end - begin) >> 1);
    741      int mi = indices[mid];
    742      DecorRange *mr = &slots[mi].range;
    743      if (mr->priority_internal < priority
    744          || (mr->priority_internal == priority && mr->ordering < ordering)) {
    745        begin = mid + 1;
    746      } else {
    747        end = mid;
    748      }
    749    }
    750 
    751    int *const item = indices + begin;
    752    memmove(item + 1, item, (size_t)(cur_end - begin) * sizeof(*item));
    753    *item = index;
    754    cur_end++;
    755  }
    756 
    757  if (fut_beg < count) {
    758    DecorRange *r = &slots[indices[fut_beg]].range;
    759    if (r->start_row == row) {
    760      col_until = MIN(col_until, r->start_col - 1);
    761    }
    762  }
    763 
    764  int new_cur_end = 0;
    765 
    766  int attr = 0;
    767  int conceal = 0;
    768  schar_T conceal_char = 0;
    769  int conceal_attr = 0;
    770  TriState spell = kNone;
    771 
    772  for (int i = 0; i < cur_end; i++) {
    773    int const index = indices[i];
    774    DecorRangeSlot *const slot = slots + index;
    775    DecorRange *const r = &slot->range;
    776 
    777    bool keep;
    778    if (r->end_row < row || (r->end_row == row && r->end_col <= col)) {
    779      keep = r->start_row >= row && decor_virt_pos(r);
    780    } else {
    781      keep = true;
    782 
    783      if (r->end_row == row && r->end_col > col) {
    784        col_until = MIN(col_until, r->end_col - 1);
    785      }
    786 
    787      if (r->attr_id > 0) {
    788        attr = hl_combine_attr(attr, r->attr_id);
    789      }
    790 
    791      if (r->kind == kDecorKindHighlight && (r->data.sh.flags & kSHConceal)) {
    792        conceal = 1;
    793        if (r->start_row == row && r->start_col == col) {
    794          DecorSignHighlight *sh = &r->data.sh;
    795          conceal = 2;
    796          conceal_char = sh->text[0];
    797          col_until = MIN(col_until, r->start_col);
    798          conceal_attr = r->attr_id;
    799        }
    800      }
    801 
    802      if (r->kind == kDecorKindHighlight) {
    803        if (r->data.sh.flags & kSHSpellOn) {
    804          spell = kTrue;
    805        } else if (r->data.sh.flags & kSHSpellOff) {
    806          spell = kFalse;
    807        }
    808        if (r->data.sh.url != NULL) {
    809          attr = hl_add_url(attr, r->data.sh.url);
    810        }
    811      }
    812    }
    813 
    814    if (r->start_row == row && r->start_col <= col
    815        && decor_virt_pos(r) && r->draw_col == -10) {
    816      decor_init_draw_col(win_col, hidden, r);
    817    }
    818 
    819    if (keep) {
    820      indices[new_cur_end++] = index;
    821    } else {
    822      if (r->owned) {
    823        if (r->kind == kDecorKindVirtText) {
    824          clear_virttext(&r->data.vt->data.virt_text);
    825          xfree(r->data.vt);
    826        } else if (r->kind == kDecorKindHighlight) {
    827          xfree((void *)r->data.sh.url);
    828        }
    829      }
    830 
    831      int *fi = &state->free_slot_i;
    832      slot->next_free_i = *fi;
    833      *fi = index;
    834    }
    835  }
    836  cur_end = new_cur_end;
    837 
    838  if (fut_beg == count) {
    839    fut_beg = count = cur_end;
    840  }
    841 
    842  kv_size(state->ranges_i) = (size_t)count;
    843  state->future_begin = fut_beg;
    844  state->current_end = cur_end;
    845  state->col_until = col_until;
    846 
    847  state->current = attr;
    848  state->conceal = conceal;
    849  state->conceal_char = conceal_char;
    850  state->conceal_attr = conceal_attr;
    851  state->spell = spell;
    852  return attr;
    853 }
    854 
    855 static const uint32_t conceal_filter[kMTMetaCount] = {[kMTMetaConcealLines] = kMTFilterSelect };
    856 
    857 /// Called by draw, move and plines code to determine whether a line is concealed.
    858 /// Scans the marktree for conceal_line marks on "row" and invokes any
    859 /// _on_conceal_line decoration provider callbacks, if necessary.
    860 ///
    861 /// @param check_cursor If true, avoid an early return for an unconcealed cursorline.
    862 ///                     Depending on the callsite, we still want to know whether the
    863 ///                     cursor line would be concealed if it was not the cursorline.
    864 ///
    865 /// @return whether "row" is concealed
    866 bool decor_conceal_line(win_T *wp, int row, bool check_cursor)
    867 {
    868  if (row < 0 || wp->w_p_cole < 2
    869      || (!check_cursor && wp == curwin && row + 1 == wp->w_cursor.lnum
    870          && !conceal_cursor_line(wp))) {
    871    return false;
    872  }
    873 
    874  // No need to scan the marktree if there are no conceal_line marks.
    875  if (!buf_meta_total(wp->w_buffer, kMTMetaConcealLines)) {
    876    return decor_providers_invoke_conceal_line(wp, row);
    877  }
    878 
    879  // Scan the marktree for any conceal_line marks on this row.
    880  MTPair pair;
    881  MarkTreeIter itr[1];
    882  marktree_itr_get_overlap(wp->w_buffer->b_marktree, row, 0, itr);
    883  while (marktree_itr_step_overlap(wp->w_buffer->b_marktree, itr, &pair)) {
    884    if (mt_conceal_lines(pair.start) && ns_in_win(pair.start.ns, wp)) {
    885      return true;
    886    }
    887  }
    888 
    889  marktree_itr_step_out_filter(wp->w_buffer->b_marktree, itr, conceal_filter);
    890 
    891  while (itr->x) {
    892    MTKey mark = marktree_itr_current(itr);
    893    if (mark.pos.row > row) {
    894      break;
    895    }
    896    if (mt_conceal_lines(mark) && ns_in_win(pair.start.ns, wp)) {
    897      return true;
    898    }
    899    marktree_itr_next_filter(wp->w_buffer->b_marktree, itr, row + 1, 0, conceal_filter);
    900  }
    901 
    902  return decor_providers_invoke_conceal_line(wp, row);
    903 }
    904 
    905 /// @return whether a window may have folded or concealed lines
    906 bool win_lines_concealed(win_T *wp)
    907 {
    908  return hasAnyFolding(wp) || wp->w_p_cole >= 2;
    909 }
    910 
    911 int sign_item_cmp(const void *p1, const void *p2)
    912 {
    913  const SignItem *s1 = (SignItem *)p1;
    914  const SignItem *s2 = (SignItem *)p2;
    915 
    916  if (s1->sh->priority != s2->sh->priority) {
    917    return s1->sh->priority < s2->sh->priority ? 1 : -1;
    918  }
    919 
    920  if (s1->id != s2->id) {
    921    return s1->id < s2->id ? 1 : -1;
    922  }
    923 
    924  if (s1->sh->sign_add_id != s2->sh->sign_add_id) {
    925    return s1->sh->sign_add_id < s2->sh->sign_add_id ? 1 : -1;
    926  }
    927 
    928  return 0;
    929 }
    930 
    931 static const uint32_t sign_filter[kMTMetaCount] = {[kMTMetaSignText] = kMTFilterSelect,
    932                                                   [kMTMetaSignHL] = kMTFilterSelect };
    933 
    934 /// Return the signs and highest priority sign attributes on a row.
    935 ///
    936 /// @param[out] sattrs Output array for sign text and texthl id
    937 /// @param[out] line_id Highest priority linehl id
    938 /// @param[out] cul_id Highest priority culhl id
    939 /// @param[out] num_id Highest priority numhl id
    940 void decor_redraw_signs(win_T *wp, buf_T *buf, int row, SignTextAttrs sattrs[], int *line_id,
    941                        int *cul_id, int *num_id)
    942 {
    943  if (!buf_has_signs(buf)) {
    944    return;
    945  }
    946 
    947  MTPair pair;
    948  int num_text = 0;
    949  MarkTreeIter itr[1];
    950  kvec_t(SignItem) signs = KV_INITIAL_VALUE;
    951  // TODO(bfredl): integrate with main decor loop.
    952  marktree_itr_get_overlap(buf->b_marktree, row, 0, itr);
    953  while (marktree_itr_step_overlap(buf->b_marktree, itr, &pair)) {
    954    if (!mt_invalid(pair.start) && mt_decor_sign(pair.start) && ns_in_win(pair.start.ns, wp)) {
    955      DecorSignHighlight *sh = decor_find_sign(mt_decor(pair.start));
    956      num_text += (sh->text[0] != NUL);
    957      kv_push(signs, ((SignItem){ sh, pair.start.id }));
    958    }
    959  }
    960 
    961  marktree_itr_step_out_filter(buf->b_marktree, itr, sign_filter);
    962 
    963  while (itr->x) {
    964    MTKey mark = marktree_itr_current(itr);
    965    if (mark.pos.row != row) {
    966      break;
    967    }
    968    if (!mt_invalid(mark) && !mt_end(mark) && mt_decor_sign(mark) && ns_in_win(mark.ns, wp)) {
    969      DecorSignHighlight *sh = decor_find_sign(mt_decor(mark));
    970      num_text += (sh->text[0] != NUL);
    971      kv_push(signs, ((SignItem){ sh, mark.id }));
    972    }
    973 
    974    marktree_itr_next_filter(buf->b_marktree, itr, row + 1, 0, sign_filter);
    975  }
    976 
    977  if (kv_size(signs)) {
    978    int width = wp->w_minscwidth == SCL_NUM ? 1 : wp->w_scwidth;
    979    int len = MIN(width, num_text);
    980    int idx = 0;
    981    qsort((void *)&kv_A(signs, 0), kv_size(signs), sizeof(kv_A(signs, 0)), sign_item_cmp);
    982 
    983    for (size_t i = 0; i < kv_size(signs); i++) {
    984      DecorSignHighlight *sh = kv_A(signs, i).sh;
    985      if (sattrs && idx < len && sh->text[0]) {
    986        memcpy(sattrs[idx].text, sh->text, SIGN_WIDTH * sizeof(sattr_T));
    987        sattrs[idx++].hl_id = sh->hl_id;
    988      }
    989      if (num_id != NULL && *num_id <= 0) {
    990        *num_id = sh->number_hl_id;
    991      }
    992      if (line_id != NULL && *line_id <= 0) {
    993        *line_id = sh->line_hl_id;
    994      }
    995      if (cul_id != NULL && *cul_id <= 0) {
    996        *cul_id = sh->cursorline_hl_id;
    997      }
    998    }
    999    kv_destroy(signs);
   1000  }
   1001 }
   1002 
   1003 DecorSignHighlight *decor_find_sign(DecorInline decor)
   1004 {
   1005  if (!decor.ext) {
   1006    return NULL;
   1007  }
   1008  uint32_t decor_id = decor.data.ext.sh_idx;
   1009  while (true) {
   1010    if (decor_id == DECOR_ID_INVALID) {
   1011      return NULL;
   1012    }
   1013    DecorSignHighlight *sh = &kv_A(decor_items, decor_id);
   1014    if (sh->flags & kSHIsSign) {
   1015      return sh;
   1016    }
   1017    decor_id = sh->next;
   1018  }
   1019 }
   1020 
   1021 static const uint32_t signtext_filter[kMTMetaCount] = {[kMTMetaSignText] = kMTFilterSelect };
   1022 
   1023 /// Count the number of signs in a range after adding/removing a sign, or to
   1024 /// (re-)initialize a range in "b_signcols.count".
   1025 ///
   1026 /// @param add  1, -1 or 0 for an added, deleted or initialized range.
   1027 /// @param clear  kFalse, kTrue or kNone for an, added/deleted, cleared, or initialized range.
   1028 void buf_signcols_count_range(buf_T *buf, int row1, int row2, int add, TriState clear)
   1029 {
   1030  if (!buf->b_signcols.autom || row2 < row1 || !buf_meta_total(buf, kMTMetaSignText)) {
   1031    return;
   1032  }
   1033 
   1034  // Allocate an array of integers holding the number of signs in the range.
   1035  int *count = xcalloc((size_t)(row2 + 1 - row1), sizeof(int));
   1036  MarkTreeIter itr[1];
   1037  MTPair pair = { 0 };
   1038 
   1039  // Increment count array for signs that start before "row1" but do overlap the range.
   1040  marktree_itr_get_overlap(buf->b_marktree, row1, 0, itr);
   1041  while (marktree_itr_step_overlap(buf->b_marktree, itr, &pair)) {
   1042    if ((pair.start.flags & MT_FLAG_DECOR_SIGNTEXT) && !mt_invalid(pair.start)) {
   1043      for (int i = row1; i <= MIN(row2, pair.end_pos.row); i++) {
   1044        count[i - row1]++;
   1045      }
   1046    }
   1047  }
   1048 
   1049  marktree_itr_step_out_filter(buf->b_marktree, itr, signtext_filter);
   1050 
   1051  // Continue traversing the marktree until beyond "row2".
   1052  while (itr->x) {
   1053    MTKey mark = marktree_itr_current(itr);
   1054    if (mark.pos.row > row2) {
   1055      break;
   1056    }
   1057    if ((mark.flags & MT_FLAG_DECOR_SIGNTEXT) && !mt_invalid(mark) && !mt_end(mark)) {
   1058      // Increment count array for the range of a paired sign mark.
   1059      MTPos end = marktree_get_altpos(buf->b_marktree, mark, NULL);
   1060      for (int i = mark.pos.row; i <= MIN(row2, end.row); i++) {
   1061        count[i - row1]++;
   1062      }
   1063    }
   1064 
   1065    marktree_itr_next_filter(buf->b_marktree, itr, row2 + 1, 0, signtext_filter);
   1066  }
   1067 
   1068  // For each row increment "b_signcols.count" at the number of counted signs,
   1069  // and decrement at the previous number of signs. These two operations are
   1070  // split in separate calls if "clear" is not kFalse (surrounding a marktree splice).
   1071  for (int i = 0; i < row2 + 1 - row1; i++) {
   1072    int prevwidth = MIN(SIGN_SHOW_MAX, count[i] - add);
   1073    if (clear != kNone && prevwidth > 0) {
   1074      buf->b_signcols.count[prevwidth - 1]--;
   1075 #ifndef RELDEBUG
   1076      // TODO(bfredl): correct marktree splicing so that this doesn't fail
   1077      assert(buf->b_signcols.count[prevwidth - 1] >= 0);
   1078 #endif
   1079    }
   1080    int width = MIN(SIGN_SHOW_MAX, count[i]);
   1081    if (clear != kTrue && width > 0) {
   1082      buf->b_signcols.count[width - 1]++;
   1083      if (width > buf->b_signcols.max) {
   1084        buf->b_signcols.max = width;
   1085      }
   1086    }
   1087  }
   1088 
   1089  xfree(count);
   1090 }
   1091 
   1092 void decor_redraw_end(DecorState *state)
   1093 {
   1094  state->win = NULL;
   1095 }
   1096 
   1097 bool decor_redraw_eol(win_T *wp, DecorState *state, int *eol_attr, int eol_col)
   1098 {
   1099  decor_redraw_col(wp, MAXCOL, MAXCOL, false, state);
   1100  state->eol_col = eol_col;
   1101 
   1102  int const count = state->current_end;
   1103  int *const indices = state->ranges_i.items;
   1104  DecorRangeSlot *const slots = state->slots.items;
   1105 
   1106  bool has_virt_pos = false;
   1107  for (int i = 0; i < count; i++) {
   1108    DecorRange *r = &slots[indices[i]].range;
   1109    has_virt_pos |= r->start_row == state->row && decor_virt_pos(r);
   1110 
   1111    if (r->kind == kDecorKindHighlight && (r->data.sh.flags & kSHHlEol)) {
   1112      *eol_attr = hl_combine_attr(*eol_attr, r->attr_id);
   1113    }
   1114  }
   1115  return has_virt_pos;
   1116 }
   1117 
   1118 static const uint32_t lines_filter[kMTMetaCount] = {[kMTMetaLines] = kMTFilterSelect };
   1119 
   1120 /// @param apply_folds Only count virtual lines that are not in folds.
   1121 int decor_virt_lines(win_T *wp, int start_row, int end_row, int *num_below, VirtLines *lines,
   1122                     bool apply_folds)
   1123 {
   1124  buf_T *buf = wp->w_buffer;
   1125  if (!buf_meta_total(buf, kMTMetaLines)) {
   1126    // Only pay for what you use: in case virt_lines feature is not active
   1127    // in a buffer, plines do not need to access the marktree at all
   1128    return 0;
   1129  }
   1130 
   1131  MarkTreeIter itr[1] = { 0 };
   1132  if (!marktree_itr_get_filter(buf->b_marktree, MAX(start_row - 1, 0), 0, end_row, 0,
   1133                               lines_filter, itr)) {
   1134    return 0;
   1135  }
   1136 
   1137  assert(start_row >= 0);
   1138 
   1139  int virt_lines = 0;
   1140  while (true) {
   1141    MTKey mark = marktree_itr_current(itr);
   1142    DecorVirtText *vt = mt_decor_virt(mark);
   1143    if (!mt_invalid(mark) && ns_in_win(mark.ns, wp)) {
   1144      while (vt) {
   1145        if (vt->flags & kVTIsLines) {
   1146          bool above = vt->flags & kVTLinesAbove;
   1147          int mrow = mark.pos.row;
   1148          int draw_row = mrow + (above ? 0 : 1);
   1149          if (draw_row >= start_row && draw_row < end_row
   1150              && (!apply_folds || !(hasFolding(wp, mrow + 1, NULL, NULL)
   1151                                    || decor_conceal_line(wp, mrow, false)))) {
   1152            virt_lines += (int)kv_size(vt->data.virt_lines);
   1153            if (lines) {
   1154              kv_splice(*lines, vt->data.virt_lines);
   1155            }
   1156            if (num_below && !above) {
   1157              (*num_below) += (int)kv_size(vt->data.virt_lines);
   1158            }
   1159          }
   1160        }
   1161        vt = vt->next;
   1162      }
   1163    }
   1164 
   1165    if (!marktree_itr_next_filter(buf->b_marktree, itr, end_row, 0, lines_filter)) {
   1166      break;
   1167    }
   1168  }
   1169 
   1170  return virt_lines;
   1171 }
   1172 
   1173 /// This assumes maximum one entry of each kind, which will not always be the case.
   1174 ///
   1175 /// NB: assumes caller has allocated enough space in dict for all fields!
   1176 void decor_to_dict_legacy(Dict *dict, DecorInline decor, bool hl_name, Arena *arena)
   1177 {
   1178  DecorSignHighlight sh_hl = DECOR_SIGN_HIGHLIGHT_INIT;
   1179  DecorSignHighlight sh_sign = DECOR_SIGN_HIGHLIGHT_INIT;
   1180  DecorVirtText *virt_text = NULL;
   1181  DecorVirtText *virt_lines = NULL;
   1182  int32_t priority = -1;  // sentinel value which cannot actually be set
   1183 
   1184  if (decor.ext) {
   1185    DecorVirtText *vt = decor.data.ext.vt;
   1186    while (vt) {
   1187      if (vt->flags & kVTIsLines) {
   1188        virt_lines = vt;
   1189      } else {
   1190        virt_text = vt;
   1191      }
   1192      vt = vt->next;
   1193    }
   1194 
   1195    uint32_t idx = decor.data.ext.sh_idx;
   1196    while (idx != DECOR_ID_INVALID) {
   1197      DecorSignHighlight *sh = &kv_A(decor_items, idx);
   1198      if (sh->flags & (kSHIsSign)) {
   1199        sh_sign = *sh;
   1200      } else {
   1201        sh_hl = *sh;
   1202      }
   1203      idx = sh->next;
   1204    }
   1205  } else {
   1206    sh_hl = decor_sh_from_inline(decor.data.hl);
   1207  }
   1208 
   1209  if (sh_hl.hl_id) {
   1210    PUT_C(*dict, "hl_group", hl_group_name(sh_hl.hl_id, hl_name));
   1211    PUT_C(*dict, "hl_eol", BOOLEAN_OBJ(sh_hl.flags & kSHHlEol));
   1212    priority = sh_hl.priority;
   1213  }
   1214 
   1215  if (sh_hl.flags & kSHConceal) {
   1216    char buf[MAX_SCHAR_SIZE];
   1217    schar_get(buf, sh_hl.text[0]);
   1218    PUT_C(*dict, "conceal", CSTR_TO_ARENA_OBJ(arena, buf));
   1219  }
   1220 
   1221  if (sh_hl.flags & kSHConcealLines) {
   1222    PUT_C(*dict, "conceal_lines", STRING_OBJ(cstr_as_string("")));
   1223  }
   1224 
   1225  if (sh_hl.flags & kSHSpellOn) {
   1226    PUT_C(*dict, "spell", BOOLEAN_OBJ(true));
   1227  } else if (sh_hl.flags & kSHSpellOff) {
   1228    PUT_C(*dict, "spell", BOOLEAN_OBJ(false));
   1229  }
   1230 
   1231  if (sh_hl.flags & kSHUIWatched) {
   1232    PUT_C(*dict, "ui_watched", BOOLEAN_OBJ(true));
   1233  }
   1234 
   1235  if (sh_hl.url != NULL) {
   1236    PUT_C(*dict, "url", STRING_OBJ(cstr_as_string(sh_hl.url)));
   1237  }
   1238 
   1239  if (virt_text) {
   1240    if (virt_text->hl_mode) {
   1241      PUT_C(*dict, "hl_mode", CSTR_AS_OBJ(hl_mode_str[virt_text->hl_mode]));
   1242    }
   1243 
   1244    Array chunks = virt_text_to_array(virt_text->data.virt_text, hl_name, arena);
   1245    PUT_C(*dict, "virt_text", ARRAY_OBJ(chunks));
   1246    PUT_C(*dict, "virt_text_hide", BOOLEAN_OBJ(virt_text->flags & kVTHide));
   1247    PUT_C(*dict, "virt_text_repeat_linebreak", BOOLEAN_OBJ(virt_text->flags & kVTRepeatLinebreak));
   1248    if (virt_text->pos == kVPosWinCol) {
   1249      PUT_C(*dict, "virt_text_win_col", INTEGER_OBJ(virt_text->col));
   1250    }
   1251    PUT_C(*dict, "virt_text_pos", CSTR_AS_OBJ(virt_text_pos_str[virt_text->pos]));
   1252    priority = virt_text->priority;
   1253  }
   1254 
   1255  if (virt_lines) {
   1256    Array all_chunks = arena_array(arena, kv_size(virt_lines->data.virt_lines));
   1257    int virt_lines_flags = 0;
   1258    for (size_t i = 0; i < kv_size(virt_lines->data.virt_lines); i++) {
   1259      virt_lines_flags = kv_A(virt_lines->data.virt_lines, i).flags;
   1260      Array chunks = virt_text_to_array(kv_A(virt_lines->data.virt_lines, i).line, hl_name, arena);
   1261      ADD(all_chunks, ARRAY_OBJ(chunks));
   1262    }
   1263    PUT_C(*dict, "virt_lines", ARRAY_OBJ(all_chunks));
   1264    PUT_C(*dict, "virt_lines_above", BOOLEAN_OBJ(virt_lines->flags & kVTLinesAbove));
   1265    PUT_C(*dict, "virt_lines_leftcol", BOOLEAN_OBJ(virt_lines_flags & kVLLeftcol));
   1266    PUT_C(*dict, "virt_lines_overflow",
   1267          CSTR_AS_OBJ(virt_lines_flags & kVLScroll ? "scroll" : "trunc"));
   1268    priority = virt_lines->priority;
   1269  }
   1270 
   1271  if (sh_sign.flags & kSHIsSign) {
   1272    if (sh_sign.text[0]) {
   1273      char buf[SIGN_WIDTH * MAX_SCHAR_SIZE];
   1274      describe_sign_text(buf, sh_sign.text);
   1275      PUT_C(*dict, "sign_text", CSTR_TO_ARENA_OBJ(arena, buf));
   1276    }
   1277 
   1278    if (sh_sign.sign_name) {
   1279      PUT_C(*dict, "sign_name", CSTR_AS_OBJ(sh_sign.sign_name));
   1280    }
   1281 
   1282    // uncrustify:off
   1283 
   1284    struct { char *name; const int val; } hls[] = {
   1285      { "sign_hl_group"      , sh_sign.hl_id            },
   1286      { "number_hl_group"    , sh_sign.number_hl_id     },
   1287      { "line_hl_group"      , sh_sign.line_hl_id       },
   1288      { "cursorline_hl_group", sh_sign.cursorline_hl_id },
   1289      { NULL, 0 },
   1290    };
   1291 
   1292    // uncrustify:on
   1293 
   1294    for (int j = 0; hls[j].name; j++) {
   1295      if (hls[j].val) {
   1296        PUT_C(*dict, hls[j].name, hl_group_name(hls[j].val, hl_name));
   1297      }
   1298    }
   1299    priority = sh_sign.priority;
   1300  }
   1301 
   1302  if (priority != -1) {
   1303    PUT_C(*dict, "priority", INTEGER_OBJ(priority));
   1304  }
   1305 }
   1306 
   1307 uint16_t decor_type_flags(DecorInline decor)
   1308 {
   1309  if (decor.ext) {
   1310    uint16_t type_flags = kExtmarkNone;
   1311    DecorVirtText *vt = decor.data.ext.vt;
   1312    while (vt) {
   1313      type_flags |= (vt->flags & kVTIsLines) ? kExtmarkVirtLines : kExtmarkVirtText;
   1314      vt = vt->next;
   1315    }
   1316    uint32_t idx = decor.data.ext.sh_idx;
   1317    while (idx != DECOR_ID_INVALID) {
   1318      DecorSignHighlight *sh = &kv_A(decor_items, idx);
   1319      type_flags |= (sh->flags & kSHIsSign) ? kExtmarkSign : kExtmarkHighlight;
   1320      idx = sh->next;
   1321    }
   1322    return type_flags;
   1323  } else {
   1324    return (decor.data.hl.flags & kSHIsSign) ? kExtmarkSign : kExtmarkHighlight;
   1325  }
   1326 }
   1327 
   1328 Object hl_group_name(int hl_id, bool hl_name)
   1329 {
   1330  if (hl_name) {
   1331    return CSTR_AS_OBJ(syn_id2name(hl_id));
   1332  } else {
   1333    return INTEGER_OBJ(hl_id);
   1334  }
   1335 }