LexCoffeeScript.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. // Scintilla source code edit control
  2. /** @file LexCoffeeScript.cxx
  3. ** Lexer for CoffeeScript.
  4. **/
  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.
  9. #include <stdlib.h>
  10. #include <string.h>
  11. #include <stdio.h>
  12. #include <stdarg.h>
  13. #include <assert.h>
  14. #include <ctype.h>
  15. #include "Platform.h"
  16. #include "ILexer.h"
  17. #include "Scintilla.h"
  18. #include "SciLexer.h"
  19. #include "WordList.h"
  20. #include "LexAccessor.h"
  21. #include "Accessor.h"
  22. #include "StyleContext.h"
  23. #include "CharacterSet.h"
  24. #include "LexerModule.h"
  25. #ifdef SCI_NAMESPACE
  26. using namespace Scintilla;
  27. #endif
  28. static bool IsSpaceEquiv(int state) {
  29. return (state == SCE_COFFEESCRIPT_DEFAULT
  30. || state == SCE_COFFEESCRIPT_COMMENTLINE
  31. || state == SCE_COFFEESCRIPT_COMMENTBLOCK
  32. || state == SCE_COFFEESCRIPT_VERBOSE_REGEX
  33. || state == SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT
  34. || state == SCE_COFFEESCRIPT_WORD
  35. || state == SCE_COFFEESCRIPT_REGEX);
  36. }
  37. // Store the current lexer state and brace count prior to starting a new
  38. // `#{}` interpolation level.
  39. // Based on LexRuby.cxx.
  40. static void enterInnerExpression(int *p_inner_string_types,
  41. int *p_inner_expn_brace_counts,
  42. int& inner_string_count,
  43. int state,
  44. int& brace_counts
  45. ) {
  46. p_inner_string_types[inner_string_count] = state;
  47. p_inner_expn_brace_counts[inner_string_count] = brace_counts;
  48. brace_counts = 0;
  49. ++inner_string_count;
  50. }
  51. // Restore the lexer state and brace count for the previous `#{}` interpolation
  52. // level upon returning to it.
  53. // Note the previous lexer state is the return value and needs to be restored
  54. // manually by the StyleContext.
  55. // Based on LexRuby.cxx.
  56. static int exitInnerExpression(int *p_inner_string_types,
  57. int *p_inner_expn_brace_counts,
  58. int& inner_string_count,
  59. int& brace_counts
  60. ) {
  61. --inner_string_count;
  62. brace_counts = p_inner_expn_brace_counts[inner_string_count];
  63. return p_inner_string_types[inner_string_count];
  64. }
  65. // Preconditions: sc.currentPos points to a character after '+' or '-'.
  66. // The test for pos reaching 0 should be redundant,
  67. // and is in only for safety measures.
  68. // Limitation: this code will give the incorrect answer for code like
  69. // a = b+++/ptn/...
  70. // Putting a space between the '++' post-inc operator and the '+' binary op
  71. // fixes this, and is highly recommended for readability anyway.
  72. static bool FollowsPostfixOperator(StyleContext &sc, Accessor &styler) {
  73. Sci_Position pos = (Sci_Position) sc.currentPos;
  74. while (--pos > 0) {
  75. char ch = styler[pos];
  76. if (ch == '+' || ch == '-') {
  77. return styler[pos - 1] == ch;
  78. }
  79. }
  80. return false;
  81. }
  82. static bool followsKeyword(StyleContext &sc, Accessor &styler) {
  83. Sci_Position pos = (Sci_Position) sc.currentPos;
  84. Sci_Position currentLine = styler.GetLine(pos);
  85. Sci_Position lineStartPos = styler.LineStart(currentLine);
  86. while (--pos > lineStartPos) {
  87. char ch = styler.SafeGetCharAt(pos);
  88. if (ch != ' ' && ch != '\t') {
  89. break;
  90. }
  91. }
  92. styler.Flush();
  93. return styler.StyleAt(pos) == SCE_COFFEESCRIPT_WORD;
  94. }
  95. static void ColouriseCoffeeScriptDoc(Sci_PositionU startPos, Sci_Position length, int initStyle, WordList *keywordlists[],
  96. Accessor &styler) {
  97. WordList &keywords = *keywordlists[0];
  98. WordList &keywords2 = *keywordlists[1];
  99. WordList &keywords4 = *keywordlists[3];
  100. CharacterSet setOKBeforeRE(CharacterSet::setNone, "([{=,:;!%^&*|?~+-");
  101. CharacterSet setCouldBePostOp(CharacterSet::setNone, "+-");
  102. CharacterSet setWordStart(CharacterSet::setAlpha, "_$@", 0x80, true);
  103. CharacterSet setWord(CharacterSet::setAlphaNum, "._$", 0x80, true);
  104. int chPrevNonWhite = ' ';
  105. int visibleChars = 0;
  106. // String/Regex interpolation variables, based on LexRuby.cxx.
  107. // In most cases a value of 2 should be ample for the code the user is
  108. // likely to enter. For example,
  109. // "Filling the #{container} with #{liquid}..."
  110. // from the CoffeeScript homepage nests to a level of 2
  111. // If the user actually hits a 6th occurrence of '#{' in a double-quoted
  112. // string (including regexes), it will stay as a string. The problem with
  113. // this is that quotes might flip, a 7th '#{' will look like a comment,
  114. // and code-folding might be wrong.
  115. #define INNER_STRINGS_MAX_COUNT 5
  116. // These vars track our instances of "...#{,,,'..#{,,,}...',,,}..."
  117. int inner_string_types[INNER_STRINGS_MAX_COUNT];
  118. // Track # braces when we push a new #{ thing
  119. int inner_expn_brace_counts[INNER_STRINGS_MAX_COUNT];
  120. int inner_string_count = 0;
  121. int brace_counts = 0; // Number of #{ ... } things within an expression
  122. for (int i = 0; i < INNER_STRINGS_MAX_COUNT; i++) {
  123. inner_string_types[i] = 0;
  124. inner_expn_brace_counts[i] = 0;
  125. }
  126. // look back to set chPrevNonWhite properly for better regex colouring
  127. Sci_Position endPos = startPos + length;
  128. if (startPos > 0 && IsSpaceEquiv(initStyle)) {
  129. Sci_PositionU back = startPos;
  130. styler.Flush();
  131. while (back > 0 && IsSpaceEquiv(styler.StyleAt(--back)))
  132. ;
  133. if (styler.StyleAt(back) == SCE_COFFEESCRIPT_OPERATOR) {
  134. chPrevNonWhite = styler.SafeGetCharAt(back);
  135. }
  136. if (startPos != back) {
  137. initStyle = styler.StyleAt(back);
  138. if (IsSpaceEquiv(initStyle)) {
  139. initStyle = SCE_COFFEESCRIPT_DEFAULT;
  140. }
  141. }
  142. startPos = back;
  143. }
  144. StyleContext sc(startPos, endPos - startPos, initStyle, styler);
  145. for (; sc.More();) {
  146. if (sc.atLineStart) {
  147. // Reset states to beginning of colourise so no surprises
  148. // if different sets of lines lexed.
  149. visibleChars = 0;
  150. }
  151. // Determine if the current state should terminate.
  152. switch (sc.state) {
  153. case SCE_COFFEESCRIPT_OPERATOR:
  154. sc.SetState(SCE_COFFEESCRIPT_DEFAULT);
  155. break;
  156. case SCE_COFFEESCRIPT_NUMBER:
  157. // We accept almost anything because of hex. and number suffixes
  158. if (!setWord.Contains(sc.ch) || sc.Match('.', '.')) {
  159. sc.SetState(SCE_COFFEESCRIPT_DEFAULT);
  160. }
  161. break;
  162. case SCE_COFFEESCRIPT_IDENTIFIER:
  163. if (!setWord.Contains(sc.ch) || (sc.ch == '.') || (sc.ch == '$')) {
  164. char s[1000];
  165. sc.GetCurrent(s, sizeof(s));
  166. if (keywords.InList(s)) {
  167. sc.ChangeState(SCE_COFFEESCRIPT_WORD);
  168. } else if (keywords2.InList(s)) {
  169. sc.ChangeState(SCE_COFFEESCRIPT_WORD2);
  170. } else if (keywords4.InList(s)) {
  171. sc.ChangeState(SCE_COFFEESCRIPT_GLOBALCLASS);
  172. } else if (sc.LengthCurrent() > 0 && s[0] == '@') {
  173. sc.ChangeState(SCE_COFFEESCRIPT_INSTANCEPROPERTY);
  174. }
  175. sc.SetState(SCE_COFFEESCRIPT_DEFAULT);
  176. }
  177. break;
  178. case SCE_COFFEESCRIPT_WORD:
  179. case SCE_COFFEESCRIPT_WORD2:
  180. case SCE_COFFEESCRIPT_GLOBALCLASS:
  181. case SCE_COFFEESCRIPT_INSTANCEPROPERTY:
  182. if (!setWord.Contains(sc.ch)) {
  183. sc.SetState(SCE_COFFEESCRIPT_DEFAULT);
  184. }
  185. break;
  186. case SCE_COFFEESCRIPT_COMMENTLINE:
  187. if (sc.atLineStart) {
  188. sc.SetState(SCE_COFFEESCRIPT_DEFAULT);
  189. }
  190. break;
  191. case SCE_COFFEESCRIPT_STRING:
  192. if (sc.ch == '\\') {
  193. if (sc.chNext == '\"' || sc.chNext == '\'' || sc.chNext == '\\') {
  194. sc.Forward();
  195. }
  196. } else if (sc.ch == '\"') {
  197. sc.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT);
  198. } else if (sc.ch == '#' && sc.chNext == '{' && inner_string_count < INNER_STRINGS_MAX_COUNT) {
  199. // process interpolated code #{ ... }
  200. enterInnerExpression(inner_string_types,
  201. inner_expn_brace_counts,
  202. inner_string_count,
  203. sc.state,
  204. brace_counts);
  205. sc.SetState(SCE_COFFEESCRIPT_OPERATOR);
  206. sc.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT);
  207. }
  208. break;
  209. case SCE_COFFEESCRIPT_CHARACTER:
  210. if (sc.ch == '\\') {
  211. if (sc.chNext == '\"' || sc.chNext == '\'' || sc.chNext == '\\') {
  212. sc.Forward();
  213. }
  214. } else if (sc.ch == '\'') {
  215. sc.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT);
  216. }
  217. break;
  218. case SCE_COFFEESCRIPT_REGEX:
  219. if (sc.atLineStart) {
  220. sc.SetState(SCE_COFFEESCRIPT_DEFAULT);
  221. } else if (sc.ch == '/') {
  222. sc.Forward();
  223. while ((sc.ch < 0x80) && islower(sc.ch))
  224. sc.Forward(); // gobble regex flags
  225. sc.SetState(SCE_COFFEESCRIPT_DEFAULT);
  226. } else if (sc.ch == '\\') {
  227. // Gobble up the quoted character
  228. if (sc.chNext == '\\' || sc.chNext == '/') {
  229. sc.Forward();
  230. }
  231. }
  232. break;
  233. case SCE_COFFEESCRIPT_STRINGEOL:
  234. if (sc.atLineStart) {
  235. sc.SetState(SCE_COFFEESCRIPT_DEFAULT);
  236. }
  237. break;
  238. case SCE_COFFEESCRIPT_COMMENTBLOCK:
  239. if (sc.Match("###")) {
  240. sc.Forward();
  241. sc.Forward();
  242. sc.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT);
  243. } else if (sc.ch == '\\') {
  244. sc.Forward();
  245. }
  246. break;
  247. case SCE_COFFEESCRIPT_VERBOSE_REGEX:
  248. if (sc.Match("///")) {
  249. sc.Forward();
  250. sc.Forward();
  251. sc.ForwardSetState(SCE_COFFEESCRIPT_DEFAULT);
  252. } else if (sc.Match('#')) {
  253. sc.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT);
  254. } else if (sc.ch == '\\') {
  255. sc.Forward();
  256. }
  257. break;
  258. case SCE_COFFEESCRIPT_VERBOSE_REGEX_COMMENT:
  259. if (sc.atLineStart) {
  260. sc.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX);
  261. }
  262. break;
  263. }
  264. // Determine if a new state should be entered.
  265. if (sc.state == SCE_COFFEESCRIPT_DEFAULT) {
  266. if (IsADigit(sc.ch) || (sc.ch == '.' && IsADigit(sc.chNext))) {
  267. sc.SetState(SCE_COFFEESCRIPT_NUMBER);
  268. } else if (setWordStart.Contains(sc.ch)) {
  269. sc.SetState(SCE_COFFEESCRIPT_IDENTIFIER);
  270. } else if (sc.Match("///")) {
  271. sc.SetState(SCE_COFFEESCRIPT_VERBOSE_REGEX);
  272. sc.Forward();
  273. sc.Forward();
  274. } else if (sc.ch == '/'
  275. && (setOKBeforeRE.Contains(chPrevNonWhite)
  276. || followsKeyword(sc, styler))
  277. && (!setCouldBePostOp.Contains(chPrevNonWhite)
  278. || !FollowsPostfixOperator(sc, styler))) {
  279. sc.SetState(SCE_COFFEESCRIPT_REGEX); // JavaScript's RegEx
  280. } else if (sc.ch == '\"') {
  281. sc.SetState(SCE_COFFEESCRIPT_STRING);
  282. } else if (sc.ch == '\'') {
  283. sc.SetState(SCE_COFFEESCRIPT_CHARACTER);
  284. } else if (sc.ch == '#') {
  285. if (sc.Match("###")) {
  286. sc.SetState(SCE_COFFEESCRIPT_COMMENTBLOCK);
  287. sc.Forward();
  288. sc.Forward();
  289. } else {
  290. sc.SetState(SCE_COFFEESCRIPT_COMMENTLINE);
  291. }
  292. } else if (isoperator(static_cast<char>(sc.ch))) {
  293. sc.SetState(SCE_COFFEESCRIPT_OPERATOR);
  294. // Handle '..' and '...' operators correctly.
  295. if (sc.ch == '.') {
  296. for (int i = 0; i < 2 && sc.chNext == '.'; i++, sc.Forward()) ;
  297. } else if (sc.ch == '{') {
  298. ++brace_counts;
  299. } else if (sc.ch == '}' && --brace_counts <= 0 && inner_string_count > 0) {
  300. // Return to previous state before #{ ... }
  301. sc.ForwardSetState(exitInnerExpression(inner_string_types,
  302. inner_expn_brace_counts,
  303. inner_string_count,
  304. brace_counts));
  305. continue; // skip sc.Forward() at loop end
  306. }
  307. }
  308. }
  309. if (!IsASpace(sc.ch) && !IsSpaceEquiv(sc.state)) {
  310. chPrevNonWhite = sc.ch;
  311. visibleChars++;
  312. }
  313. sc.Forward();
  314. }
  315. sc.Complete();
  316. }
  317. static bool IsCommentLine(Sci_Position line, Accessor &styler) {
  318. Sci_Position pos = styler.LineStart(line);
  319. Sci_Position eol_pos = styler.LineStart(line + 1) - 1;
  320. for (Sci_Position i = pos; i < eol_pos; i++) {
  321. char ch = styler[i];
  322. if (ch == '#')
  323. return true;
  324. else if (ch != ' ' && ch != '\t')
  325. return false;
  326. }
  327. return false;
  328. }
  329. static void FoldCoffeeScriptDoc(Sci_PositionU startPos, Sci_Position length, int,
  330. WordList *[], Accessor &styler) {
  331. // A simplified version of FoldPyDoc
  332. const Sci_Position maxPos = startPos + length;
  333. const Sci_Position maxLines = styler.GetLine(maxPos - 1); // Requested last line
  334. const Sci_Position docLines = styler.GetLine(styler.Length() - 1); // Available last line
  335. // property fold.coffeescript.comment
  336. const bool foldComment = styler.GetPropertyInt("fold.coffeescript.comment") != 0;
  337. const bool foldCompact = styler.GetPropertyInt("fold.compact") != 0;
  338. // Backtrack to previous non-blank line so we can determine indent level
  339. // for any white space lines
  340. // and so we can fix any preceding fold level (which is why we go back
  341. // at least one line in all cases)
  342. int spaceFlags = 0;
  343. Sci_Position lineCurrent = styler.GetLine(startPos);
  344. int indentCurrent = styler.IndentAmount(lineCurrent, &spaceFlags, NULL);
  345. while (lineCurrent > 0) {
  346. lineCurrent--;
  347. indentCurrent = styler.IndentAmount(lineCurrent, &spaceFlags, NULL);
  348. if (!(indentCurrent & SC_FOLDLEVELWHITEFLAG)
  349. && !IsCommentLine(lineCurrent, styler))
  350. break;
  351. }
  352. int indentCurrentLevel = indentCurrent & SC_FOLDLEVELNUMBERMASK;
  353. // Set up initial loop state
  354. int prevComment = 0;
  355. if (lineCurrent >= 1)
  356. prevComment = foldComment && IsCommentLine(lineCurrent - 1, styler);
  357. // Process all characters to end of requested range
  358. // or comment that hangs over the end of the range. Cap processing in all cases
  359. // to end of document (in case of comment at end).
  360. while ((lineCurrent <= docLines) && ((lineCurrent <= maxLines) || prevComment)) {
  361. // Gather info
  362. int lev = indentCurrent;
  363. Sci_Position lineNext = lineCurrent + 1;
  364. int indentNext = indentCurrent;
  365. if (lineNext <= docLines) {
  366. // Information about next line is only available if not at end of document
  367. indentNext = styler.IndentAmount(lineNext, &spaceFlags, NULL);
  368. }
  369. const int comment = foldComment && IsCommentLine(lineCurrent, styler);
  370. const int comment_start = (comment && !prevComment && (lineNext <= docLines) &&
  371. IsCommentLine(lineNext, styler) && (lev > SC_FOLDLEVELBASE));
  372. const int comment_continue = (comment && prevComment);
  373. if (!comment)
  374. indentCurrentLevel = indentCurrent & SC_FOLDLEVELNUMBERMASK;
  375. if (indentNext & SC_FOLDLEVELWHITEFLAG)
  376. indentNext = SC_FOLDLEVELWHITEFLAG | indentCurrentLevel;
  377. if (comment_start) {
  378. // Place fold point at start of a block of comments
  379. lev |= SC_FOLDLEVELHEADERFLAG;
  380. } else if (comment_continue) {
  381. // Add level to rest of lines in the block
  382. lev = lev + 1;
  383. }
  384. // Skip past any blank lines for next indent level info; we skip also
  385. // comments (all comments, not just those starting in column 0)
  386. // which effectively folds them into surrounding code rather
  387. // than screwing up folding.
  388. while ((lineNext < docLines) &&
  389. ((indentNext & SC_FOLDLEVELWHITEFLAG) ||
  390. (lineNext <= docLines && IsCommentLine(lineNext, styler)))) {
  391. lineNext++;
  392. indentNext = styler.IndentAmount(lineNext, &spaceFlags, NULL);
  393. }
  394. const int levelAfterComments = indentNext & SC_FOLDLEVELNUMBERMASK;
  395. const int levelBeforeComments = Platform::Maximum(indentCurrentLevel,levelAfterComments);
  396. // Now set all the indent levels on the lines we skipped
  397. // Do this from end to start. Once we encounter one line
  398. // which is indented more than the line after the end of
  399. // the comment-block, use the level of the block before
  400. Sci_Position skipLine = lineNext;
  401. int skipLevel = levelAfterComments;
  402. while (--skipLine > lineCurrent) {
  403. int skipLineIndent = styler.IndentAmount(skipLine, &spaceFlags, NULL);
  404. if (foldCompact) {
  405. if ((skipLineIndent & SC_FOLDLEVELNUMBERMASK) > levelAfterComments)
  406. skipLevel = levelBeforeComments;
  407. int whiteFlag = skipLineIndent & SC_FOLDLEVELWHITEFLAG;
  408. styler.SetLevel(skipLine, skipLevel | whiteFlag);
  409. } else {
  410. if ((skipLineIndent & SC_FOLDLEVELNUMBERMASK) > levelAfterComments &&
  411. !(skipLineIndent & SC_FOLDLEVELWHITEFLAG) &&
  412. !IsCommentLine(skipLine, styler))
  413. skipLevel = levelBeforeComments;
  414. styler.SetLevel(skipLine, skipLevel);
  415. }
  416. }
  417. // Set fold header on non-comment line
  418. if (!comment && !(indentCurrent & SC_FOLDLEVELWHITEFLAG)) {
  419. if ((indentCurrent & SC_FOLDLEVELNUMBERMASK) < (indentNext & SC_FOLDLEVELNUMBERMASK))
  420. lev |= SC_FOLDLEVELHEADERFLAG;
  421. }
  422. // Keep track of block comment state of previous line
  423. prevComment = comment_start || comment_continue;
  424. // Set fold level for this line and move to next line
  425. styler.SetLevel(lineCurrent, lev);
  426. indentCurrent = indentNext;
  427. lineCurrent = lineNext;
  428. }
  429. }
  430. static const char *const csWordLists[] = {
  431. "Keywords",
  432. "Secondary keywords",
  433. "Unused",
  434. "Global classes",
  435. 0,
  436. };
  437. LexerModule lmCoffeeScript(SCLEX_COFFEESCRIPT, ColouriseCoffeeScriptDoc, "coffeescript", FoldCoffeeScriptDoc, csWordLists);