1 module dlangui.dml.dmlhighlight;
2 
3 import dlangui.core.editable;
4 import dlangui.core.linestream;
5 import dlangui.core.textsource;
6 import dlangui.core.logger;
7 import dlangui.dml.parser;
8 import dlangui.widgets.metadata;
9 
10 class DMLSyntaxSupport : SyntaxSupport {
11 
12     EditableContent _content;
13     SourceFile _file;
14     this (string filename) {
15         _file = new SourceFile(filename);
16     }
17 
18     TokenPropString[] _props;
19 
20     /// returns editable content
21     @property EditableContent content() { return _content; }
22     /// set editable content
23     @property SyntaxSupport content(EditableContent content) {
24         _content = content;
25         return this;
26     }
27 
28     private enum BracketMatch {
29         CONTINUE,
30         FOUND,
31         ERROR
32     }
33 
34     private static struct BracketStack {
35         dchar[] buf;
36         int pos;
37         bool reverse;
38         void initialize(bool reverse) {
39             this.reverse = reverse;
40             pos = 0;
41         }
42         void push(dchar ch) {
43             if (buf.length <= pos)
44                 buf.length = pos + 16;
45             buf[pos++] = ch;
46         }
47         dchar pop() {
48             if (pos <= 0)
49                 return 0;
50             return buf[--pos];
51         }
52         BracketMatch process(dchar ch) {
53             if (reverse) {
54                 if (isCloseBracket(ch)) {
55                     push(ch);
56                     return BracketMatch.CONTINUE;
57                 } else {
58                     if (pop() != pairedBracket(ch))
59                         return BracketMatch.ERROR;
60                     if (pos == 0)
61                         return BracketMatch.FOUND;
62                     return BracketMatch.CONTINUE;
63                 }
64             } else {
65                 if (isOpenBracket(ch)) {
66                     push(ch);
67                     return BracketMatch.CONTINUE;
68                 } else {
69                     if (pop() != pairedBracket(ch))
70                         return BracketMatch.ERROR;
71                     if (pos == 0)
72                         return BracketMatch.FOUND;
73                     return BracketMatch.CONTINUE;
74                 }
75             }
76         }
77     }
78     BracketStack _bracketStack;
79     static bool isBracket(dchar ch) {
80         return pairedBracket(ch) != 0;
81     }
82     static dchar pairedBracket(dchar ch) {
83         switch (ch) {
84             case '(':
85                 return ')';
86             case ')':
87                 return '(';
88             case '{':
89                 return '}';
90             case '}':
91                 return '{';
92             case '[':
93                 return ']';
94             case ']':
95                 return '[';
96             default:
97                 return 0; // not a bracket
98         }
99     }
100     static bool isOpenBracket(dchar ch) {
101         switch (ch) {
102             case '(':
103             case '{':
104             case '[':
105                 return true;
106             default:
107                 return false;
108         }
109     }
110     static bool isCloseBracket(dchar ch) {
111         switch (ch) {
112             case ')':
113             case '}':
114             case ']':
115                 return true;
116             default:
117                 return false;
118         }
119     }
120 
121     protected dchar nextBracket(int dir, ref TextPosition p) {
122         for (;;) {
123             TextPosition oldpos = p;
124             p = dir < 0 ? _content.prevCharPos(p) : _content.nextCharPos(p);
125             if (p == oldpos)
126                 return 0;
127             auto prop = _content.tokenProp(p);
128             if (tokenCategory(prop) == TokenCategory.Op) {
129                 dchar ch = _content[p];
130                 if (isBracket(ch))
131                     return ch;
132             }
133         }
134     }
135 
136     /// returns paired bracket {} () [] for char at position p, returns paired char position or p if not found or not bracket
137     override TextPosition findPairedBracket(TextPosition p) {
138         if (p.line < 0 || p.line >= content.length)
139             return p;
140         dstring s = content.line(p.line);
141         if (p.pos < 0 || p.pos >= s.length)
142             return p;
143         dchar ch = content[p];
144         dchar paired = pairedBracket(ch);
145         if (!paired)
146             return p;
147         TextPosition startPos = p;
148         int dir = isOpenBracket(ch) ? 1 : -1;
149         _bracketStack.initialize(dir < 0);
150         _bracketStack.process(ch);
151         for (;;) {
152             ch = nextBracket(dir, p);
153             if (!ch) // no more brackets
154                 return startPos;
155             auto match = _bracketStack.process(ch);
156             if (match == BracketMatch.FOUND)
157                 return p;
158             if (match == BracketMatch.ERROR)
159                 return startPos;
160             // continue
161         }
162     }
163 
164 
165     /// return true if toggle line comment is supported for file type
166     override @property bool supportsToggleLineComment() {
167         return true;
168     }
169 
170     /// return true if can toggle line comments for specified text range
171     override bool canToggleLineComment(TextRange range) {
172         TextRange r = content.fullLinesRange(range);
173         if (isInsideBlockComment(r.start) || isInsideBlockComment(r.end))
174             return false;
175         return true;
176     }
177 
178     protected bool isLineComment(dstring s) {
179         foreach(i; 0 .. s.length - 1) {
180             if (s[i] == '/' && s[i + 1] == '/')
181                 return true;
182             else if (s[i] != ' ' && s[i] != '\t')
183                 return false;
184         }
185         return false;
186     }
187 
188     protected dstring commentLine(dstring s, int commentX) {
189         dchar[] res;
190         int x = 0;
191         bool commented = false;
192         foreach(i; 0 .. s.length) {
193             dchar ch = s[i];
194             if (ch == '\t') {
195                 int newX = (x + _content.tabSize) / _content.tabSize * _content.tabSize;
196                 if (!commented && newX >= commentX) {
197                     commented = true;
198                     if (newX != commentX) {
199                         // replace tab with space
200                         for (; x <= commentX; x++)
201                             res ~= ' ';
202                     } else {
203                         res ~= ch;
204                         x = newX;
205                     }
206                     res ~= "//"d;
207                     x += 2;
208                 } else {
209                     res ~= ch;
210                     x = newX;
211                 }
212             } else {
213                 if (!commented && x == commentX) {
214                     commented = true;
215                     res ~= "//"d;
216                     res ~= ch;
217                     x += 3;
218                 } else {
219                     res ~= ch;
220                     x++;
221                 }
222             }
223         }
224         if (!commented) {
225             for (; x < commentX; x++)
226                 res ~= ' ';
227             res ~= "//"d;
228         }
229         return cast(dstring)res;
230     }
231 
232     /// remove single line comment from beginning of line
233     protected dstring uncommentLine(dstring s) {
234         int p = -1;
235         foreach(int i; 0 .. cast(int)s.length - 1) {
236             if (s[i] == '/' && s[i + 1] == '/') {
237                 p = i;
238                 break;
239             }
240         }
241         if (p < 0)
242             return s;
243         s = s[0..p] ~ s[p + 2 .. $];
244         foreach(i; 0 .. s.length) {
245             if (s[i] != ' ' && s[i] != '\t') {
246                 return s;
247             }
248         }
249         return null;
250     }
251 
252     /// searches for neares token start before or equal to position
253     protected TextPosition tokenStart(TextPosition pos) {
254         TextPosition p = pos;
255         for (;;) {
256             TextPosition prevPos = content.prevCharPos(p);
257             if (p == prevPos)
258                 return p; // begin of file
259             TokenProp prop = content.tokenProp(p);
260             TokenProp prevProp = content.tokenProp(prevPos);
261             if (prop && prop != prevProp)
262                 return p;
263             p = prevPos;
264         }
265     }
266 
267     static struct TokenWithRange {
268         Token token;
269         TextRange range;
270         @property string toString() const {
271             return token.toString ~ range.toString;
272         }
273     }
274 
275     protected Token[] _tokens;
276     protected int _tokenIndex;
277 
278     protected bool initTokenizer() {
279         _tokens = tokenizeML(content.lines);
280         _tokenIndex = 0;
281         return true;
282     }
283 
284     protected TokenWithRange nextToken() {
285         TokenWithRange res;
286         if (_tokenIndex < _tokens.length) {
287             res.range.start = TextPosition(_tokens[_tokenIndex].line, _tokens[_tokenIndex].pos);
288             if (_tokenIndex + 1 < _tokens.length)
289                 res.range.end = TextPosition(_tokens[_tokenIndex + 1].line, _tokens[_tokenIndex + 1].pos);
290             else
291                 res.range.end = content.endOfFile();
292             res.token = _tokens[_tokenIndex];
293             _tokenIndex++;
294         } else {
295             res.range.end = res.range.start = content.endOfFile();
296         }
297         return res;
298     }
299 
300     protected TokenWithRange getPositionToken(TextPosition pos) {
301         initTokenizer();
302         for (;;) {
303             TokenWithRange tokenRange = nextToken();
304             //Log.d("read token: ", tokenRange);
305             if (tokenRange.token.type == TokenType.eof) {
306                 //Log.d("end of file");
307                 return tokenRange;
308             }
309             if (pos >= tokenRange.range.start && pos < tokenRange.range.end) {
310                 //Log.d("found: ", pos, " in ", tokenRange);
311                 return tokenRange;
312             }
313         }
314     }
315 
316     protected TokenWithRange[] getRangeTokens(TextRange range) {
317         TokenWithRange[] res;
318         initTokenizer();
319         for (;;) {
320             TokenWithRange tokenRange = nextToken();
321             //Log.d("read token: ", tokenRange);
322             if (tokenRange.token.type == TokenType.eof) {
323                 return res;
324             }
325             if (tokenRange.range.intersects(range)) {
326                 //Log.d("found: ", pos, " in ", tokenRange);
327                 res ~= tokenRange;
328             }
329         }
330     }
331 
332     protected bool isInsideBlockComment(TextPosition pos) {
333         TokenWithRange tokenRange = getPositionToken(pos);
334         if (tokenRange.token.type == TokenType.comment && tokenRange.token.isMultilineComment)
335             return pos > tokenRange.range.start && pos < tokenRange.range.end;
336         return false;
337     }
338 
339     /// toggle line comments for specified text range
340     override void toggleLineComment(TextRange range, Object source) {
341         TextRange r = content.fullLinesRange(range);
342         if (isInsideBlockComment(r.start) || isInsideBlockComment(r.end))
343             return;
344         int lineCount = r.end.line - r.start.line;
345         bool noEolAtEndOfRange = false;
346         if (lineCount == 0 || r.end.pos > 0) {
347             noEolAtEndOfRange = true;
348             lineCount++;
349         }
350         int minLeftX = -1;
351         bool hasComments = false;
352         bool hasNoComments = false;
353         bool hasNonEmpty = false;
354         dstring[] srctext;
355         dstring[] dsttext;
356         foreach(i; 0 .. lineCount) {
357             int lineIndex = r.start.line + i;
358             dstring s = content.line(lineIndex);
359             srctext ~= s;
360             TextLineMeasure m = content.measureLine(lineIndex);
361             if (!m.empty) {
362                 if (minLeftX < 0 || minLeftX > m.firstNonSpaceX)
363                     minLeftX = m.firstNonSpaceX;
364                 hasNonEmpty = true;
365                 if (isLineComment(s))
366                     hasComments = true;
367                 else
368                     hasNoComments = true;
369             }
370         }
371         if (minLeftX < 0)
372             minLeftX = 0;
373         if (hasNoComments || !hasComments) {
374             // comment
375             foreach(i; 0 .. lineCount) {
376                 dsttext ~= commentLine(srctext[i], minLeftX);
377             }
378             if (!noEolAtEndOfRange)
379                 dsttext ~= ""d;
380             EditOperation op = new EditOperation(EditAction.Replace, r, dsttext);
381             _content.performOperation(op, source);
382         } else {
383             // uncomment
384             foreach(i; 0 .. lineCount) {
385                 dsttext ~= uncommentLine(srctext[i]);
386             }
387             if (!noEolAtEndOfRange)
388                 dsttext ~= ""d;
389             EditOperation op = new EditOperation(EditAction.Replace, r, dsttext);
390             _content.performOperation(op, source);
391         }
392     }
393 
394     /// return true if toggle block comment is supported for file type
395     override @property bool supportsToggleBlockComment() {
396         return true;
397     }
398     /// return true if can toggle block comments for specified text range
399     override bool canToggleBlockComment(TextRange range) {
400         TokenWithRange startToken = getPositionToken(range.start);
401         TokenWithRange endToken = getPositionToken(range.end);
402         //Log.d("canToggleBlockComment: startToken=", startToken, " endToken=", endToken);
403         if (startToken.range == endToken.range && startToken.token.isMultilineComment) {
404             //Log.d("canToggleBlockComment: can uncomment");
405             return true;
406         }
407         if (range.empty)
408             return false;
409         TokenWithRange[] tokens = getRangeTokens(range);
410         foreach(ref t; tokens) {
411             if (t.token.type == TokenType.comment) {
412                 if (t.token.isMultilineComment) {
413                     // disable until nested comments support is implemented
414                     return false;
415                 } else {
416                     // single line comment
417                     if (t.range.isInside(range.start) || t.range.isInside(range.end))
418                         return false;
419                 }
420             }
421         }
422         return true;
423     }
424     /// toggle block comments for specified text range
425     override void toggleBlockComment(TextRange srcrange, Object source) {
426         TokenWithRange startToken = getPositionToken(srcrange.start);
427         TokenWithRange endToken = getPositionToken(srcrange.end);
428         if (startToken.range == endToken.range && startToken.token.isMultilineComment) {
429             TextRange range = startToken.range;
430             dstring[] dsttext;
431             foreach(i; range.start.line .. range.end.line + 1) {
432                 dstring s = content.line(i);
433                 int charsRemoved = 0;
434                 if (i == range.start.line) {
435                     int maxp = content.lineLength(range.start.line);
436                     if (i == range.end.line)
437                         maxp = range.end.pos - 2;
438                     charsRemoved = 2;
439                     foreach(j; range.start.pos + charsRemoved .. maxp) {
440                         if (s[j] != s[j - 1])
441                             break;
442                         charsRemoved++;
443                     }
444                     //Log.d("line before removing start of comment:", s);
445                     s = s[range.start.pos + charsRemoved .. $];
446                     //Log.d("line after removing start of comment:", s);
447                     charsRemoved += range.start.pos;
448                 }
449                 if (i == range.end.line) {
450                     int endp = range.end.pos;
451                     if (charsRemoved > 0)
452                         endp -= charsRemoved;
453                     int endRemoved = 2;
454                     for (int j = endp - endRemoved; j >= 0; j--) {
455                         if (s[j] != s[j + 1])
456                             break;
457                         endRemoved++;
458                     }
459                     //Log.d("line before removing end of comment:", s);
460                     s = s[0 .. endp - endRemoved];
461                     //Log.d("line after removing end of comment:", s);
462                 }
463                 dsttext ~= s;
464             }
465             EditOperation op = new EditOperation(EditAction.Replace, range, dsttext);
466             _content.performOperation(op, source);
467             return;
468         } else {
469             if (srcrange.empty)
470                 return;
471             TokenWithRange[] tokens = getRangeTokens(srcrange);
472             foreach(ref t; tokens) {
473                 if (t.token.type == TokenType.comment) {
474                     if (t.token.isMultilineComment) {
475                         // disable until nested comments support is implemented
476                         return;
477                     } else {
478                         // single line comment
479                         if (t.range.isInside(srcrange.start) || t.range.isInside(srcrange.end))
480                             return;
481                     }
482                 }
483             }
484             dstring[] dsttext;
485             foreach(i; srcrange.start.line .. srcrange.end.line + 1) {
486                 dstring s = content.line(i);
487                 int charsAdded = 0;
488                 if (i == srcrange.start.line) {
489                     int p = srcrange.start.pos;
490                     if (p < s.length) {
491                         s = s[p .. $];
492                         charsAdded = -p;
493                     } else {
494                         charsAdded = -(cast(int)s.length);
495                         s = null;
496                     }
497                     s = "/*" ~ s;
498                     charsAdded += 2;
499                 }
500                 if (i == srcrange.end.line) {
501                     int p = srcrange.end.pos + charsAdded;
502                     s = p > 0 ? s[0..p] : null;
503                     s ~= "*/";
504                 }
505                 dsttext ~= s;
506             }
507             EditOperation op = new EditOperation(EditAction.Replace, srcrange, dsttext);
508             _content.performOperation(op, source);
509             return;
510         }
511 
512     }
513 
514     /// categorize characters in content by token types
515     void updateHighlight(dstring[] lines, TokenPropString[] props, int changeStartLine, int changeEndLine) {
516 
517         initTokenizer();
518         _props = props;
519         changeStartLine = 0;
520         changeEndLine = cast(int)lines.length;
521         int tokenPos = 0;
522         int tokenLine = 0;
523         ubyte category = 0;
524         try {
525             for (;;) {
526                 TokenWithRange token = nextToken();
527                 if (token.token.type == TokenType.eof) {
528                     break;
529                 }
530                 uint newPos = token.range.start.pos;
531                 uint newLine = token.range.start.line;
532 
533                 // fill with category
534                 foreach(int i; tokenLine .. newLine + 1) {
535                     int start = i > tokenLine ? 0 : tokenPos;
536                     int end = i < newLine ? cast(int)lines[i].length : newPos;
537                     foreach(j; start .. end) {
538                         if (j < _props[i].length) {
539                             _props[i][j] = category;
540                         }
541                     }
542                 }
543 
544                 // handle token - convert to category
545                 switch(token.token.type) with(TokenType)
546                 {
547                     case comment:
548                         category = TokenCategory.Comment;
549                         break;
550                     case ident:
551                         if (isWidgetClassName(token.token.text))
552                             category = TokenCategory.Identifier_Class;
553                         else
554                             category = TokenCategory.Identifier;
555                         break;
556                     case str:
557                         category = TokenCategory.String;
558                         break;
559                     case integer:
560                         category = TokenCategory.Integer;
561                         break;
562                     case floating:
563                         category = TokenCategory.Float;
564                         break;
565                     case error:
566                         category = TokenCategory.Error;
567                         break;
568                     default:
569                         if (token.token.type >= colon)
570                             category = TokenCategory.Op;
571                         else
572                             category = 0;
573                         break;
574                 }
575                 tokenPos = newPos;
576                 tokenLine= newLine;
577 
578             }
579         } catch (Exception e) {
580             Log.e("exception while trying to parse DML source", e);
581         }
582         _props = null;
583     }
584 
585 
586     /// returns true if smart indent is supported
587     override bool supportsSmartIndents() {
588         return true;
589     }
590 
591     protected bool _opInProgress;
592     protected void applyNewLineSmartIndent(EditOperation op, Object source) {
593         int line = op.newRange.end.line;
594         if (line == 0)
595             return; // not for first line
596         int prevLine = line - 1;
597         TextLineMeasure lineMeasurement = _content.measureLine(line);
598         TextLineMeasure prevLineMeasurement = _content.measureLine(prevLine);
599         while (prevLineMeasurement.empty && prevLine > 0) {
600             prevLine--;
601             prevLineMeasurement = _content.measureLine(prevLine);
602         }
603         if (lineMeasurement.firstNonSpaceX >= 0 && lineMeasurement.firstNonSpaceX < prevLineMeasurement.firstNonSpaceX) {
604             dstring prevLineText = _content.line(prevLine);
605             TokenPropString prevLineTokenProps = _content.lineTokenProps(prevLine);
606             dchar lastOpChar = 0;
607             for (int j = prevLineMeasurement.lastNonSpace; j >= 0; j--) {
608                 auto cat = j < prevLineTokenProps.length ? tokenCategory(prevLineTokenProps[j]) : 0;
609                 if (cat == TokenCategory.Op) {
610                     lastOpChar = prevLineText[j];
611                     break;
612                 } else if (cat != TokenCategory.Comment && cat != TokenCategory.WhiteSpace) {
613                     break;
614                 }
615             }
616             int spacex = prevLineMeasurement.firstNonSpaceX;
617             if (lastOpChar == '{')
618                 spacex = _content.nextTab(spacex);
619             dstring txt = _content.fillSpace(spacex);
620             EditOperation op2 = new EditOperation(EditAction.Replace, TextRange(TextPosition(line, 0), TextPosition(line, lineMeasurement.firstNonSpace >= 0 ? lineMeasurement.firstNonSpace : 0)), [txt]);
621             _opInProgress = true;
622             _content.performOperation(op2, source);
623             _opInProgress = false;
624         }
625     }
626 
627     protected void applyClosingCurlySmartIndent(EditOperation op, Object source) {
628         int line = op.newRange.end.line;
629         TextPosition p2 = findPairedBracket(op.newRange.start);
630         if (p2 == op.newRange.start || p2.line > op.newRange.start.line)
631             return;
632         int prevLine = p2.line;
633         TextLineMeasure lineMeasurement = _content.measureLine(line);
634         TextLineMeasure prevLineMeasurement = _content.measureLine(prevLine);
635         if (lineMeasurement.firstNonSpace != op.newRange.start.pos)
636             return; // not in beginning of line
637         if (lineMeasurement.firstNonSpaceX >= 0 && lineMeasurement.firstNonSpaceX != prevLineMeasurement.firstNonSpaceX) {
638             int spacex = prevLineMeasurement.firstNonSpaceX;
639             if (spacex != lineMeasurement.firstNonSpaceX) {
640                 dstring txt = _content.fillSpace(spacex);
641                 txt = txt ~ "}";
642                 EditOperation op2 = new EditOperation(EditAction.Replace, TextRange(TextPosition(line, 0), TextPosition(line, lineMeasurement.firstNonSpace >= 0 ? lineMeasurement.firstNonSpace + 1 : 0)), [txt]);
643                 _opInProgress = true;
644                 _content.performOperation(op2, source);
645                 _opInProgress = false;
646             }
647         }
648     }
649 
650     /// apply smart indent, if supported
651     override void applySmartIndent(EditOperation op, Object source) {
652         if (_opInProgress)
653             return;
654         if (op.isInsertNewLine) {
655             // Enter key pressed - new line inserted or splitted
656             applyNewLineSmartIndent(op, source);
657         } else if (op.singleChar == '}') {
658             // } entered - probably need unindent
659             applyClosingCurlySmartIndent(op, source);
660         } else if (op.singleChar == '{') {
661             // { entered - probably need auto closing }
662         }
663     }
664 
665 }
666