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 // Preconditions: sc.currentPos points to a character after '+' or '-'.
44 // The test for pos reaching 0 should be redundant,
45 // and is in only for safety measures.
46 // Limitation: this code will give the incorrect answer for code like
48 // Putting a space between the '++' post-inc operator and the '+' binary op
49 // fixes this, and is highly recommended for readability anyway.
50 static bool FollowsPostfixOperator(StyleContext
&sc
, Accessor
&styler
) {
51 int pos
= (int) sc
.currentPos
;
53 char ch
= styler
[pos
];
54 if (ch
== '+' || ch
== '-') {
55 return styler
[pos
- 1] == ch
;
61 static bool followsReturnKeyword(StyleContext
&sc
, Accessor
&styler
) {
62 // Don't look at styles, so no need to flush.
63 int pos
= (int) sc
.currentPos
;
64 int currentLine
= styler
.GetLine(pos
);
65 int lineStartPos
= styler
.LineStart(currentLine
);
66 while (--pos
> lineStartPos
) {
67 char ch
= styler
.SafeGetCharAt(pos
);
68 if (ch
!= ' ' && ch
!= '\t') {
72 const char *retBack
= "nruter";
73 const char *s
= retBack
;
75 && pos
>= lineStartPos
76 && styler
.SafeGetCharAt(pos
) == *s
) {
83 static void ColouriseCoffeeScriptDoc(unsigned int startPos
, int length
, int initStyle
, WordList
*keywordlists
[],
86 WordList
&keywords
= *keywordlists
[0];
87 WordList
&keywords2
= *keywordlists
[1];
88 WordList
&keywords4
= *keywordlists
[3];
90 CharacterSet
setOKBeforeRE(CharacterSet::setNone
, "([{=,:;!%^&*|?~+-");
91 CharacterSet
setCouldBePostOp(CharacterSet::setNone
, "+-");
93 CharacterSet
setWordStart(CharacterSet::setAlpha
, "_$@", 0x80, true);
94 CharacterSet
setWord(CharacterSet::setAlphaNum
, "._$", 0x80, true);
96 int chPrevNonWhite
= ' ';
99 // look back to set chPrevNonWhite properly for better regex colouring
100 int endPos
= startPos
+ length
;
101 if (startPos
> 0 && IsSpaceEquiv(initStyle
)) {
102 unsigned int back
= startPos
;
104 while (back
> 0 && IsSpaceEquiv(styler
.StyleAt(--back
)))
106 if (styler
.StyleAt(back
) == SCE_COFFEESCRIPT_OPERATOR
) {
107 chPrevNonWhite
= styler
.SafeGetCharAt(back
);
109 if (startPos
!= back
) {
110 initStyle
= styler
.StyleAt(back
);
111 if (IsSpaceEquiv(initStyle
)) {
112 initStyle
= SCE_COFFEESCRIPT_DEFAULT
;
118 StyleContext
sc(startPos
, endPos
- startPos
, initStyle
, styler
);
120 for (; sc
.More(); sc
.Forward()) {
122 if (sc
.atLineStart
) {
123 // Reset states to beginning of colourise so no surprises
124 // if different sets of lines lexed.
128 // Determine if the current state should terminate.
130 case SCE_COFFEESCRIPT_OPERATOR
:
131 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
133 case SCE_COFFEESCRIPT_NUMBER
:
134 // We accept almost anything because of hex. and number suffixes
135 if (!setWord
.Contains(sc
.ch
) || sc
.Match('.', '.')) {
136 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
139 case SCE_COFFEESCRIPT_IDENTIFIER
:
140 if (!setWord
.Contains(sc
.ch
) || (sc
.ch
== '.') || (sc
.ch
== '$')) {
142 sc
.GetCurrent(s
, sizeof(s
));
143 if (keywords
.InList(s
)) {
144 sc
.ChangeState(SCE_COFFEESCRIPT_WORD
);
145 } else if (keywords2
.InList(s
)) {
146 sc
.ChangeState(SCE_COFFEESCRIPT_WORD2
);
147 } else if (keywords4
.InList(s
)) {
148 sc
.ChangeState(SCE_COFFEESCRIPT_GLOBALCLASS
);
150 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
153 case SCE_COFFEESCRIPT_WORD
:
154 case SCE_COFFEESCRIPT_WORD2
:
155 case SCE_COFFEESCRIPT_GLOBALCLASS
:
156 if (!setWord
.Contains(sc
.ch
)) {
157 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
160 case SCE_COFFEESCRIPT_COMMENTLINE
:
161 if (sc
.atLineStart
) {
162 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
165 case SCE_COFFEESCRIPT_STRING
:
167 if (sc
.chNext
== '\"' || sc
.chNext
== '\'' || sc
.chNext
== '\\') {
170 } else if (sc
.ch
== '\"') {
171 sc
.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT
);
174 case SCE_COFFEESCRIPT_CHARACTER
:
176 if (sc
.chNext
== '\"' || sc
.chNext
== '\'' || sc
.chNext
== '\\') {
179 } else if (sc
.ch
== '\'') {
180 sc
.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT
);
183 case SCE_COFFEESCRIPT_REGEX
:
184 if (sc
.atLineStart
) {
185 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
186 } else if (sc
.ch
== '/') {
188 while ((sc
.ch
< 0x80) && islower(sc
.ch
))
189 sc
.Forward(); // gobble regex flags
190 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
191 } else if (sc
.ch
== '\\') {
192 // Gobble up the quoted character
193 if (sc
.chNext
== '\\' || sc
.chNext
== '/') {
198 case SCE_COFFEESCRIPT_STRINGEOL
:
199 if (sc
.atLineStart
) {
200 sc
.SetState(SCE_COFFEESCRIPT_DEFAULT
);
203 case SCE_COFFEESCRIPT_COMMENTBLOCK
:
204 if (sc
.Match("###")) {
207 sc
.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT
);
208 } else if (sc
.ch
== '\\') {
212 case SCE_COFFEESCRIPT_VERBOSE_REGEX
:
213 if (sc
.Match("///")) {
216 sc
.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT
);
217 } else if (sc
.Match('#')) {
218 sc
.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT
);
219 } else if (sc
.ch
== '\\') {
223 case SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT
:
224 if (sc
.atLineStart
) {
225 sc
.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX
);
230 // Determine if a new state should be entered.
231 if (sc
.state
== SCE_COFFEESCRIPT_DEFAULT
) {
232 if (IsADigit(sc
.ch
) || (sc
.ch
== '.' && IsADigit(sc
.chNext
))) {
233 sc
.SetState(SCE_COFFEESCRIPT_NUMBER
);
234 } else if (setWordStart
.Contains(sc
.ch
)) {
235 sc
.SetState(SCE_COFFEESCRIPT_IDENTIFIER
);
236 } else if (sc
.Match("///")) {
237 sc
.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX
);
240 } else if (sc
.ch
== '/'
241 && (setOKBeforeRE
.Contains(chPrevNonWhite
)
242 || followsReturnKeyword(sc
, styler
))
243 && (!setCouldBePostOp
.Contains(chPrevNonWhite
)
244 || !FollowsPostfixOperator(sc
, styler
))) {
245 sc
.SetState(SCE_COFFEESCRIPT_REGEX
); // JavaScript's RegEx
246 } else if (sc
.ch
== '\"') {
247 sc
.SetState(SCE_COFFEESCRIPT_STRING
);
248 } else if (sc
.ch
== '\'') {
249 sc
.SetState(SCE_COFFEESCRIPT_CHARACTER
);
250 } else if (sc
.ch
== '#') {
251 if (sc
.Match("###")) {
252 sc
.SetState(SCE_COFFEESCRIPT_COMMENTBLOCK
);
256 sc
.SetState(SCE_COFFEESCRIPT_COMMENTLINE
);
258 } else if (isoperator(static_cast<char>(sc
.ch
))) {
259 sc
.SetState(SCE_COFFEESCRIPT_OPERATOR
);
263 if (!IsASpace(sc
.ch
) && !IsSpaceEquiv(sc
.state
)) {
264 chPrevNonWhite
= sc
.ch
;
271 static bool IsCommentLine(int line
, Accessor
&styler
) {
272 int pos
= styler
.LineStart(line
);
273 int eol_pos
= styler
.LineStart(line
+ 1) - 1;
274 for (int i
= pos
; i
< eol_pos
; i
++) {
278 else if (ch
!= ' ' && ch
!= '\t')
284 static void FoldCoffeeScriptDoc(unsigned int startPos
, int length
, int,
285 WordList
*[], Accessor
&styler
) {
286 // A simplified version of FoldPyDoc
287 const int maxPos
= startPos
+ length
;
288 const int maxLines
= styler
.GetLine(maxPos
- 1); // Requested last line
289 const int docLines
= styler
.GetLine(styler
.Length() - 1); // Available last line
291 // property fold.coffeescript.comment
292 const bool foldComment
= styler
.GetPropertyInt("fold.coffeescript.comment") != 0;
294 const bool foldCompact
= styler
.GetPropertyInt("fold.compact") != 0;
296 // Backtrack to previous non-blank line so we can determine indent level
297 // for any white space lines
298 // and so we can fix any preceding fold level (which is why we go back
299 // at least one line in all cases)
301 int lineCurrent
= styler
.GetLine(startPos
);
302 int indentCurrent
= styler
.IndentAmount(lineCurrent
, &spaceFlags
, NULL
);
303 while (lineCurrent
> 0) {
305 indentCurrent
= styler
.IndentAmount(lineCurrent
, &spaceFlags
, NULL
);
306 if (!(indentCurrent
& SC_FOLDLEVELWHITEFLAG
)
307 && !IsCommentLine(lineCurrent
, styler
))
310 int indentCurrentLevel
= indentCurrent
& SC_FOLDLEVELNUMBERMASK
;
312 // Set up initial loop state
314 if (lineCurrent
>= 1)
315 prevComment
= foldComment
&& IsCommentLine(lineCurrent
- 1, styler
);
317 // Process all characters to end of requested range
318 // or comment that hangs over the end of the range. Cap processing in all cases
319 // to end of document (in case of comment at end).
320 while ((lineCurrent
<= docLines
) && ((lineCurrent
<= maxLines
) || prevComment
)) {
323 int lev
= indentCurrent
;
324 int lineNext
= lineCurrent
+ 1;
325 int indentNext
= indentCurrent
;
326 if (lineNext
<= docLines
) {
327 // Information about next line is only available if not at end of document
328 indentNext
= styler
.IndentAmount(lineNext
, &spaceFlags
, NULL
);
330 const int comment
= foldComment
&& IsCommentLine(lineCurrent
, styler
);
331 const int comment_start
= (comment
&& !prevComment
&& (lineNext
<= docLines
) &&
332 IsCommentLine(lineNext
, styler
) && (lev
> SC_FOLDLEVELBASE
));
333 const int comment_continue
= (comment
&& prevComment
);
335 indentCurrentLevel
= indentCurrent
& SC_FOLDLEVELNUMBERMASK
;
336 if (indentNext
& SC_FOLDLEVELWHITEFLAG
)
337 indentNext
= SC_FOLDLEVELWHITEFLAG
| indentCurrentLevel
;
340 // Place fold point at start of a block of comments
341 lev
|= SC_FOLDLEVELHEADERFLAG
;
342 } else if (comment_continue
) {
343 // Add level to rest of lines in the block
347 // Skip past any blank lines for next indent level info; we skip also
348 // comments (all comments, not just those starting in column 0)
349 // which effectively folds them into surrounding code rather
350 // than screwing up folding.
352 while ((lineNext
< docLines
) &&
353 ((indentNext
& SC_FOLDLEVELWHITEFLAG
) ||
354 (lineNext
<= docLines
&& IsCommentLine(lineNext
, styler
)))) {
357 indentNext
= styler
.IndentAmount(lineNext
, &spaceFlags
, NULL
);
360 const int levelAfterComments
= indentNext
& SC_FOLDLEVELNUMBERMASK
;
361 const int levelBeforeComments
= Platform::Maximum(indentCurrentLevel
,levelAfterComments
);
363 // Now set all the indent levels on the lines we skipped
364 // Do this from end to start. Once we encounter one line
365 // which is indented more than the line after the end of
366 // the comment-block, use the level of the block before
368 int skipLine
= lineNext
;
369 int skipLevel
= levelAfterComments
;
371 while (--skipLine
> lineCurrent
) {
372 int skipLineIndent
= styler
.IndentAmount(skipLine
, &spaceFlags
, NULL
);
375 if ((skipLineIndent
& SC_FOLDLEVELNUMBERMASK
) > levelAfterComments
)
376 skipLevel
= levelBeforeComments
;
378 int whiteFlag
= skipLineIndent
& SC_FOLDLEVELWHITEFLAG
;
380 styler
.SetLevel(skipLine
, skipLevel
| whiteFlag
);
382 if ((skipLineIndent
& SC_FOLDLEVELNUMBERMASK
) > levelAfterComments
&&
383 !(skipLineIndent
& SC_FOLDLEVELWHITEFLAG
) &&
384 !IsCommentLine(skipLine
, styler
))
385 skipLevel
= levelBeforeComments
;
387 styler
.SetLevel(skipLine
, skipLevel
);
391 // Set fold header on non-comment line
392 if (!comment
&& !(indentCurrent
& SC_FOLDLEVELWHITEFLAG
)) {
393 if ((indentCurrent
& SC_FOLDLEVELNUMBERMASK
) < (indentNext
& SC_FOLDLEVELNUMBERMASK
))
394 lev
|= SC_FOLDLEVELHEADERFLAG
;
397 // Keep track of block comment state of previous line
398 prevComment
= comment_start
|| comment_continue
;
400 // Set fold level for this line and move to next line
401 styler
.SetLevel(lineCurrent
, lev
);
402 indentCurrent
= indentNext
;
403 lineCurrent
= lineNext
;
407 static const char *const csWordLists
[] = {
409 "Secondary keywords",
415 LexerModule
lmCoffeeScript(SCLEX_COFFEESCRIPT
, ColouriseCoffeeScriptDoc
, "coffeescript", FoldCoffeeScriptDoc
, csWordLists
);