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