1
// Scintilla source code edit control
3
** Lexer for SQL, including PL/SQL and SQL*Plus.
5
// Copyright 1998-2011 by Neil Hodgson <neilh@scintilla.org>
6
// The License.txt file describes the conditions under which this software may be distributed.
16
#pragma warning(disable: 4786)
25
#include "Scintilla.h"
29
#include "LexAccessor.h"
31
#include "StyleContext.h"
32
#include "CharacterSet.h"
33
#include "LexerModule.h"
34
#include "OptionSet.h"
37
using namespace Scintilla;
40
static inline bool IsAWordChar(int ch, bool sqlAllowDottedWord) {
41
if (!sqlAllowDottedWord)
42
return (ch < 0x80) && (isalnum(ch) || ch == '_');
44
return (ch < 0x80) && (isalnum(ch) || ch == '_' || ch == '.');
47
static inline bool IsAWordStart(int ch) {
48
return (ch < 0x80) && (isalpha(ch) || ch == '_');
51
static inline bool IsADoxygenChar(int ch) {
52
return (islower(ch) || ch == '$' || ch == '@' ||
53
ch == '\\' || ch == '&' || ch == '<' ||
54
ch == '>' || ch == '#' || ch == '{' ||
55
ch == '}' || ch == '[' || ch == ']');
58
static inline bool IsANumberChar(int ch) {
59
// Not exactly following number definition (several dots are seen as OK, etc.)
60
// but probably enough in most cases.
62
(isdigit(ch) || toupper(ch) == 'E' ||
63
ch == '.' || ch == '-' || ch == '+');
69
void Set(int lineNumber, unsigned short int sqlStatesLine) {
70
if (!sqlStatement.size() == 0 || !sqlStatesLine == 0) {
71
sqlStatement.resize(lineNumber + 1, 0);
72
sqlStatement[lineNumber] = sqlStatesLine;
76
unsigned short int IgnoreWhen (unsigned short int sqlStatesLine, bool enable) {
78
sqlStatesLine |= MASK_IGNORE_WHEN;
80
sqlStatesLine &= ~MASK_IGNORE_WHEN;
85
unsigned short int IntoCondition (unsigned short int sqlStatesLine, bool enable) {
87
sqlStatesLine |= MASK_INTO_CONDITION;
89
sqlStatesLine &= ~MASK_INTO_CONDITION;
94
unsigned short int IntoExceptionBlock (unsigned short int sqlStatesLine, bool enable) {
96
sqlStatesLine |= MASK_INTO_EXCEPTION;
98
sqlStatesLine &= ~MASK_INTO_EXCEPTION;
100
return sqlStatesLine;
103
unsigned short int IntoDeclareBlock (unsigned short int sqlStatesLine, bool enable) {
105
sqlStatesLine |= MASK_INTO_DECLARE;
107
sqlStatesLine &= ~MASK_INTO_DECLARE;
109
return sqlStatesLine;
112
unsigned short int BeginCaseBlock (unsigned short int sqlStatesLine) {
113
if ((sqlStatesLine & MASK_NESTED_CASES) < MASK_NESTED_CASES) {
116
return sqlStatesLine;
119
unsigned short int EndCaseBlock (unsigned short int sqlStatesLine) {
120
if ((sqlStatesLine & MASK_NESTED_CASES) > 0) {
123
return sqlStatesLine;
126
bool IsIgnoreWhen (unsigned short int sqlStatesLine) {
127
return (sqlStatesLine & MASK_IGNORE_WHEN) != 0;
130
bool IsIntoCondition (unsigned short int sqlStatesLine) {
131
return (sqlStatesLine & MASK_INTO_CONDITION) != 0;
134
bool IsIntoCaseBlock (unsigned short int sqlStatesLine) {
135
return (sqlStatesLine & MASK_NESTED_CASES) != 0;
138
bool IsIntoExceptionBlock (unsigned short int sqlStatesLine) {
139
return (sqlStatesLine & MASK_INTO_EXCEPTION) != 0;
142
bool IsIntoDeclareBlock (unsigned short int sqlStatesLine) {
143
return (sqlStatesLine & MASK_INTO_DECLARE) != 0;
146
unsigned short int ForLine(int lineNumber) {
147
if ((lineNumber > 0) && (sqlStatement.size() > static_cast<size_t>(lineNumber))) {
148
return sqlStatement[lineNumber];
157
std::vector <unsigned short int> sqlStatement;
159
MASK_INTO_DECLARE = 0x1000,
160
MASK_INTO_EXCEPTION = 0x2000,
161
MASK_INTO_CONDITION = 0x4000,
162
MASK_IGNORE_WHEN = 0x8000,
163
MASK_NESTED_CASES = 0x0FFF
167
// Options used for LexerSQL
174
bool sqlBackticksIdentifier;
175
bool sqlNumbersignComment;
176
bool sqlBackslashEscapes;
177
bool sqlAllowDottedWord;
183
foldOnlyBegin = false;
184
sqlBackticksIdentifier = false;
185
sqlNumbersignComment = false;
186
sqlBackslashEscapes = false;
187
sqlAllowDottedWord = false;
191
static const char * const sqlWordListDesc[] = {
203
struct OptionSetSQL : public OptionSet<OptionsSQL> {
205
DefineProperty("fold", &OptionsSQL::fold);
207
DefineProperty("lexer.sql.fold.at.else", &OptionsSQL::foldAtElse,
208
"This option enables SQL folding on a \"ELSE\" and \"ELSIF\"line of an IF statement.");
210
DefineProperty("fold.comment", &OptionsSQL::foldComment);
212
DefineProperty("fold.compact", &OptionsSQL::foldCompact);
214
DefineProperty("fold.sql.only.begin", &OptionsSQL::foldOnlyBegin);
216
DefineProperty("lexer.sql.backticks.identifier", &OptionsSQL::sqlBackticksIdentifier);
218
DefineProperty("lexer.sql.numbersign.comment", &OptionsSQL::sqlNumbersignComment,
219
"If \"lexer.sql.numbersign.comment\" property is set to 0 a line beginning with '#' will not be a comment.");
221
DefineProperty("sql.backslash.escapes", &OptionsSQL::sqlBackslashEscapes,
222
"Enables backslash as an escape character in SQL.");
224
DefineProperty("lexer.sql.allow.dotted.word", &OptionsSQL::sqlAllowDottedWord,
225
"Set to 1 to colourise recognized words with dots "
226
"(recommended for Oracle PL/SQL objects).");
228
DefineWordListSets(sqlWordListDesc);
232
class LexerSQL : public ILexer {
236
int SCI_METHOD Version () const {
240
void SCI_METHOD Release() {
244
const char * SCI_METHOD PropertyNames() {
245
return osSQL.PropertyNames();
248
int SCI_METHOD PropertyType(const char *name) {
249
return osSQL.PropertyType(name);
252
const char * SCI_METHOD DescribeProperty(const char *name) {
253
return osSQL.DescribeProperty(name);
256
int SCI_METHOD PropertySet(const char *key, const char *val) {
257
if (osSQL.PropertySet(&options, key, val)) {
263
const char * SCI_METHOD DescribeWordListSets() {
264
return osSQL.DescribeWordListSets();
267
int SCI_METHOD WordListSet(int n, const char *wl);
268
void SCI_METHOD Lex (unsigned int startPos, int lengthDoc, int initStyle, IDocument *pAccess);
269
void SCI_METHOD Fold(unsigned int startPos, int lengthDoc, int initStyle, IDocument *pAccess);
271
void * SCI_METHOD PrivateCall(int, void *) {
275
static ILexer *LexerFactorySQL() {
276
return new LexerSQL();
279
bool IsStreamCommentStyle(int style) {
280
return style == SCE_SQL_COMMENT ||
281
style == SCE_SQL_COMMENTDOC ||
282
style == SCE_SQL_COMMENTDOCKEYWORD ||
283
style == SCE_SQL_COMMENTDOCKEYWORDERROR;
300
int SCI_METHOD LexerSQL::WordListSet(int n, const char *wl) {
301
WordList *wordListN = 0;
304
wordListN = &keywords1;
307
wordListN = &keywords2;
310
wordListN = &kw_pldoc;
313
wordListN = &kw_sqlplus;
316
wordListN = &kw_user1;
319
wordListN = &kw_user2;
322
wordListN = &kw_user3;
325
wordListN = &kw_user4;
327
int firstModification = -1;
331
if (*wordListN != wlNew) {
333
firstModification = 0;
336
return firstModification;
339
void SCI_METHOD LexerSQL::Lex(unsigned int startPos, int length, int initStyle, IDocument *pAccess) {
340
LexAccessor styler(pAccess);
341
StyleContext sc(startPos, length, initStyle, styler);
342
int styleBeforeDCKeyword = SCE_SQL_DEFAULT;
344
for (; sc.More(); sc.Forward(), offset++) {
345
// Determine if the current state should terminate.
347
case SCE_SQL_OPERATOR:
348
sc.SetState(SCE_SQL_DEFAULT);
351
// We stop the number definition on non-numerical non-dot non-eE non-sign char
352
if (!IsANumberChar(sc.ch)) {
353
sc.SetState(SCE_SQL_DEFAULT);
356
case SCE_SQL_IDENTIFIER:
357
if (!IsAWordChar(sc.ch, options.sqlAllowDottedWord)) {
358
int nextState = SCE_SQL_DEFAULT;
360
sc.GetCurrentLowered(s, sizeof(s));
361
if (keywords1.InList(s)) {
362
sc.ChangeState(SCE_SQL_WORD);
363
} else if (keywords2.InList(s)) {
364
sc.ChangeState(SCE_SQL_WORD2);
365
} else if (kw_sqlplus.InListAbbreviated(s, '~')) {
366
sc.ChangeState(SCE_SQL_SQLPLUS);
367
if (strncmp(s, "rem", 3) == 0) {
368
nextState = SCE_SQL_SQLPLUS_COMMENT;
369
} else if (strncmp(s, "pro", 3) == 0) {
370
nextState = SCE_SQL_SQLPLUS_PROMPT;
372
} else if (kw_user1.InList(s)) {
373
sc.ChangeState(SCE_SQL_USER1);
374
} else if (kw_user2.InList(s)) {
375
sc.ChangeState(SCE_SQL_USER2);
376
} else if (kw_user3.InList(s)) {
377
sc.ChangeState(SCE_SQL_USER3);
378
} else if (kw_user4.InList(s)) {
379
sc.ChangeState(SCE_SQL_USER4);
381
sc.SetState(nextState);
384
case SCE_SQL_QUOTEDIDENTIFIER:
386
if (sc.chNext == 0x60) {
387
sc.Forward(); // Ignore it
389
sc.ForwardSetState(SCE_SQL_DEFAULT);
393
case SCE_SQL_COMMENT:
394
if (sc.Match('*', '/')) {
396
sc.ForwardSetState(SCE_SQL_DEFAULT);
399
case SCE_SQL_COMMENTDOC:
400
if (sc.Match('*', '/')) {
402
sc.ForwardSetState(SCE_SQL_DEFAULT);
403
} else if (sc.ch == '@' || sc.ch == '\\') { // Doxygen support
404
// Verify that we have the conditions to mark a comment-doc-keyword
405
if ((IsASpace(sc.chPrev) || sc.chPrev == '*') && (!IsASpace(sc.chNext))) {
406
styleBeforeDCKeyword = SCE_SQL_COMMENTDOC;
407
sc.SetState(SCE_SQL_COMMENTDOCKEYWORD);
411
case SCE_SQL_COMMENTLINE:
412
case SCE_SQL_COMMENTLINEDOC:
413
case SCE_SQL_SQLPLUS_COMMENT:
414
case SCE_SQL_SQLPLUS_PROMPT:
415
if (sc.atLineStart) {
416
sc.SetState(SCE_SQL_DEFAULT);
419
case SCE_SQL_COMMENTDOCKEYWORD:
420
if ((styleBeforeDCKeyword == SCE_SQL_COMMENTDOC) && sc.Match('*', '/')) {
421
sc.ChangeState(SCE_SQL_COMMENTDOCKEYWORDERROR);
423
sc.ForwardSetState(SCE_SQL_DEFAULT);
424
} else if (!IsADoxygenChar(sc.ch)) {
426
sc.GetCurrentLowered(s, sizeof(s));
427
if (!isspace(sc.ch) || !kw_pldoc.InList(s + 1)) {
428
sc.ChangeState(SCE_SQL_COMMENTDOCKEYWORDERROR);
430
sc.SetState(styleBeforeDCKeyword);
433
case SCE_SQL_CHARACTER:
434
if (options.sqlBackslashEscapes && sc.ch == '\\') {
436
} else if (sc.ch == '\'') {
437
if (sc.chNext == '\"') {
440
sc.ForwardSetState(SCE_SQL_DEFAULT);
448
} else if (sc.ch == '\"') {
449
if (sc.chNext == '\"') {
452
sc.ForwardSetState(SCE_SQL_DEFAULT);
458
// Determine if a new state should be entered.
459
if (sc.state == SCE_SQL_DEFAULT) {
460
if (IsADigit(sc.ch) || (sc.ch == '.' && IsADigit(sc.chNext))) {
461
sc.SetState(SCE_SQL_NUMBER);
462
} else if (IsAWordStart(sc.ch)) {
463
sc.SetState(SCE_SQL_IDENTIFIER);
464
} else if (sc.ch == 0x60 && options.sqlBackticksIdentifier) {
465
sc.SetState(SCE_SQL_QUOTEDIDENTIFIER);
466
} else if (sc.Match('/', '*')) {
467
if (sc.Match("/**") || sc.Match("/*!")) { // Support of Doxygen doc. style
468
sc.SetState(SCE_SQL_COMMENTDOC);
470
sc.SetState(SCE_SQL_COMMENT);
472
sc.Forward(); // Eat the * so it isn't used for the end of the comment
473
} else if (sc.Match('-', '-')) {
474
// MySQL requires a space or control char after --
475
// http://dev.mysql.com/doc/mysql/en/ansi-diff-comments.html
476
// Perhaps we should enforce that with proper property:
477
//~ } else if (sc.Match("-- ")) {
478
sc.SetState(SCE_SQL_COMMENTLINE);
479
} else if (sc.ch == '#' && options.sqlNumbersignComment) {
480
sc.SetState(SCE_SQL_COMMENTLINEDOC);
481
} else if (sc.ch == '\'') {
482
sc.SetState(SCE_SQL_CHARACTER);
483
} else if (sc.ch == '\"') {
484
sc.SetState(SCE_SQL_STRING);
485
} else if (isoperator(static_cast<char>(sc.ch))) {
486
sc.SetState(SCE_SQL_OPERATOR);
493
void SCI_METHOD LexerSQL::Fold(unsigned int startPos, int length, int initStyle, IDocument *pAccess) {
496
LexAccessor styler(pAccess);
497
unsigned int endPos = startPos + length;
498
int visibleChars = 0;
499
int lineCurrent = styler.GetLine(startPos);
500
int levelCurrent = SC_FOLDLEVELBASE;
501
if (lineCurrent > 0) {
502
levelCurrent = styler.LevelAt(lineCurrent - 1) >> 16;
504
int levelNext = levelCurrent;
505
char chNext = styler[startPos];
506
int styleNext = styler.StyleAt(startPos);
507
int style = initStyle;
508
bool endFound = false;
509
bool isUnfoldingIgnored = false;
510
// this statementFound flag avoids to fold when the statement is on only one line by ignoring ELSE or ELSIF
511
// eg. "IF condition1 THEN ... ELSIF condition2 THEN ... ELSE ... END IF;"
512
bool statementFound = false;
513
unsigned short int sqlStatesCurrentLine = 0;
514
if (!options.foldOnlyBegin) {
515
sqlStatesCurrentLine = sqlStates.ForLine(lineCurrent);
517
for (unsigned int i = startPos; i < endPos; i++) {
519
chNext = styler.SafeGetCharAt(i + 1);
520
int stylePrev = style;
522
styleNext = styler.StyleAt(i + 1);
523
bool atEOL = (ch == '\r' && chNext != '\n') || (ch == '\n');
524
if (atEOL || (ch == ';')) {
526
//Maybe this is the end of "EXCEPTION" BLOCK (eg. "BEGIN ... EXCEPTION ... END;")
527
sqlStatesCurrentLine = sqlStates.IntoExceptionBlock(sqlStatesCurrentLine, false);
529
// set endFound and isUnfoldingIgnored to false if EOL is reached or ';' is found
531
isUnfoldingIgnored = false;
533
if (options.foldComment && IsStreamCommentStyle(style)) {
534
if (!IsStreamCommentStyle(stylePrev)) {
536
} else if (!IsStreamCommentStyle(styleNext) && !atEOL) {
537
// Comments don't end at end of line and the next character may be unstyled.
541
if (options.foldComment && (style == SCE_SQL_COMMENTLINE)) {
542
// MySQL needs -- comments to be followed by space or control char
543
if ((ch == '-') && (chNext == '-')) {
544
char chNext2 = styler.SafeGetCharAt(i + 2);
545
char chNext3 = styler.SafeGetCharAt(i + 3);
546
if (chNext2 == '{' || chNext3 == '{') {
548
} else if (chNext2 == '}' || chNext3 == '}') {
553
if (style == SCE_SQL_OPERATOR) {
555
if (levelCurrent > levelNext)
558
} else if (ch == ')') {
560
} else if ((!options.foldOnlyBegin) && ch == ';') {
561
sqlStatesCurrentLine = sqlStates.IgnoreWhen(sqlStatesCurrentLine, false);
564
// If new keyword (cannot trigger on elseif or nullif, does less tests)
565
if (style == SCE_SQL_WORD && stylePrev != SCE_SQL_WORD) {
566
const int MAX_KW_LEN = 9; // Maximum length of folding keywords
567
char s[MAX_KW_LEN + 2];
569
for (; j < MAX_KW_LEN + 1; j++) {
570
if (!iswordchar(styler[i + j])) {
573
s[j] = static_cast<char>(tolower(styler[i + j]));
575
if (j == MAX_KW_LEN + 1) {
576
// Keyword too long, don't test it
582
if (strcmp(s, "if") == 0) {
585
if (options.foldOnlyBegin && !isUnfoldingIgnored) {
586
// this end isn't for begin block, but for if block ("end if;")
587
// so ignore previous "end" by increment levelNext.
591
if (!options.foldOnlyBegin)
592
sqlStatesCurrentLine = sqlStates.IntoCondition(sqlStatesCurrentLine, true);
593
if (levelCurrent > levelNext) {
594
// doesn't include this line into the folding block
595
// because doesn't hide IF (eg "END; IF")
596
levelCurrent = levelNext;
599
} else if (!options.foldOnlyBegin &&
600
strcmp(s, "then") == 0 &&
601
sqlStates.IsIntoCondition(sqlStatesCurrentLine)) {
602
sqlStatesCurrentLine = sqlStates.IntoCondition(sqlStatesCurrentLine, false);
603
if (!options.foldOnlyBegin) {
604
if (levelCurrent > levelNext) {
605
levelCurrent = levelNext;
610
statementFound = true;
611
} else if (levelCurrent > levelNext) {
612
// doesn't include this line into the folding block
613
// because doesn't hide LOOP or CASE (eg "END; LOOP" or "END; CASE")
614
levelCurrent = levelNext;
616
} else if (strcmp(s, "loop") == 0 ||
617
strcmp(s, "case") == 0) {
620
if (options.foldOnlyBegin && !isUnfoldingIgnored) {
621
// this end isn't for begin block, but for loop block ("end loop;") or case block ("end case;")
622
// so ignore previous "end" by increment levelNext.
625
if ((!options.foldOnlyBegin) && strcmp(s, "case") == 0) {
626
sqlStatesCurrentLine = sqlStates.EndCaseBlock(sqlStatesCurrentLine);
627
levelNext--; //again for the "end case;" and block when
629
} else if (!options.foldOnlyBegin) {
630
if (strcmp(s, "case") == 0) {
631
sqlStatesCurrentLine = sqlStates.BeginCaseBlock(sqlStatesCurrentLine);
633
//for case block increment 2 times
638
if (levelCurrent > levelNext) {
639
levelCurrent = levelNext;
644
statementFound = true;
645
} else if (levelCurrent > levelNext) {
646
// doesn't include this line into the folding block
647
// because doesn't hide LOOP or CASE (eg "END; LOOP" or "END; CASE")
648
levelCurrent = levelNext;
650
} else if ((!options.foldOnlyBegin) && (
651
// folding for ELSE and ELSIF block only if foldAtElse is set
652
// and IF or CASE aren't on only one line with ELSE or ELSIF (with flag statementFound)
653
options.foldAtElse && !statementFound) && strcmp(s, "elsif") == 0) {
654
sqlStatesCurrentLine = sqlStates.IntoCondition(sqlStatesCurrentLine, true);
657
} else if ((!options.foldOnlyBegin) && (
658
// folding for ELSE and ELSIF block only if foldAtElse is set
659
// and IF or CASE aren't on only one line with ELSE or ELSIF (with flag statementFound)
660
options.foldAtElse && !statementFound) && strcmp(s, "else") == 0) {
661
// prevent also ELSE is on the same line (eg. "ELSE ... END IF;")
662
statementFound = true;
663
// we are in same case "} ELSE {" in C language
666
} else if (strcmp(s, "begin") == 0) {
668
sqlStatesCurrentLine = sqlStates.IntoDeclareBlock(sqlStatesCurrentLine, false);
669
} else if ((strcmp(s, "end") == 0) ||
670
// SQL Anywhere permits IF ... ELSE ... ENDIF
671
// will only be active if "endif" appears in the
673
(strcmp(s, "endif") == 0)) {
676
if (levelNext < SC_FOLDLEVELBASE) {
677
levelNext = SC_FOLDLEVELBASE;
678
isUnfoldingIgnored = true;
680
} else if ((!options.foldOnlyBegin) &&
681
strcmp(s, "when") == 0 &&
682
!sqlStates.IsIgnoreWhen(sqlStatesCurrentLine) &&
683
!sqlStates.IsIntoExceptionBlock(sqlStatesCurrentLine) &&
684
sqlStates.IsIntoCaseBlock(sqlStatesCurrentLine)) {
685
sqlStatesCurrentLine = sqlStates.IntoCondition(sqlStatesCurrentLine, true);
687
// Don't foldind when CASE and WHEN are on the same line (with flag statementFound) (eg. "CASE selector WHEN expression1 THEN sequence_of_statements1;\n")
688
if (!statementFound) {
692
} else if ((!options.foldOnlyBegin) && strcmp(s, "exit") == 0) {
693
sqlStatesCurrentLine = sqlStates.IgnoreWhen(sqlStatesCurrentLine, true);
694
} else if ((!options.foldOnlyBegin) && !sqlStates.IsIntoDeclareBlock(sqlStatesCurrentLine) && strcmp(s, "exception") == 0) {
695
sqlStatesCurrentLine = sqlStates.IntoExceptionBlock(sqlStatesCurrentLine, true);
696
} else if ((!options.foldOnlyBegin) &&
697
(strcmp(s, "declare") == 0 ||
698
strcmp(s, "function") == 0 ||
699
strcmp(s, "procedure") == 0 ||
700
strcmp(s, "package") == 0)) {
701
sqlStatesCurrentLine = sqlStates.IntoDeclareBlock(sqlStatesCurrentLine, true);
705
int levelUse = levelCurrent;
706
int lev = levelUse | levelNext << 16;
707
if (visibleChars == 0 && options.foldCompact)
708
lev |= SC_FOLDLEVELWHITEFLAG;
709
if (levelUse < levelNext)
710
lev |= SC_FOLDLEVELHEADERFLAG;
711
if (lev != styler.LevelAt(lineCurrent)) {
712
styler.SetLevel(lineCurrent, lev);
715
levelCurrent = levelNext;
717
statementFound = false;
718
if (!options.foldOnlyBegin)
719
sqlStates.Set(lineCurrent, sqlStatesCurrentLine);
721
if (!isspacechar(ch)) {
727
LexerModule lmSQL(SCLEX_SQL, LexerSQL::LexerFactorySQL, "sql", sqlWordListDesc);