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