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 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 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 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 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 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 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 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 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 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 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 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 * 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 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