1 // Scintilla source code edit control
2 /** @file LexCoffeeScript.cxx
3 ** Lexer for CoffeeScript.
5 // Copyright 1998-2011 by Neil Hodgson <neilh@scintilla.org>
6 // Based on the Scintilla C++ Lexer
7 // Written by Eric Promislow <ericp@activestate.com> in 2011 for the Komodo IDE
8 // The License.txt file describes the conditions under which this software may be distributed.
19 #include "Scintilla.h"
23 #include "LexAccessor.h"
25 #include "StyleContext.h"
26 #include "CharacterSet.h"
27 #include "LexerModule.h"
30 using namespace Scintilla
;
33 static bool IsSpaceEquiv(int state
) {
34 return (state
<= SCE_C_COMMENTDOC
35 // including SCE_C_DEFAULT, SCE_C_COMMENT, SCE_C_COMMENTLINE
36 || state
== SCE_C_COMMENTLINEDOC
37 || state
== SCE_C_COMMENTDOCKEYWORD
38 || state
== SCE_C_COMMENTDOCKEYWORDERROR
39 || state
== SCE_COFFEESCRIPT_COMMENTBLOCK
40 || state
== SCE_COFFEESCRIPT_VERBOSE_REGEX
41 || state
== SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT
42 || state
== SCE_C_WORD
43 || state
== SCE_C_REGEX
);
46 // Preconditions: sc.currentPos points to a character after '+' or '-'.
47 // The test for pos reaching 0 should be redundant,
48 // and is in only for safety measures.
49 // Limitation: this code will give the incorrect answer for code like
51 // Putting a space between the '++' post-inc operator and the '+' binary op
52 // fixes this, and is highly recommended for readability anyway.
53 static bool FollowsPostfixOperator(StyleContext
&sc
, Accessor
&styler
) {
54 int pos
= (int) sc
.currentPos
;
56 char ch
= styler
[pos
];
57 if (ch
== '+' || ch
== '-') {
58 return styler
[pos
- 1] == ch
;
64 static bool followsReturnKeyword(StyleContext
&sc
, Accessor
&styler
) {
65 // Don't look at styles, so no need to flush.
66 int pos
= (int) sc
.currentPos
;
67 int currentLine
= styler
.GetLine(pos
);
68 int lineStartPos
= styler
.LineStart(currentLine
);
70 while (--pos
> lineStartPos
) {
71 ch
= styler
.SafeGetCharAt(pos
);
72 if (ch
!= ' ' && ch
!= '\t') {
76 const char *retBack
= "nruter";
77 const char *s
= retBack
;
79 && pos
>= lineStartPos
80 && styler
.SafeGetCharAt(pos
) == *s
) {
87 static void ColouriseCoffeeScriptDoc(unsigned int startPos
, int length
, int initStyle
, WordList
*keywordlists
[],
90 WordList
&keywords
= *keywordlists
[0];
91 WordList
&keywords2
= *keywordlists
[1];
92 WordList
&keywords3
= *keywordlists
[2];
93 WordList
&keywords4
= *keywordlists
[3];
95 // property styling.within.preprocessor
96 // For C++ code, determines whether all preprocessor code is styled in the preprocessor style (0, the default)
97 // or only from the initial # to the end of the command word(1).
98 bool stylingWithinPreprocessor
= styler
.GetPropertyInt("styling.within.preprocessor") != 0;
100 CharacterSet
setOKBeforeRE(CharacterSet::setNone
, "([{=,:;!%^&*|?~+-");
101 CharacterSet
setCouldBePostOp(CharacterSet::setNone
, "+-");
103 CharacterSet
setDoxygen(CharacterSet::setAlpha
, "$@\\&<>#{}[]");
105 CharacterSet
setWordStart(CharacterSet::setAlpha
, "_", 0x80, true);
106 CharacterSet
setWord(CharacterSet::setAlphaNum
, "._", 0x80, true);
108 // property lexer.cpp.allow.dollars
109 // Set to 0 to disallow the '$' character in identifiers with the cpp lexer.
110 if (styler
.GetPropertyInt("lexer.cpp.allow.dollars", 1) != 0) {
111 setWordStart
.Add('$');
115 int chPrevNonWhite
= ' ';
116 int visibleChars
= 0;
117 bool lastWordWasUUID
= false;
118 int styleBeforeDCKeyword
= SCE_C_DEFAULT
;
119 bool continuationLine
= false;
120 bool isIncludePreprocessor
= false;
122 if (initStyle
== SCE_C_PREPROCESSOR
) {
123 // Set continuationLine if last character of previous line is '\'
124 int lineCurrent
= styler
.GetLine(startPos
);
125 if (lineCurrent
> 0) {
126 int chBack
= styler
.SafeGetCharAt(startPos
-1, 0);
127 int chBack2
= styler
.SafeGetCharAt(startPos
-2, 0);
128 int lineEndChar
= '!';
129 if (chBack2
== '\r' && chBack
== '\n') {
130 lineEndChar
= styler
.SafeGetCharAt(startPos
-3, 0);
131 } else if (chBack
== '\n' || chBack
== '\r') {
132 lineEndChar
= chBack2
;
134 continuationLine
= lineEndChar
== '\\';
138 // look back to set chPrevNonWhite properly for better regex colouring
139 int endPos
= startPos
+ length
;
141 unsigned int back
= startPos
;
143 while (back
> 0 && IsSpaceEquiv(styler
.StyleAt(--back
)))
145 if (styler
.StyleAt(back
) == SCE_C_OPERATOR
) {
146 chPrevNonWhite
= styler
.SafeGetCharAt(back
);
148 if (startPos
!= back
) {
149 initStyle
= styler
.StyleAt(back
);
154 StyleContext
sc(startPos
, endPos
- startPos
, initStyle
, styler
);
156 for (; sc
.More(); sc
.Forward()) {
158 if (sc
.atLineStart
) {
159 // Reset states to begining of colourise so no surprises
160 // if different sets of lines lexed.
162 lastWordWasUUID
= false;
163 isIncludePreprocessor
= false;
166 // Handle line continuation generically.
168 if (sc
.chNext
== '\n' || sc
.chNext
== '\r') {
170 if (sc
.ch
== '\r' && sc
.chNext
== '\n') {
173 continuationLine
= true;
178 // Determine if the current state should terminate.
181 sc
.SetState(SCE_C_DEFAULT
);
184 // We accept almost anything because of hex. and number suffixes
185 if (!setWord
.Contains(sc
.ch
)) {
186 sc
.SetState(SCE_C_DEFAULT
);
189 case SCE_C_IDENTIFIER
:
190 if (!setWord
.Contains(sc
.ch
) || (sc
.ch
== '.') || (sc
.ch
== '$')) {
192 sc
.GetCurrent(s
, sizeof(s
));
193 if (keywords
.InList(s
)) {
194 lastWordWasUUID
= strcmp(s
, "uuid") == 0;
195 sc
.ChangeState(SCE_C_WORD
);
196 } else if (keywords2
.InList(s
)) {
197 sc
.ChangeState(SCE_C_WORD2
);
198 } else if (keywords4
.InList(s
)) {
199 sc
.ChangeState(SCE_C_GLOBALCLASS
);
201 sc
.SetState(SCE_C_DEFAULT
);
204 case SCE_C_PREPROCESSOR
:
205 if (sc
.atLineStart
&& !continuationLine
) {
206 sc
.SetState(SCE_C_DEFAULT
);
207 } else if (stylingWithinPreprocessor
) {
208 if (IsASpace(sc
.ch
)) {
209 sc
.SetState(SCE_C_DEFAULT
);
212 if (sc
.Match('/', '*') || sc
.Match('/', '/')) {
213 sc
.SetState(SCE_C_DEFAULT
);
218 if (sc
.Match('*', '/')) {
220 sc
.ForwardSetState(SCE_C_DEFAULT
);
223 case SCE_C_COMMENTDOC
:
224 if (sc
.Match('*', '/')) {
226 sc
.ForwardSetState(SCE_C_DEFAULT
);
227 } else if (sc
.ch
== '@' || sc
.ch
== '\\') { // JavaDoc and Doxygen support
228 // Verify that we have the conditions to mark a comment-doc-keyword
229 if ((IsASpace(sc
.chPrev
) || sc
.chPrev
== '*') && (!IsASpace(sc
.chNext
))) {
230 styleBeforeDCKeyword
= SCE_C_COMMENTDOC
;
231 sc
.SetState(SCE_C_COMMENTDOCKEYWORD
);
235 case SCE_C_COMMENTLINE
:
236 if (sc
.atLineStart
) {
237 sc
.SetState(SCE_C_DEFAULT
);
240 case SCE_C_COMMENTLINEDOC
:
241 if (sc
.atLineStart
) {
242 sc
.SetState(SCE_C_DEFAULT
);
243 } else if (sc
.ch
== '@' || sc
.ch
== '\\') { // JavaDoc and Doxygen support
244 // Verify that we have the conditions to mark a comment-doc-keyword
245 if ((IsASpace(sc
.chPrev
) || sc
.chPrev
== '/' || sc
.chPrev
== '!') && (!IsASpace(sc
.chNext
))) {
246 styleBeforeDCKeyword
= SCE_C_COMMENTLINEDOC
;
247 sc
.SetState(SCE_C_COMMENTDOCKEYWORD
);
251 case SCE_C_COMMENTDOCKEYWORD
:
252 if ((styleBeforeDCKeyword
== SCE_C_COMMENTDOC
) && sc
.Match('*', '/')) {
253 sc
.ChangeState(SCE_C_COMMENTDOCKEYWORDERROR
);
255 sc
.ForwardSetState(SCE_C_DEFAULT
);
256 } else if (!setDoxygen
.Contains(sc
.ch
)) {
258 sc
.GetCurrent(s
, sizeof(s
));
259 if (!IsASpace(sc
.ch
) || !keywords3
.InList(s
+ 1)) {
260 sc
.ChangeState(SCE_C_COMMENTDOCKEYWORDERROR
);
262 sc
.SetState(styleBeforeDCKeyword
);
266 if (isIncludePreprocessor
) {
268 sc
.ForwardSetState(SCE_C_DEFAULT
);
269 isIncludePreprocessor
= false;
271 } else if (sc
.ch
== '\\') {
272 if (sc
.chNext
== '\"' || sc
.chNext
== '\'' || sc
.chNext
== '\\') {
275 } else if (sc
.ch
== '\"') {
276 sc
.ForwardSetState(SCE_C_DEFAULT
);
279 case SCE_C_CHARACTER
:
281 if (sc
.chNext
== '\"' || sc
.chNext
== '\'' || sc
.chNext
== '\\') {
284 } else if (sc
.ch
== '\'') {
285 sc
.ForwardSetState(SCE_C_DEFAULT
);
289 if (sc
.atLineStart
) {
290 sc
.SetState(SCE_C_DEFAULT
);
291 } else if (sc
.ch
== '/') {
293 while ((sc
.ch
< 0x80) && islower(sc
.ch
))
294 sc
.Forward(); // gobble regex flags
295 sc
.SetState(SCE_C_DEFAULT
);
296 } else if (sc
.ch
== '\\') {
297 // Gobble up the quoted character
298 if (sc
.chNext
== '\\' || sc
.chNext
== '/') {
303 case SCE_C_STRINGEOL
:
304 if (sc
.atLineStart
) {
305 sc
.SetState(SCE_C_DEFAULT
);
310 if (sc
.chNext
== '\"') {
313 sc
.ForwardSetState(SCE_C_DEFAULT
);
318 if (sc
.ch
== '\r' || sc
.ch
== '\n' || sc
.ch
== ')') {
319 sc
.SetState(SCE_C_DEFAULT
);
322 case SCE_COFFEESCRIPT_COMMENTBLOCK
:
323 if (sc
.Match("###")) {
324 sc
.ChangeState(SCE_C_COMMENT
);
327 sc
.ForwardSetState(SCE_C_DEFAULT
);
328 } else if (sc
.ch
== '\\') {
332 case SCE_COFFEESCRIPT_VERBOSE_REGEX
:
333 if (sc
.Match("///")) {
336 sc
.ChangeState(SCE_C_REGEX
);
337 sc
.ForwardSetState(SCE_C_DEFAULT
);
338 } else if (sc
.Match('#')) {
339 sc
.ChangeState(SCE_C_REGEX
);
340 sc
.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT
);
341 } else if (sc
.ch
== '\\') {
345 case SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT
:
346 if (sc
.atLineStart
) {
347 sc
.ChangeState(SCE_C_COMMENT
);
348 sc
.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX
);
353 // Determine if a new state should be entered.
354 if (sc
.state
== SCE_C_DEFAULT
) {
355 if (sc
.Match('@', '\"')) {
356 sc
.SetState(SCE_C_VERBATIM
);
358 } else if (IsADigit(sc
.ch
) || (sc
.ch
== '.' && IsADigit(sc
.chNext
))) {
359 if (lastWordWasUUID
) {
360 sc
.SetState(SCE_C_UUID
);
361 lastWordWasUUID
= false;
363 sc
.SetState(SCE_C_NUMBER
);
365 } else if (setWordStart
.Contains(sc
.ch
) || (sc
.ch
== '@') || (sc
.ch
== '$')) {
366 if (lastWordWasUUID
) {
367 sc
.SetState(SCE_C_UUID
);
368 lastWordWasUUID
= false;
370 sc
.SetState(SCE_C_IDENTIFIER
);
372 } else if (sc
.Match('/', '*')) {
373 if (sc
.Match("/**") || sc
.Match("/*!")) { // Support of Qt/Doxygen doc. style
374 sc
.SetState(SCE_C_COMMENTDOC
);
376 sc
.SetState(SCE_C_COMMENT
);
378 sc
.Forward(); // Eat the * so it isn't used for the end of the comment
379 } else if (sc
.Match("///")) {
380 sc
.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX
);
381 } else if (sc
.ch
== '/'
382 && (setOKBeforeRE
.Contains(chPrevNonWhite
)
383 || followsReturnKeyword(sc
, styler
))
384 && (!setCouldBePostOp
.Contains(chPrevNonWhite
)
385 || !FollowsPostfixOperator(sc
, styler
))) {
386 sc
.SetState(SCE_C_REGEX
); // JavaScript's RegEx
387 } else if (sc
.ch
== '\"') {
388 sc
.SetState(SCE_C_STRING
);
389 isIncludePreprocessor
= false; // ensure that '>' won't end the string
390 } else if (isIncludePreprocessor
&& sc
.ch
== '<') {
391 sc
.SetState(SCE_C_STRING
);
392 } else if (sc
.ch
== '\'') {
393 sc
.SetState(SCE_C_CHARACTER
);
394 } else if (sc
.ch
== '#') {
395 if (sc
.Match("###")) {
396 sc
.SetState(SCE_COFFEESCRIPT_COMMENTBLOCK
);
398 sc
.SetState(SCE_C_COMMENTLINE
);
400 } else if (isoperator(static_cast<char>(sc
.ch
))) {
401 sc
.SetState(SCE_C_OPERATOR
);
405 if (!IsASpace(sc
.ch
) && !IsSpaceEquiv(sc
.state
)) {
406 chPrevNonWhite
= sc
.ch
;
409 continuationLine
= false;
411 // Change temporary coffeescript states into standard C ones.
413 case SCE_COFFEESCRIPT_COMMENTBLOCK
:
414 sc
.ChangeState(SCE_C_COMMENT
);
416 case SCE_COFFEESCRIPT_VERBOSE_REGEX
:
417 sc
.ChangeState(SCE_C_REGEX
);
419 case SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT
:
420 sc
.ChangeState(SCE_C_COMMENTLINE
);
426 static bool IsCommentLine(int line
, Accessor
&styler
) {
427 int pos
= styler
.LineStart(line
);
428 int eol_pos
= styler
.LineStart(line
+ 1) - 1;
429 for (int i
= pos
; i
< eol_pos
; i
++) {
435 && styler
[i
+ 1] == '*')
437 else if (ch
!= ' ' && ch
!= '\t')
443 static void FoldCoffeeScriptDoc(unsigned int startPos
, int length
, int,
444 WordList
*[], Accessor
&styler
) {
445 // A simplified version of FoldPyDoc
446 const int maxPos
= startPos
+ length
;
447 const int maxLines
= styler
.GetLine(maxPos
- 1); // Requested last line
448 const int docLines
= styler
.GetLine(styler
.Length() - 1); // Available last line
450 // property fold.coffeescript.comment
451 const bool foldComment
= styler
.GetPropertyInt("fold.coffeescript.comment") != 0;
453 const bool foldCompact
= styler
.GetPropertyInt("fold.compact") != 0;
455 // Backtrack to previous non-blank line so we can determine indent level
456 // for any white space lines
457 // and so we can fix any preceding fold level (which is why we go back
458 // at least one line in all cases)
460 int lineCurrent
= styler
.GetLine(startPos
);
461 int indentCurrent
= styler
.IndentAmount(lineCurrent
, &spaceFlags
, NULL
);
462 while (lineCurrent
> 0) {
464 indentCurrent
= styler
.IndentAmount(lineCurrent
, &spaceFlags
, NULL
);
465 if (!(indentCurrent
& SC_FOLDLEVELWHITEFLAG
)
466 && !IsCommentLine(lineCurrent
, styler
))
469 int indentCurrentLevel
= indentCurrent
& SC_FOLDLEVELNUMBERMASK
;
471 // Set up initial loop state
473 if (lineCurrent
>= 1)
474 prevComment
= foldComment
&& IsCommentLine(lineCurrent
- 1, styler
);
476 // Process all characters to end of requested range
477 // or comment that hangs over the end of the range. Cap processing in all cases
478 // to end of document (in case of comment at end).
479 while ((lineCurrent
<= docLines
) && ((lineCurrent
<= maxLines
) || prevComment
)) {
482 int lev
= indentCurrent
;
483 int lineNext
= lineCurrent
+ 1;
484 int indentNext
= indentCurrent
;
485 if (lineNext
<= docLines
) {
486 // Information about next line is only available if not at end of document
487 indentNext
= styler
.IndentAmount(lineNext
, &spaceFlags
, NULL
);
489 const int comment
= foldComment
&& IsCommentLine(lineCurrent
, styler
);
490 const int comment_start
= (comment
&& !prevComment
&& (lineNext
<= docLines
) &&
491 IsCommentLine(lineNext
, styler
) && (lev
> SC_FOLDLEVELBASE
));
492 const int comment_continue
= (comment
&& prevComment
);
494 indentCurrentLevel
= indentCurrent
& SC_FOLDLEVELNUMBERMASK
;
495 if (indentNext
& SC_FOLDLEVELWHITEFLAG
)
496 indentNext
= SC_FOLDLEVELWHITEFLAG
| indentCurrentLevel
;
499 // Place fold point at start of a block of comments
500 lev
|= SC_FOLDLEVELHEADERFLAG
;
501 } else if (comment_continue
) {
502 // Add level to rest of lines in the block
506 // Skip past any blank lines for next indent level info; we skip also
507 // comments (all comments, not just those starting in column 0)
508 // which effectively folds them into surrounding code rather
509 // than screwing up folding.
511 while ((lineNext
< docLines
) &&
512 ((indentNext
& SC_FOLDLEVELWHITEFLAG
) ||
513 (lineNext
<= docLines
&& IsCommentLine(lineNext
, styler
)))) {
516 indentNext
= styler
.IndentAmount(lineNext
, &spaceFlags
, NULL
);
519 const int levelAfterComments
= indentNext
& SC_FOLDLEVELNUMBERMASK
;
520 const int levelBeforeComments
= Platform::Maximum(indentCurrentLevel
,levelAfterComments
);
522 // Now set all the indent levels on the lines we skipped
523 // Do this from end to start. Once we encounter one line
524 // which is indented more than the line after the end of
525 // the comment-block, use the level of the block before
527 int skipLine
= lineNext
;
528 int skipLevel
= levelAfterComments
;
530 while (--skipLine
> lineCurrent
) {
531 int skipLineIndent
= styler
.IndentAmount(skipLine
, &spaceFlags
, NULL
);
534 if ((skipLineIndent
& SC_FOLDLEVELNUMBERMASK
) > levelAfterComments
)
535 skipLevel
= levelBeforeComments
;
537 int whiteFlag
= skipLineIndent
& SC_FOLDLEVELWHITEFLAG
;
539 styler
.SetLevel(skipLine
, skipLevel
| whiteFlag
);
541 if ((skipLineIndent
& SC_FOLDLEVELNUMBERMASK
) > levelAfterComments
&&
542 !(skipLineIndent
& SC_FOLDLEVELWHITEFLAG
) &&
543 !IsCommentLine(skipLine
, styler
))
544 skipLevel
= levelBeforeComments
;
546 styler
.SetLevel(skipLine
, skipLevel
);
550 // Set fold header on non-comment line
551 if (!comment
&& !(indentCurrent
& SC_FOLDLEVELWHITEFLAG
)) {
552 if ((indentCurrent
& SC_FOLDLEVELNUMBERMASK
) < (indentNext
& SC_FOLDLEVELNUMBERMASK
))
553 lev
|= SC_FOLDLEVELHEADERFLAG
;
556 // Keep track of block comment state of previous line
557 prevComment
= comment_start
|| comment_continue
;
559 // Set fold level for this line and move to next line
560 styler
.SetLevel(lineCurrent
, lev
);
561 indentCurrent
= indentNext
;
562 lineCurrent
= lineNext
;
566 static const char *const csWordLists
[] = {
571 LexerModule
lmCoffeeScript(SCLEX_COFFEESCRIPT
, ColouriseCoffeeScriptDoc
, "coffeescript", FoldCoffeeScriptDoc
, csWordLists
);