1// This is an open source non-commercial project. Dear PVS-Studio, please check
2// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3
4#include <stdbool.h>
5
6#include "nvim/mouse.h"
7#include "nvim/vim.h"
8#include "nvim/ascii.h"
9#include "nvim/window.h"
10#include "nvim/state.h"
11#include "nvim/strings.h"
12#include "nvim/screen.h"
13#include "nvim/syntax.h"
14#include "nvim/ui.h"
15#include "nvim/ui_compositor.h"
16#include "nvim/os_unix.h"
17#include "nvim/fold.h"
18#include "nvim/diff.h"
19#include "nvim/move.h"
20#include "nvim/misc1.h"
21#include "nvim/cursor.h"
22#include "nvim/buffer_defs.h"
23#include "nvim/memline.h"
24#include "nvim/charset.h"
25
26#ifdef INCLUDE_GENERATED_DECLARATIONS
27# include "mouse.c.generated.h"
28#endif
29
30static linenr_T orig_topline = 0;
31static int orig_topfill = 0;
32
33// Move the cursor to the specified row and column on the screen.
34// Change current window if necessary. Returns an integer with the
35// CURSOR_MOVED bit set if the cursor has moved or unset otherwise.
36//
37// The MOUSE_FOLD_CLOSE bit is set when clicked on the '-' in a fold column.
38// The MOUSE_FOLD_OPEN bit is set when clicked on the '+' in a fold column.
39//
40// If flags has MOUSE_FOCUS, then the current window will not be changed, and
41// if the mouse is outside the window then the text will scroll, or if the
42// mouse was previously on a status line, then the status line may be dragged.
43//
44// If flags has MOUSE_MAY_VIS, then VIsual mode will be started before the
45// cursor is moved unless the cursor was on a status line.
46// This function returns one of IN_UNKNOWN, IN_BUFFER, IN_STATUS_LINE or
47// IN_SEP_LINE depending on where the cursor was clicked.
48//
49// If flags has MOUSE_MAY_STOP_VIS, then Visual mode will be stopped, unless
50// the mouse is on the status line of the same window.
51//
52// If flags has MOUSE_DID_MOVE, nothing is done if the mouse didn't move since
53// the last call.
54//
55// If flags has MOUSE_SETPOS, nothing is done, only the current position is
56// remembered.
57int jump_to_mouse(int flags,
58 bool *inclusive, // used for inclusive operator, can be NULL
59 int which_button) // MOUSE_LEFT, MOUSE_RIGHT, MOUSE_MIDDLE
60{
61 static int on_status_line = 0; // #lines below bottom of window
62 static int on_sep_line = 0; // on separator right of window
63 static int prev_row = -1;
64 static int prev_col = -1;
65 static win_T *dragwin = NULL; // window being dragged
66 static int did_drag = false; // drag was noticed
67
68 win_T *wp, *old_curwin;
69 pos_T old_cursor;
70 int count;
71 bool first;
72 int row = mouse_row;
73 int col = mouse_col;
74 int grid = mouse_grid;
75 int mouse_char;
76
77 mouse_past_bottom = false;
78 mouse_past_eol = false;
79
80 if (flags & MOUSE_RELEASED) {
81 // On button release we may change window focus if positioned on a
82 // status line and no dragging happened.
83 if (dragwin != NULL && !did_drag)
84 flags &= ~(MOUSE_FOCUS | MOUSE_DID_MOVE);
85 dragwin = NULL;
86 did_drag = false;
87 }
88
89 if ((flags & MOUSE_DID_MOVE)
90 && prev_row == mouse_row
91 && prev_col == mouse_col) {
92retnomove:
93 // before moving the cursor for a left click which is NOT in a status
94 // line, stop Visual mode
95 if (on_status_line)
96 return IN_STATUS_LINE;
97 if (on_sep_line)
98 return IN_SEP_LINE;
99 if (flags & MOUSE_MAY_STOP_VIS) {
100 end_visual_mode();
101 redraw_curbuf_later(INVERTED); // delete the inversion
102 }
103 return IN_BUFFER;
104 }
105
106 prev_row = mouse_row;
107 prev_col = mouse_col;
108
109 if (flags & MOUSE_SETPOS)
110 goto retnomove; // ugly goto...
111
112 // Remember the character under the mouse, it might be a '-' or '+' in the
113 // fold column. NB: only works for ASCII chars!
114 if (row >= 0 && row < Rows && col >= 0 && col <= Columns
115 && default_grid.chars != NULL) {
116 mouse_char = default_grid.chars[default_grid.line_offset[row]
117 + (unsigned)col][0];
118 } else {
119 mouse_char = ' ';
120 }
121
122 old_curwin = curwin;
123 old_cursor = curwin->w_cursor;
124
125 if (!(flags & MOUSE_FOCUS)) {
126 if (row < 0 || col < 0) // check if it makes sense
127 return IN_UNKNOWN;
128
129 // find the window where the row is in
130 wp = mouse_find_win(&grid, &row, &col);
131 if (wp == NULL) {
132 return IN_UNKNOWN;
133 }
134 dragwin = NULL;
135 // winpos and height may change in win_enter()!
136 if (grid == DEFAULT_GRID_HANDLE && row >= wp->w_height) {
137 // In (or below) status line
138 on_status_line = row - wp->w_height + 1;
139 dragwin = wp;
140 } else {
141 on_status_line = 0;
142 }
143
144 if (grid == DEFAULT_GRID_HANDLE && col >= wp->w_width) {
145 // In separator line
146 on_sep_line = col - wp->w_width + 1;
147 dragwin = wp;
148 } else {
149 on_sep_line = 0;
150 }
151
152 // The rightmost character of the status line might be a vertical
153 // separator character if there is no connecting window to the right.
154 if (on_status_line && on_sep_line) {
155 if (stl_connected(wp))
156 on_sep_line = 0;
157 else
158 on_status_line = 0;
159 }
160
161 // Before jumping to another buffer, or moving the cursor for a left
162 // click, stop Visual mode.
163 if (VIsual_active
164 && (wp->w_buffer != curwin->w_buffer
165 || (!on_status_line
166 && !on_sep_line
167 && (wp->w_p_rl
168 ? col < wp->w_width_inner - wp->w_p_fdc
169 : col >= wp->w_p_fdc + (cmdwin_type == 0 && wp == curwin
170 ? 0 : 1))
171 && (flags & MOUSE_MAY_STOP_VIS)))) {
172 end_visual_mode();
173 redraw_curbuf_later(INVERTED); // delete the inversion
174 }
175 if (cmdwin_type != 0 && wp != curwin) {
176 // A click outside the command-line window: Use modeless
177 // selection if possible. Allow dragging the status lines.
178 on_sep_line = 0;
179 row = 0;
180 col += wp->w_wincol;
181 wp = curwin;
182 }
183 // Only change window focus when not clicking on or dragging the
184 // status line. Do change focus when releasing the mouse button
185 // (MOUSE_FOCUS was set above if we dragged first).
186 if (dragwin == NULL || (flags & MOUSE_RELEASED))
187 win_enter(wp, true); // can make wp invalid!
188 // set topline, to be able to check for double click ourselves
189 if (curwin != old_curwin)
190 set_mouse_topline(curwin);
191 if (on_status_line) { // In (or below) status line
192 // Don't use start_arrow() if we're in the same window
193 if (curwin == old_curwin)
194 return IN_STATUS_LINE;
195 else
196 return IN_STATUS_LINE | CURSOR_MOVED;
197 }
198 if (on_sep_line) { // In (or below) status line
199 // Don't use start_arrow() if we're in the same window
200 if (curwin == old_curwin)
201 return IN_SEP_LINE;
202 else
203 return IN_SEP_LINE | CURSOR_MOVED;
204 }
205
206 curwin->w_cursor.lnum = curwin->w_topline;
207 } else if (on_status_line && which_button == MOUSE_LEFT) {
208 if (dragwin != NULL) {
209 // Drag the status line
210 count = row - dragwin->w_winrow - dragwin->w_height + 1
211 - on_status_line;
212 win_drag_status_line(dragwin, count);
213 did_drag |= count;
214 }
215 return IN_STATUS_LINE; // Cursor didn't move
216 } else if (on_sep_line && which_button == MOUSE_LEFT) {
217 if (dragwin != NULL) {
218 // Drag the separator column
219 count = col - dragwin->w_wincol - dragwin->w_width + 1
220 - on_sep_line;
221 win_drag_vsep_line(dragwin, count);
222 did_drag |= count;
223 }
224 return IN_SEP_LINE; // Cursor didn't move
225 } else {
226 // keep_window_focus must be true
227 // before moving the cursor for a left click, stop Visual mode
228 if (flags & MOUSE_MAY_STOP_VIS) {
229 end_visual_mode();
230 redraw_curbuf_later(INVERTED); // delete the inversion
231 }
232
233
234 row -= curwin->w_winrow;
235 col -= curwin->w_wincol;
236
237 // When clicking beyond the end of the window, scroll the screen.
238 // Scroll by however many rows outside the window we are.
239 if (row < 0) {
240 count = 0;
241 for (first = true; curwin->w_topline > 1; ) {
242 if (curwin->w_topfill < diff_check(curwin, curwin->w_topline))
243 ++count;
244 else
245 count += plines(curwin->w_topline - 1);
246 if (!first && count > -row)
247 break;
248 first = false;
249 (void)hasFolding(curwin->w_topline, &curwin->w_topline, NULL);
250 if (curwin->w_topfill < diff_check(curwin, curwin->w_topline)) {
251 ++curwin->w_topfill;
252 } else {
253 --curwin->w_topline;
254 curwin->w_topfill = 0;
255 }
256 }
257 check_topfill(curwin, false);
258 curwin->w_valid &=
259 ~(VALID_WROW|VALID_CROW|VALID_BOTLINE|VALID_BOTLINE_AP);
260 redraw_later(VALID);
261 row = 0;
262 } else if (row >= curwin->w_height_inner) {
263 count = 0;
264 for (first = true; curwin->w_topline < curbuf->b_ml.ml_line_count; ) {
265 if (curwin->w_topfill > 0) {
266 ++count;
267 } else {
268 count += plines(curwin->w_topline);
269 }
270
271 if (!first && count > row - curwin->w_height_inner + 1) {
272 break;
273 }
274 first = false;
275
276 if (hasFolding(curwin->w_topline, NULL, &curwin->w_topline)
277 && curwin->w_topline == curbuf->b_ml.ml_line_count) {
278 break;
279 }
280
281 if (curwin->w_topfill > 0) {
282 --curwin->w_topfill;
283 } else {
284 ++curwin->w_topline;
285 curwin->w_topfill =
286 diff_check_fill(curwin, curwin->w_topline);
287 }
288 }
289 check_topfill(curwin, false);
290 redraw_later(VALID);
291 curwin->w_valid &=
292 ~(VALID_WROW|VALID_CROW|VALID_BOTLINE|VALID_BOTLINE_AP);
293 row = curwin->w_height_inner - 1;
294 } else if (row == 0) {
295 // When dragging the mouse, while the text has been scrolled up as
296 // far as it goes, moving the mouse in the top line should scroll
297 // the text down (done later when recomputing w_topline).
298 if (mouse_dragging > 0
299 && curwin->w_cursor.lnum
300 == curwin->w_buffer->b_ml.ml_line_count
301 && curwin->w_cursor.lnum == curwin->w_topline) {
302 curwin->w_valid &= ~(VALID_TOPLINE);
303 }
304 }
305 }
306
307 // Check for position outside of the fold column.
308 if (curwin->w_p_rl ? col < curwin->w_width_inner - curwin->w_p_fdc :
309 col >= curwin->w_p_fdc + (cmdwin_type == 0 ? 0 : 1)) {
310 mouse_char = ' ';
311 }
312
313 // compute the position in the buffer line from the posn on the screen
314 if (mouse_comp_pos(curwin, &row, &col, &curwin->w_cursor.lnum)) {
315 mouse_past_bottom = true;
316 }
317
318 if (!(flags & MOUSE_RELEASED) && which_button == MOUSE_LEFT) {
319 col = mouse_adjust_click(curwin, row, col);
320 }
321
322 // Start Visual mode before coladvance(), for when 'sel' != "old"
323 if ((flags & MOUSE_MAY_VIS) && !VIsual_active) {
324 VIsual = old_cursor;
325 VIsual_active = true;
326 VIsual_reselect = true;
327 // if 'selectmode' contains "mouse", start Select mode
328 may_start_select('o');
329 setmouse();
330
331 if (p_smd && msg_silent == 0) {
332 redraw_cmdline = true; // show visual mode later
333 }
334 }
335
336 curwin->w_curswant = col;
337 curwin->w_set_curswant = false; // May still have been true
338 if (coladvance(col) == FAIL) { // Mouse click beyond end of line
339 if (inclusive != NULL) {
340 *inclusive = true;
341 }
342 mouse_past_eol = true;
343 } else if (inclusive != NULL) {
344 *inclusive = false;
345 }
346
347 count = IN_BUFFER;
348 if (curwin != old_curwin || curwin->w_cursor.lnum != old_cursor.lnum
349 || curwin->w_cursor.col != old_cursor.col) {
350 count |= CURSOR_MOVED; // Cursor has moved
351 }
352
353 if (mouse_char == '+') {
354 count |= MOUSE_FOLD_OPEN;
355 } else if (mouse_char != ' ') {
356 count |= MOUSE_FOLD_CLOSE;
357 }
358
359 return count;
360}
361
362// Compute the position in the buffer line from the posn on the screen in
363// window "win".
364// Returns true if the position is below the last line.
365bool mouse_comp_pos(win_T *win, int *rowp, int *colp, linenr_T *lnump)
366{
367 int col = *colp;
368 int row = *rowp;
369 linenr_T lnum;
370 bool retval = false;
371 int off;
372 int count;
373
374 if (win->w_p_rl) {
375 col = win->w_width_inner - 1 - col;
376 }
377
378 lnum = win->w_topline;
379
380 while (row > 0) {
381 // Don't include filler lines in "count"
382 if (win->w_p_diff
383 && !hasFoldingWin(win, lnum, NULL, NULL, true, NULL)) {
384 if (lnum == win->w_topline) {
385 row -= win->w_topfill;
386 } else {
387 row -= diff_check_fill(win, lnum);
388 }
389 count = plines_win_nofill(win, lnum, true);
390 } else {
391 count = plines_win(win, lnum, true);
392 }
393
394 if (count > row) {
395 break; // Position is in this buffer line.
396 }
397
398 (void)hasFoldingWin(win, lnum, NULL, &lnum, true, NULL);
399
400 if (lnum == win->w_buffer->b_ml.ml_line_count) {
401 retval = true;
402 break; // past end of file
403 }
404 row -= count;
405 ++lnum;
406 }
407
408 if (!retval) {
409 // Compute the column without wrapping.
410 off = win_col_off(win) - win_col_off2(win);
411 if (col < off)
412 col = off;
413 col += row * (win->w_width_inner - off);
414 // add skip column (for long wrapping line)
415 col += win->w_skipcol;
416 }
417
418 if (!win->w_p_wrap) {
419 col += win->w_leftcol;
420 }
421
422 // skip line number and fold column in front of the line
423 col -= win_col_off(win);
424 if (col < 0) {
425 col = 0;
426 }
427
428 *colp = col;
429 *rowp = row;
430 *lnump = lnum;
431 return retval;
432}
433
434/// Find the window at "grid" position "*rowp" and "*colp". The positions are
435/// updated to become relative to the top-left of the window.
436///
437/// @return NULL when something is wrong.
438win_T *mouse_find_win(int *gridp, int *rowp, int *colp)
439{
440 win_T *wp_grid = mouse_find_grid_win(gridp, rowp, colp);
441 if (wp_grid) {
442 return wp_grid;
443 } else if (*gridp > 1) {
444 return NULL;
445 }
446
447
448 frame_T *fp;
449
450 fp = topframe;
451 *rowp -= firstwin->w_winrow;
452 for (;; ) {
453 if (fp->fr_layout == FR_LEAF)
454 break;
455 if (fp->fr_layout == FR_ROW) {
456 for (fp = fp->fr_child; fp->fr_next != NULL; fp = fp->fr_next) {
457 if (*colp < fp->fr_width)
458 break;
459 *colp -= fp->fr_width;
460 }
461 } else { // fr_layout == FR_COL
462 for (fp = fp->fr_child; fp->fr_next != NULL; fp = fp->fr_next) {
463 if (*rowp < fp->fr_height)
464 break;
465 *rowp -= fp->fr_height;
466 }
467 }
468 }
469 // When using a timer that closes a window the window might not actually
470 // exist.
471 FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
472 if (wp == fp->fr_win) {
473 return wp;
474 }
475 }
476 return NULL;
477}
478
479static win_T *mouse_find_grid_win(int *gridp, int *rowp, int *colp)
480{
481 if (*gridp == msg_grid.handle) {
482 rowp += msg_grid_pos;
483 *gridp = DEFAULT_GRID_HANDLE;
484 } else if (*gridp > 1) {
485 win_T *wp = get_win_by_grid_handle(*gridp);
486 if (wp && wp->w_grid.chars
487 && !(wp->w_floating && !wp->w_float_config.focusable)) {
488 *rowp = MIN(*rowp, wp->w_grid.Rows-1);
489 *colp = MIN(*colp, wp->w_grid.Columns-1);
490 return wp;
491 }
492 } else if (*gridp == 0) {
493 ScreenGrid *grid = ui_comp_mouse_focus(*rowp, *colp);
494 FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
495 if (&wp->w_grid != grid) {
496 continue;
497 }
498 *gridp = grid->handle;
499 *rowp -= grid->comp_row;
500 *colp -= grid->comp_col;
501 return wp;
502 }
503
504 // no float found, click on the default grid
505 // TODO(bfredl): grid can be &pum_grid, allow select pum items by mouse?
506 *gridp = DEFAULT_GRID_HANDLE;
507 }
508 return NULL;
509}
510
511/*
512 * setmouse() - switch mouse on/off depending on current mode and 'mouse'
513 */
514void setmouse(void)
515{
516 int checkfor;
517
518 ui_cursor_shape();
519
520 /* be quick when mouse is off */
521 if (*p_mouse == NUL)
522 return;
523
524 if (VIsual_active)
525 checkfor = MOUSE_VISUAL;
526 else if (State == HITRETURN || State == ASKMORE || State == SETWSIZE)
527 checkfor = MOUSE_RETURN;
528 else if (State & INSERT)
529 checkfor = MOUSE_INSERT;
530 else if (State & CMDLINE)
531 checkfor = MOUSE_COMMAND;
532 else if (State == CONFIRM || State == EXTERNCMD)
533 checkfor = ' '; /* don't use mouse for ":confirm" or ":!cmd" */
534 else
535 checkfor = MOUSE_NORMAL; /* assume normal mode */
536
537 if (mouse_has(checkfor)) {
538 ui_call_mouse_on();
539 } else {
540 ui_call_mouse_off();
541 }
542}
543
544/*
545 * Return true if
546 * - "c" is in 'mouse', or
547 * - 'a' is in 'mouse' and "c" is in MOUSE_A, or
548 * - the current buffer is a help file and 'h' is in 'mouse' and we are in a
549 * normal editing mode (not at hit-return message).
550 */
551int mouse_has(int c)
552{
553 for (char_u *p = p_mouse; *p; ++p)
554 switch (*p) {
555 case 'a': if (vim_strchr((char_u *)MOUSE_A, c) != NULL)
556 return true;
557 break;
558 case MOUSE_HELP: if (c != MOUSE_RETURN && curbuf->b_help)
559 return true;
560 break;
561 default: if (c == *p) return true; break;
562 }
563 return false;
564}
565
566// Set orig_topline. Used when jumping to another window, so that a double
567// click still works.
568void set_mouse_topline(win_T *wp)
569{
570 orig_topline = wp->w_topline;
571 orig_topfill = wp->w_topfill;
572}
573
574///
575/// Return length of line "lnum" for horizontal scrolling.
576///
577static colnr_T scroll_line_len(linenr_T lnum)
578{
579 colnr_T col = 0;
580 char_u *line = ml_get(lnum);
581 if (*line != NUL) {
582 for (;;) {
583 int numchar = chartabsize(line, col);
584 MB_PTR_ADV(line);
585 if (*line == NUL) { // don't count the last character
586 break;
587 }
588 col += numchar;
589 }
590 }
591 return col;
592}
593
594///
595/// Find longest visible line number.
596///
597static linenr_T find_longest_lnum(void)
598{
599 linenr_T ret = 0;
600
601 // Calculate maximum for horizontal scrollbar. Check for reasonable
602 // line numbers, topline and botline can be invalid when displaying is
603 // postponed.
604 if (curwin->w_topline <= curwin->w_cursor.lnum
605 && curwin->w_botline > curwin->w_cursor.lnum
606 && curwin->w_botline <= curbuf->b_ml.ml_line_count + 1) {
607 long max = 0;
608
609 // Use maximum of all visible lines. Remember the lnum of the
610 // longest line, closest to the cursor line. Used when scrolling
611 // below.
612 for (linenr_T lnum = curwin->w_topline; lnum < curwin->w_botline; lnum++) {
613 colnr_T len = scroll_line_len(lnum);
614 if (len > (colnr_T)max) {
615 max = len;
616 ret = lnum;
617 } else if (len == (colnr_T)max
618 && abs((int)(lnum - curwin->w_cursor.lnum))
619 < abs((int)(ret - curwin->w_cursor.lnum))) {
620 ret = lnum;
621 }
622 }
623 } else {
624 // Use cursor line only.
625 ret = curwin->w_cursor.lnum;
626 }
627
628 return ret;
629}
630
631///
632/// Do a horizontal scroll. Return TRUE if the cursor moved, FALSE otherwise.
633///
634bool mouse_scroll_horiz(int dir)
635{
636 if (curwin->w_p_wrap) {
637 return false;
638 }
639
640 int step = 6;
641 if (mod_mask & (MOD_MASK_SHIFT | MOD_MASK_CTRL)) {
642 step = curwin->w_width_inner;
643 }
644
645 int leftcol = curwin->w_leftcol + (dir == MSCR_RIGHT ? -step : +step);
646 if (leftcol < 0) {
647 leftcol = 0;
648 }
649
650 if (curwin->w_leftcol == leftcol) {
651 return false;
652 }
653
654 curwin->w_leftcol = (colnr_T)leftcol;
655
656 // When the line of the cursor is too short, move the cursor to the
657 // longest visible line.
658 if (!virtual_active()
659 && (colnr_T)leftcol > scroll_line_len(curwin->w_cursor.lnum)) {
660 curwin->w_cursor.lnum = find_longest_lnum();
661 curwin->w_cursor.col = 0;
662 }
663
664 return leftcol_changed();
665}
666
667/// Adjusts the clicked column position when 'conceallevel' > 0
668static int mouse_adjust_click(win_T *wp, int row, int col)
669{
670 if (!(wp->w_p_cole > 0 && curbuf->b_p_smc > 0
671 && wp->w_leftcol < curbuf->b_p_smc && conceal_cursor_line(wp))) {
672 return col;
673 }
674
675 // `col` is the position within the current line that is highlighted by the
676 // cursor without consideration for concealed characters. The current line is
677 // scanned *up to* `col`, nudging it left or right when concealed characters
678 // are encountered.
679 //
680 // chartabsize() is used to keep track of the virtual column position relative
681 // to the line's bytes. For example: if col == 9 and the line starts with a
682 // tab that's 8 columns wide, we would want the cursor to be highlighting the
683 // second byte, not the ninth.
684
685 linenr_T lnum = wp->w_cursor.lnum;
686 char_u *line = ml_get(lnum);
687 char_u *ptr = line;
688 char_u *ptr_end;
689 char_u *ptr_row_offset = line; // Where we begin adjusting `ptr_end`
690
691 // Find the offset where scanning should begin.
692 int offset = wp->w_leftcol;
693 if (row > 0) {
694 offset += row * (wp->w_width_inner - win_col_off(wp) - win_col_off2(wp) -
695 wp->w_leftcol + wp->w_skipcol);
696 }
697
698 int vcol;
699
700 if (offset) {
701 // Skip everything up to an offset since nvim takes care of displaying the
702 // correct portion of the line when horizontally scrolling.
703 // When 'wrap' is enabled, only the row (of the wrapped line) needs to be
704 // checked for concealed characters.
705 vcol = 0;
706 while (vcol < offset && *ptr != NUL) {
707 vcol += chartabsize(ptr, vcol);
708 ptr += utfc_ptr2len(ptr);
709 }
710
711 ptr_row_offset = ptr;
712 }
713
714 // Align `ptr_end` with `col`
715 vcol = offset;
716 ptr_end = ptr_row_offset;
717 while (vcol < col && *ptr_end != NUL) {
718 vcol += chartabsize(ptr_end, vcol);
719 ptr_end += utfc_ptr2len(ptr_end);
720 }
721
722 int matchid;
723 int prev_matchid;
724 int nudge = 0;
725 int cwidth = 0;
726
727 vcol = offset;
728
729#define incr() nudge++; ptr_end += utfc_ptr2len(ptr_end)
730#define decr() nudge--; ptr_end -= utfc_ptr2len(ptr_end)
731
732 while (ptr < ptr_end && *ptr != NUL) {
733 cwidth = chartabsize(ptr, vcol);
734 vcol += cwidth;
735 if (cwidth > 1 && *ptr == '\t' && nudge > 0) {
736 // A tab will "absorb" any previous adjustments.
737 cwidth = MIN(cwidth, nudge);
738 while (cwidth > 0) {
739 decr();
740 cwidth--;
741 }
742 }
743
744 matchid = syn_get_concealed_id(wp, lnum, (colnr_T)(ptr - line));
745 if (matchid != 0) {
746 if (wp->w_p_cole == 3) {
747 incr();
748 } else {
749 if (!(row > 0 && ptr == ptr_row_offset)
750 && (wp->w_p_cole == 1 || (wp->w_p_cole == 2
751 && (wp->w_p_lcs_chars.conceal != NUL
752 || syn_get_sub_char() != NUL)))) {
753 // At least one placeholder character will be displayed.
754 decr();
755 }
756
757 prev_matchid = matchid;
758
759 while (prev_matchid == matchid && *ptr != NUL) {
760 incr();
761 ptr += utfc_ptr2len(ptr);
762 matchid = syn_get_concealed_id(wp, lnum, (colnr_T)(ptr - line));
763 }
764
765 continue;
766 }
767 }
768
769 ptr += utfc_ptr2len(ptr);
770 }
771
772 return col + nudge;
773}
774