]> git.neil.brown.name Git - edlib.git/blob - lib-whitespace.c
TODO: clean out done items.
[edlib.git] / lib-whitespace.c
1 /*
2  * Copyright Neil Brown ©2019-2023 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * This module provides highlighting of interesting white-space,
6  * and possibly other spacing-related issues.
7  * Currently:
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")
16  *
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
20  * there is one.
21  */
22
23 #define _GNU_SOURCE /*  for asprintf */
24 #include <unistd.h>
25 #include <stdlib.h>
26 #include <stdio.h>
27 #include <string.h>
28 #include <wchar.h>
29 #include <wctype.h>
30 #define PANE_DATA_TYPE struct ws_info
31 #include "core.h"
32
33 struct ws_info {
34         struct mark *mymark;
35         int mycol;
36         int warn_width;
37         int max_spaces;
38         bool indent_space;
39         bool single_blanks;
40 };
41 #include "core-pane.h"
42
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))
45
46 static void choose_next(struct pane *focus safe, struct mark *pm safe,
47                         struct ws_info *ws safe, int skip)
48 {
49         struct mark *m = ws->mymark;
50
51         if (m == NULL) {
52                 m = mark_dup(pm);
53                 ws->mymark = m;
54         } else
55                 mark_to_mark(m, pm);
56         while (skip > 0) {
57                 /* Need to look beyond the current location */
58                 wint_t ch;
59                 ch = doc_next(focus, m);
60                 skip -= 1;
61                 if (ch == '\t')
62                         ws->mycol = (ws->mycol | 7) + 1;
63                 else if (ch != WEOF && !is_eol(ch))
64                         ws->mycol += 1;
65                 else
66                         skip = 0;
67         }
68
69         while(1) {
70                 int cnt, rewind, rewindcol, col;
71                 wint_t ch = doc_following(focus, m);
72                 wint_t ch2;
73
74                 if (ch == WEOF || is_eol(ch)) {
75                         if (ws->mycol > 0)
76                                 break;
77                         if (ch == WEOF || !ws->single_blanks)
78                                 break;
79                         /* A blank line must not be preceeded or followed
80                          * by EOF of a blank line.
81                          */
82                         doc_next(focus, m);
83                         ch = doc_following(focus, m);
84                         doc_prev(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);
88                                 if (ch == WEOF) {
89                                         /* blank line at start of file */
90                                 } else if (!is_eol(ch)) {
91                                         /* should be impossible, so ignore */
92                                         doc_next(focus, m);
93                                         break;
94                                 } else {
95                                         ch = doc_prior(focus, m);
96                                         doc_next(focus, m);
97                                         if (ch != WEOF && ch != '\n')
98                                                 /* previous line not blank either */
99                                                 break;
100                                 }
101                         }
102                         attr_set_str(&m->attrs, "render:whitespace",
103                                      "bg:red,vis-nl");
104                         attr_set_int(&m->attrs, "attr-len", 1);
105                         return;
106                 }
107                 if (ws->mycol >= ws->warn_width) {
108                         /* everything from here is an error */
109                         attr_set_str(&m->attrs, "render:whitespace",
110                                      "bg:red-80+50");
111                         attr_set_int(&m->attrs, "attr-len", INT_MAX);
112                         return;
113                 }
114                 if (!ISWBLANK(ch)) {
115                         /* Nothing to highlight here, move forward */
116                         doc_next(focus, m);
117                         ws->mycol++;
118                         continue;
119                 }
120                 /* If only spaces/tabs until EOL, then RED,
121                  * else keep looking
122                  */
123
124                 cnt = 0;
125                 rewind = INT_MAX;
126                 rewindcol = 0;
127                 col = ws->mycol;
128                 while ((ch = doc_next(focus, m)) != WEOF &&
129                        ISWBLANK(ch)) {
130                         if (ch != ' ' && cnt < rewind) {
131                                 /* This may be different colours depending on
132                                  * what we find, so remember this location.
133                                  */
134                                 rewind = cnt;
135                                 rewindcol = col;
136                         }
137                         if (ch == '\t')
138                                 col = (ws->mycol|7)+1;
139                         else
140                                 col += 1;
141                         cnt += 1;
142                 }
143                 if (ch != WEOF)
144                         doc_prev(focus, m);
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
148                          */
149                         doc_move(focus, m, -cnt);
150                         attr_set_str(&m->attrs, "render:whitespace",
151                                      "bg:red");
152                         attr_set_int(&m->attrs, "attr-len", cnt);
153                         return;
154                 }
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);
158                         if (rewind < cnt)
159                                 cnt = rewind;
160                         attr_set_str(&m->attrs, "render:whitespace",
161                                      "bg:red+60");
162                         attr_set_int(&m->attrs, "attr-len", cnt);
163                         return;
164                 }
165                 /*
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.
169                  */
170                 if (ch == WEOF || is_eol(ch)) {
171                         /* Blanks all the way to EOL.  This is highlighted unless
172                          * point is at EOL
173                          */
174                         struct mark *p = call_ret(mark, "doc:point",
175                                                   focus);
176                         if (p && mark_same(m, p))
177                                 ch = 'x';
178                 }
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",
183                                      "bg:red");
184                         attr_set_int(&m->attrs, "attr-len", cnt);
185                         return;
186                 }
187                 if (rewind > cnt) {
188                         /* no non-space, nothing to do here */
189                         ws->mycol = col;
190                         continue;
191                 }
192
193                 /* That first blank is no RED, set normal colour */
194                 doc_move(focus, m, rewind - cnt);
195                 ws->mycol = rewindcol;
196
197                 /* handle tab */
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",
204                                      "bg:red-80");
205                 else if (ch2 == '\t')
206                         attr_set_str(&m->attrs, "render:whitespace",
207                                      "bg:yellow-80+80");
208                 else
209                         /* non-space or tab, must be unicode blank of some sort */
210                         attr_set_str(&m->attrs, "render:whitespace",
211                                      "bg:red-80+80");
212                 attr_set_int(&m->attrs, "attr-len", 1);
213
214                 return;
215         }
216         attr_set_str(&m->attrs, "render:whitespace", NULL);
217 }
218
219 DEF_CMD(ws_attrs)
220 {
221         struct ws_info *ws = ci->home->data;
222
223         if (!ci->str || !ci->mark)
224                 return Enoarg;
225         if (strcmp(ci->str, "start-of-line") == 0) {
226                 if (ws->mymark)
227                         mark_free(ws->mymark);
228                 ws->mymark = NULL;
229                 ws->mycol = 0;
230                 choose_next(ci->focus, ci->mark, ws, 0);
231                 return Efallthrough;
232         }
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");
237                 if (len <= 0)
238                         len = 1;
239                 choose_next(ci->focus, ci->mark, ws, len);
240                 return comm_call(ci->comm2, "attr:callback", ci->focus, len,
241                                  ci->mark, s, 110);
242         }
243         return Efallthrough;
244 }
245
246 DEF_CMD_CLOSED(ws_close)
247 {
248         struct ws_info *ws = ci->home->data;
249
250         mark_free(ws->mymark);
251         ws->mymark = NULL;
252         return 1;
253 }
254
255 static struct map *ws_map safe;
256 DEF_LOOKUP_CMD(whitespace_handle, ws_map);
257
258 static struct pane *ws_attach(struct pane *f safe)
259 {
260         struct ws_info *ws;
261         struct pane *p;
262         char *w;
263
264         p = pane_register(f, 0, &whitespace_handle.c);
265         if (!p)
266                 return p;
267         ws = p->data;
268
269         w = pane_attr_get(f, "whitespace-width");
270         if (w) {
271                 ws->warn_width = atoi(w);
272                 if (ws->warn_width < 8)
273                         ws->warn_width = INT_MAX;
274         } else
275                 ws->warn_width = 80;
276
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");
281         if (w) {
282                 ws->max_spaces = atoi(w);
283                 if (ws->max_spaces < 1)
284                         ws->max_spaces = 7;
285         } else
286                 ws->max_spaces = INT_MAX;
287
288         w = pane_attr_get(f, "whitespace-single-blank-lines");
289         if (w && strcasecmp(w, "no") != 0)
290                 ws->single_blanks = True;
291
292         return p;
293 }
294
295 DEF_CMD(ws_clone)
296 {
297         struct pane *p;
298
299         p = ws_attach(ci->focus);
300         if (p)
301                 pane_clone_children(ci->home, p);
302         return 1;
303 }
304
305 DEF_CMD(whitespace_attach)
306 {
307         struct pane *p;
308
309         p = ws_attach(ci->focus);
310         if (!p)
311                 return Efail;
312         return comm_call(ci->comm2, "callback:attach", p);
313
314 }
315
316 DEF_CMD(whitespace_activate)
317 {
318         struct pane *p;
319
320         p = call_ret(pane, "attach-whitespace", ci->focus);
321         if (!p)
322                 return Efail;
323         call("doc:append:view-default", p, 0, NULL, ",whitespace");
324         return 1;
325 }
326
327 void edlib_init(struct pane *ed safe)
328 {
329         ws_map = key_alloc();
330
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");
338
339 }