root/usr.bin/mg/cmode.c
/* $OpenBSD: cmode.c,v 1.23 2024/05/21 05:00:48 jsg Exp $ */
/*
 * This file is in the public domain.
 *
 * Author: Kjell Wooding <kjell@openbsd.org>
 */

/*
 * Implement an non-irritating KNF-compliant mode for editing
 * C code.
 */

#include <sys/queue.h>
#include <ctype.h>
#include <signal.h>
#include <stdio.h>

#include "def.h"
#include "funmap.h"
#include "kbd.h"

/* Pull in from modes.c */
extern int changemode(int, int, char *);

static int cc_strip_trailp = TRUE;      /* Delete Trailing space? */
static int cc_basic_indent = 8;         /* Basic Indent multiple */
static int cc_cont_indent = 4;          /* Continued line indent */
static int cc_colon_indent = -8;        /* Label / case indent */

static int getmatch(int, int);
static int getindent(const struct line *, int *);
static int in_whitespace(struct line *, int);
static int findcolpos(const struct buffer *, const struct line *, int);
static struct line *findnonblank(struct line *);
static int isnonblank(const struct line *, int);

void cmode_init(void);

/* Keymaps */

static PF cmode_brace[] = {
        cc_brace,       /* } */
};

static PF cmode_cCP[] = {
        compile,                /* C-c P */
};


static PF cmode_cc[] = {
        NULL,           /* ^C */
        rescan,         /* ^D */
        rescan,         /* ^E */
        rescan,         /* ^F */
        rescan,         /* ^G */
        rescan,         /* ^H */
        cc_tab,         /* ^I */
        rescan,         /* ^J */
        rescan,         /* ^K */
        rescan,         /* ^L */
        cc_lfindent,    /* ^M */
};

static PF cmode_spec[] = {
        cc_char,        /* : */
};

static struct KEYMAPE (1) cmode_cmap = {
        1,
        1,
        rescan,
        {
                { 'P', 'P', cmode_cCP, NULL }
        }
};

static struct KEYMAPE (3) cmodemap = {
        3,
        3,
        rescan,
        {
                { CCHR('C'), CCHR('M'), cmode_cc, (KEYMAP *) &cmode_cmap },
                { ':', ':', cmode_spec, NULL },
                { '}', '}', cmode_brace, NULL }
        }
};

/* Function, Mode hooks */

void
cmode_init(void)
{
        funmap_add(cmode, "c-mode", 0);
        funmap_add(cc_char, "c-handle-special-char", 0);
        funmap_add(cc_brace, "c-handle-special-brace", 0);
        funmap_add(cc_tab, "c-tab-or-indent", 0);
        funmap_add(cc_indent, "c-indent", 0);
        funmap_add(cc_lfindent, "c-indent-and-newline", 0);
        maps_add((KEYMAP *)&cmodemap, "c");
}

/*
 * Enable/toggle c-mode
 */
int
cmode(int f, int n)
{
        return(changemode(f, n, "c"));
}

/*
 * Handle special C character - selfinsert then indent.
 */
int
cc_char(int f, int n)
{
        if (n < 0)
                return (FALSE);
        if (selfinsert(FFRAND, n) == FALSE)
                return (FALSE);
        return (cc_indent(FFRAND, n));
}

/*
 * Handle special C character - selfinsert then indent.
 */
int
cc_brace(int f, int n)
{
        if (n < 0)
                return (FALSE);
        if (showmatch(FFRAND, 1) == FALSE)
                return (FALSE);
        return (cc_indent(FFRAND, n));
}


/*
 * If we are in the whitespace at the beginning of the line,
 * simply act as a regular tab. If we are not, indent
 * current line according to whitespace rules.
 */
int
cc_tab(int f, int n)
{
        int inwhitep = FALSE;   /* In leading whitespace? */

        inwhitep = in_whitespace(curwp->w_dotp, llength(curwp->w_dotp));

        /* If empty line, or in whitespace */
        if (llength(curwp->w_dotp) == 0 || inwhitep)
                return (selfinsert(f, n));

        return (cc_indent(FFRAND, 1));
}

/*
 * Attempt to indent current line according to KNF rules.
 */
int
cc_indent(int f, int n)
{
        int pi, mi;                     /* Previous indents (mi is ignored) */
        int ci;                         /* current indent */
        struct line *lp;
        int ret;

        if (n < 0)
                return (FALSE);

        undo_boundary_enable(FFRAND, 0);
        if (cc_strip_trailp)
                deltrailwhite(FFRAND, 1);

        /*
         * Search backwards for a non-blank, non-preprocessor,
         * non-comment line
         */

        lp = findnonblank(curwp->w_dotp);

        pi = getindent(lp, &mi);

        /* Strip leading space on current line */
        delleadwhite(FFRAND, 1);
        /* current indent is computed only to current position */
        (void)getindent(curwp->w_dotp, &ci);

        if (pi + ci < 0)
                ret = indent(FFOTHARG, 0);
        else
                ret = indent(FFOTHARG, pi + ci);

        undo_boundary_enable(FFRAND, 1);

        return (ret);
}

/*
 * Indent-and-newline (technically, newline then indent)
 */
int
cc_lfindent(int f, int n)
{
        if (n < 0)
                return (FALSE);
        if (cc_strip_trailp)
                (void)delwhite(FFRAND, 1);
        if (enewline(FFRAND, 1) == FALSE)
                return (FALSE);
        return (cc_indent(FFRAND, n));
}

/*
 * Get the level of indentation after line lp is processed
 * Note getindent has two returns:
 * curi = value if indenting current line.
 * return value = value affecting subsequent lines.
 */
static int
getindent(const struct line *lp, int *curi)
{
        int lo, co;             /* leading space,  current offset*/
        int nicol = 0;          /* position count */
        int c = '\0';           /* current char */
        int newind = 0;         /* new index value */
        int stringp = FALSE;    /* in string? */
        int escp = FALSE;       /* Escape char? */
        int lastc = '\0';       /* Last matched string delimiter */
        int nparen = 0;         /* paren count */
        int obrace = 0;         /* open brace count */
        int cbrace = 0;         /* close brace count */
        int firstnwsp = FALSE;  /* First nonspace encountered? */
        int colonp = FALSE;     /* Did we see a colon? */
        int questionp = FALSE;  /* Did we see a question mark? */
        int slashp = FALSE;     /* Slash? */
        int astp = FALSE;       /* Asterisk? */
        int cpos = -1;          /* comment position */
        int cppp  = FALSE;      /* Preprocessor command? */

        *curi = 0;

        /* Compute leading space */
        for (lo = 0; lo < llength(lp); lo++) {
                if (!isspace(c = lgetc(lp, lo)))
                        break;
                if (c == '\t')
                        nicol = ntabstop(nicol, curbp->b_tabw);
                else
                        nicol++;
        }

        /* If last line was blank, choose 0 */
        if (lo == llength(lp))
                nicol = 0;

        newind = 0;
        /* Compute modifiers */
        for (co = lo; co < llength(lp); co++) {
                c = lgetc(lp, co);
                /* We have a non-whitespace char */
                if (!firstnwsp && !isspace(c)) {
                        if (c == '#')
                                cppp = TRUE;
                        firstnwsp = TRUE; 
                }
                if (c == '\\')
                        escp = !escp;
                else if (stringp) {
                        if (!escp && (c == '"' || c == '\'')) {
                                /* unescaped string char */
                                if (getmatch(c, lastc))
                                        stringp = FALSE;
                        }
                } else if (c == '"' || c == '\'') {
                        stringp = TRUE;
                        lastc = c;
                } else if (c == '(') {
                        nparen++;
                } else if (c == ')') {
                        nparen--;
                } else if (c == '{') {
                        obrace++;
                        firstnwsp = FALSE;
                } else if (c == '}') {
                        cbrace++;
                } else if (c == '?') {
                        questionp = TRUE;
                } else if (c == ':') {
                        /* ignore (foo ? bar : baz) construct */
                        if (!questionp)
                                colonp = TRUE;
                } else if (c == '/') {
                        /* first nonwhitespace? -> indent */
                        if (firstnwsp) {
                                /* If previous char asterisk -> close */
                                if (astp)
                                        cpos = -1;
                                else
                                        slashp = TRUE;
                        }
                } else if (c == '*') {
                        /* If previous char slash -> open */
                        if (slashp)
                                cpos = co;
                        else
                                astp = TRUE;
                } else if (firstnwsp) {
                        firstnwsp = FALSE;
                }

                /* Reset matches that apply to next character only */
                if (c != '\\')
                        escp = FALSE;
                if (c != '*')
                        astp = FALSE;
                if (c != '/')
                        slashp = FALSE;
        }
        /*
         * If not terminated with a semicolon, and brace or paren open.
         * we continue
         */
        if (colonp) {
                *curi += cc_colon_indent;
                newind -= cc_colon_indent;
        }

        *curi -= (cbrace) * cc_basic_indent;
        newind += obrace * cc_basic_indent;

        if (nparen < 0)
                newind -= cc_cont_indent;
        else if (nparen > 0)
                newind += cc_cont_indent;

        *curi += nicol;

        /* Ignore preprocessor. Otherwise, add current column */
        if (cppp) {
                newind = nicol;
                *curi = 0;
        } else {
                newind += nicol;
        }

        if (cpos != -1)
                newind = findcolpos(curbp, lp, cpos);

        return (newind);
}

/*
 * Given a delimiter and its purported mate, tell us if they
 * match.
 */
static int
getmatch(int c, int mc)
{
        int match = FALSE;

        switch (c) {
        case '"':
                match = (mc == '"');
                break;
        case '\'':
                match = (mc == '\'');
                break;
        case '(':
                match = (mc == ')');
                break;
        case '[':
                match = (mc == ']');
                break;
        case '{':
                match = (mc == '}');
                break;
        }

        return (match);
}

static int
in_whitespace(struct line *lp, int len)
{
        int lo;
        int inwhitep = FALSE;

        for (lo = 0; lo < len; lo++) {
                if (!isspace(lgetc(lp, lo)))
                        break;
                if (lo == len - 1)
                        inwhitep = TRUE;
        }

        return (inwhitep);
}


/* convert a line/offset pair to a column position (for indenting) */
static int
findcolpos(const struct buffer *bp, const struct line *lp, int lo)
{
        int     col, i, c;
        char tmp[5];

        /* determine column */
        col = 0;

        for (i = 0; i < lo; ++i) {
                c = lgetc(lp, i);
                if (c == '\t') {
                        col = ntabstop(col, curbp->b_tabw);
                } else if (ISCTRL(c) != FALSE)
                        col += 2;
                else if (isprint(c)) {
                        col++;
                } else {
                        col += snprintf(tmp, sizeof(tmp), "\\%o", c);
                }

        }
        return (col);
}

/*
 * Find a non-blank line, searching backwards from the supplied line pointer.
 * For C, nonblank is non-preprocessor, non C++, and accounts
 * for complete C-style comments.
 */
static struct line *
findnonblank(struct line *lp)
{
        int lo;
        int nonblankp = FALSE;
        int commentp = FALSE;
        int slashp;
        int astp;
        int c;

        while (lback(lp) != curbp->b_headp && (commentp || !nonblankp)) {
                lp = lback(lp);
                slashp = FALSE;
                astp = FALSE;

                /* Potential nonblank? */
                nonblankp = isnonblank(lp, llength(lp));

                /*
                 * Search from end, removing complete C-style
                 * comments. If one is found, ignore it and
                 * test for nonblankness from where it starts.
                 */
                for (lo = llength(lp) - 1; lo >= 0; lo--) {
                        if (!isspace(c = lgetc(lp, lo))) {
                                if (commentp) { /* find comment "open" */
                                        if (c == '*')
                                                astp = TRUE;
                                        else if (astp && c == '/') {
                                                commentp = FALSE;
                                                /* whitespace to here? */
                                                nonblankp = isnonblank(lp, lo);
                                        }
                                } else { /* find comment "close" */
                                        if (c == '/')
                                                slashp = TRUE;
                                        else if (slashp && c == '*')
                                                /* found a comment */
                                                commentp = TRUE;
                                }
                        }
                }
        }

        /* Rewound to start of file? */
        if (lback(lp) == curbp->b_headp && !nonblankp)
                return (curbp->b_headp);

        return (lp);
}

/*
 * Given a line, scan forward to 'omax' and determine if we
 * are all C whitespace.
 * Note that preprocessor directives and C++-style comments
 * count as whitespace. C-style comments do not, and must
 * be handled elsewhere.
 */
static int
isnonblank(const struct line *lp, int omax)
{
        int nonblankp = FALSE;          /* Return value */
        int slashp = FALSE;             /* Encountered slash */
        int lo;                         /* Loop index */
        int c;                          /* char being read */

        /* Scan from front for preprocessor, C++ comments */
        for (lo = 0; lo < omax; lo++) {
                if (!isspace(c = lgetc(lp, lo))) {
                        /* Possible nonblank line */
                        nonblankp = TRUE;
                        /* skip // and # starts */
                        if (c == '#' || (slashp && c == '/')) {
                                nonblankp = FALSE;
                                break;
                        } else if (!slashp && c == '/') {
                                slashp = TRUE;
                                continue;
                        }
                }
                slashp = FALSE;
        }
        return (nonblankp);
}