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 | |
34 | using namespace Lexilla; |
35 | |
36 | static inline bool IsAWordChar(int ch) { |
37 | return (ch < 0x80) && (isalnum(ch) || ch == '_'); |
38 | } |
39 | |
40 | static inline bool IsAWordStart(int ch) { |
41 | return (ch < 0x80) && (isalpha(ch) || ch == '_'); |
42 | } |
43 | |
44 | static 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 | */ |
57 | static 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 | |
93 | static 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 | |
101 | static 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 | |
109 | static 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 | */ |
324 | static bool (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 | */ |
335 | static 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. |
350 | static void FoldMySQLDoc(Sci_PositionU startPos, Sci_Position length, int initStyle, WordList *[], Accessor &styler) |
351 | { |
352 | bool = 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 | |
564 | static 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 | |
577 | LexerModule lmMySQL(SCLEX_MYSQL, ColouriseMySQLDoc, "mysql" , FoldMySQLDoc, mysqlWordListDesc); |
578 | |