xref: /openbsd/usr.bin/mg/cmode.c (revision ce7279d8)
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