1 /* $OpenBSD: cmode.c,v 1.22 2023/04/21 13:39:37 op 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 nicol = ntabstop(nicol, curbp->b_tabw); 250 else 251 nicol++; 252 } 253 254 /* If last line was blank, choose 0 */ 255 if (lo == llength(lp)) 256 nicol = 0; 257 258 newind = 0; 259 /* Compute modifiers */ 260 for (co = lo; co < llength(lp); co++) { 261 c = lgetc(lp, co); 262 /* We have a non-whitespace char */ 263 if (!firstnwsp && !isspace(c)) { 264 if (c == '#') 265 cppp = TRUE; 266 firstnwsp = TRUE; 267 } 268 if (c == '\\') 269 escp = !escp; 270 else if (stringp) { 271 if (!escp && (c == '"' || c == '\'')) { 272 /* unescaped string char */ 273 if (getmatch(c, lastc)) 274 stringp = FALSE; 275 } 276 } else if (c == '"' || c == '\'') { 277 stringp = TRUE; 278 lastc = c; 279 } else if (c == '(') { 280 nparen++; 281 } else if (c == ')') { 282 nparen--; 283 } else if (c == '{') { 284 obrace++; 285 firstnwsp = FALSE; 286 } else if (c == '}') { 287 cbrace++; 288 } else if (c == '?') { 289 questionp = TRUE; 290 } else if (c == ':') { 291 /* ignore (foo ? bar : baz) construct */ 292 if (!questionp) 293 colonp = TRUE; 294 } else if (c == '/') { 295 /* first nonwhitespace? -> indent */ 296 if (firstnwsp) { 297 /* If previous char asterisk -> close */ 298 if (astp) 299 cpos = -1; 300 else 301 slashp = TRUE; 302 } 303 } else if (c == '*') { 304 /* If previous char slash -> open */ 305 if (slashp) 306 cpos = co; 307 else 308 astp = TRUE; 309 } else if (firstnwsp) { 310 firstnwsp = FALSE; 311 } 312 313 /* Reset matches that apply to next character only */ 314 if (c != '\\') 315 escp = FALSE; 316 if (c != '*') 317 astp = FALSE; 318 if (c != '/') 319 slashp = FALSE; 320 } 321 /* 322 * If not terminated with a semicolon, and brace or paren open. 323 * we continue 324 */ 325 if (colonp) { 326 *curi += cc_colon_indent; 327 newind -= cc_colon_indent; 328 } 329 330 *curi -= (cbrace) * cc_basic_indent; 331 newind += obrace * cc_basic_indent; 332 333 if (nparen < 0) 334 newind -= cc_cont_indent; 335 else if (nparen > 0) 336 newind += cc_cont_indent; 337 338 *curi += nicol; 339 340 /* Ignore preprocessor. Otherwise, add current column */ 341 if (cppp) { 342 newind = nicol; 343 *curi = 0; 344 } else { 345 newind += nicol; 346 } 347 348 if (cpos != -1) 349 newind = findcolpos(curbp, lp, cpos); 350 351 return (newind); 352 } 353 354 /* 355 * Given a delimiter and its purported mate, tell us if they 356 * match. 357 */ 358 static int 359 getmatch(int c, int mc) 360 { 361 int match = FALSE; 362 363 switch (c) { 364 case '"': 365 match = (mc == '"'); 366 break; 367 case '\'': 368 match = (mc == '\''); 369 break; 370 case '(': 371 match = (mc == ')'); 372 break; 373 case '[': 374 match = (mc == ']'); 375 break; 376 case '{': 377 match = (mc == '}'); 378 break; 379 } 380 381 return (match); 382 } 383 384 static int 385 in_whitespace(struct line *lp, int len) 386 { 387 int lo; 388 int inwhitep = FALSE; 389 390 for (lo = 0; lo < len; lo++) { 391 if (!isspace(lgetc(lp, lo))) 392 break; 393 if (lo == len - 1) 394 inwhitep = TRUE; 395 } 396 397 return (inwhitep); 398 } 399 400 401 /* convert a line/offset pair to a column position (for indenting) */ 402 static int 403 findcolpos(const struct buffer *bp, const struct line *lp, int lo) 404 { 405 int col, i, c; 406 char tmp[5]; 407 408 /* determine column */ 409 col = 0; 410 411 for (i = 0; i < lo; ++i) { 412 c = lgetc(lp, i); 413 if (c == '\t') { 414 col = ntabstop(col, curbp->b_tabw); 415 } else if (ISCTRL(c) != FALSE) 416 col += 2; 417 else if (isprint(c)) { 418 col++; 419 } else { 420 col += snprintf(tmp, sizeof(tmp), "\\%o", c); 421 } 422 423 } 424 return (col); 425 } 426 427 /* 428 * Find a non-blank line, searching backwards from the supplied line pointer. 429 * For C, nonblank is non-preprocessor, non C++, and accounts 430 * for complete C-style comments. 431 */ 432 static struct line * 433 findnonblank(struct line *lp) 434 { 435 int lo; 436 int nonblankp = FALSE; 437 int commentp = FALSE; 438 int slashp; 439 int astp; 440 int c; 441 442 while (lback(lp) != curbp->b_headp && (commentp || !nonblankp)) { 443 lp = lback(lp); 444 slashp = FALSE; 445 astp = FALSE; 446 447 /* Potential nonblank? */ 448 nonblankp = isnonblank(lp, llength(lp)); 449 450 /* 451 * Search from end, removing complete C-style 452 * comments. If one is found, ignore it and 453 * test for nonblankness from where it starts. 454 */ 455 for (lo = llength(lp) - 1; lo >= 0; lo--) { 456 if (!isspace(c = lgetc(lp, lo))) { 457 if (commentp) { /* find comment "open" */ 458 if (c == '*') 459 astp = TRUE; 460 else if (astp && c == '/') { 461 commentp = FALSE; 462 /* whitespace to here? */ 463 nonblankp = isnonblank(lp, lo); 464 } 465 } else { /* find comment "close" */ 466 if (c == '/') 467 slashp = TRUE; 468 else if (slashp && c == '*') 469 /* found a comment */ 470 commentp = TRUE; 471 } 472 } 473 } 474 } 475 476 /* Rewound to start of file? */ 477 if (lback(lp) == curbp->b_headp && !nonblankp) 478 return (curbp->b_headp); 479 480 return (lp); 481 } 482 483 /* 484 * Given a line, scan forward to 'omax' and determine if we 485 * are all C whitespace. 486 * Note that preprocessor directives and C++-style comments 487 * count as whitespace. C-style comments do not, and must 488 * be handled elsewhere. 489 */ 490 static int 491 isnonblank(const struct line *lp, int omax) 492 { 493 int nonblankp = FALSE; /* Return value */ 494 int slashp = FALSE; /* Encountered slash */ 495 int lo; /* Loop index */ 496 int c; /* char being read */ 497 498 /* Scan from front for preprocessor, C++ comments */ 499 for (lo = 0; lo < omax; lo++) { 500 if (!isspace(c = lgetc(lp, lo))) { 501 /* Possible nonblank line */ 502 nonblankp = TRUE; 503 /* skip // and # starts */ 504 if (c == '#' || (slashp && c == '/')) { 505 nonblankp = FALSE; 506 break; 507 } else if (!slashp && c == '/') { 508 slashp = TRUE; 509 continue; 510 } 511 } 512 slashp = FALSE; 513 } 514 return (nonblankp); 515 } 516