extmark.c (47909B)
1 #include <assert.h> 2 #include <lauxlib.h> 3 #include <stdbool.h> 4 #include <stdint.h> 5 #include <string.h> 6 7 #include "klib/kvec.h" 8 #include "nvim/api/extmark.h" 9 #include "nvim/api/keysets_defs.h" 10 #include "nvim/api/private/defs.h" 11 #include "nvim/api/private/dispatch.h" 12 #include "nvim/api/private/helpers.h" 13 #include "nvim/api/private/validate.h" 14 #include "nvim/buffer_defs.h" 15 #include "nvim/charset.h" 16 #include "nvim/decoration.h" 17 #include "nvim/decoration_defs.h" 18 #include "nvim/decoration_provider.h" 19 #include "nvim/drawscreen.h" 20 #include "nvim/extmark.h" 21 #include "nvim/globals.h" 22 #include "nvim/grid.h" 23 #include "nvim/highlight_group.h" 24 #include "nvim/map_defs.h" 25 #include "nvim/marktree.h" 26 #include "nvim/marktree_defs.h" 27 #include "nvim/mbyte.h" 28 #include "nvim/memline.h" 29 #include "nvim/memory.h" 30 #include "nvim/memory_defs.h" 31 #include "nvim/move.h" 32 #include "nvim/pos_defs.h" 33 #include "nvim/sign.h" 34 35 #include "api/extmark.c.generated.h" 36 37 void api_extmark_free_all_mem(void) 38 { 39 String name; 40 map_foreach_key(&namespace_ids, name, { 41 xfree(name.data); 42 }) 43 map_destroy(String, &namespace_ids); 44 set_destroy(uint32_t, &namespace_localscope); 45 } 46 47 /// Creates a new namespace or gets an existing one. [namespace]() 48 /// 49 /// Namespaces are used for buffer highlights and virtual text, see 50 /// |nvim_buf_set_extmark()|. 51 /// 52 /// Namespaces can be named or anonymous. If `name` matches an existing 53 /// namespace, the associated id is returned. If `name` is an empty string 54 /// a new, anonymous namespace is created. 55 /// 56 /// @param name Namespace name or empty string 57 /// @return Namespace id 58 Integer nvim_create_namespace(String name) 59 FUNC_API_SINCE(5) 60 { 61 handle_T id = map_get(String, int)(&namespace_ids, name); 62 if (id > 0) { 63 return (Integer)id; 64 } 65 id = next_namespace_id++; 66 if (name.size > 0) { 67 String name_alloc = copy_string(name, NULL); 68 map_put(String, int)(&namespace_ids, name_alloc, id); 69 } 70 return (Integer)id; 71 } 72 73 /// Gets existing, non-anonymous |namespace|s. 74 /// 75 /// @return dict that maps from names to namespace ids. 76 DictOf(Integer) nvim_get_namespaces(Arena *arena) 77 FUNC_API_SINCE(5) 78 { 79 Dict retval = arena_dict(arena, map_size(&namespace_ids)); 80 String name; 81 handle_T id; 82 83 map_foreach(&namespace_ids, name, id, { 84 PUT_C(retval, name.data, INTEGER_OBJ(id)); 85 }) 86 87 return retval; 88 } 89 90 const char *describe_ns(NS ns_id, const char *unknown) 91 { 92 String name; 93 handle_T id; 94 map_foreach(&namespace_ids, name, id, { 95 if ((NS)id == ns_id && name.size) { 96 return name.data; 97 } 98 }) 99 return unknown; 100 } 101 102 // Is the Namespace in use? 103 bool ns_initialized(uint32_t ns) 104 { 105 if (ns < 1) { 106 return false; 107 } 108 return ns < (uint32_t)next_namespace_id; 109 } 110 111 Array virt_text_to_array(VirtText vt, bool hl_name, Arena *arena) 112 { 113 Array chunks = arena_array(arena, kv_size(vt)); 114 for (size_t i = 0; i < kv_size(vt); i++) { 115 size_t j = i; 116 for (; j < kv_size(vt); j++) { 117 if (kv_A(vt, j).text != NULL) { 118 break; 119 } 120 } 121 122 Array hl_array = arena_array(arena, i < j ? j - i + 1 : 0); 123 for (; i < j; i++) { 124 int hl_id = kv_A(vt, i).hl_id; 125 if (hl_id >= 0) { 126 ADD_C(hl_array, hl_group_name(hl_id, hl_name)); 127 } 128 } 129 130 char *text = kv_A(vt, i).text; 131 int hl_id = kv_A(vt, i).hl_id; 132 Array chunk = arena_array(arena, 2); 133 ADD_C(chunk, CSTR_AS_OBJ(text)); 134 if (hl_array.size > 0) { 135 if (hl_id >= 0) { 136 ADD_C(hl_array, hl_group_name(hl_id, hl_name)); 137 } 138 ADD_C(chunk, ARRAY_OBJ(hl_array)); 139 } else if (hl_id >= 0) { 140 ADD_C(chunk, hl_group_name(hl_id, hl_name)); 141 } 142 ADD_C(chunks, ARRAY_OBJ(chunk)); 143 } 144 return chunks; 145 } 146 147 static Array extmark_to_array(MTPair extmark, bool id, bool add_dict, bool hl_name, Arena *arena) 148 { 149 MTKey start = extmark.start; 150 Array rv = arena_array(arena, 4); 151 if (id) { 152 ADD_C(rv, INTEGER_OBJ((Integer)start.id)); 153 } 154 ADD_C(rv, INTEGER_OBJ(start.pos.row)); 155 ADD_C(rv, INTEGER_OBJ(start.pos.col)); 156 157 if (add_dict) { 158 // TODO(bfredl): coding the size like this is a bit fragile. 159 // We want ArrayOf(Dict(set_extmark)) as the return type.. 160 Dict dict = arena_dict(arena, ARRAY_SIZE(set_extmark_table)); 161 162 PUT_C(dict, "ns_id", INTEGER_OBJ((Integer)start.ns)); 163 164 PUT_C(dict, "right_gravity", BOOLEAN_OBJ(mt_right(start))); 165 166 if (mt_paired(start)) { 167 PUT_C(dict, "end_row", INTEGER_OBJ(extmark.end_pos.row)); 168 PUT_C(dict, "end_col", INTEGER_OBJ(extmark.end_pos.col)); 169 PUT_C(dict, "end_right_gravity", BOOLEAN_OBJ(extmark.end_right_gravity)); 170 } 171 172 if (mt_no_undo(start)) { 173 PUT_C(dict, "undo_restore", BOOLEAN_OBJ(false)); 174 } 175 176 if (mt_invalidate(start)) { 177 PUT_C(dict, "invalidate", BOOLEAN_OBJ(true)); 178 } 179 if (mt_invalid(start)) { 180 PUT_C(dict, "invalid", BOOLEAN_OBJ(true)); 181 } 182 183 decor_to_dict_legacy(&dict, mt_decor(start), hl_name, arena); 184 185 ADD_C(rv, DICT_OBJ(dict)); 186 } 187 188 return rv; 189 } 190 191 /// Gets the position (0-indexed) of an |extmark|. 192 /// 193 /// @param buffer Buffer id, or 0 for current buffer 194 /// @param ns_id Namespace id from |nvim_create_namespace()| 195 /// @param id Extmark id 196 /// @param opts Optional parameters. Keys: 197 /// - details: Whether to include the details dict 198 /// - hl_name: Whether to include highlight group name instead of id, true if omitted 199 /// @param[out] err Error details, if any 200 /// @return 0-indexed (row, col, details?) tuple or empty list () if extmark id was absent. The 201 /// optional `details` dictionary contains the same keys as `opts` in |nvim_buf_set_extmark()|, 202 /// except for `id`, `conceal_lines` and `ephemeral`. It also contains the following keys: 203 /// 204 /// - ns_id: |namespace| id 205 /// - invalid: boolean that indicates whether the mark is hidden because the entirety of 206 /// text span range is deleted. See also the key `invalidate` in |nvim_buf_set_extmark()|. 207 Tuple(Integer, Integer, *DictAs(extmark_details)) 208 nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, Integer id, Dict(get_extmark) * opts, 209 Arena *arena, Error *err) 210 FUNC_API_SINCE(7) 211 { 212 Array rv = ARRAY_DICT_INIT; 213 214 buf_T *buf = find_buffer_by_handle(buffer, err); 215 216 if (!buf) { 217 return rv; 218 } 219 220 VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, { 221 return rv; 222 }); 223 224 bool details = opts->details; 225 226 bool hl_name = GET_BOOL_OR_TRUE(opts, get_extmark, hl_name); 227 228 MTPair extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id); 229 if (extmark.start.pos.row < 0) { 230 return rv; 231 } 232 return extmark_to_array(extmark, false, details, hl_name, arena); 233 } 234 235 /// Gets |extmarks| in "traversal order" from a |charwise| region defined by 236 /// buffer positions (inclusive, 0-indexed |api-indexing|). 237 /// 238 /// Region can be given as (row,col) tuples, or valid extmark ids (whose 239 /// positions define the bounds). 0 and -1 are understood as (0,0) and (-1,-1) 240 /// respectively, thus the following are equivalent: 241 /// 242 /// ```lua 243 /// vim.api.nvim_buf_get_extmarks(0, my_ns, 0, -1, {}) 244 /// vim.api.nvim_buf_get_extmarks(0, my_ns, {0,0}, {-1,-1}, {}) 245 /// ``` 246 /// 247 /// If `end` is less than `start`, marks are returned in reverse order. 248 /// (Useful with `limit`, to get the first marks prior to a given position.) 249 /// 250 /// Note: For a reverse range, `limit` does not actually affect the traversed 251 /// range, just how many marks are returned 252 /// 253 /// Note: when using extmark ranges (marks with a end_row/end_col position) 254 /// the `overlap` option might be useful. Otherwise only the start position 255 /// of an extmark will be considered. 256 /// 257 /// Note: legacy signs placed through the |:sign| commands are implemented 258 /// as extmarks and will show up here. Their details array will contain a 259 /// `sign_name` field. 260 /// 261 /// Example: 262 /// 263 /// ```lua 264 /// local api = vim.api 265 /// local pos = api.nvim_win_get_cursor(0) 266 /// local ns = api.nvim_create_namespace('my-plugin') 267 /// -- Create new extmark at line 1, column 1. 268 /// local m1 = api.nvim_buf_set_extmark(0, ns, 0, 0, {}) 269 /// -- Create new extmark at line 3, column 1. 270 /// local m2 = api.nvim_buf_set_extmark(0, ns, 2, 0, {}) 271 /// -- Get extmarks only from line 3. 272 /// local ms = api.nvim_buf_get_extmarks(0, ns, {2,0}, {2,0}, {}) 273 /// -- Get all marks in this buffer + namespace. 274 /// local all = api.nvim_buf_get_extmarks(0, ns, 0, -1, {}) 275 /// vim.print(ms) 276 /// ``` 277 /// 278 /// @param buffer Buffer id, or 0 for current buffer 279 /// @param ns_id Namespace id from |nvim_create_namespace()| or -1 for all namespaces 280 /// @param start Start of range: a 0-indexed (row, col) or valid extmark id 281 /// (whose position defines the bound). |api-indexing| 282 /// @param end End of range (inclusive): a 0-indexed (row, col) or valid 283 /// extmark id (whose position defines the bound). |api-indexing| 284 /// @param opts Optional parameters. Keys: 285 /// - limit: Maximum number of marks to return 286 /// - details: Whether to include the details dict 287 /// - hl_name: Whether to include highlight group name instead of id, true if omitted 288 /// - overlap: Also include marks which overlap the range, even if 289 /// their start position is less than `start` 290 /// - type: Filter marks by type: "highlight", "sign", "virt_text" and "virt_lines" 291 /// @param[out] err Error details, if any 292 /// @return List of `[extmark_id, row, col, details?]` tuples in "traversal order". For the 293 /// `details` dictionary, see |nvim_buf_get_extmark_by_id()|. 294 ArrayOf(DictAs(get_extmark_item)) nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, 295 Object end, 296 Dict(get_extmarks) *opts, Arena *arena, 297 Error *err) 298 FUNC_API_SINCE(7) 299 { 300 Array rv = ARRAY_DICT_INIT; 301 302 buf_T *buf = find_buffer_by_handle(buffer, err); 303 if (!buf) { 304 return rv; 305 } 306 307 VALIDATE_INT(ns_id == -1 || ns_initialized((uint32_t)ns_id), "ns_id", ns_id, { 308 return rv; 309 }); 310 311 bool details = opts->details; 312 bool hl_name = GET_BOOL_OR_TRUE(opts, get_extmarks, hl_name); 313 314 ExtmarkType type = kExtmarkNone; 315 if (HAS_KEY(opts, get_extmarks, type)) { 316 if (strequal(opts->type.data, "sign")) { 317 type = kExtmarkSign; 318 } else if (strequal(opts->type.data, "virt_text")) { 319 type = kExtmarkVirtText; 320 } else if (strequal(opts->type.data, "virt_lines")) { 321 type = kExtmarkVirtLines; 322 } else if (strequal(opts->type.data, "highlight")) { 323 type = kExtmarkHighlight; 324 } else { 325 VALIDATE_EXP(false, "type", "sign, virt_text, virt_lines or highlight", opts->type.data, { 326 return rv; 327 }); 328 } 329 } 330 331 Integer limit = HAS_KEY(opts, get_extmarks, limit) ? opts->limit : -1; 332 333 if (limit == 0) { 334 return rv; 335 } else if (limit < 0) { 336 limit = INT64_MAX; 337 } 338 339 int l_row; 340 colnr_T l_col; 341 if (!extmark_get_index_from_obj(buf, ns_id, start, &l_row, &l_col, err)) { 342 return rv; 343 } 344 345 int u_row; 346 colnr_T u_col; 347 if (!extmark_get_index_from_obj(buf, ns_id, end, &u_row, &u_col, err)) { 348 return rv; 349 } 350 351 size_t rv_limit = (size_t)limit; 352 bool reverse = l_row > u_row || (l_row == u_row && l_col > u_col); 353 if (reverse) { 354 limit = INT64_MAX; // limit the return value instead 355 int row = l_row; 356 l_row = u_row; 357 u_row = row; 358 colnr_T col = l_col; 359 l_col = u_col; 360 u_col = col; 361 } 362 363 // note: ns_id=-1 allowed, represented as UINT32_MAX 364 ExtmarkInfoArray marks = extmark_get(buf, (uint32_t)ns_id, l_row, l_col, u_row, 365 u_col, (int64_t)limit, type, opts->overlap); 366 367 rv = arena_array(arena, MIN(kv_size(marks), rv_limit)); 368 if (reverse) { 369 for (int i = (int)kv_size(marks) - 1; i >= 0 && kv_size(rv) < rv_limit; i--) { 370 ADD_C(rv, ARRAY_OBJ(extmark_to_array(kv_A(marks, i), true, details, hl_name, arena))); 371 } 372 } else { 373 for (size_t i = 0; i < kv_size(marks); i++) { 374 ADD_C(rv, ARRAY_OBJ(extmark_to_array(kv_A(marks, i), true, details, hl_name, arena))); 375 } 376 } 377 378 kv_destroy(marks); 379 return rv; 380 } 381 382 /// Creates or updates an |extmark|. 383 /// 384 /// By default a new extmark is created when no id is passed in, but it is also 385 /// possible to create a new mark by passing in a previously unused id or move 386 /// an existing mark by passing in its id. The caller must then keep track of 387 /// existing and unused ids itself. (Useful over RPC, to avoid waiting for the 388 /// return value.) 389 /// 390 /// Using the optional arguments, it is possible to use this to highlight 391 /// a range of text, and also to associate virtual text to the mark. 392 /// 393 /// If present, the position defined by `end_col` and `end_row` should be after 394 /// the start position in order for the extmark to cover a range. 395 /// An earlier end position is not an error, but then it behaves like an empty 396 /// range (no highlighting). 397 /// 398 /// @param buffer Buffer id, or 0 for current buffer 399 /// @param ns_id Namespace id from |nvim_create_namespace()| 400 /// @param line Line where to place the mark, 0-based. |api-indexing| 401 /// @param col Column where to place the mark, 0-based. |api-indexing| 402 /// @param opts Optional parameters. 403 /// - id : id of the extmark to edit. 404 /// - end_row : ending line of the mark, 0-based inclusive. 405 /// - end_col : ending col of the mark, 0-based exclusive. 406 /// - hl_group : highlight group used for the text range. This and below 407 /// highlight groups can be supplied either as a string or as an integer, 408 /// the latter of which can be obtained using |nvim_get_hl_id_by_name()|. 409 /// 410 /// Multiple highlight groups can be stacked by passing an array (highest 411 /// priority last). 412 /// - hl_eol : when true, for a multiline highlight covering the 413 /// EOL of a line, continue the highlight for the rest 414 /// of the screen line (just like for diff and 415 /// cursorline highlight). 416 /// - virt_text : [](virtual-text) to link to this mark. 417 /// A list of `[text, highlight]` tuples, each representing a 418 /// text chunk with specified highlight. `highlight` element 419 /// can either be a single highlight group, or an array of 420 /// multiple highlight groups that will be stacked 421 /// (highest priority last). 422 /// - virt_text_pos : position of virtual text. Possible values: 423 /// - "eol": right after eol character (default). 424 /// - "eol_right_align": display right aligned in the window 425 /// unless the virtual text is longer than 426 /// the space available. If the virtual 427 /// text is too long, it is truncated to 428 /// fit in the window after the EOL 429 /// character. If the line is wrapped, the 430 /// virtual text is shown after the end of 431 /// the line rather than the previous 432 /// screen line. 433 /// - "overlay": display over the specified column, without 434 /// shifting the underlying text. 435 /// - "right_align": display right aligned in the window. 436 /// - "inline": display at the specified column, and 437 /// shift the buffer text to the right as needed. 438 /// - virt_text_win_col : position the virtual text at a fixed 439 /// window column (starting from the first 440 /// text column of the screen line) instead 441 /// of "virt_text_pos". 442 /// - virt_text_hide : hide the virtual text when the background 443 /// text is selected or hidden because of 444 /// scrolling with 'nowrap' or 'smoothscroll'. 445 /// Currently only affects "overlay" virt_text. 446 /// - virt_text_repeat_linebreak : repeat the virtual text on 447 /// wrapped lines. 448 /// - hl_mode : control how highlights are combined with the 449 /// highlights of the text. Currently only affects 450 /// virt_text highlights, but might affect `hl_group` 451 /// in later versions. 452 /// - "replace": only show the virt_text color. This is the default. 453 /// - "combine": combine with background text color. 454 /// - "blend": blend with background text color. 455 /// Not supported for "inline" virt_text. 456 /// 457 /// - virt_lines : virtual lines to add next to this mark 458 /// This should be an array over lines, where each line in 459 /// turn is an array over `[text, highlight]` tuples. In 460 /// general, buffer and window options do not affect the 461 /// display of the text. In particular 'wrap' 462 /// and 'linebreak' options do not take effect, so 463 /// the number of extra screen lines will always match 464 /// the size of the array. However the 'tabstop' buffer 465 /// option is still used for hard tabs. By default lines are 466 /// placed below the buffer line containing the mark. 467 /// 468 /// - virt_lines_above: place virtual lines above instead. 469 /// - virt_lines_leftcol: Place virtual lines in the leftmost 470 /// column of the window, bypassing 471 /// sign and number columns. 472 /// - virt_lines_overflow: controls how to handle virtual lines wider 473 /// than the window. Currently takes the one of the following values: 474 /// - "trunc": truncate virtual lines on the right (default). 475 /// - "scroll": virtual lines can scroll horizontally with 'nowrap', 476 /// otherwise the same as "trunc". 477 /// - ephemeral : for use with |nvim_set_decoration_provider()| 478 /// callbacks. The mark will only be used for the current 479 /// redraw cycle, and not be permanently stored in the buffer. 480 /// - right_gravity : boolean that indicates the direction 481 /// the extmark will be shifted in when new text is inserted 482 /// (true for right, false for left). Defaults to true. 483 /// - end_right_gravity : boolean that indicates the direction 484 /// the extmark end position (if it exists) will be shifted 485 /// in when new text is inserted (true for right, false 486 /// for left). Defaults to false. 487 /// - undo_restore : Restore the exact position of the mark 488 /// if text around the mark was deleted and then restored by undo. 489 /// Defaults to true. 490 /// - invalidate : boolean that indicates whether to hide the 491 /// extmark if the entirety of its range is deleted. For 492 /// hidden marks, an "invalid" key is added to the "details" 493 /// array of |nvim_buf_get_extmarks()| and family. If 494 /// "undo_restore" is false, the extmark is deleted instead. 495 /// - priority: a priority value for the highlight group, sign 496 /// attribute or virtual text. For virtual text, item with 497 /// highest priority is drawn last. For example treesitter 498 /// highlighting uses a value of 100. 499 /// - strict: boolean that indicates extmark should not be placed 500 /// if the line or column value is past the end of the 501 /// buffer or end of the line respectively. Defaults to true. 502 /// - sign_text: string of length 1-2 used to display in the 503 /// sign column. 504 /// - sign_hl_group: highlight group used for the sign column text. 505 /// - number_hl_group: highlight group used for the number column. 506 /// - line_hl_group: highlight group used for the whole line. 507 /// - cursorline_hl_group: highlight group used for the sign 508 /// column text when the cursor is on the same line as the 509 /// mark and 'cursorline' is enabled. 510 /// - conceal: string which should be either empty or a single 511 /// character. Enable concealing similar to |:syn-conceal|. 512 /// When a character is supplied it is used as |:syn-cchar|. 513 /// "hl_group" is used as highlight for the cchar if provided, 514 /// otherwise it defaults to |hl-Conceal|. 515 /// - conceal_lines: string which should be empty. When 516 /// provided, lines in the range are not drawn at all 517 /// (according to 'conceallevel'); the next unconcealed line 518 /// is drawn instead. 519 /// - spell: boolean indicating that spell checking should be 520 /// performed within this extmark 521 /// - ui_watched: boolean that indicates the mark should be drawn 522 /// by a UI. When set, the UI will receive win_extmark events. 523 /// Note: the mark is positioned by virt_text attributes. Can be 524 /// used together with virt_text. 525 /// - url: A URL to associate with this extmark. In the TUI, the OSC 8 control 526 /// sequence is used to generate a clickable hyperlink to this URL. 527 /// 528 /// @param[out] err Error details, if any 529 /// @return Id of the created/updated extmark 530 Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer col, 531 Dict(set_extmark) *opts, Error *err) 532 FUNC_API_SINCE(7) 533 { 534 DecorHighlightInline hl = DECOR_HIGHLIGHT_INLINE_INIT; 535 // TODO(bfredl): in principle signs with max one (1) hl group and max 4 bytes of text. 536 // should be a candidate for inlining as well. 537 DecorSignHighlight sign = DECOR_SIGN_HIGHLIGHT_INIT; 538 DecorVirtText virt_text = DECOR_VIRT_TEXT_INIT; 539 DecorVirtText virt_lines = DECOR_VIRT_LINES_INIT; 540 char *url = NULL; 541 bool has_hl = false; 542 bool has_hl_multiple = false; 543 544 buf_T *buf = find_buffer_by_handle(buffer, err); 545 if (!buf) { 546 goto error; 547 } 548 549 VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, { 550 goto error; 551 }); 552 553 uint32_t id = 0; 554 if (HAS_KEY(opts, set_extmark, id)) { 555 VALIDATE_EXP((opts->id > 0), "id", "positive Integer", NULL, { 556 goto error; 557 }); 558 559 id = (uint32_t)opts->id; 560 } 561 562 int line2 = -1; 563 bool did_end_line = false; 564 565 // For backward compatibility we support "end_line" as an alias for "end_row" 566 if (HAS_KEY(opts, set_extmark, end_line)) { 567 VALIDATE(!HAS_KEY(opts, set_extmark, end_row), 568 "%s", "cannot use both 'end_row' and 'end_line'", { 569 goto error; 570 }); 571 572 opts->end_row = opts->end_line; 573 did_end_line = true; 574 } 575 576 bool strict = GET_BOOL_OR_TRUE(opts, set_extmark, strict); 577 578 if (HAS_KEY(opts, set_extmark, end_row) || did_end_line) { 579 Integer val = opts->end_row; 580 VALIDATE_RANGE((val >= 0 && !(val > buf->b_ml.ml_line_count && strict)), "end_row", { 581 goto error; 582 }); 583 line2 = (int)val; 584 } 585 586 colnr_T col2 = -1; 587 if (HAS_KEY(opts, set_extmark, end_col)) { 588 Integer val = opts->end_col; 589 VALIDATE_RANGE((val >= 0 && val <= MAXCOL), "end_col", { 590 goto error; 591 }); 592 col2 = (int)val; 593 } 594 595 if (HAS_KEY(opts, set_extmark, hl_group)) { 596 if (opts->hl_group.type == kObjectTypeArray) { 597 Array arr = opts->hl_group.data.array; 598 if (arr.size >= 1) { 599 hl.hl_id = object_to_hl_id(arr.items[0], "hl_group item", err); 600 if (ERROR_SET(err)) { 601 goto error; 602 } 603 } 604 for (size_t i = 1; i < arr.size; i++) { 605 int hl_id = object_to_hl_id(arr.items[i], "hl_group item", err); 606 if (ERROR_SET(err)) { 607 goto error; 608 } 609 if (hl_id) { 610 has_hl_multiple = true; 611 } 612 } 613 } else { 614 hl.hl_id = object_to_hl_id(opts->hl_group, "hl_group", err); 615 if (ERROR_SET(err)) { 616 goto error; 617 } 618 } 619 has_hl = hl.hl_id > 0; 620 } 621 622 sign.hl_id = (int)opts->sign_hl_group; 623 sign.cursorline_hl_id = (int)opts->cursorline_hl_group; 624 sign.number_hl_id = (int)opts->number_hl_group; 625 sign.line_hl_id = (int)opts->line_hl_group; 626 627 if (sign.hl_id || sign.cursorline_hl_id || sign.number_hl_id || sign.line_hl_id) { 628 sign.flags |= kSHIsSign; 629 } 630 631 if (HAS_KEY(opts, set_extmark, conceal)) { 632 hl.flags |= kSHConceal; 633 has_hl = true; 634 if (opts->conceal.size > 0) { 635 int ch; 636 hl.conceal_char = utfc_ptr2schar(opts->conceal.data, &ch); 637 VALIDATE(hl.conceal_char && vim_isprintc(ch), "%s", "conceal char has to be printable", { 638 goto error; 639 }); 640 } 641 } 642 643 if (HAS_KEY(opts, set_extmark, conceal_lines)) { 644 hl.flags |= kSHConcealLines; 645 has_hl = true; 646 if (opts->conceal_lines.size > 0) { 647 VALIDATE(*opts->conceal_lines.data == NUL, "%s", "conceal_lines has to be an empty string", { 648 goto error; 649 }); 650 } 651 } 652 653 if (HAS_KEY(opts, set_extmark, virt_text)) { 654 virt_text.data.virt_text = parse_virt_text(opts->virt_text, err, &virt_text.width); 655 if (ERROR_SET(err)) { 656 goto error; 657 } 658 } 659 660 if (HAS_KEY(opts, set_extmark, virt_text_pos)) { 661 String str = opts->virt_text_pos; 662 if (strequal("eol", str.data)) { 663 virt_text.pos = kVPosEndOfLine; 664 } else if (strequal("overlay", str.data)) { 665 virt_text.pos = kVPosOverlay; 666 } else if (strequal("right_align", str.data)) { 667 virt_text.pos = kVPosRightAlign; 668 } else if (strequal("eol_right_align", str.data)) { 669 virt_text.pos = kVPosEndOfLineRightAlign; 670 } else if (strequal("inline", str.data)) { 671 virt_text.pos = kVPosInline; 672 } else { 673 VALIDATE_S(false, "virt_text_pos", str.data, { 674 goto error; 675 }); 676 } 677 } 678 679 if (HAS_KEY(opts, set_extmark, virt_text_win_col)) { 680 virt_text.col = (int)opts->virt_text_win_col; 681 virt_text.pos = kVPosWinCol; 682 } 683 684 hl.flags |= opts->hl_eol ? kSHHlEol : 0; 685 virt_text.flags |= ((opts->virt_text_hide ? kVTHide : 0) 686 | (opts->virt_text_repeat_linebreak ? kVTRepeatLinebreak : 0)); 687 688 if (HAS_KEY(opts, set_extmark, hl_mode)) { 689 String str = opts->hl_mode; 690 if (strequal("replace", str.data)) { 691 virt_text.hl_mode = kHlModeReplace; 692 } else if (strequal("combine", str.data)) { 693 virt_text.hl_mode = kHlModeCombine; 694 } else if (strequal("blend", str.data)) { 695 if (virt_text.pos == kVPosInline) { 696 VALIDATE(false, "%s", "cannot use 'blend' hl_mode with inline virtual text", { 697 goto error; 698 }); 699 } 700 virt_text.hl_mode = kHlModeBlend; 701 } else { 702 VALIDATE_S(false, "hl_mode", str.data, { 703 goto error; 704 }); 705 } 706 } 707 708 int virt_lines_flags = opts->virt_lines_leftcol ? kVLLeftcol : 0; 709 if (HAS_KEY(opts, set_extmark, virt_lines_overflow)) { 710 String str = opts->virt_lines_overflow; 711 if (strequal("scroll", str.data)) { 712 virt_lines_flags |= kVLScroll; 713 } else if (!strequal("trunc", str.data)) { 714 VALIDATE_S(false, "virt_lines_overflow", str.data, { 715 goto error; 716 }); 717 } 718 } 719 720 if (HAS_KEY(opts, set_extmark, virt_lines)) { 721 Array a = opts->virt_lines; 722 for (size_t j = 0; j < a.size; j++) { 723 VALIDATE_T("virt_text_line", kObjectTypeArray, a.items[j].type, { 724 goto error; 725 }); 726 int dummig; 727 VirtText jtem = parse_virt_text(a.items[j].data.array, err, &dummig); 728 kv_push(virt_lines.data.virt_lines, ((struct virt_line){ jtem, virt_lines_flags })); 729 if (ERROR_SET(err)) { 730 goto error; 731 } 732 } 733 } 734 735 virt_lines.flags |= opts->virt_lines_above ? kVTLinesAbove : 0; 736 737 if (HAS_KEY(opts, set_extmark, priority)) { 738 VALIDATE_RANGE((opts->priority >= 0 && opts->priority <= UINT16_MAX), "priority", { 739 goto error; 740 }); 741 hl.priority = (DecorPriority)opts->priority; 742 sign.priority = (DecorPriority)opts->priority; 743 virt_text.priority = (DecorPriority)opts->priority; 744 virt_lines.priority = (DecorPriority)opts->priority; 745 } 746 747 if (HAS_KEY(opts, set_extmark, sign_text)) { 748 sign.text[0] = 0; 749 VALIDATE_S(init_sign_text(NULL, sign.text, opts->sign_text.data), "sign_text", "", { 750 goto error; 751 }); 752 sign.flags |= kSHIsSign; 753 } 754 755 bool right_gravity = GET_BOOL_OR_TRUE(opts, set_extmark, right_gravity); 756 757 // Only error out if they try to set end_right_gravity without 758 // setting end_col or end_row 759 VALIDATE(!(line2 == -1 && col2 == -1 && HAS_KEY(opts, set_extmark, end_right_gravity)), 760 "%s", "cannot set end_right_gravity without end_row or end_col", { 761 goto error; 762 }); 763 764 colnr_T len = 0; 765 766 if (HAS_KEY(opts, set_extmark, spell)) { 767 hl.flags |= (opts->spell) ? kSHSpellOn : kSHSpellOff; 768 has_hl = true; 769 } 770 771 if (HAS_KEY(opts, set_extmark, url)) { 772 url = string_to_cstr(opts->url); 773 has_hl = true; 774 } 775 776 if (opts->ui_watched) { 777 hl.flags |= kSHUIWatched; 778 if (virt_text.pos == kVPosOverlay) { 779 // TODO(bfredl): in a revised interface this should be the default. 780 hl.flags |= kSHUIWatchedOverlay; 781 } 782 has_hl = true; 783 } 784 785 VALIDATE_RANGE((line >= 0), "line", { 786 goto error; 787 }); 788 789 if (line > buf->b_ml.ml_line_count) { 790 VALIDATE_RANGE(!strict, "line", { 791 goto error; 792 }); 793 line = buf->b_ml.ml_line_count; 794 } else if (line < buf->b_ml.ml_line_count) { 795 len = opts->ephemeral ? MAXCOL : ml_get_buf_len(buf, (linenr_T)line + 1); 796 } 797 798 if (col == -1) { 799 col = len; 800 } else if (col > len) { 801 VALIDATE_RANGE(!strict, "col", { 802 goto error; 803 }); 804 col = len; 805 } else if (col < -1) { 806 VALIDATE_RANGE(false, "col", { 807 goto error; 808 }); 809 } 810 811 if (col2 >= 0) { 812 if (line2 >= 0 && line2 < buf->b_ml.ml_line_count) { 813 len = opts->ephemeral ? MAXCOL : ml_get_buf_len(buf, (linenr_T)line2 + 1); 814 } else if (line2 == buf->b_ml.ml_line_count) { 815 // We are trying to add an extmark past final newline 816 len = 0; 817 } else { 818 // reuse len from before 819 line2 = (int)line; 820 } 821 if (col2 > len) { 822 VALIDATE_RANGE(!strict, "end_col", { 823 goto error; 824 }); 825 col2 = len; 826 } 827 } else if (line2 >= 0) { 828 col2 = 0; 829 } 830 831 if (opts->ephemeral && decor_state.win && decor_state.win->w_buffer == buf) { 832 int r = (int)line; 833 int c = (int)col; 834 if (line2 == -1) { 835 line2 = r; 836 col2 = c; 837 } 838 839 DecorPriority subpriority = 0; 840 if (HAS_KEY(opts, set_extmark, _subpriority)) { 841 VALIDATE_RANGE((opts->_subpriority >= 0 && opts->_subpriority <= UINT16_MAX), 842 "_subpriority", { 843 goto error; 844 }); 845 subpriority = (DecorPriority)opts->_subpriority; 846 } 847 848 if (kv_size(virt_text.data.virt_text)) { 849 decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_text, NULL), true); 850 } 851 if (kv_size(virt_lines.data.virt_lines)) { 852 decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_lines, NULL), true); 853 } 854 if (has_hl) { 855 DecorSignHighlight sh = decor_sh_from_inline(hl); 856 sh.url = url; 857 decor_range_add_sh(&decor_state, r, c, line2, col2, &sh, true, (uint32_t)ns_id, id, 858 subpriority); 859 } 860 } else { 861 if (opts->ephemeral) { 862 api_set_error(err, kErrorTypeException, 863 "cannot set emphemeral mark outside of a decoration provider"); 864 goto error; 865 } 866 867 uint16_t decor_flags = 0; 868 869 DecorVirtText *decor_alloc = NULL; 870 if (kv_size(virt_text.data.virt_text)) { 871 decor_alloc = decor_put_vt(virt_text, decor_alloc); 872 if (virt_text.pos == kVPosInline) { 873 decor_flags |= MT_FLAG_DECOR_VIRT_TEXT_INLINE; 874 } 875 } 876 if (kv_size(virt_lines.data.virt_lines)) { 877 decor_alloc = decor_put_vt(virt_lines, decor_alloc); 878 decor_flags |= MT_FLAG_DECOR_VIRT_LINES; 879 } 880 881 uint32_t decor_indexed = DECOR_ID_INVALID; 882 883 if (sign.flags & kSHIsSign) { 884 sign.next = decor_indexed; 885 decor_indexed = decor_put_sh(sign); 886 if (sign.text[0]) { 887 decor_flags |= MT_FLAG_DECOR_SIGNTEXT; 888 } 889 if (sign.number_hl_id || sign.line_hl_id || sign.cursorline_hl_id) { 890 decor_flags |= MT_FLAG_DECOR_SIGNHL; 891 } 892 } 893 894 if (has_hl_multiple) { 895 Array arr = opts->hl_group.data.array; 896 for (size_t i = arr.size - 1; i > 0; i--) { // skip hl_group[0], handled as hl.hl_id below 897 int hl_id = object_to_hl_id(arr.items[i], "hl_group item", err); 898 if (hl_id > 0) { 899 DecorSignHighlight sh = DECOR_SIGN_HIGHLIGHT_INIT; 900 sh.hl_id = hl_id; 901 sh.flags = opts->hl_eol ? kSHHlEol : 0; 902 sh.next = decor_indexed; 903 decor_indexed = decor_put_sh(sh); 904 decor_flags |= MT_FLAG_DECOR_HL; 905 } 906 } 907 } 908 909 if (hl.flags & kSHConcealLines) { 910 decor_flags |= MT_FLAG_DECOR_CONCEAL_LINES; 911 } 912 913 DecorInline decor = DECOR_INLINE_INIT; 914 if (decor_alloc || decor_indexed != DECOR_ID_INVALID || url != NULL 915 || schar_high(hl.conceal_char)) { 916 if (has_hl) { 917 DecorSignHighlight sh = decor_sh_from_inline(hl); 918 sh.url = url; 919 sh.next = decor_indexed; 920 decor_indexed = decor_put_sh(sh); 921 } 922 decor.ext = true; 923 decor.data.ext = (DecorExt){ .sh_idx = decor_indexed, .vt = decor_alloc }; 924 } else { 925 decor.data.hl = hl; 926 } 927 928 if (has_hl) { 929 decor_flags |= MT_FLAG_DECOR_HL; 930 } 931 932 extmark_set(buf, (uint32_t)ns_id, &id, (int)line, (colnr_T)col, line2, col2, 933 decor, decor_flags, right_gravity, opts->end_right_gravity, 934 !GET_BOOL_OR_TRUE(opts, set_extmark, undo_restore), 935 opts->invalidate, err); 936 if (ERROR_SET(err)) { 937 decor_free(decor); 938 return 0; 939 } 940 } 941 942 return (Integer)id; 943 944 error: 945 clear_virttext(&virt_text.data.virt_text); 946 clear_virtlines(&virt_lines.data.virt_lines); 947 if (url != NULL) { 948 xfree(url); 949 } 950 951 return 0; 952 } 953 954 /// Removes an |extmark|. 955 /// 956 /// @param buffer Buffer id, or 0 for current buffer 957 /// @param ns_id Namespace id from |nvim_create_namespace()| 958 /// @param id Extmark id 959 /// @param[out] err Error details, if any 960 /// @return true if the extmark was found, else false 961 Boolean nvim_buf_del_extmark(Buffer buffer, Integer ns_id, Integer id, Error *err) 962 FUNC_API_SINCE(7) 963 { 964 buf_T *buf = find_buffer_by_handle(buffer, err); 965 966 if (!buf) { 967 return false; 968 } 969 VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, { 970 return false; 971 }); 972 973 return extmark_del_id(buf, (uint32_t)ns_id, (uint32_t)id); 974 } 975 976 /// Clears |namespace|d objects (highlights, |extmarks|, virtual text) from 977 /// a region. 978 /// 979 /// Lines are 0-indexed. |api-indexing| To clear the namespace in the entire 980 /// buffer, specify line_start=0 and line_end=-1. 981 /// 982 /// @param buffer Buffer id, or 0 for current buffer 983 /// @param ns_id Namespace to clear, or -1 to clear all namespaces. 984 /// @param line_start Start of range of lines to clear 985 /// @param line_end End of range of lines to clear (exclusive) or -1 to clear 986 /// to end of buffer. 987 /// @param[out] err Error details, if any 988 void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start, Integer line_end, 989 Error *err) 990 FUNC_API_SINCE(5) 991 { 992 buf_T *buf = find_buffer_by_handle(buffer, err); 993 if (!buf) { 994 return; 995 } 996 997 VALIDATE_RANGE((line_start >= 0 && line_start < MAXLNUM), "line number", { 998 return; 999 }); 1000 1001 if (line_end < 0 || line_end > MAXLNUM) { 1002 line_end = MAXLNUM; 1003 } 1004 extmark_clear(buf, (ns_id < 0 ? 0 : (uint32_t)ns_id), 1005 (int)line_start, 0, 1006 (int)line_end - 1, MAXCOL); 1007 } 1008 1009 /// Set or change decoration provider for a |namespace| 1010 /// 1011 /// This is a very general purpose interface for having Lua callbacks 1012 /// being triggered during the redraw code. 1013 /// 1014 /// The expected usage is to set |extmarks| for the currently 1015 /// redrawn buffer. |nvim_buf_set_extmark()| can be called to add marks 1016 /// on a per-window or per-lines basis. Use the `ephemeral` key to only 1017 /// use the mark for the current screen redraw (the callback will be called 1018 /// again for the next redraw). 1019 /// 1020 /// Note: this function should not be called often. Rather, the callbacks 1021 /// themselves can be used to throttle unneeded callbacks. the `on_start` 1022 /// callback can return `false` to disable the provider until the next redraw. 1023 /// Similarly, return `false` in `on_win` will skip the `on_line` and `on_range` calls 1024 /// for that window (but any extmarks set in `on_win` will still be used). 1025 /// A plugin managing multiple sources of decoration should ideally only set 1026 /// one provider, and merge the sources internally. You can use multiple `ns_id` 1027 /// for the extmarks set/modified inside the callback anyway. 1028 /// 1029 /// Note: doing anything other than setting extmarks is considered experimental. 1030 /// Doing things like changing options are not explicitly forbidden, but is 1031 /// likely to have unexpected consequences (such as 100% CPU consumption). 1032 /// Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious 1033 /// for the moment. 1034 /// 1035 /// Note: It is not allowed to remove or update extmarks in `on_line` or `on_range` callbacks. 1036 /// 1037 /// @param ns_id Namespace id from |nvim_create_namespace()| 1038 /// @param opts Table of callbacks: 1039 /// - on_start: called first on each screen redraw 1040 /// ``` 1041 /// ["start", tick] 1042 /// ``` 1043 /// - on_buf: called for each buffer being redrawn (once per edit, 1044 /// before window callbacks) 1045 /// ``` 1046 /// ["buf", bufnr, tick] 1047 /// ``` 1048 /// - on_win: called when starting to redraw a specific window. 1049 /// ``` 1050 /// ["win", winid, bufnr, toprow, botrow] 1051 /// ``` 1052 /// - on_line: (deprecated, use on_range instead) 1053 /// ``` 1054 /// ["line", winid, bufnr, row] 1055 /// ``` 1056 /// - on_range: called for each buffer range being redrawn. 1057 /// Range is end-exclusive and may span multiple lines. Range 1058 /// bounds point to the first byte of a character. An end position 1059 /// of the form (lnum, 0), including (number of lines, 0), is valid 1060 /// and indicates that EOL of the preceding line is included. 1061 /// ``` 1062 /// ["range", winid, bufnr, begin_row, begin_col, end_row, end_col] 1063 /// ``` 1064 /// 1065 /// In addition to returning a boolean, it is also allowed to 1066 /// return a `skip_row, skip_col` pair of integers. This implies 1067 /// that this function does not need to be called until a range 1068 /// which continues beyond the skipped position. A single integer 1069 /// return value `skip_row` is short for `skip_row, 0` 1070 /// 1071 /// - on_end: called at the end of a redraw cycle 1072 /// ``` 1073 /// ["end", tick] 1074 /// ``` 1075 void nvim_set_decoration_provider(Integer ns_id, Dict(set_decoration_provider) *opts, Error *err) 1076 FUNC_API_SINCE(7) FUNC_API_LUA_ONLY 1077 { 1078 DecorProvider *p = get_decor_provider((NS)ns_id, true); 1079 assert(p != NULL); 1080 decor_provider_clear(p); 1081 1082 // regardless of what happens, it seems good idea to redraw 1083 redraw_all_later(UPD_NOT_VALID); // TODO(bfredl): too soon? 1084 1085 struct { 1086 const char *name; 1087 LuaRef *source; 1088 LuaRef *dest; 1089 } cbs[] = { 1090 { "on_start", &opts->on_start, &p->redraw_start }, 1091 { "on_buf", &opts->on_buf, &p->redraw_buf }, 1092 { "on_win", &opts->on_win, &p->redraw_win }, 1093 { "on_line", &opts->on_line, &p->redraw_line }, 1094 { "on_range", &opts->on_range, &p->redraw_range }, 1095 { "on_end", &opts->on_end, &p->redraw_end }, 1096 { "_on_hl_def", &opts->_on_hl_def, &p->hl_def }, 1097 { "_on_spell_nav", &opts->_on_spell_nav, &p->spell_nav }, 1098 { "_on_conceal_line", &opts->_on_conceal_line, &p->conceal_line }, 1099 { NULL, NULL, NULL }, 1100 }; 1101 1102 for (size_t i = 0; cbs[i].source && cbs[i].dest && cbs[i].name; i++) { 1103 LuaRef *v = cbs[i].source; 1104 if (*v <= 0) { 1105 continue; 1106 } 1107 1108 *(cbs[i].dest) = *v; 1109 *v = LUA_NOREF; 1110 } 1111 1112 p->state = kDecorProviderActive; 1113 p->hl_valid++; 1114 p->hl_cached = false; 1115 } 1116 1117 /// Gets the line and column of an |extmark|. 1118 /// 1119 /// Extmarks may be queried by position, name or even special names 1120 /// in the future such as "cursor". 1121 /// 1122 /// @param[out] lnum extmark line 1123 /// @param[out] colnr extmark column 1124 /// 1125 /// @return true if the extmark was found, else false 1126 static bool extmark_get_index_from_obj(buf_T *buf, Integer ns_id, Object obj, int *row, 1127 colnr_T *col, Error *err) 1128 { 1129 // Check if it is mark id 1130 if (obj.type == kObjectTypeInteger) { 1131 Integer id = obj.data.integer; 1132 if (id == 0) { 1133 *row = 0; 1134 *col = 0; 1135 return true; 1136 } else if (id == -1) { 1137 *row = MAXLNUM; 1138 *col = MAXCOL; 1139 return true; 1140 } else if (id < 0) { 1141 VALIDATE_INT(false, "mark id", id, { 1142 return false; 1143 }); 1144 } 1145 1146 MTPair extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id); 1147 1148 VALIDATE_INT((extmark.start.pos.row >= 0), "mark id (not found)", id, { 1149 return false; 1150 }); 1151 *row = extmark.start.pos.row; 1152 *col = extmark.start.pos.col; 1153 return true; 1154 1155 // Check if it is a position 1156 } else if (obj.type == kObjectTypeArray) { 1157 Array pos = obj.data.array; 1158 VALIDATE_EXP((pos.size == 2 1159 && pos.items[0].type == kObjectTypeInteger 1160 && pos.items[1].type == kObjectTypeInteger), 1161 "mark position", "2 Integer items", NULL, { 1162 return false; 1163 }); 1164 1165 Integer pos_row = pos.items[0].data.integer; 1166 Integer pos_col = pos.items[1].data.integer; 1167 *row = (int)(pos_row >= 0 ? pos_row : MAXLNUM); 1168 *col = (colnr_T)(pos_col >= 0 ? pos_col : MAXCOL); 1169 return true; 1170 } else { 1171 VALIDATE_EXP(false, "mark position", "mark id Integer or 2-item Array", NULL, { 1172 return false; 1173 }); 1174 } 1175 } 1176 1177 VirtText parse_virt_text(Array chunks, Error *err, int *width) 1178 { 1179 VirtText virt_text = KV_INITIAL_VALUE; 1180 int w = 0; 1181 for (size_t i = 0; i < chunks.size; i++) { 1182 VALIDATE_T("chunk", kObjectTypeArray, chunks.items[i].type, { 1183 goto free_exit; 1184 }); 1185 Array chunk = chunks.items[i].data.array; 1186 VALIDATE((chunk.size > 0 && chunk.size <= 2 && chunk.items[0].type == kObjectTypeString), 1187 "%s", "Invalid chunk: expected Array with 1 or 2 Strings", { 1188 goto free_exit; 1189 }); 1190 1191 String str = chunk.items[0].data.string; 1192 1193 int hl_id = -1; 1194 if (chunk.size == 2) { 1195 Object hl = chunk.items[1]; 1196 if (hl.type == kObjectTypeArray) { 1197 Array arr = hl.data.array; 1198 for (size_t j = 0; j < arr.size; j++) { 1199 hl_id = object_to_hl_id(arr.items[j], "virt_text highlight", err); 1200 if (ERROR_SET(err)) { 1201 goto free_exit; 1202 } 1203 if (j < arr.size - 1) { 1204 kv_push(virt_text, ((VirtTextChunk){ .text = NULL, .hl_id = hl_id })); 1205 } 1206 } 1207 } else { 1208 hl_id = object_to_hl_id(hl, "virt_text highlight", err); 1209 if (ERROR_SET(err)) { 1210 goto free_exit; 1211 } 1212 } 1213 } 1214 1215 char *text = transstr(str.size > 0 ? str.data : "", false); // allocates 1216 w += (int)mb_string2cells(text); 1217 1218 kv_push(virt_text, ((VirtTextChunk){ .text = text, .hl_id = hl_id })); 1219 } 1220 1221 if (width != NULL) { 1222 *width = w; 1223 } 1224 return virt_text; 1225 1226 free_exit: 1227 clear_virttext(&virt_text); 1228 return virt_text; 1229 } 1230 1231 /// @nodoc 1232 String nvim__buf_debug_extmarks(Buffer buffer, Boolean keys, Boolean dot, Error *err) 1233 FUNC_API_SINCE(7) FUNC_API_RET_ALLOC 1234 { 1235 buf_T *buf = find_buffer_by_handle(buffer, err); 1236 if (!buf) { 1237 return NULL_STRING; 1238 } 1239 1240 return mt_inspect(buf->b_marktree, keys, dot); 1241 } 1242 1243 /// EXPERIMENTAL: this API will change in the future. 1244 /// 1245 /// Set some properties for namespace 1246 /// 1247 /// @param ns_id Namespace 1248 /// @param opts Optional parameters to set: 1249 /// - wins: a list of windows to be scoped in 1250 /// 1251 void nvim__ns_set(Integer ns_id, Dict(ns_opts) *opts, Error *err) 1252 { 1253 VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, { 1254 return; 1255 }); 1256 1257 bool set_scoped = true; 1258 1259 if (HAS_KEY(opts, ns_opts, wins)) { 1260 if (opts->wins.size == 0) { 1261 set_scoped = false; 1262 } 1263 1264 Set(ptr_t) windows = SET_INIT; 1265 for (size_t i = 0; i < opts->wins.size; i++) { 1266 Integer win = opts->wins.items[i].data.integer; 1267 1268 win_T *wp = find_window_by_handle((Window)win, err); 1269 if (!wp) { 1270 return; 1271 } 1272 1273 set_put(ptr_t, &windows, wp); 1274 } 1275 1276 FOR_ALL_TAB_WINDOWS(tp, wp) { 1277 if (set_has(ptr_t, &windows, wp) && !set_has(uint32_t, &wp->w_ns_set, (uint32_t)ns_id)) { 1278 set_put(uint32_t, &wp->w_ns_set, (uint32_t)ns_id); 1279 1280 if (map_has(uint32_t, wp->w_buffer->b_extmark_ns, (uint32_t)ns_id)) { 1281 changed_window_setting(wp); 1282 } 1283 } 1284 1285 if (set_has(uint32_t, &wp->w_ns_set, (uint32_t)ns_id) && !set_has(ptr_t, &windows, wp)) { 1286 set_del(uint32_t, &wp->w_ns_set, (uint32_t)ns_id); 1287 1288 if (map_has(uint32_t, wp->w_buffer->b_extmark_ns, (uint32_t)ns_id)) { 1289 changed_window_setting(wp); 1290 } 1291 } 1292 } 1293 1294 set_destroy(ptr_t, &windows); 1295 } 1296 1297 if (set_scoped && !set_has(uint32_t, &namespace_localscope, (uint32_t)ns_id)) { 1298 set_put(uint32_t, &namespace_localscope, (uint32_t)ns_id); 1299 1300 // When a namespace becomes scoped, any window which contains 1301 // elements associated with namespace needs to be redrawn 1302 FOR_ALL_TAB_WINDOWS(tp, wp) { 1303 if (map_has(uint32_t, wp->w_buffer->b_extmark_ns, (uint32_t)ns_id)) { 1304 changed_window_setting(wp); 1305 } 1306 } 1307 } else if (!set_scoped && set_has(uint32_t, &namespace_localscope, (uint32_t)ns_id)) { 1308 set_del(uint32_t, &namespace_localscope, (uint32_t)ns_id); 1309 1310 // When a namespace becomes unscoped, any window which does not 1311 // contain elements associated with namespace needs to be redrawn 1312 FOR_ALL_TAB_WINDOWS(tp, wp) { 1313 if (map_has(uint32_t, wp->w_buffer->b_extmark_ns, (uint32_t)ns_id)) { 1314 changed_window_setting(wp); 1315 } 1316 } 1317 } 1318 } 1319 1320 /// EXPERIMENTAL: this API will change in the future. 1321 /// 1322 /// Get the properties for namespace 1323 /// 1324 /// @param ns_id Namespace 1325 /// @return Map defining the namespace properties, see |nvim__ns_set()| 1326 Dict(ns_opts) nvim__ns_get(Integer ns_id, Arena *arena, Error *err) 1327 { 1328 Dict(ns_opts) opts = KEYDICT_INIT; 1329 1330 Array windows = ARRAY_DICT_INIT; 1331 1332 PUT_KEY(opts, ns_opts, wins, windows); 1333 1334 VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, { 1335 return opts; 1336 }); 1337 1338 if (!set_has(uint32_t, &namespace_localscope, (uint32_t)ns_id)) { 1339 return opts; 1340 } 1341 1342 size_t count = 0; 1343 FOR_ALL_TAB_WINDOWS(tp, wp) { 1344 if (set_has(uint32_t, &wp->w_ns_set, (uint32_t)ns_id)) { 1345 count++; 1346 } 1347 } 1348 1349 windows = arena_array(arena, count); 1350 1351 FOR_ALL_TAB_WINDOWS(tp, wp) { 1352 if (set_has(uint32_t, &wp->w_ns_set, (uint32_t)ns_id)) { 1353 ADD(windows, INTEGER_OBJ(wp->handle)); 1354 } 1355 } 1356 1357 PUT_KEY(opts, ns_opts, wins, windows); 1358 1359 return opts; 1360 }