/* * Groovy mode for QEmacs. * * Copyright (c) 2015-2017 Charlie Gordon. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ static const char groovy_keywords[] = { /* language specific keywords */ "as|def|in|trait|" /* documented java keywords */ "assert|break|case|catch|class|const|continue|" "default|do|else|enum|extends|final|finally|for|goto|" "if|implements|import|instanceof|interface|new|" "package|return|super|switch|" "this|throw|throws|try|while|" /* boolean and null literals */ "false|null|true|" /* other java keywords */ "abstract|native|private|protected|public|static|strictfp|" "synchronized|threadsafe|transient|volatile|" }; static const char groovy_types[] = { "void|boolean|byte|char|short|int|long|double|float|" }; enum { IN_GROOVY_COMMENT = 0x01, IN_GROOVY_STRING = 0x02, IN_GROOVY_STRING2 = 0x04, IN_GROOVY_LONG_STRING = 0x08, IN_GROOVY_LONG_STRING2 = 0x10, IN_GROOVY_DOLLAR_STRING = 0x20, }; enum { GROOVY_STYLE_TEXT = QE_STYLE_DEFAULT, GROOVY_STYLE_PREPROCESS = QE_STYLE_PREPROCESS, GROOVY_STYLE_COMMENT = QE_STYLE_COMMENT, GROOVY_STYLE_STRING = QE_STYLE_STRING, GROOVY_STYLE_DOLLAR_STRING = QE_STYLE_STRING, GROOVY_STYLE_REGEX = QE_STYLE_STRING_Q, GROOVY_STYLE_NUMBER = QE_STYLE_NUMBER, GROOVY_STYLE_KEYWORD = QE_STYLE_KEYWORD, GROOVY_STYLE_TYPE = QE_STYLE_TYPE, GROOVY_STYLE_FUNCTION = QE_STYLE_FUNCTION, GROOVY_STYLE_ERROR = QE_STYLE_ERROR, }; static int qe_is_groovy_letter(int c) { return qe_isalpha_(c) || (qe_inrange(c, 0x00C0, 0xFFFE) && c != 0x00D7 && c != 0x00F7); } static int java_scan_number(unsigned int *str0, int flavor) { unsigned int *str = str0; int c = *str++; int octal = 0, nonoctal = 0, isfloat = 0; /* Number types: * binary integers: 0[bB][01]([01_]*[01])?[gliGLI]? * octal integers: 0([0-7_]*[0-7])?[gliGLI]? * hex integers: 0[xX][0-9a-zA-Z]([0-9a-zA-Z_]*[0-9a-zA-Z])? * [gliGLI]? * decimal integers: [1-9]([0-9_]*[0-9])?[gliGLI]? * decimal floats: [0-9]([0-9_]*[0-9])? * [.]([0-9]([0-9_]*[0-9])?)? * ([eE][-+]?[0-9]([0-9_]*[0-9])?)?[dfDF]? * [.][0-9]([0-9_]*[0-9])? * ([eE][-+]?[0-9]([0-9_]*[0-9])?)?[dfDF]? * [0-9]([0-9_]*[0-9])? * [eE][-+]?[0-9]([0-9_]*[0-9])?[dfDF]? * [0-9]([0-9_]*[0-9])?[dfDF] * hex floats: 0[xX][0-9a-zA-Z]([0-9a-zA-Z_]*[0-9a-zA-Z])? * ([.]([0-9a-zA-Z]([0-9a-zA-Z_]*[0-9a-zA-Z])?)?)? * [pP][-+]?[0-9]([0-9_]*[0-9])?[dfDF]? * 0[xX][.][0-9a-zA-Z]([0-9a-zA-Z_]*[0-9a-zA-Z])? * [pP][-+]?[0-9]([0-9_]*[0-9])?[dfDF]? * * Groovy requires a digit after the decimal point. * * This scanner is relaxed to allow for partial numbers at end of * line to avoid showing errors as numbers are typed. */ if (c == '0') { if (qe_match2(*str, 'b', 'B')) { /* binary numbers */ str++; if (!*str) goto done; if (!qe_isbindigit(*str)) goto error; for (str += 1; qe_isbindigit_(*str); str++) continue; if (!*str) goto done; if (str[-1] == '_') goto error; if (qe_findchar("gliGLI", *str)) str++; goto done; } if (qe_match2(*str, 'x', 'X')) { /* hexadecimal numbers */ str++; if (!*str) goto done; if (*str != '.') { if (!qe_isxdigit(*str)) goto error; for (str += 1; qe_isxdigit_(*str); str++) continue; if (!*str) goto done; if (str[-1] == '_') goto error; if (qe_findchar("gliGLI", *str)) { str++; goto done; } } if (qe_findchar(".pP", *str)) { isfloat = 1; if (*str == '.') { if (str == str0 + 2 && !qe_isxdigit(str[1])) goto error; if (flavor == CLANG_GROOVY && !qe_isxdigit(str[1])) goto done; for (str += 1; qe_isxdigit_(*str); str++) continue; } if (!*str) goto done; if (!qe_match2(*str, 'p', 'P')) goto error; str++; if (qe_match2(*str, '+', '-')) str++; if (!*str) goto done; if (!qe_isdigit(*str)) goto error; for (str += 1; qe_isdigit_(*str); str++) continue; if (str[-1] == '_') goto error; } if (qe_findchar("dfDF", *str)) str++; goto done; } octal = 1; } else if (c == '.') str--; /* decimal and octal numbers */ for (; qe_isdigit_(*str); str++) { nonoctal |= qe_match2(*str, '8', '9'); } if (!*str) goto done; if (str[-1] == '_') goto error; if (*str == '.') { if (str == str0 && !qe_isdigit(str[1])) goto done; if (flavor == CLANG_GROOVY && !qe_isdigit(str[1])) goto done; str++; isfloat = 1; if (!*str) goto done; if (qe_isdigit(*str)) { for (str += 1; qe_isdigit_(*str); str++) continue; if (!*str) goto done; if (str[-1] == '_') goto error; } } if (qe_match2(*str, 'e', 'E')) { str++; isfloat = 1; if (qe_match2(*str, '+', '-')) str++; if (!*str) goto done; if (!qe_isdigit(*str)) goto error; for (str += 1; qe_isdigit_(*str); str++) continue; if (!*str) goto done; if (str[-1] == '_') goto error; } if (qe_findchar("dfDF", *str)) { str++; isfloat = 1; goto done; } if (!*str) goto done; if (!isfloat) { if (octal && nonoctal) goto error; if (qe_findchar("gliGLI", *str)) { str++; goto done; } } done: if (!qe_isalnum_(*str)) { return str - str0; } error: while (qe_isalnum_(*str)) str++; return -(str - str0); } static void groovy_colorize_line(QEColorizeContext *cp, unsigned int *str, int n, ModeDef *syn) { int i = 0, start = i, c, style, sep = 0, klen, haslower; int state = cp->colorize_state; char kbuf[64]; /* all these states are exclusive */ if (state & IN_GROOVY_COMMENT) { goto parse_comment; } if (state & IN_GROOVY_STRING) { sep = '\''; goto parse_string; } if (state & IN_GROOVY_STRING2) { sep = '\"'; goto parse_string; } if (state & IN_GROOVY_LONG_STRING) { sep = '\''; goto parse_long_string; } if (state & IN_GROOVY_LONG_STRING2) { sep = '\"'; goto parse_long_string; } if (state & IN_GROOVY_DOLLAR_STRING) { goto parse_dollar_string; } style = 0; while (i < n) { start = i; c = str[i++]; switch (c) { case '#': if (start == 0 && str[i] == '!') { /* shebang line (should do this generically) */ i = n; style = GROOVY_STYLE_PREPROCESS; break; } continue; case '~': while (qe_isblank(str[i])) i++; if (str[i] == '/') { /* parse slashy string as regex */ sep = '/'; start = i++; goto parse_string; } continue; case '/': if (str[i] == '*') { /* normal comment */ i++; parse_comment: state |= IN_GROOVY_COMMENT; for (; i < n; i++) { if (str[i] == '*' && str[i + 1] == '/') { i += 2; state &= ~IN_GROOVY_COMMENT; break; } } style = GROOVY_STYLE_COMMENT; break; } else if (str[i] == '/') { /* line comment */ i = n; style = GROOVY_STYLE_COMMENT; break; } /* XXX: should handle slashy strings */ continue; case '\'': case '\"': /* parse string const */ i--; sep = str[i++]; /* XXX: should colorize interpolated strings */ if (str[i] == (unsigned int)sep && str[i + 1] == (unsigned int)sep) { /* long string */ state |= (sep == '\"') ? IN_GROOVY_LONG_STRING2 : IN_GROOVY_LONG_STRING; i += 2; parse_long_string: while (i < n) { c = str[i++]; if (c == '\\') { if (i < n) { i += 1; } } else if (c == sep && str[i] == (unsigned int)sep && str[i + 1] == (unsigned int)sep) { i += 2; state &= (sep == '\"') ? ~IN_GROOVY_LONG_STRING2 : ~IN_GROOVY_LONG_STRING; break; } } } else { state |= (sep == '\"') ? IN_GROOVY_STRING2 : IN_GROOVY_STRING; parse_string: while (i < n) { c = str[i++]; if (c == '\\') { if (i < n) { i += 1; } } else if (c == sep) { state &= (sep == '\"') ? ~IN_GROOVY_STRING2 : ~IN_GROOVY_STRING; break; } } } style = GROOVY_STYLE_STRING; break; case '$': if (str[i] == '/') { /* $ slashy string */ i++; state |= IN_GROOVY_DOLLAR_STRING; parse_dollar_string: while (i < n) { c = str[i++]; if (c == '$') { if (i < n) { i += 1; } } else if (c == '/' && str[i] == '$') { state &= ~IN_GROOVY_DOLLAR_STRING; i += 2; break; } } style = GROOVY_STYLE_DOLLAR_STRING; break; } goto hasname; case '@': if (qe_isalpha(str[i])) { while (qe_isalnum_(str[i]) || qe_is_groovy_letter(str[i]) || str[i] == '.') i++; if (start == 0 || str[start - 1] != '.') style = GROOVY_STYLE_PREPROCESS; break; } continue; case '.': if (!qe_isdigit(str[i])) continue; /* fallthru */ case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': i--; klen = java_scan_number(str + i, CLANG_GROOVY); if (klen > 0) { i += klen; style = GROOVY_STYLE_NUMBER; break; } else if (klen < 0) { /* Malformed number constants */ i -= klen; style = GROOVY_STYLE_ERROR; break; } i++; continue; default: if (qe_is_groovy_letter(c)) { hasname: haslower = 0; klen = 0; kbuf[klen++] = c; for (; qe_isalnum_(str[i]) || qe_is_groovy_letter(str[i]); i++) { haslower |= qe_islower(str[i]); if (klen < countof(kbuf) - 1) kbuf[klen++] = str[i]; } kbuf[klen] = '\0'; /* keywords are not recognised after '.', * nor before a single '.' nor a map key indicator ':' */ if ((start == 0 || str[start - 1] != '.') && (str[i] != '.' || str[i + 1] == '.') && str[i] != ':') { if ((qe_isupper(c) && haslower && !check_fcall(str, i)) || strfind(syn->types, kbuf)) { style = GROOVY_STYLE_TYPE; break; } if (strfind(syn->keywords, kbuf)) { style = GROOVY_STYLE_KEYWORD; break; } } if (check_fcall(str, i)) { style = GROOVY_STYLE_FUNCTION; break; } continue; } continue; } if (style) { SET_COLOR(str, start, i, style); style = 0; } } /* set style on eol char */ SET_COLOR1(str, n, style); cp->colorize_state = state; } static ModeDef groovy_mode = { .name = "Groovy", .extensions = "groovy|gradle", .shell_handlers = "groovy", .colorize_func = groovy_colorize_line, .colorize_flags = CLANG_GROOVY, .keywords = groovy_keywords, .types = groovy_types, .indent_func = c_indent_line, .auto_indent = 1, .fallback = &c_mode, }; static int groovy_init(void) { qe_register_mode(&groovy_mode, MODEF_SYNTAX); return 0; }