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_COFFEESCRIPT_DEFAULT
35 || state
== SCE_COFFEESCRIPT_COMMENTLINE
36 || state
== SCE_COFFEESCRIPT_COMMENTBLOCK
37 || state
== SCE_COFFEESCRIPT_VERBOSE_REGEX
38 || state
== SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT
39 || state
== SCE_COFFEESCRIPT_WORD
40 || state
== SCE_COFFEESCRIPT_REGEX
);
43 // Store the current lexer state and brace count prior to starting a new
44 // `#{}` interpolation level.
45 // Based on LexRuby.cxx.
46 static void enterInnerExpression(int *p_inner_string_types
,
47 int *p_inner_expn_brace_counts
,
48 int& inner_string_count
,
52 p_inner_string_types
[inner_string_count
] = state
;
53 p_inner_expn_brace_counts
[inner_string_count
] = brace_counts
;
58 // Restore the lexer state and brace count for the previous `#{}` interpolation
59 // level upon returning to it.
60 // Note the previous lexer state is the return value and needs to be restored
61 // manually by the StyleContext.
62 // Based on LexRuby.cxx.
63 static int exitInnerExpression(int *p_inner_string_types
,
64 int *p_inner_expn_brace_counts
,
65 int& inner_string_count
,
69 brace_counts
= p_inner_expn_brace_counts
[inner_string_count
];
70 return p_inner_string_types
[inner_string_count
];
73 // Preconditions: sc.currentPos points to a character after '+' or '-'.
74 // The test for pos reaching 0 should be redundant,
75 // and is in only for safety measures.
76 // Limitation: this code will give the incorrect answer for code like
78 // Putting a space between the '++' post-inc operator and the '+' binary op
79 // fixes this, and is highly recommended for readability anyway.
80 static bool FollowsPostfixOperator(StyleContext
&sc
, Accessor
&styler
) {
81 Sci_Position pos
= (Sci_Position
) sc
.currentPos
;
83 char ch
= styler
[pos
];
84 if (ch
== '+' || ch
== '-') {
85 return styler
[pos
- 1] == ch
;
91 static bool followsKeyword(StyleContext
&sc
, Accessor
&styler
) {
92 Sci_Position pos
= (Sci_Position
) sc
.currentPos
;
93 Sci_Position currentLine
= styler
.GetLine(pos
);
94 Sci_Position lineStartPos
= styler
.LineStart(currentLine
);
95 while (--pos
> lineStartPos
) {
96 char ch
= styler
.SafeGetCharAt(pos
);
97 if (ch
!= ' ' && ch
!= '\t') {
102 return styler
.StyleAt(pos
) == SCE_COFFEESCRIPT_WORD
;
105 static void ColouriseCoffeeScriptDoc(Sci_PositionU startPos
, Sci_Position length
, int initStyle
, WordList
*keywordlists
[],
108 WordList
&keywords
= *keywordlists
[0];
109 WordList
&keywords2
= *keywordlists
[1];
110 WordList
&keywords4
= *keywordlists
[3];
112 CharacterSet
setOKBeforeRE(CharacterSet::setNone
, "([{=,:;!%^&*|?~+-");
113 CharacterSet
setCouldBePostOp(CharacterSet::setNone
, "+-");
115 CharacterSet
setWordStart(CharacterSet::setAlpha
, "_$@", 0x80, true);
116 CharacterSet
setWord(CharacterSet::setAlphaNum
, "._$", 0x80, true);
118 int chPrevNonWhite
= ' ';
119 int visibleChars
= 0;
121 // String/Regex interpolation variables, based on LexRuby.cxx.
122 // In most cases a value of 2 should be ample for the code the user is
123 // likely to enter. For example,
124 // "Filling the #{container} with #{liquid}..."
125 // from the CoffeeScript homepage nests to a level of 2
126 // If the user actually hits a 6th occurrence of '#{' in a double-quoted
127 // string (including regexes), it will stay as a string. The problem with
128 // this is that quotes might flip, a 7th '#{' will look like a comment,
129 // and code-folding might be wrong.
130 #define INNER_STRINGS_MAX_COUNT 5
131 // These vars track our instances of "...#{,,,'..#{,,,}...',,,}..."
132 int inner_string_types
[INNER_STRINGS_MAX_COUNT
];
133 // Track # braces when we push a new #{ thing
134 int inner_expn_brace_counts
[INNER_STRINGS_MAX_COUNT
];
135 int inner_string_count
= 0;
136 int brace_counts
= 0; // Number of #{ ... } things within an expression
137 for (int i
= 0; i
< INNER_STRINGS_MAX_COUNT
; i
++) {
138 inner_string_types
[i
] = 0;
139 inner_expn_brace_counts
[i
] = 0;
142 // look back to set chPrevNonWhite properly for better regex colouring
143 Sci_Position endPos
= startPos
+ length
;
144 if (startPos
> 0 && IsSpaceEquiv(initStyle
)) {
145 Sci_PositionU back
= startPos
;
147 while (back
> 0 && IsSpaceEquiv(styler
.StyleAt(--back
)))
149 if (styler
.StyleAt(back
) == SCE_COFFEESCRIPT_OPERATOR
) {
150 chPrevNonWhite
= styler
.SafeGetCharAt(back
);
152 if (startPos
!= back
) {
153 initStyle
= styler
.StyleAt(back
);
154 if (IsSpaceEquiv(initStyle
)) {
155 initStyle
= SCE_COFFEESCRIPT_DEFAULT
;
161 StyleContext
sc(startPos
, endPos
- startPos
, initStyle
, styler
);
165 if (sc
.atLineStart
) {
166 // Reset states to beginning of colourise so no surprises
167 // if different sets of lines lexed.
171 // Determine if the current state should terminate.
173 case SCE_COFFEESCRIPT_OPERATOR
:
174 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
176 case SCE_COFFEESCRIPT_NUMBER
:
177 // We accept almost anything because of hex. and number suffixes
178 if (!setWord
.Contains(sc
.ch
) || sc
.Match('.', '.')) {
179 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
182 case SCE_COFFEESCRIPT_IDENTIFIER
:
183 if (!setWord
.Contains(sc
.ch
) || (sc
.ch
== '.') || (sc
.ch
== '$')) {
185 sc
.GetCurrent(s
, sizeof(s
));
186 if (keywords
.InList(s
)) {
187 sc
.ChangeState(SCE_COFFEESCRIPT_WORD
);
188 } else if (keywords2
.InList(s
)) {
189 sc
.ChangeState(SCE_COFFEESCRIPT_WORD2
);
190 } else if (keywords4
.InList(s
)) {
191 sc
.ChangeState(SCE_COFFEESCRIPT_GLOBALCLASS
);
192 } else if (sc
.LengthCurrent() > 0 && s
[0] == '@') {
193 sc
.ChangeState(SCE_COFFEESCRIPT_INSTANCEPROPERTY
);
195 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
198 case SCE_COFFEESCRIPT_WORD
:
199 case SCE_COFFEESCRIPT_WORD2
:
200 case SCE_COFFEESCRIPT_GLOBALCLASS
:
201 case SCE_COFFEESCRIPT_INSTANCEPROPERTY
:
202 if (!setWord
.Contains(sc
.ch
)) {
203 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
206 case SCE_COFFEESCRIPT_COMMENTLINE
:
207 if (sc
.atLineStart
) {
208 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
211 case SCE_COFFEESCRIPT_STRING
:
213 if (sc
.chNext
== '\"' || sc
.chNext
== '\'' || sc
.chNext
== '\\') {
216 } else if (sc
.ch
== '\"') {
217 sc
.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT
);
218 } else if (sc
.ch
== '#' && sc
.chNext
== '{' && inner_string_count
< INNER_STRINGS_MAX_COUNT
) {
219 // process interpolated code #{ ... }
220 enterInnerExpression(inner_string_types
,
221 inner_expn_brace_counts
,
225 sc
.SetState(SCE_COFFEESCRIPT_OPERATOR
);
226 sc
.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT
);
229 case SCE_COFFEESCRIPT_CHARACTER
:
231 if (sc
.chNext
== '\"' || sc
.chNext
== '\'' || sc
.chNext
== '\\') {
234 } else if (sc
.ch
== '\'') {
235 sc
.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT
);
238 case SCE_COFFEESCRIPT_REGEX
:
239 if (sc
.atLineStart
) {
240 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
241 } else if (sc
.ch
== '/') {
243 while ((sc
.ch
< 0x80) && islower(sc
.ch
))
244 sc
.Forward(); // gobble regex flags
245 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
246 } else if (sc
.ch
== '\\') {
247 // Gobble up the quoted character
248 if (sc
.chNext
== '\\' || sc
.chNext
== '/') {
253 case SCE_COFFEESCRIPT_STRINGEOL
:
254 if (sc
.atLineStart
) {
255 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
258 case SCE_COFFEESCRIPT_COMMENTBLOCK
:
259 if (sc
.Match("###")) {
262 sc
.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT
);
263 } else if (sc
.ch
== '\\') {
267 case SCE_COFFEESCRIPT_VERBOSE_REGEX
:
268 if (sc
.Match("///")) {
271 sc
.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT
);
272 } else if (sc
.Match('#')) {
273 sc
.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT
);
274 } else if (sc
.ch
== '\\') {
278 case SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT
:
279 if (sc
.atLineStart
) {
280 sc
.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX
);
285 // Determine if a new state should be entered.
286 if (sc
.state
== SCE_COFFEESCRIPT_DEFAULT
) {
287 if (IsADigit(sc
.ch
) || (sc
.ch
== '.' && IsADigit(sc
.chNext
))) {
288 sc
.SetState(SCE_COFFEESCRIPT_NUMBER
);
289 } else if (setWordStart
.Contains(sc
.ch
)) {
290 sc
.SetState(SCE_COFFEESCRIPT_IDENTIFIER
);
291 } else if (sc
.Match("///")) {
292 sc
.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX
);
295 } else if (sc
.ch
== '/'
296 && (setOKBeforeRE
.Contains(chPrevNonWhite
)
297 || followsKeyword(sc
, styler
))
298 && (!setCouldBePostOp
.Contains(chPrevNonWhite
)
299 || !FollowsPostfixOperator(sc
, styler
))) {
300 sc
.SetState(SCE_COFFEESCRIPT_REGEX
); // JavaScript's RegEx
301 } else if (sc
.ch
== '\"') {
302 sc
.SetState(SCE_COFFEESCRIPT_STRING
);
303 } else if (sc
.ch
== '\'') {
304 sc
.SetState(SCE_COFFEESCRIPT_CHARACTER
);
305 } else if (sc
.ch
== '#') {
306 if (sc
.Match("###")) {
307 sc
.SetState(SCE_COFFEESCRIPT_COMMENTBLOCK
);
311 sc
.SetState(SCE_COFFEESCRIPT_COMMENTLINE
);
313 } else if (isoperator(static_cast<char>(sc
.ch
))) {
314 sc
.SetState(SCE_COFFEESCRIPT_OPERATOR
);
315 // Handle '..' and '...' operators correctly.
317 for (int i
= 0; i
< 2 && sc
.chNext
== '.'; i
++, sc
.Forward()) ;
318 } else if (sc
.ch
== '{') {
320 } else if (sc
.ch
== '}' && --brace_counts
<= 0 && inner_string_count
> 0) {
321 // Return to previous state before #{ ... }
322 sc
.ForwardSetState(exitInnerExpression(inner_string_types
,
323 inner_expn_brace_counts
,
326 continue; // skip sc.Forward() at loop end
331 if (!IsASpace(sc
.ch
) && !IsSpaceEquiv(sc
.state
)) {
332 chPrevNonWhite
= sc
.ch
;
340 static bool IsCommentLine(Sci_Position line
, Accessor
&styler
) {
341 Sci_Position pos
= styler
.LineStart(line
);
342 Sci_Position eol_pos
= styler
.LineStart(line
+ 1) - 1;
343 for (Sci_Position i
= pos
; i
< eol_pos
; i
++) {
347 else if (ch
!= ' ' && ch
!= '\t')
353 static void FoldCoffeeScriptDoc(Sci_PositionU startPos
, Sci_Position length
, int,
354 WordList
*[], Accessor
&styler
) {
355 // A simplified version of FoldPyDoc
356 const Sci_Position maxPos
= startPos
+ length
;
357 const Sci_Position maxLines
= styler
.GetLine(maxPos
- 1); // Requested last line
358 const Sci_Position docLines
= styler
.GetLine(styler
.Length() - 1); // Available last line
360 // property fold.coffeescript.comment
361 const bool foldComment
= styler
.GetPropertyInt("fold.coffeescript.comment") != 0;
363 const bool foldCompact
= styler
.GetPropertyInt("fold.compact") != 0;
365 // Backtrack to previous non-blank line so we can determine indent level
366 // for any white space lines
367 // and so we can fix any preceding fold level (which is why we go back
368 // at least one line in all cases)
370 Sci_Position lineCurrent
= styler
.GetLine(startPos
);
371 int indentCurrent
= styler
.IndentAmount(lineCurrent
, &spaceFlags
, NULL
);
372 while (lineCurrent
> 0) {
374 indentCurrent
= styler
.IndentAmount(lineCurrent
, &spaceFlags
, NULL
);
375 if (!(indentCurrent
& SC_FOLDLEVELWHITEFLAG
)
376 && !IsCommentLine(lineCurrent
, styler
))
379 int indentCurrentLevel
= indentCurrent
& SC_FOLDLEVELNUMBERMASK
;
381 // Set up initial loop state
383 if (lineCurrent
>= 1)
384 prevComment
= foldComment
&& IsCommentLine(lineCurrent
- 1, styler
);
386 // Process all characters to end of requested range
387 // or comment that hangs over the end of the range. Cap processing in all cases
388 // to end of document (in case of comment at end).
389 while ((lineCurrent
<= docLines
) && ((lineCurrent
<= maxLines
) || prevComment
)) {
392 int lev
= indentCurrent
;
393 Sci_Position lineNext
= lineCurrent
+ 1;
394 int indentNext
= indentCurrent
;
395 if (lineNext
<= docLines
) {
396 // Information about next line is only available if not at end of document
397 indentNext
= styler
.IndentAmount(lineNext
, &spaceFlags
, NULL
);
399 const int comment
= foldComment
&& IsCommentLine(lineCurrent
, styler
);
400 const int comment_start
= (comment
&& !prevComment
&& (lineNext
<= docLines
) &&
401 IsCommentLine(lineNext
, styler
) && (lev
> SC_FOLDLEVELBASE
));
402 const int comment_continue
= (comment
&& prevComment
);
404 indentCurrentLevel
= indentCurrent
& SC_FOLDLEVELNUMBERMASK
;
405 if (indentNext
& SC_FOLDLEVELWHITEFLAG
)
406 indentNext
= SC_FOLDLEVELWHITEFLAG
| indentCurrentLevel
;
409 // Place fold point at start of a block of comments
410 lev
|= SC_FOLDLEVELHEADERFLAG
;
411 } else if (comment_continue
) {
412 // Add level to rest of lines in the block
416 // Skip past any blank lines for next indent level info; we skip also
417 // comments (all comments, not just those starting in column 0)
418 // which effectively folds them into surrounding code rather
419 // than screwing up folding.
421 while ((lineNext
< docLines
) &&
422 ((indentNext
& SC_FOLDLEVELWHITEFLAG
) ||
423 (lineNext
<= docLines
&& IsCommentLine(lineNext
, styler
)))) {
426 indentNext
= styler
.IndentAmount(lineNext
, &spaceFlags
, NULL
);
429 const int levelAfterComments
= indentNext
& SC_FOLDLEVELNUMBERMASK
;
430 const int levelBeforeComments
= Platform::Maximum(indentCurrentLevel
,levelAfterComments
);
432 // Now set all the indent levels on the lines we skipped
433 // Do this from end to start. Once we encounter one line
434 // which is indented more than the line after the end of
435 // the comment-block, use the level of the block before
437 Sci_Position skipLine
= lineNext
;
438 int skipLevel
= levelAfterComments
;
440 while (--skipLine
> lineCurrent
) {
441 int skipLineIndent
= styler
.IndentAmount(skipLine
, &spaceFlags
, NULL
);
444 if ((skipLineIndent
& SC_FOLDLEVELNUMBERMASK
) > levelAfterComments
)
445 skipLevel
= levelBeforeComments
;
447 int whiteFlag
= skipLineIndent
& SC_FOLDLEVELWHITEFLAG
;
449 styler
.SetLevel(skipLine
, skipLevel
| whiteFlag
);
451 if ((skipLineIndent
& SC_FOLDLEVELNUMBERMASK
) > levelAfterComments
&&
452 !(skipLineIndent
& SC_FOLDLEVELWHITEFLAG
) &&
453 !IsCommentLine(skipLine
, styler
))
454 skipLevel
= levelBeforeComments
;
456 styler
.SetLevel(skipLine
, skipLevel
);
460 // Set fold header on non-comment line
461 if (!comment
&& !(indentCurrent
& SC_FOLDLEVELWHITEFLAG
)) {
462 if ((indentCurrent
& SC_FOLDLEVELNUMBERMASK
) < (indentNext
& SC_FOLDLEVELNUMBERMASK
))
463 lev
|= SC_FOLDLEVELHEADERFLAG
;
466 // Keep track of block comment state of previous line
467 prevComment
= comment_start
|| comment_continue
;
469 // Set fold level for this line and move to next line
470 styler
.SetLevel(lineCurrent
, lev
);
471 indentCurrent
= indentNext
;
472 lineCurrent
= lineNext
;
476 static const char *const csWordLists
[] = {
478 "Secondary keywords",
484 LexerModule
lmCoffeeScript(SCLEX_COFFEESCRIPT
, ColouriseCoffeeScriptDoc
, "coffeescript", FoldCoffeeScriptDoc
, csWordLists
);