2 * Copyright Neil Brown ©2019-2023 <neil@brown.name>
3 * May be distributed under terms of GPLv2 - see file:COPYING
5 * This module provides highlighting of interesting white-space,
6 * and possibly other spacing-related issues.
8 * tabs are in a different colour (yellow-80+80)
9 * unicode spaces a different colour (red+80-80)
10 * space at EOL are RED (red)
11 * TAB after space are RED (red-80)
12 * anything beyond configured line length is RED (red-80+50 "whitespace-width" or 80)
13 * non-space as first char RED if configured ("whitespace-intent-space")
14 * >=8 spaces RED if configured ("whitespace-max-spaces")
15 * blank line adjacent to blank or start/end of file if configured ("whitespace-single-blank-lines")
17 * This is achieved by capturing the "start-of-line" attribute request,
18 * reporting attributes that apply to leading chars, and placing a
19 * mark with a "render:whitespace" attribute at the next interesting place, if
23 #define _GNU_SOURCE /* for asprintf */
30 #define PANE_DATA_TYPE struct ws_info
41 #include "core-pane.h"
43 /* a0 and 2007 are non-breaking an not in iswblank, but I want them. */
44 #define ISWBLANK(c) ((c) == 0xa0 || (c) == 0x2007 || iswblank(c))
46 static void choose_next(struct pane *focus safe, struct mark *pm safe,
47 struct ws_info *ws safe, int skip)
49 struct mark *m = ws->mymark;
57 /* Need to look beyond the current location */
59 ch = doc_next(focus, m);
62 ws->mycol = (ws->mycol | 7) + 1;
63 else if (ch != WEOF && !is_eol(ch))
70 int cnt, rewind, rewindcol, col;
71 wint_t ch = doc_following(focus, m);
74 if (ch == WEOF || is_eol(ch)) {
77 if (ch == WEOF || !ws->single_blanks)
79 /* A blank line must not be preceeded or followed
80 * by EOF of a blank line.
83 ch = doc_following(focus, m);
85 if (ch != WEOF && !is_eol(ch)) {
86 /* Next line isn't blank. need to check behind */
87 ch = doc_prev(focus, m);
89 /* blank line at start of file */
90 } else if (!is_eol(ch)) {
91 /* should be impossible, so ignore */
95 ch = doc_prior(focus, m);
97 if (ch != WEOF && ch != '\n')
98 /* previous line not blank either */
102 attr_set_str(&m->attrs, "render:whitespace",
104 attr_set_int(&m->attrs, "attr-len", 1);
107 if (ws->mycol >= ws->warn_width) {
108 /* everything from here is an error */
109 attr_set_str(&m->attrs, "render:whitespace",
111 attr_set_int(&m->attrs, "attr-len", INT_MAX);
115 /* Nothing to highlight here, move forward */
120 /* If only spaces/tabs until EOL, then RED,
128 while ((ch = doc_next(focus, m)) != WEOF &&
130 if (ch != ' ' && cnt < rewind) {
131 /* This may be different colours depending on
132 * what we find, so remember this location.
138 col = (ws->mycol|7)+1;
145 if (ws->mycol == 0 && ws->indent_space && rewind == 0) {
146 /* Indents must be space, but this is something else,
147 * so highlight all the indent
149 doc_move(focus, m, -cnt);
150 attr_set_str(&m->attrs, "render:whitespace",
152 attr_set_int(&m->attrs, "attr-len", cnt);
155 if (cnt > ws->max_spaces && rewind > ws->max_spaces) {
156 /* Too many spaces - not too loud a highlight*/
157 doc_move(focus, m, -cnt);
160 attr_set_str(&m->attrs, "render:whitespace",
162 attr_set_int(&m->attrs, "attr-len", cnt);
166 * 'm' is just after last blank. ch is next (non-blank)
167 * char. 'cnt' is the number of blanks.
168 * 'rewind' is distance from start where first non-space seen.
170 if (ch == WEOF || is_eol(ch)) {
171 /* Blanks all the way to EOL. This is highlighted unless
174 struct mark *p = call_ret(mark, "doc:point",
176 if (p && mark_same(m, p))
179 if (ch == WEOF || is_eol(ch)) {
180 doc_move(focus, m, -cnt);
181 /* Set the blanks at EOL to red */
182 attr_set_str(&m->attrs, "render:whitespace",
184 attr_set_int(&m->attrs, "attr-len", cnt);
188 /* no non-space, nothing to do here */
193 /* That first blank is no RED, set normal colour */
194 doc_move(focus, m, rewind - cnt);
195 ws->mycol = rewindcol;
198 /* If previous is non-tab, then RED, else YELLOW */
199 ch = doc_prior(focus, m);
200 ch2 = doc_following(focus, m);
201 if (ch2 == '\t' && ch != WEOF && ch != '\t' && ISWBLANK(ch))
202 /* Tab after non-tab blank - bad */
203 attr_set_str(&m->attrs, "render:whitespace",
205 else if (ch2 == '\t')
206 attr_set_str(&m->attrs, "render:whitespace",
209 /* non-space or tab, must be unicode blank of some sort */
210 attr_set_str(&m->attrs, "render:whitespace",
212 attr_set_int(&m->attrs, "attr-len", 1);
216 attr_set_str(&m->attrs, "render:whitespace", NULL);
221 struct ws_info *ws = ci->home->data;
223 if (!ci->str || !ci->mark)
225 if (strcmp(ci->str, "start-of-line") == 0) {
227 mark_free(ws->mymark);
230 choose_next(ci->focus, ci->mark, ws, 0);
233 if (ci->mark == ws->mymark &&
234 strcmp(ci->str, "render:whitespace") == 0) {
235 char *s = strsave(ci->focus, ci->str2);
236 int len = attr_find_int(ci->mark->attrs, "attr-len");
239 choose_next(ci->focus, ci->mark, ws, len);
240 return comm_call(ci->comm2, "attr:callback", ci->focus, len,
246 DEF_CMD_CLOSED(ws_close)
248 struct ws_info *ws = ci->home->data;
250 mark_free(ws->mymark);
255 static struct map *ws_map safe;
256 DEF_LOOKUP_CMD(whitespace_handle, ws_map);
258 static struct pane *ws_attach(struct pane *f safe)
264 p = pane_register(f, 0, &whitespace_handle.c);
269 w = pane_attr_get(f, "whitespace-width");
271 ws->warn_width = atoi(w);
272 if (ws->warn_width < 8)
273 ws->warn_width = INT_MAX;
277 w = pane_attr_get(f, "whitespace-indent-space");
278 if (w && strcasecmp(w, "no") != 0)
279 ws->indent_space = True;
280 w = pane_attr_get(f, "whitespace-max-spaces");
282 ws->max_spaces = atoi(w);
283 if (ws->max_spaces < 1)
286 ws->max_spaces = INT_MAX;
288 w = pane_attr_get(f, "whitespace-single-blank-lines");
289 if (w && strcasecmp(w, "no") != 0)
290 ws->single_blanks = True;
299 p = ws_attach(ci->focus);
301 pane_clone_children(ci->home, p);
305 DEF_CMD(whitespace_attach)
309 p = ws_attach(ci->focus);
312 return comm_call(ci->comm2, "callback:attach", p);
316 DEF_CMD(whitespace_activate)
320 p = call_ret(pane, "attach-whitespace", ci->focus);
323 call("doc:append:view-default", p, 0, NULL, ",whitespace");
327 void edlib_init(struct pane *ed safe)
329 ws_map = key_alloc();
331 key_add(ws_map, "map-attr", &ws_attrs);
332 key_add(ws_map, "Close", &ws_close);
333 key_add(ws_map, "Clone", &ws_clone);
334 call_comm("global-set-command", ed, &whitespace_attach,
335 0, NULL, "attach-whitespace");
336 call_comm("global-set-command", ed, &whitespace_activate,
337 0, NULL, "interactive-cmd-whitespace-mode");