1/**
2 * Scintilla source code edit control
3 * @file LexMySQL.cxx
4 * Lexer for MySQL
5 *
6 * Improved by Mike Lischke <mike.lischke@oracle.com>
7 * Adopted from LexSQL.cxx by Anders Karlsson <anders@mysql.com>
8 * Original work by Neil Hodgson <neilh@scintilla.org>
9 * Copyright 1998-2005 by Neil Hodgson <neilh@scintilla.org>
10 * The License.txt file describes the conditions under which this software may be distributed.
11 */
12
13#include <stdlib.h>
14#include <string.h>
15#include <stdio.h>
16#include <stdarg.h>
17#include <assert.h>
18#include <ctype.h>
19
20#include <string>
21#include <string_view>
22
23#include "ILexer.h"
24#include "Scintilla.h"
25#include "SciLexer.h"
26
27#include "WordList.h"
28#include "LexAccessor.h"
29#include "Accessor.h"
30#include "StyleContext.h"
31#include "CharacterSet.h"
32#include "LexerModule.h"
33
34using namespace Lexilla;
35
36static inline bool IsAWordChar(int ch) {
37 return (ch < 0x80) && (isalnum(ch) || ch == '_');
38}
39
40static inline bool IsAWordStart(int ch) {
41 return (ch < 0x80) && (isalpha(ch) || ch == '_');
42}
43
44static inline bool IsANumberChar(int ch) {
45 // Not exactly following number definition (several dots are seen as OK, etc.)
46 // but probably enough in most cases.
47 return (ch < 0x80) &&
48 (isdigit(ch) || toupper(ch) == 'E' ||
49 ch == '.' || ch == '-' || ch == '+');
50}
51
52//--------------------------------------------------------------------------------------------------
53
54/**
55 * Check if the current content context represent a keyword and set the context state if so.
56 */
57static void CheckForKeyword(StyleContext& sc, WordList* keywordlists[], int activeState)
58{
59 Sci_Position length = sc.LengthCurrent() + 1; // +1 for the next char
60 char* s = new char[length];
61 sc.GetCurrentLowered(s, length);
62 if (keywordlists[0]->InList(s))
63 sc.ChangeState(SCE_MYSQL_MAJORKEYWORD | activeState);
64 else
65 if (keywordlists[1]->InList(s))
66 sc.ChangeState(SCE_MYSQL_KEYWORD | activeState);
67 else
68 if (keywordlists[2]->InList(s))
69 sc.ChangeState(SCE_MYSQL_DATABASEOBJECT | activeState);
70 else
71 if (keywordlists[3]->InList(s))
72 sc.ChangeState(SCE_MYSQL_FUNCTION | activeState);
73 else
74 if (keywordlists[5]->InList(s))
75 sc.ChangeState(SCE_MYSQL_PROCEDUREKEYWORD | activeState);
76 else
77 if (keywordlists[6]->InList(s))
78 sc.ChangeState(SCE_MYSQL_USER1 | activeState);
79 else
80 if (keywordlists[7]->InList(s))
81 sc.ChangeState(SCE_MYSQL_USER2 | activeState);
82 else
83 if (keywordlists[8]->InList(s))
84 sc.ChangeState(SCE_MYSQL_USER3 | activeState);
85 delete [] s;
86}
87
88//--------------------------------------------------------------------------------------------------
89
90#define HIDDENCOMMAND_STATE 0x40 // Offset for states within a hidden command.
91#define MASKACTIVE(style) (style & ~HIDDENCOMMAND_STATE)
92
93static void SetDefaultState(StyleContext& sc, int activeState)
94{
95 if (activeState == 0)
96 sc.SetState(SCE_MYSQL_DEFAULT);
97 else
98 sc.SetState(SCE_MYSQL_HIDDENCOMMAND);
99}
100
101static void ForwardDefaultState(StyleContext& sc, int activeState)
102{
103 if (activeState == 0)
104 sc.ForwardSetState(SCE_MYSQL_DEFAULT);
105 else
106 sc.ForwardSetState(SCE_MYSQL_HIDDENCOMMAND);
107}
108
109static void ColouriseMySQLDoc(Sci_PositionU startPos, Sci_Position length, int initStyle, WordList *keywordlists[],
110 Accessor &styler)
111{
112 StyleContext sc(startPos, length, initStyle, styler, 127);
113 int activeState = (initStyle == SCE_MYSQL_HIDDENCOMMAND) ? HIDDENCOMMAND_STATE : initStyle & HIDDENCOMMAND_STATE;
114
115 for (; sc.More(); sc.Forward())
116 {
117 // Determine if the current state should terminate.
118 switch (MASKACTIVE(sc.state))
119 {
120 case SCE_MYSQL_OPERATOR:
121 SetDefaultState(sc, activeState);
122 break;
123 case SCE_MYSQL_NUMBER:
124 // We stop the number definition on non-numerical non-dot non-eE non-sign char.
125 if (!IsANumberChar(sc.ch))
126 SetDefaultState(sc, activeState);
127 break;
128 case SCE_MYSQL_IDENTIFIER:
129 // Switch from identifier to keyword state and open a new state for the new char.
130 if (!IsAWordChar(sc.ch))
131 {
132 CheckForKeyword(sc, keywordlists, activeState);
133
134 // Additional check for function keywords needed.
135 // A function name must be followed by an opening parenthesis.
136 if (MASKACTIVE(sc.state) == SCE_MYSQL_FUNCTION && sc.ch != '(')
137 {
138 if (activeState > 0)
139 sc.ChangeState(SCE_MYSQL_HIDDENCOMMAND);
140 else
141 sc.ChangeState(SCE_MYSQL_DEFAULT);
142 }
143
144 SetDefaultState(sc, activeState);
145 }
146 break;
147 case SCE_MYSQL_VARIABLE:
148 if (!IsAWordChar(sc.ch))
149 SetDefaultState(sc, activeState);
150 break;
151 case SCE_MYSQL_SYSTEMVARIABLE:
152 if (!IsAWordChar(sc.ch))
153 {
154 Sci_Position length = sc.LengthCurrent() + 1;
155 char* s = new char[length];
156 sc.GetCurrentLowered(s, length);
157
158 // Check for known system variables here.
159 if (keywordlists[4]->InList(&s[2]))
160 sc.ChangeState(SCE_MYSQL_KNOWNSYSTEMVARIABLE | activeState);
161 delete [] s;
162
163 SetDefaultState(sc, activeState);
164 }
165 break;
166 case SCE_MYSQL_QUOTEDIDENTIFIER:
167 if (sc.ch == '`')
168 {
169 if (sc.chNext == '`')
170 sc.Forward(); // Ignore it
171 else
172 ForwardDefaultState(sc, activeState);
173 }
174 break;
175 case SCE_MYSQL_COMMENT:
176 if (sc.Match('*', '/'))
177 {
178 sc.Forward();
179 ForwardDefaultState(sc, activeState);
180 }
181 break;
182 case SCE_MYSQL_COMMENTLINE:
183 if (sc.atLineStart)
184 SetDefaultState(sc, activeState);
185 break;
186 case SCE_MYSQL_SQSTRING:
187 if (sc.ch == '\\')
188 sc.Forward(); // Escape sequence
189 else
190 if (sc.ch == '\'')
191 {
192 // End of single quoted string reached?
193 if (sc.chNext == '\'')
194 sc.Forward();
195 else
196 ForwardDefaultState(sc, activeState);
197 }
198 break;
199 case SCE_MYSQL_DQSTRING:
200 if (sc.ch == '\\')
201 sc.Forward(); // Escape sequence
202 else
203 if (sc.ch == '\"')
204 {
205 // End of single quoted string reached?
206 if (sc.chNext == '\"')
207 sc.Forward();
208 else
209 ForwardDefaultState(sc, activeState);
210 }
211 break;
212 case SCE_MYSQL_PLACEHOLDER:
213 if (sc.Match('}', '>'))
214 {
215 sc.Forward();
216 ForwardDefaultState(sc, activeState);
217 }
218 break;
219 }
220
221 if (sc.state == SCE_MYSQL_HIDDENCOMMAND && sc.Match('*', '/'))
222 {
223 activeState = 0;
224 sc.Forward();
225 ForwardDefaultState(sc, activeState);
226 }
227
228 // Determine if a new state should be entered.
229 if (sc.state == SCE_MYSQL_DEFAULT || sc.state == SCE_MYSQL_HIDDENCOMMAND)
230 {
231 switch (sc.ch)
232 {
233 case '@':
234 if (sc.chNext == '@')
235 {
236 sc.SetState(SCE_MYSQL_SYSTEMVARIABLE | activeState);
237 sc.Forward(2); // Skip past @@.
238 }
239 else
240 if (IsAWordStart(sc.ch))
241 {
242 sc.SetState(SCE_MYSQL_VARIABLE | activeState);
243 sc.Forward(); // Skip past @.
244 }
245 else
246 sc.SetState(SCE_MYSQL_OPERATOR | activeState);
247 break;
248 case '`':
249 sc.SetState(SCE_MYSQL_QUOTEDIDENTIFIER | activeState);
250 break;
251 case '#':
252 sc.SetState(SCE_MYSQL_COMMENTLINE | activeState);
253 break;
254 case '\'':
255 sc.SetState(SCE_MYSQL_SQSTRING | activeState);
256 break;
257 case '\"':
258 sc.SetState(SCE_MYSQL_DQSTRING | activeState);
259 break;
260 default:
261 if (IsADigit(sc.ch) || (sc.ch == '.' && IsADigit(sc.chNext)))
262 sc.SetState(SCE_MYSQL_NUMBER | activeState);
263 else
264 if (IsAWordStart(sc.ch))
265 sc.SetState(SCE_MYSQL_IDENTIFIER | activeState);
266 else
267 if (sc.Match('/', '*'))
268 {
269 sc.SetState(SCE_MYSQL_COMMENT | activeState);
270
271 // Skip first char of comment introducer and check for hidden command.
272 // The second char is skipped by the outer loop.
273 sc.Forward();
274 if (sc.GetRelativeCharacter(1) == '!')
275 {
276 // Version comment found. Skip * now.
277 sc.Forward();
278 activeState = HIDDENCOMMAND_STATE;
279 sc.ChangeState(SCE_MYSQL_HIDDENCOMMAND);
280 }
281 }
282 else if (sc.Match('<', '{'))
283 {
284 sc.SetState(SCE_MYSQL_PLACEHOLDER | activeState);
285 }
286 else
287 if (sc.Match("--"))
288 {
289 // Special MySQL single line comment.
290 sc.SetState(SCE_MYSQL_COMMENTLINE | activeState);
291 sc.Forward(2);
292
293 // Check the third character too. It must be a space or EOL.
294 if (sc.ch != ' ' && sc.ch != '\n' && sc.ch != '\r')
295 sc.ChangeState(SCE_MYSQL_OPERATOR | activeState);
296 }
297 else
298 if (isoperator(static_cast<char>(sc.ch)))
299 sc.SetState(SCE_MYSQL_OPERATOR | activeState);
300 }
301 }
302 }
303
304 // Do a final check for keywords if we currently have an identifier, to highlight them
305 // also at the end of a line.
306 if (sc.state == SCE_MYSQL_IDENTIFIER)
307 {
308 CheckForKeyword(sc, keywordlists, activeState);
309
310 // Additional check for function keywords needed.
311 // A function name must be followed by an opening parenthesis.
312 if (sc.state == SCE_MYSQL_FUNCTION && sc.ch != '(')
313 SetDefaultState(sc, activeState);
314 }
315
316 sc.Complete();
317}
318
319//--------------------------------------------------------------------------------------------------
320
321/**
322 * Helper function to determine if we have a foldable comment currently.
323 */
324static bool IsStreamCommentStyle(int style)
325{
326 return MASKACTIVE(style) == SCE_MYSQL_COMMENT;
327}
328
329//--------------------------------------------------------------------------------------------------
330
331/**
332 * Code copied from StyleContext and modified to work here. Should go into Accessor as a
333 * companion to Match()...
334 */
335static bool MatchIgnoreCase(Accessor &styler, Sci_Position currentPos, const char *s)
336{
337 for (Sci_Position n = 0; *s; n++)
338 {
339 if (*s != tolower(styler.SafeGetCharAt(currentPos + n)))
340 return false;
341 s++;
342 }
343 return true;
344}
345
346//--------------------------------------------------------------------------------------------------
347
348// Store both the current line's fold level and the next lines in the
349// level store to make it easy to pick up with each increment.
350static void FoldMySQLDoc(Sci_PositionU startPos, Sci_Position length, int initStyle, WordList *[], Accessor &styler)
351{
352 bool foldComment = styler.GetPropertyInt("fold.comment") != 0;
353 bool foldCompact = styler.GetPropertyInt("fold.compact", 1) != 0;
354 bool foldOnlyBegin = styler.GetPropertyInt("fold.sql.only.begin", 0) != 0;
355
356 int visibleChars = 0;
357 Sci_Position lineCurrent = styler.GetLine(startPos);
358 int levelCurrent = SC_FOLDLEVELBASE;
359 if (lineCurrent > 0)
360 levelCurrent = styler.LevelAt(lineCurrent - 1) >> 16;
361 int levelNext = levelCurrent;
362
363 int styleNext = styler.StyleAt(startPos);
364 int style = initStyle;
365 int activeState = (style == SCE_MYSQL_HIDDENCOMMAND) ? HIDDENCOMMAND_STATE : style & HIDDENCOMMAND_STATE;
366
367 bool endPending = false;
368 bool whenPending = false;
369 bool elseIfPending = false;
370
371 char nextChar = styler.SafeGetCharAt(startPos);
372 for (Sci_PositionU i = startPos; length > 0; i++, length--)
373 {
374 int stylePrev = style;
375 int lastActiveState = activeState;
376 style = styleNext;
377 styleNext = styler.StyleAt(i + 1);
378 activeState = (style == SCE_MYSQL_HIDDENCOMMAND) ? HIDDENCOMMAND_STATE : style & HIDDENCOMMAND_STATE;
379
380 char currentChar = nextChar;
381 nextChar = styler.SafeGetCharAt(i + 1);
382 bool atEOL = (currentChar == '\r' && nextChar != '\n') || (currentChar == '\n');
383
384 switch (MASKACTIVE(style))
385 {
386 case SCE_MYSQL_COMMENT:
387 if (foldComment)
388 {
389 // Multiline comment style /* .. */ just started or is still in progress.
390 if (IsStreamCommentStyle(style) && !IsStreamCommentStyle(stylePrev))
391 levelNext++;
392 }
393 break;
394 case SCE_MYSQL_COMMENTLINE:
395 if (foldComment)
396 {
397 // Not really a standard, but we add support for single line comments
398 // with special curly braces syntax as foldable comments too.
399 // MySQL needs -- comments to be followed by space or control char
400 if (styler.Match(i, "--"))
401 {
402 char chNext2 = styler.SafeGetCharAt(i + 2);
403 char chNext3 = styler.SafeGetCharAt(i + 3);
404 if (chNext2 == '{' || chNext3 == '{')
405 levelNext++;
406 else
407 if (chNext2 == '}' || chNext3 == '}')
408 levelNext--;
409 }
410 }
411 break;
412 case SCE_MYSQL_HIDDENCOMMAND:
413 /*
414 if (endPending)
415 {
416 // A conditional command is not a white space so it should end the current block
417 // before opening a new one.
418 endPending = false;
419 levelNext--;
420 if (levelNext < SC_FOLDLEVELBASE)
421 levelNext = SC_FOLDLEVELBASE;
422 }
423 }*/
424 if (activeState != lastActiveState)
425 levelNext++;
426 break;
427 case SCE_MYSQL_OPERATOR:
428 if (endPending)
429 {
430 endPending = false;
431 levelNext--;
432 if (levelNext < SC_FOLDLEVELBASE)
433 levelNext = SC_FOLDLEVELBASE;
434 }
435 if (currentChar == '(')
436 levelNext++;
437 else
438 if (currentChar == ')')
439 {
440 levelNext--;
441 if (levelNext < SC_FOLDLEVELBASE)
442 levelNext = SC_FOLDLEVELBASE;
443 }
444 break;
445 case SCE_MYSQL_MAJORKEYWORD:
446 case SCE_MYSQL_KEYWORD:
447 case SCE_MYSQL_FUNCTION:
448 case SCE_MYSQL_PROCEDUREKEYWORD:
449 // Reserved and other keywords.
450 if (style != stylePrev)
451 {
452 // END decreases the folding level, regardless which keyword follows.
453 bool endFound = MatchIgnoreCase(styler, i, "end");
454 if (endPending)
455 {
456 levelNext--;
457 if (levelNext < SC_FOLDLEVELBASE)
458 levelNext = SC_FOLDLEVELBASE;
459 }
460 else
461 if (!endFound)
462 {
463 if (MatchIgnoreCase(styler, i, "begin"))
464 levelNext++;
465 else
466 {
467 if (!foldOnlyBegin)
468 {
469 bool whileFound = MatchIgnoreCase(styler, i, "while");
470 bool loopFound = MatchIgnoreCase(styler, i, "loop");
471 bool repeatFound = MatchIgnoreCase(styler, i, "repeat");
472 bool caseFound = MatchIgnoreCase(styler, i, "case");
473
474 if (whileFound || loopFound || repeatFound || caseFound)
475 levelNext++;
476 else
477 {
478 // IF alone does not increase the fold level as it is also used in non-block'ed
479 // code like DROP PROCEDURE blah IF EXISTS.
480 // Instead THEN opens the new level (if not part of an ELSEIF or WHEN (case) branch).
481 if (MatchIgnoreCase(styler, i, "then"))
482 {
483 if (!elseIfPending && !whenPending)
484 levelNext++;
485 else
486 {
487 elseIfPending = false;
488 whenPending = false;
489 }
490 }
491 else
492 {
493 // Neither of if/then/while/loop/repeat/case, so check for
494 // sub parts of IF and CASE.
495 if (MatchIgnoreCase(styler, i, "elseif"))
496 elseIfPending = true;
497 if (MatchIgnoreCase(styler, i, "when"))
498 whenPending = true;
499 }
500 }
501 }
502 }
503 }
504
505 // Keep the current end state for the next round.
506 endPending = endFound;
507 }
508 break;
509
510 default:
511 if (!isspacechar(currentChar) && endPending)
512 {
513 // END followed by a non-whitespace character (not covered by other cases like identifiers)
514 // also should end a folding block. Typical case: END followed by self defined delimiter.
515 levelNext--;
516 if (levelNext < SC_FOLDLEVELBASE)
517 levelNext = SC_FOLDLEVELBASE;
518 }
519 break;
520 }
521
522 // Go up one level if we just ended a multi line comment.
523 if (IsStreamCommentStyle(stylePrev) && !IsStreamCommentStyle(style))
524 {
525 levelNext--;
526 if (levelNext < SC_FOLDLEVELBASE)
527 levelNext = SC_FOLDLEVELBASE;
528 }
529
530 if (activeState == 0 && lastActiveState != 0)
531 {
532 // Decrease fold level when we left a hidden command.
533 levelNext--;
534 if (levelNext < SC_FOLDLEVELBASE)
535 levelNext = SC_FOLDLEVELBASE;
536 }
537
538 if (atEOL)
539 {
540 // Apply the new folding level to this line.
541 // Leave pending states as they are otherwise a line break will de-sync
542 // code folding and valid syntax.
543 int levelUse = levelCurrent;
544 int lev = levelUse | levelNext << 16;
545 if (visibleChars == 0 && foldCompact)
546 lev |= SC_FOLDLEVELWHITEFLAG;
547 if (levelUse < levelNext)
548 lev |= SC_FOLDLEVELHEADERFLAG;
549 if (lev != styler.LevelAt(lineCurrent))
550 styler.SetLevel(lineCurrent, lev);
551
552 lineCurrent++;
553 levelCurrent = levelNext;
554 visibleChars = 0;
555 }
556
557 if (!isspacechar(currentChar))
558 visibleChars++;
559 }
560}
561
562//--------------------------------------------------------------------------------------------------
563
564static const char * const mysqlWordListDesc[] = {
565 "Major Keywords",
566 "Keywords",
567 "Database Objects",
568 "Functions",
569 "System Variables",
570 "Procedure keywords",
571 "User Keywords 1",
572 "User Keywords 2",
573 "User Keywords 3",
574 0
575};
576
577LexerModule lmMySQL(SCLEX_MYSQL, ColouriseMySQLDoc, "mysql", FoldMySQLDoc, mysqlWordListDesc);
578