1 /* $OpenBSD: cmode.c,v 1.23 2024/05/21 05:00:48 jsg Exp $ */
2 /*
3 * This file is in the public domain.
4 *
5 * Author: Kjell Wooding <kjell@openbsd.org>
6 */
7
8 /*
9 * Implement an non-irritating KNF-compliant mode for editing
10 * C code.
11 */
12
13 #include <sys/queue.h>
14 #include <ctype.h>
15 #include <signal.h>
16 #include <stdio.h>
17
18 #include "def.h"
19 #include "funmap.h"
20 #include "kbd.h"
21
22 /* Pull in from modes.c */
23 extern int changemode(int, int, char *);
24
25 static int cc_strip_trailp = TRUE; /* Delete Trailing space? */
26 static int cc_basic_indent = 8; /* Basic Indent multiple */
27 static int cc_cont_indent = 4; /* Continued line indent */
28 static int cc_colon_indent = -8; /* Label / case indent */
29
30 static int getmatch(int, int);
31 static int getindent(const struct line *, int *);
32 static int in_whitespace(struct line *, int);
33 static int findcolpos(const struct buffer *, const struct line *, int);
34 static struct line *findnonblank(struct line *);
35 static int isnonblank(const struct line *, int);
36
37 void cmode_init(void);
38
39 /* Keymaps */
40
41 static PF cmode_brace[] = {
42 cc_brace, /* } */
43 };
44
45 static PF cmode_cCP[] = {
46 compile, /* C-c P */
47 };
48
49
50 static PF cmode_cc[] = {
51 NULL, /* ^C */
52 rescan, /* ^D */
53 rescan, /* ^E */
54 rescan, /* ^F */
55 rescan, /* ^G */
56 rescan, /* ^H */
57 cc_tab, /* ^I */
58 rescan, /* ^J */
59 rescan, /* ^K */
60 rescan, /* ^L */
61 cc_lfindent, /* ^M */
62 };
63
64 static PF cmode_spec[] = {
65 cc_char, /* : */
66 };
67
68 static struct KEYMAPE (1) cmode_cmap = {
69 1,
70 1,
71 rescan,
72 {
73 { 'P', 'P', cmode_cCP, NULL }
74 }
75 };
76
77 static struct KEYMAPE (3) cmodemap = {
78 3,
79 3,
80 rescan,
81 {
82 { CCHR('C'), CCHR('M'), cmode_cc, (KEYMAP *) &cmode_cmap },
83 { ':', ':', cmode_spec, NULL },
84 { '}', '}', cmode_brace, NULL }
85 }
86 };
87
88 /* Function, Mode hooks */
89
90 void
cmode_init(void)91 cmode_init(void)
92 {
93 funmap_add(cmode, "c-mode", 0);
94 funmap_add(cc_char, "c-handle-special-char", 0);
95 funmap_add(cc_brace, "c-handle-special-brace", 0);
96 funmap_add(cc_tab, "c-tab-or-indent", 0);
97 funmap_add(cc_indent, "c-indent", 0);
98 funmap_add(cc_lfindent, "c-indent-and-newline", 0);
99 maps_add((KEYMAP *)&cmodemap, "c");
100 }
101
102 /*
103 * Enable/toggle c-mode
104 */
105 int
cmode(int f,int n)106 cmode(int f, int n)
107 {
108 return(changemode(f, n, "c"));
109 }
110
111 /*
112 * Handle special C character - selfinsert then indent.
113 */
114 int
cc_char(int f,int n)115 cc_char(int f, int n)
116 {
117 if (n < 0)
118 return (FALSE);
119 if (selfinsert(FFRAND, n) == FALSE)
120 return (FALSE);
121 return (cc_indent(FFRAND, n));
122 }
123
124 /*
125 * Handle special C character - selfinsert then indent.
126 */
127 int
cc_brace(int f,int n)128 cc_brace(int f, int n)
129 {
130 if (n < 0)
131 return (FALSE);
132 if (showmatch(FFRAND, 1) == FALSE)
133 return (FALSE);
134 return (cc_indent(FFRAND, n));
135 }
136
137
138 /*
139 * If we are in the whitespace at the beginning of the line,
140 * simply act as a regular tab. If we are not, indent
141 * current line according to whitespace rules.
142 */
143 int
cc_tab(int f,int n)144 cc_tab(int f, int n)
145 {
146 int inwhitep = FALSE; /* In leading whitespace? */
147
148 inwhitep = in_whitespace(curwp->w_dotp, llength(curwp->w_dotp));
149
150 /* If empty line, or in whitespace */
151 if (llength(curwp->w_dotp) == 0 || inwhitep)
152 return (selfinsert(f, n));
153
154 return (cc_indent(FFRAND, 1));
155 }
156
157 /*
158 * Attempt to indent current line according to KNF rules.
159 */
160 int
cc_indent(int f,int n)161 cc_indent(int f, int n)
162 {
163 int pi, mi; /* Previous indents (mi is ignored) */
164 int ci; /* current indent */
165 struct line *lp;
166 int ret;
167
168 if (n < 0)
169 return (FALSE);
170
171 undo_boundary_enable(FFRAND, 0);
172 if (cc_strip_trailp)
173 deltrailwhite(FFRAND, 1);
174
175 /*
176 * Search backwards for a non-blank, non-preprocessor,
177 * non-comment line
178 */
179
180 lp = findnonblank(curwp->w_dotp);
181
182 pi = getindent(lp, &mi);
183
184 /* Strip leading space on current line */
185 delleadwhite(FFRAND, 1);
186 /* current indent is computed only to current position */
187 (void)getindent(curwp->w_dotp, &ci);
188
189 if (pi + ci < 0)
190 ret = indent(FFOTHARG, 0);
191 else
192 ret = indent(FFOTHARG, pi + ci);
193
194 undo_boundary_enable(FFRAND, 1);
195
196 return (ret);
197 }
198
199 /*
200 * Indent-and-newline (technically, newline then indent)
201 */
202 int
cc_lfindent(int f,int n)203 cc_lfindent(int f, int n)
204 {
205 if (n < 0)
206 return (FALSE);
207 if (cc_strip_trailp)
208 (void)delwhite(FFRAND, 1);
209 if (enewline(FFRAND, 1) == FALSE)
210 return (FALSE);
211 return (cc_indent(FFRAND, n));
212 }
213
214 /*
215 * Get the level of indentation after line lp is processed
216 * Note getindent has two returns:
217 * curi = value if indenting current line.
218 * return value = value affecting subsequent lines.
219 */
220 static int
getindent(const struct line * lp,int * curi)221 getindent(const struct line *lp, int *curi)
222 {
223 int lo, co; /* leading space, current offset*/
224 int nicol = 0; /* position count */
225 int c = '\0'; /* current char */
226 int newind = 0; /* new index value */
227 int stringp = FALSE; /* in string? */
228 int escp = FALSE; /* Escape char? */
229 int lastc = '\0'; /* Last matched string delimiter */
230 int nparen = 0; /* paren count */
231 int obrace = 0; /* open brace count */
232 int cbrace = 0; /* close brace count */
233 int firstnwsp = FALSE; /* First nonspace encountered? */
234 int colonp = FALSE; /* Did we see a colon? */
235 int questionp = FALSE; /* Did we see a question mark? */
236 int slashp = FALSE; /* Slash? */
237 int astp = FALSE; /* Asterisk? */
238 int cpos = -1; /* comment position */
239 int cppp = FALSE; /* Preprocessor command? */
240
241 *curi = 0;
242
243 /* Compute leading space */
244 for (lo = 0; lo < llength(lp); lo++) {
245 if (!isspace(c = lgetc(lp, lo)))
246 break;
247 if (c == '\t')
248 nicol = ntabstop(nicol, curbp->b_tabw);
249 else
250 nicol++;
251 }
252
253 /* If last line was blank, choose 0 */
254 if (lo == llength(lp))
255 nicol = 0;
256
257 newind = 0;
258 /* Compute modifiers */
259 for (co = lo; co < llength(lp); co++) {
260 c = lgetc(lp, co);
261 /* We have a non-whitespace char */
262 if (!firstnwsp && !isspace(c)) {
263 if (c == '#')
264 cppp = TRUE;
265 firstnwsp = TRUE;
266 }
267 if (c == '\\')
268 escp = !escp;
269 else if (stringp) {
270 if (!escp && (c == '"' || c == '\'')) {
271 /* unescaped string char */
272 if (getmatch(c, lastc))
273 stringp = FALSE;
274 }
275 } else if (c == '"' || c == '\'') {
276 stringp = TRUE;
277 lastc = c;
278 } else if (c == '(') {
279 nparen++;
280 } else if (c == ')') {
281 nparen--;
282 } else if (c == '{') {
283 obrace++;
284 firstnwsp = FALSE;
285 } else if (c == '}') {
286 cbrace++;
287 } else if (c == '?') {
288 questionp = TRUE;
289 } else if (c == ':') {
290 /* ignore (foo ? bar : baz) construct */
291 if (!questionp)
292 colonp = TRUE;
293 } else if (c == '/') {
294 /* first nonwhitespace? -> indent */
295 if (firstnwsp) {
296 /* If previous char asterisk -> close */
297 if (astp)
298 cpos = -1;
299 else
300 slashp = TRUE;
301 }
302 } else if (c == '*') {
303 /* If previous char slash -> open */
304 if (slashp)
305 cpos = co;
306 else
307 astp = TRUE;
308 } else if (firstnwsp) {
309 firstnwsp = FALSE;
310 }
311
312 /* Reset matches that apply to next character only */
313 if (c != '\\')
314 escp = FALSE;
315 if (c != '*')
316 astp = FALSE;
317 if (c != '/')
318 slashp = FALSE;
319 }
320 /*
321 * If not terminated with a semicolon, and brace or paren open.
322 * we continue
323 */
324 if (colonp) {
325 *curi += cc_colon_indent;
326 newind -= cc_colon_indent;
327 }
328
329 *curi -= (cbrace) * cc_basic_indent;
330 newind += obrace * cc_basic_indent;
331
332 if (nparen < 0)
333 newind -= cc_cont_indent;
334 else if (nparen > 0)
335 newind += cc_cont_indent;
336
337 *curi += nicol;
338
339 /* Ignore preprocessor. Otherwise, add current column */
340 if (cppp) {
341 newind = nicol;
342 *curi = 0;
343 } else {
344 newind += nicol;
345 }
346
347 if (cpos != -1)
348 newind = findcolpos(curbp, lp, cpos);
349
350 return (newind);
351 }
352
353 /*
354 * Given a delimiter and its purported mate, tell us if they
355 * match.
356 */
357 static int
getmatch(int c,int mc)358 getmatch(int c, int mc)
359 {
360 int match = FALSE;
361
362 switch (c) {
363 case '"':
364 match = (mc == '"');
365 break;
366 case '\'':
367 match = (mc == '\'');
368 break;
369 case '(':
370 match = (mc == ')');
371 break;
372 case '[':
373 match = (mc == ']');
374 break;
375 case '{':
376 match = (mc == '}');
377 break;
378 }
379
380 return (match);
381 }
382
383 static int
in_whitespace(struct line * lp,int len)384 in_whitespace(struct line *lp, int len)
385 {
386 int lo;
387 int inwhitep = FALSE;
388
389 for (lo = 0; lo < len; lo++) {
390 if (!isspace(lgetc(lp, lo)))
391 break;
392 if (lo == len - 1)
393 inwhitep = TRUE;
394 }
395
396 return (inwhitep);
397 }
398
399
400 /* convert a line/offset pair to a column position (for indenting) */
401 static int
findcolpos(const struct buffer * bp,const struct line * lp,int lo)402 findcolpos(const struct buffer *bp, const struct line *lp, int lo)
403 {
404 int col, i, c;
405 char tmp[5];
406
407 /* determine column */
408 col = 0;
409
410 for (i = 0; i < lo; ++i) {
411 c = lgetc(lp, i);
412 if (c == '\t') {
413 col = ntabstop(col, curbp->b_tabw);
414 } else if (ISCTRL(c) != FALSE)
415 col += 2;
416 else if (isprint(c)) {
417 col++;
418 } else {
419 col += snprintf(tmp, sizeof(tmp), "\\%o", c);
420 }
421
422 }
423 return (col);
424 }
425
426 /*
427 * Find a non-blank line, searching backwards from the supplied line pointer.
428 * For C, nonblank is non-preprocessor, non C++, and accounts
429 * for complete C-style comments.
430 */
431 static struct line *
findnonblank(struct line * lp)432 findnonblank(struct line *lp)
433 {
434 int lo;
435 int nonblankp = FALSE;
436 int commentp = FALSE;
437 int slashp;
438 int astp;
439 int c;
440
441 while (lback(lp) != curbp->b_headp && (commentp || !nonblankp)) {
442 lp = lback(lp);
443 slashp = FALSE;
444 astp = FALSE;
445
446 /* Potential nonblank? */
447 nonblankp = isnonblank(lp, llength(lp));
448
449 /*
450 * Search from end, removing complete C-style
451 * comments. If one is found, ignore it and
452 * test for nonblankness from where it starts.
453 */
454 for (lo = llength(lp) - 1; lo >= 0; lo--) {
455 if (!isspace(c = lgetc(lp, lo))) {
456 if (commentp) { /* find comment "open" */
457 if (c == '*')
458 astp = TRUE;
459 else if (astp && c == '/') {
460 commentp = FALSE;
461 /* whitespace to here? */
462 nonblankp = isnonblank(lp, lo);
463 }
464 } else { /* find comment "close" */
465 if (c == '/')
466 slashp = TRUE;
467 else if (slashp && c == '*')
468 /* found a comment */
469 commentp = TRUE;
470 }
471 }
472 }
473 }
474
475 /* Rewound to start of file? */
476 if (lback(lp) == curbp->b_headp && !nonblankp)
477 return (curbp->b_headp);
478
479 return (lp);
480 }
481
482 /*
483 * Given a line, scan forward to 'omax' and determine if we
484 * are all C whitespace.
485 * Note that preprocessor directives and C++-style comments
486 * count as whitespace. C-style comments do not, and must
487 * be handled elsewhere.
488 */
489 static int
isnonblank(const struct line * lp,int omax)490 isnonblank(const struct line *lp, int omax)
491 {
492 int nonblankp = FALSE; /* Return value */
493 int slashp = FALSE; /* Encountered slash */
494 int lo; /* Loop index */
495 int c; /* char being read */
496
497 /* Scan from front for preprocessor, C++ comments */
498 for (lo = 0; lo < omax; lo++) {
499 if (!isspace(c = lgetc(lp, lo))) {
500 /* Possible nonblank line */
501 nonblankp = TRUE;
502 /* skip // and # starts */
503 if (c == '#' || (slashp && c == '/')) {
504 nonblankp = FALSE;
505 break;
506 } else if (!slashp && c == '/') {
507 slashp = TRUE;
508 continue;
509 }
510 }
511 slashp = FALSE;
512 }
513 return (nonblankp);
514 }
515