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