530 lines
14 KiB
C
Generated
530 lines
14 KiB
C
Generated
#include "tree_sitter/parser.h"
|
|
#include <string.h>
|
|
#include <wctype.h>
|
|
#include <assert.h>
|
|
|
|
#define IS_SPACE_TABS(char) ((char) == ' ' || (char) == '\t')
|
|
#define HEREDOC_MARKER_LEN 32
|
|
|
|
typedef struct {
|
|
// The EOF markers (but they can be whatever so lex that correctly)
|
|
char separator;
|
|
bool ignore_comments;
|
|
uint8_t marker_len;
|
|
char heredoc_marker[HEREDOC_MARKER_LEN];
|
|
} Scanner;
|
|
|
|
typedef struct {
|
|
char * mandat; /// Mandatory part of the command
|
|
char * opt; /// Optional part of the command
|
|
bool ignore_comments_after; /// Ignore comments until EOL
|
|
} keyword;
|
|
|
|
#include "keywords.h"
|
|
|
|
enum TokenType {
|
|
NO,
|
|
INV,
|
|
CMD_SEPARATOR,
|
|
LINE_CONTINUATION,
|
|
SCRIPT_HEREDOC_MARKER,
|
|
LET_HEREDOC_MARKER,
|
|
HEREDOC_END,
|
|
SEP_FIRST,
|
|
SEP,
|
|
SCOPE_DICT,
|
|
SCOPE,
|
|
STRING,
|
|
COMMENT,
|
|
LINE_CONTINUATION_COMMENT,
|
|
BANG_FILTER,
|
|
// Many many many many keywords that are impossible to lex otherwise, see
|
|
// src/keywords.h
|
|
KEYWORDS_BASE
|
|
};
|
|
|
|
// TODO(clason): remove when added to tree-sitter's WASM stdlib
|
|
static inline bool _iswpunct(wint_t wch) {
|
|
return (wch >= 33 && wch <= 47) ||
|
|
(wch >= 58 && wch <= 64) ||
|
|
(wch >= 91 && wch <= 96) ||
|
|
(wch >= 123 && wch <= 126);
|
|
}
|
|
|
|
void *tree_sitter_vim_external_scanner_create() {
|
|
Scanner *s = (Scanner *)malloc(sizeof(Scanner));
|
|
s->separator = '\0';
|
|
s->marker_len = 0;
|
|
s->ignore_comments = false;
|
|
memset(s->heredoc_marker, '\0', HEREDOC_MARKER_LEN);
|
|
|
|
return (void *)s;
|
|
}
|
|
|
|
void tree_sitter_vim_external_scanner_destroy(void *payload) {
|
|
Scanner *s = (Scanner *)payload;
|
|
|
|
free(s);
|
|
}
|
|
|
|
/// Serialize / deserialize
|
|
//
|
|
// Memory layout is :
|
|
//
|
|
// [ marker_len, marker ... (marker_len size) ]
|
|
|
|
#define SC_IGNORE_COMMENTS 0
|
|
#define SC_PAIRED_SEP 1
|
|
#define SC_MARK_LEN 2
|
|
#define SC_MARK 3
|
|
|
|
|
|
unsigned int tree_sitter_vim_external_scanner_serialize(void *payload,
|
|
char *buffer) {
|
|
Scanner *s = (Scanner *)payload;
|
|
buffer[SC_PAIRED_SEP] = s->separator;
|
|
buffer[SC_IGNORE_COMMENTS] = s->ignore_comments;
|
|
buffer[SC_MARK_LEN] = s->marker_len;
|
|
|
|
strncpy(buffer + SC_MARK, s->heredoc_marker, s->marker_len);
|
|
|
|
return s->marker_len + SC_MARK;
|
|
}
|
|
|
|
void tree_sitter_vim_external_scanner_deserialize(void *payload,
|
|
const char *buffer,
|
|
unsigned length) {
|
|
if (length == 0) {
|
|
return;
|
|
}
|
|
|
|
Scanner *s = (Scanner *)payload;
|
|
s->ignore_comments = buffer[SC_IGNORE_COMMENTS];
|
|
s->separator = buffer[SC_PAIRED_SEP];
|
|
s->marker_len = buffer[SC_MARK_LEN];
|
|
|
|
// Sanity check, just to be sure
|
|
assert(s->marker_len + SC_MARK == length);
|
|
assert(s->marker_len < HEREDOC_MARKER_LEN);
|
|
|
|
if (s->marker_len > 0) {
|
|
strncpy(s->heredoc_marker, buffer + SC_MARK, s->marker_len);
|
|
}
|
|
}
|
|
|
|
static void advance(TSLexer *lexer, bool skip) { lexer->advance(lexer, skip); }
|
|
|
|
static void skip_space_tabs(TSLexer *lexer) {
|
|
while (IS_SPACE_TABS(lexer->lookahead)) {
|
|
advance(lexer, true);
|
|
}
|
|
}
|
|
|
|
static bool check_prefix(TSLexer *lexer, char *prefix, unsigned int prefix_len,
|
|
enum TokenType token) {
|
|
for (unsigned int i = 0; i < prefix_len; i++) {
|
|
if (lexer->lookahead == prefix[i]) {
|
|
advance(lexer, false);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
lexer->result_symbol = token;
|
|
return true;
|
|
}
|
|
|
|
static bool try_lex_heredoc_marker(Scanner *scanner, TSLexer *lexer)
|
|
{
|
|
char marker[UINT8_MAX] = { '\0' };
|
|
uint16_t marker_len = 0;
|
|
|
|
if (iswlower(lexer->lookahead)) {
|
|
return false;
|
|
}
|
|
|
|
// We should be at the start of the script marker
|
|
// Note that :let-heredocs do not allow for spaces in the endmarker
|
|
while ((!IS_SPACE_TABS(lexer->lookahead)) && lexer->lookahead && lexer->lookahead != '\n' && lexer->lookahead != '\r' && marker_len < HEREDOC_MARKER_LEN) {
|
|
marker[marker_len] = lexer->lookahead;
|
|
marker_len++;
|
|
advance(lexer, false);
|
|
}
|
|
|
|
if (marker_len == HEREDOC_MARKER_LEN || marker_len == 0) {
|
|
return false;
|
|
}
|
|
|
|
strncpy(scanner->heredoc_marker, marker, marker_len);
|
|
scanner->marker_len = marker_len;
|
|
memset(scanner->heredoc_marker + marker_len, '\0', HEREDOC_MARKER_LEN - marker_len);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool is_valid_string_delim(char c) {
|
|
return c == '\'' || c == '"';
|
|
}
|
|
|
|
static bool lex_literal_string(TSLexer *lexer) {
|
|
while (true) {
|
|
if(lexer->lookahead == '\'') {
|
|
// Maybe end of string, but not sure, it could be double quote
|
|
advance(lexer, false);
|
|
if (lexer->lookahead == '\'') {
|
|
advance(lexer, false);
|
|
} else {
|
|
lexer->result_symbol = STRING;
|
|
lexer->mark_end(lexer);
|
|
return true;
|
|
}
|
|
} else if (lexer->lookahead == '\n' || lexer->lookahead == '\r') {
|
|
// Not sure at this point, look after that if there's not a \\ character
|
|
lexer->mark_end(lexer);
|
|
if (lexer->lookahead == '\r') {
|
|
advance(lexer, true);
|
|
if (lexer->lookahead == '\n') {
|
|
advance(lexer, true);
|
|
}
|
|
} else {
|
|
advance(lexer, true);
|
|
}
|
|
skip_space_tabs(lexer);
|
|
if (lexer->lookahead != '\\') {
|
|
// Was an invalid end...
|
|
return false;
|
|
}
|
|
} else if (lexer->lookahead == '\0') {
|
|
return false;
|
|
} else {
|
|
advance(lexer, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// FIXME: this does not support comments like `" Hello "mister" how are you ?`
|
|
static bool lex_escapable_string(TSLexer *lexer) {
|
|
while (true) {
|
|
if (lexer->lookahead == '\\') {
|
|
advance(lexer, false);
|
|
advance(lexer, false);
|
|
} else if (lexer->lookahead == '"') {
|
|
advance(lexer, false);
|
|
lexer->mark_end(lexer);
|
|
lexer->result_symbol = STRING;
|
|
return true;
|
|
} else if (lexer->lookahead == '\n' || lexer->lookahead == '\r') {
|
|
// Not sure at this point, look after that if there's not a \\ character
|
|
lexer->mark_end(lexer);
|
|
if (lexer->lookahead == '\r') {
|
|
advance(lexer, false);
|
|
if (lexer->lookahead == '\n') {
|
|
advance(lexer, false);
|
|
}
|
|
} else {
|
|
advance(lexer, false);
|
|
}
|
|
skip_space_tabs(lexer);
|
|
if (lexer->lookahead != '\\') {
|
|
// Was a comment...
|
|
lexer->mark_end(lexer);
|
|
lexer->result_symbol = COMMENT;
|
|
return true;
|
|
}
|
|
} else if (lexer->lookahead == '\0') {
|
|
return false;
|
|
} else {
|
|
advance(lexer, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool lex_string(TSLexer *lexer) {
|
|
char string_delim;
|
|
|
|
if (!is_valid_string_delim(lexer->lookahead)) {
|
|
return false;
|
|
}
|
|
|
|
string_delim = lexer->lookahead;
|
|
advance(lexer, false);
|
|
|
|
switch (string_delim) {
|
|
case '"':
|
|
return lex_escapable_string(lexer);
|
|
case '\'':
|
|
return lex_literal_string(lexer);
|
|
default:
|
|
assert(0);
|
|
}
|
|
}
|
|
|
|
static bool try_lex_keyword(char *possible, keyword keyword) {
|
|
if (strlen(possible) > strlen(keyword.mandat) + strlen(keyword.opt)) {
|
|
return false;
|
|
}
|
|
|
|
// Try lexing mandatory part
|
|
size_t i;
|
|
for (i = 0; keyword.mandat[i] && possible[i]; i++) {
|
|
if (possible[i] != keyword.mandat[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// The mandatory part is not complete
|
|
if (keyword.mandat[i] && !possible[i])
|
|
return false;
|
|
|
|
size_t mandat_len = i;
|
|
// Now try lexing optional part
|
|
for (size_t i = 0; keyword.opt[i] && possible[mandat_len + i]; i++) {
|
|
if (possible[mandat_len + i] != keyword.opt[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool scope_correct(TSLexer *lexer) {
|
|
const char *SCOPES = "lbstvwg<";
|
|
for (size_t i = 0; SCOPES[i]; i++) {
|
|
if (lexer->lookahead == SCOPES[i]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool lex_scope(TSLexer *lexer) {
|
|
if (!scope_correct(lexer)) {
|
|
return false;
|
|
}
|
|
|
|
if (lexer->lookahead == '<') {
|
|
advance(lexer, false);
|
|
const char sid[4 + 1] = "SID>";
|
|
for (size_t i = 0; sid[i] && lexer->lookahead; i++) {
|
|
if (lexer->lookahead != sid[i]) {
|
|
return false;
|
|
}
|
|
advance(lexer, false);
|
|
}
|
|
lexer->result_symbol = SCOPE;
|
|
return true;
|
|
} else {
|
|
advance(lexer, false);
|
|
|
|
if (lexer->lookahead != ':') {
|
|
return false;
|
|
}
|
|
advance(lexer, false);
|
|
|
|
if (iswalnum(lexer->lookahead) || lexer->lookahead == '{' || lexer->lookahead == '_') {
|
|
lexer->result_symbol = SCOPE;
|
|
} else {
|
|
lexer->result_symbol = SCOPE_DICT;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool tree_sitter_vim_external_scanner_scan(void *payload, TSLexer *lexer,
|
|
const bool *valid_symbols) {
|
|
Scanner *s = (Scanner *)payload;
|
|
assert(valid_symbols[LINE_CONTINUATION]);
|
|
|
|
skip_space_tabs(lexer);
|
|
if (!lexer->lookahead) {
|
|
return false;
|
|
}
|
|
|
|
// Not sure about the punctuation here...
|
|
if (valid_symbols[SEP_FIRST] && _iswpunct(lexer->lookahead)) {
|
|
s->separator = lexer->lookahead;
|
|
advance(lexer, false);
|
|
s->ignore_comments = true;
|
|
lexer->result_symbol = SEP_FIRST;
|
|
return true;
|
|
} else if (valid_symbols[SEP] && s->separator == lexer->lookahead) {
|
|
// No need to check for s->separator == 0 above because we know
|
|
// lexer->lookahead is != 0
|
|
advance(lexer, false);
|
|
s->ignore_comments = false;
|
|
lexer->result_symbol = SEP;
|
|
return true;
|
|
}
|
|
|
|
if (valid_symbols[BANG_FILTER] && lexer->lookahead == '!') {
|
|
advance(lexer, false);
|
|
s->ignore_comments = true;
|
|
lexer->result_symbol = BANG_FILTER;
|
|
return true;
|
|
}
|
|
|
|
// options can be inverted by prepending a 'no' or 'inv'
|
|
if (valid_symbols[NO] && lexer->lookahead == 'n') {
|
|
return check_prefix(lexer, "no", 2, NO);
|
|
} else if (valid_symbols[INV] && lexer->lookahead == 'i') {
|
|
return check_prefix(lexer, "inv", 3, INV);
|
|
}
|
|
|
|
// cmd separator and | this is not trivial at all because of how line
|
|
// continuations are handled after encoutering an EOL :
|
|
// - Next line starts by a `\\` ?
|
|
// - Yes : is next character `/` or `?` ?
|
|
// - Yes : Next line is another command (preceded by a range)
|
|
// - No : This is a line continuation
|
|
// - No : Next line is another command.
|
|
//
|
|
// This ambiguity forces us to use the mark_end function and lookahead more
|
|
// than just past the final newline and indentationg character.
|
|
if (lexer->lookahead == '\n' || lexer->lookahead == '\r') {
|
|
if (lexer->lookahead == '\r') {
|
|
advance(lexer, false);
|
|
if (lexer->lookahead == '\n') {
|
|
advance(lexer, false);
|
|
}
|
|
} else {
|
|
advance(lexer, false);
|
|
}
|
|
lexer->mark_end(lexer);
|
|
skip_space_tabs(lexer);
|
|
|
|
if (lexer->lookahead == '\\') {
|
|
// You think this is a line continuation ? It might not
|
|
advance(lexer, false);
|
|
|
|
if (lexer->lookahead == '/'
|
|
|| lexer->lookahead == '?'
|
|
|| lexer->lookahead == '&') {
|
|
// Actually this might be a range before a command
|
|
if (valid_symbols[CMD_SEPARATOR]) {
|
|
lexer->result_symbol = CMD_SEPARATOR;
|
|
s->ignore_comments = false;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
lexer->mark_end(lexer);
|
|
lexer->result_symbol = LINE_CONTINUATION;
|
|
return true;
|
|
} else if (s->marker_len == 0 && check_prefix(lexer, "\"\\ ", 3, LINE_CONTINUATION_COMMENT)) {
|
|
while (lexer->lookahead != '\0' && lexer->lookahead != '\n' && lexer->lookahead != '\r') {
|
|
advance(lexer, false);
|
|
}
|
|
lexer->mark_end(lexer);
|
|
return true;
|
|
} else if (valid_symbols[CMD_SEPARATOR]) {
|
|
lexer->result_symbol = CMD_SEPARATOR;
|
|
s->ignore_comments = false;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (valid_symbols[CMD_SEPARATOR] && lexer->lookahead == '|') {
|
|
advance(lexer, false);
|
|
if (lexer->lookahead == '|') {
|
|
// This is an or expression
|
|
return false;
|
|
}
|
|
lexer->result_symbol = CMD_SEPARATOR;
|
|
return true;
|
|
}
|
|
|
|
if (scope_correct(lexer) && (valid_symbols[SCOPE_DICT] || valid_symbols[SCOPE])) {
|
|
if (lex_scope(lexer)) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (valid_symbols[SCRIPT_HEREDOC_MARKER]) {
|
|
lexer->result_symbol = SCRIPT_HEREDOC_MARKER;
|
|
return try_lex_heredoc_marker(s, lexer);
|
|
}
|
|
if (valid_symbols[LET_HEREDOC_MARKER]) {
|
|
lexer->result_symbol = LET_HEREDOC_MARKER;
|
|
return try_lex_heredoc_marker(s, lexer);
|
|
}
|
|
if (valid_symbols[HEREDOC_END]) {
|
|
uint8_t marker_len = s->marker_len != 0 ? s->marker_len : 1;
|
|
char *marker = s->marker_len != 0 ? s->heredoc_marker : ".";
|
|
|
|
if (!check_prefix(lexer, marker, marker_len, HEREDOC_END)) {
|
|
return false;
|
|
}
|
|
|
|
// Ensure there aren't any remaining characters in the line
|
|
if (lexer->lookahead != '\0' && lexer->lookahead != '\n' && lexer->lookahead != '\r') {
|
|
return false;
|
|
}
|
|
|
|
// Found the end marker
|
|
s->marker_len = 0;
|
|
memset(s->heredoc_marker, '\0', HEREDOC_MARKER_LEN);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (valid_symbols[COMMENT] && !valid_symbols[STRING]
|
|
&& lexer->lookahead == '"' && !s->ignore_comments) {
|
|
// This con only be a comment
|
|
do {
|
|
advance(lexer, false);
|
|
} while (lexer->lookahead != '\n' && lexer->lookahead != '\r' && lexer->lookahead != '\0');
|
|
|
|
lexer->result_symbol = COMMENT;
|
|
return true;
|
|
} else if (valid_symbols[STRING]) {
|
|
return lex_string(lexer);
|
|
}
|
|
|
|
// Other keywords
|
|
if (iswlower(lexer->lookahead)) {
|
|
#define KEYWORD_SIZE 30
|
|
char keyword[KEYWORD_SIZE + 1] = { lexer->lookahead, 0 };
|
|
|
|
advance(lexer, false);
|
|
|
|
size_t i = 1;
|
|
for (; i < KEYWORD_SIZE && iswalpha(lexer->lookahead); i++) {
|
|
keyword[i] = lexer->lookahead;
|
|
advance(lexer, false);
|
|
}
|
|
|
|
if (i == KEYWORD_SIZE) {
|
|
return false;
|
|
}
|
|
|
|
keyword[i] = '\0';
|
|
|
|
// Now really try to find the keyword
|
|
for (kwid t = FUNCTION; t < UNKNOWN_COMMAND; t++) {
|
|
if (valid_symbols[t + KEYWORDS_BASE] && try_lex_keyword(keyword, keywords[t])) {
|
|
lexer->result_symbol = t + KEYWORDS_BASE;
|
|
s->ignore_comments = keywords[t].ignore_comments_after;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// This is quite possibly a keyword, we just don't know which one yet
|
|
if (valid_symbols[UNKNOWN_COMMAND + KEYWORDS_BASE]) {
|
|
lexer->result_symbol = UNKNOWN_COMMAND + KEYWORDS_BASE;
|
|
return true;
|
|
}
|
|
#undef KEYWORD_SIZE
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// vim: tabstop=2
|