1 module dlangide.ui.dsourceedit; 2 3 import dlangui.core.logger; 4 import dlangui.core.signals; 5 import dlangui.graphics.drawbuf; 6 import dlangui.widgets.editors; 7 import dlangui.widgets.srcedit; 8 import dlangui.widgets.menu; 9 import dlangui.widgets.popup; 10 import dlangui.widgets.controls; 11 import dlangui.widgets.scroll; 12 import dlangui.dml.dmlhighlight; 13 14 import ddc.lexer.textsource; 15 import ddc.lexer.exceptions; 16 import ddc.lexer.tokenizer; 17 18 import dlangide.workspace.workspace; 19 import dlangide.workspace.project; 20 import dlangide.ui.commands; 21 import dlangide.ui.settings; 22 import dlangide.tools.d.dsyntax; 23 import dlangide.tools.editorTool; 24 import ddebug.common.debugger; 25 26 import std.algorithm; 27 import std.utf : toUTF32; 28 import dlangui.core.types : toUTF8; 29 30 interface BreakpointListChangeListener { 31 void onBreakpointListChanged(ProjectSourceFile sourceFile, Breakpoint[] breakpoints); 32 } 33 34 interface BookmarkListChangeListener { 35 void onBookmarkListChanged(ProjectSourceFile sourceFile, EditorBookmark[] bookmarks); 36 } 37 38 /// DIDE source file editor 39 class DSourceEdit : SourceEdit, EditableContentMarksChangeListener { 40 this(string ID) { 41 super(ID); 42 static if (BACKEND_GUI) { 43 styleId = null; 44 backgroundColor = style.customColor("edit_background"); 45 } 46 onThemeChanged(); 47 //setTokenHightlightColor(TokenCategory.Identifier, 0x206000); // no colors 48 MenuItem editPopupItem = new MenuItem(null); 49 editPopupItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_EDIT_UNDO, 50 ACTION_EDIT_REDO, ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT, 51 ACTION_GET_COMPLETIONS, ACTION_GO_TO_DEFINITION, ACTION_DEBUG_TOGGLE_BREAKPOINT); 52 popupMenu = editPopupItem; 53 showIcons = true; 54 //showFolding = true; 55 showWhiteSpaceMarks = true; 56 showTabPositionMarks = true; 57 content.marksChanged = this; 58 } 59 60 this() { 61 this("SRCEDIT"); 62 } 63 64 ~this() { 65 if (_editorTool) { 66 destroy(_editorTool); 67 _editorTool = null; 68 } 69 } 70 71 Signal!BreakpointListChangeListener breakpointListChanged; 72 Signal!BookmarkListChangeListener bookmarkListChanged; 73 74 /// handle theme change: e.g. reload some themed resources 75 override void onThemeChanged() { 76 static if (BACKEND_GUI) backgroundColor = style.customColor("edit_background"); 77 setTokenHightlightColor(TokenCategory.Comment, style.customColor("syntax_highlight_comment")); // green 78 setTokenHightlightColor(TokenCategory.Keyword, style.customColor("syntax_highlight_keyword")); // blue 79 setTokenHightlightColor(TokenCategory.Integer, style.customColor("syntax_highlight_integer", 0x000000)); 80 setTokenHightlightColor(TokenCategory.Float, style.customColor("syntax_highlight_float", 0x000000)); 81 setTokenHightlightColor(TokenCategory.String, style.customColor("syntax_highlight_string")); // brown 82 setTokenHightlightColor(TokenCategory.Identifier, style.customColor("syntax_highlight_ident")); 83 setTokenHightlightColor(TokenCategory.Character, style.customColor("syntax_highlight_character")); // brown 84 setTokenHightlightColor(TokenCategory.Error, style.customColor("syntax_highlight_error")); // red 85 setTokenHightlightColor(TokenCategory.Comment_Documentation, style.customColor("syntax_highlight_comment_documentation")); 86 87 super.onThemeChanged(); 88 } 89 90 protected IDESettings _settings; 91 @property DSourceEdit settings(IDESettings s) { 92 _settings = s; 93 return this; 94 } 95 @property IDESettings settings() { 96 return _settings; 97 } 98 protected int _previousFontSizeSetting; 99 void applySettings() { 100 if (!_settings) 101 return; 102 tabSize = _settings.tabSize; 103 useSpacesForTabs = _settings.useSpacesForTabs; 104 smartIndents = _settings.smartIndents; 105 smartIndentsAfterPaste = _settings.smartIndentsAfterPaste; 106 showWhiteSpaceMarks = _settings.showWhiteSpaceMarks; 107 showTabPositionMarks = _settings.showTabPositionMarks; 108 string face = _settings.editorFontFace; 109 if (face == "Default") 110 face = null; 111 else if (face) 112 face ~= ","; 113 face ~= DEFAULT_SOURCE_EDIT_FONT_FACES; 114 fontFace = face; 115 int newFontSizeSetting = _settings.editorFontSize; 116 bool needChangeFontSize = _previousFontSizeSetting == 0 || (_previousFontSizeSetting != newFontSizeSetting && _previousFontSizeSetting.pointsToPixels == fontSize); 117 if (needChangeFontSize) { 118 fontSize = newFontSizeSetting.pointsToPixels; 119 _previousFontSizeSetting = newFontSizeSetting; 120 } 121 } 122 123 protected EditorTool _editorTool; 124 @property EditorTool editorTool() { return _editorTool; } 125 @property EditorTool editorTool(EditorTool tool) { 126 if (_editorTool && _editorTool !is tool) { 127 destroy(_editorTool); 128 _editorTool = null; 129 } 130 return _editorTool = tool; 131 }; 132 133 protected ProjectSourceFile _projectSourceFile; 134 @property ProjectSourceFile projectSourceFile() { return _projectSourceFile; } 135 @property void projectSourceFile(ProjectSourceFile v) { _projectSourceFile = v; } 136 /// load by filename 137 override bool load(string fn) { 138 _projectSourceFile = null; 139 bool res = super.load(fn); 140 setSyntaxSupport(); 141 return res; 142 } 143 144 @property bool isDSourceFile() { 145 return filename.endsWith(".d") || filename.endsWith(".dd") || filename.endsWith(".dd") || 146 filename.endsWith(".di") || filename.endsWith(".dh") || filename.endsWith(".ddoc"); 147 } 148 149 @property bool isJsonFile() { 150 return filename.endsWith(".json") || filename.endsWith(".JSON"); 151 } 152 153 @property bool isDMLFile() { 154 return filename.endsWith(".dml") || filename.endsWith(".DML"); 155 } 156 157 @property bool isXMLFile() { 158 return filename.endsWith(".xml") || filename.endsWith(".XML"); 159 } 160 161 override protected MenuItem getLeftPaneIconsPopupMenu(int line) { 162 MenuItem menu = super.getLeftPaneIconsPopupMenu(line); 163 if (isDSourceFile) { 164 Action action = ACTION_DEBUG_TOGGLE_BREAKPOINT.clone(); 165 action.longParam = line; 166 action.objectParam = this; 167 menu.add(action); 168 action = ACTION_DEBUG_ENABLE_BREAKPOINT.clone(); 169 action.longParam = line; 170 action.objectParam = this; 171 menu.add(action); 172 action = ACTION_DEBUG_DISABLE_BREAKPOINT.clone(); 173 action.longParam = line; 174 action.objectParam = this; 175 menu.add(action); 176 } 177 return menu; 178 } 179 180 uint _executionLineHighlightColor = BACKEND_GUI ? 0x808080FF : 0x000080; 181 int _executionLine = -1; 182 @property int executionLine() { return _executionLine; } 183 @property void executionLine(int line) { 184 if (line == _executionLine) 185 return; 186 _executionLine = line; 187 if (_executionLine >= 0) { 188 setCaretPos(_executionLine, 0, true); 189 } 190 invalidate(); 191 } 192 /// override to custom highlight of line background 193 override protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 194 if (lineIndex == _executionLine) { 195 buf.fillRect(visibleRect, _executionLineHighlightColor); 196 } 197 super.drawLineBackground(buf, lineIndex, lineRect, visibleRect); 198 } 199 200 void setSyntaxSupport() { 201 if (isDSourceFile) { 202 content.syntaxSupport = new SimpleDSyntaxSupport(filename); 203 } else if (isJsonFile) { 204 content.syntaxSupport = new DMLSyntaxSupport(filename); 205 } else if (isDMLFile) { 206 content.syntaxSupport = new DMLSyntaxSupport(filename); 207 } else { 208 content.syntaxSupport = null; 209 } 210 } 211 212 /// returns project import paths - if file from project is opened in current editor 213 string[] importPaths() { 214 if (_projectSourceFile) 215 return _projectSourceFile.project.importPaths; 216 return null; 217 } 218 219 /// load by project item 220 bool load(ProjectSourceFile f) { 221 if (!load(f.filename)) { 222 _projectSourceFile = null; 223 return false; 224 } 225 _projectSourceFile = f; 226 setSyntaxSupport(); 227 return true; 228 } 229 230 /// save to the same file 231 bool save() { 232 return _content.save(); 233 } 234 235 /// save to the same file 236 override bool save(string fn) { 237 bool res = super.save(fn); 238 //if (res && projectSourceFile) 239 // projectSourceFile.setFilename(filename); 240 return res; 241 } 242 243 void insertCompletion(dstring completionText) { 244 TextRange range; 245 TextPosition p = caretPos; 246 range.start = range.end = p; 247 dstring lineText = content.line(p.line); 248 dchar prevChar = p.pos > 0 ? lineText[p.pos - 1] : 0; 249 dchar nextChar = p.pos < lineText.length ? lineText[p.pos] : 0; 250 if (isIdentMiddleChar(prevChar)) { 251 while(range.start.pos > 0 && isIdentMiddleChar(lineText[range.start.pos - 1])) 252 range.start.pos--; 253 if (isIdentMiddleChar(nextChar)) { 254 while(range.end.pos < lineText.length && isIdentMiddleChar(lineText[range.end.pos])) 255 range.end.pos++; 256 } 257 } 258 EditOperation edit = new EditOperation(EditAction.Replace, range, completionText); 259 _content.performOperation(edit, this); 260 setFocus(); 261 } 262 263 /// override to handle specific actions 264 override bool handleAction(const Action a) { 265 import ddc.lexer.tokenizer; 266 if (a) { 267 switch (a.id) { 268 case IDEActions.FileSave: 269 save(); 270 return true; 271 case IDEActions.InsertCompletion: 272 insertCompletion(a.label); 273 return true; 274 case IDEActions.DebugToggleBreakpoint: 275 case IDEActions.DebugEnableBreakpoint: 276 case IDEActions.DebugDisableBreakpoint: 277 handleBreakpointAction(a); 278 return true; 279 case EditorActions.ToggleBookmark: 280 super.handleAction(a); 281 notifyBookmarkListChanged(); 282 return true; 283 default: 284 break; 285 } 286 } 287 return super.handleAction(a); 288 } 289 290 /// Handle Ctrl + Left mouse click on text 291 override protected void onControlClick() { 292 window.dispatchAction(ACTION_GO_TO_DEFINITION); 293 } 294 295 296 /// left button click on icons panel: toggle breakpoint 297 override protected bool handleLeftPaneIconsMouseClick(MouseEvent event, Rect rc, int line) { 298 if (event.button == MouseButton.Left) { 299 LineIcon icon = content.lineIcons.findByLineAndType(line, LineIconType.breakpoint); 300 if (icon) 301 removeBreakpoint(line, icon); 302 else 303 addBreakpoint(line); 304 return true; 305 } 306 return super.handleLeftPaneIconsMouseClick(event, rc, line); 307 } 308 309 protected void addBreakpoint(int line) { 310 import std.path; 311 Breakpoint bp = new Breakpoint(); 312 bp.file = baseName(filename); 313 bp.line = line + 1; 314 bp.fullFilePath = filename; 315 if (projectSourceFile) { 316 bp.projectName = toUTF8(projectSourceFile.project.name); 317 bp.projectFilePath = projectSourceFile.project.absoluteToRelativePath(filename); 318 } 319 LineIcon icon = new LineIcon(LineIconType.breakpoint, line, bp); 320 content.lineIcons.add(icon); 321 notifyBreakpointListChanged(); 322 } 323 324 protected void removeBreakpoint(int line, LineIcon icon) { 325 content.lineIcons.remove(icon); 326 notifyBreakpointListChanged(); 327 } 328 329 void setBreakpointList(Breakpoint[] breakpoints) { 330 // remove all existing breakpoints 331 content.lineIcons.removeByType(LineIconType.breakpoint); 332 // add new breakpoints 333 foreach(bp; breakpoints) { 334 LineIcon icon = new LineIcon(LineIconType.breakpoint, bp.line - 1, bp); 335 content.lineIcons.add(icon); 336 } 337 } 338 339 Breakpoint[] getBreakpointList() { 340 LineIcon[] icons = content.lineIcons.findByType(LineIconType.breakpoint); 341 Breakpoint[] breakpoints; 342 foreach(icon; icons) { 343 Breakpoint bp = cast(Breakpoint)icon.objectParam; 344 if (bp) 345 breakpoints ~= bp; 346 } 347 return breakpoints; 348 } 349 350 void setBookmarkList(EditorBookmark[] bookmarks) { 351 // remove all existing breakpoints 352 content.lineIcons.removeByType(LineIconType.bookmark); 353 // add new breakpoints 354 foreach(bp; bookmarks) { 355 LineIcon icon = new LineIcon(LineIconType.bookmark, bp.line - 1); 356 content.lineIcons.add(icon); 357 } 358 } 359 360 EditorBookmark[] getBookmarkList() { 361 import std.path; 362 LineIcon[] icons = content.lineIcons.findByType(LineIconType.bookmark); 363 EditorBookmark[] bookmarks; 364 if (projectSourceFile) { 365 foreach(icon; icons) { 366 EditorBookmark bp = new EditorBookmark(); 367 bp.line = icon.line + 1; 368 bp.file = baseName(filename); 369 bp.projectName = projectSourceFile.project.name8; 370 bp.fullFilePath = filename; 371 bp.projectFilePath = projectSourceFile.project.absoluteToRelativePath(filename); 372 bookmarks ~= bp; 373 } 374 } 375 return bookmarks; 376 } 377 378 protected void onMarksChange(EditableContent content, LineIcon[] movedMarks, LineIcon[] removedMarks) { 379 bool changed = false; 380 bool bookmarkChanged = false; 381 foreach(moved; movedMarks) { 382 if (moved.type == LineIconType.breakpoint) { 383 Breakpoint bp = cast(Breakpoint)moved.objectParam; 384 if (bp) { 385 // update Breakpoint line 386 bp.line = moved.line + 1; 387 changed = true; 388 } 389 } else if (moved.type == LineIconType.bookmark) { 390 EditorBookmark bp = cast(EditorBookmark)moved.objectParam; 391 if (bp) { 392 // update Breakpoint line 393 bp.line = moved.line + 1; 394 bookmarkChanged = true; 395 } 396 } 397 } 398 foreach(removed; removedMarks) { 399 if (removed.type == LineIconType.breakpoint) { 400 Breakpoint bp = cast(Breakpoint)removed.objectParam; 401 if (bp) { 402 changed = true; 403 } 404 } else if (removed.type == LineIconType.bookmark) { 405 EditorBookmark bp = cast(EditorBookmark)removed.objectParam; 406 if (bp) { 407 bookmarkChanged = true; 408 } 409 } 410 } 411 if (changed) 412 notifyBreakpointListChanged(); 413 if (bookmarkChanged) 414 notifyBookmarkListChanged(); 415 } 416 417 protected void notifyBreakpointListChanged() { 418 if (projectSourceFile) { 419 if (breakpointListChanged.assigned) 420 breakpointListChanged(projectSourceFile, getBreakpointList()); 421 } 422 } 423 424 protected void notifyBookmarkListChanged() { 425 if (projectSourceFile) { 426 if (bookmarkListChanged.assigned) 427 bookmarkListChanged(projectSourceFile, getBookmarkList()); 428 } 429 } 430 431 protected void handleBreakpointAction(const Action a) { 432 int line = a.longParam >= 0 ? cast(int)a.longParam : caretPos.line; 433 LineIcon icon = content.lineIcons.findByLineAndType(line, LineIconType.breakpoint); 434 switch(a.id) { 435 case IDEActions.DebugToggleBreakpoint: 436 if (icon) 437 removeBreakpoint(line, icon); 438 else 439 addBreakpoint(line); 440 break; 441 case IDEActions.DebugEnableBreakpoint: 442 break; 443 case IDEActions.DebugDisableBreakpoint: 444 break; 445 default: 446 break; 447 } 448 } 449 450 /// override to handle specific actions state (e.g. change enabled state for supported actions) 451 override bool handleActionStateRequest(const Action a) { 452 switch (a.id) { 453 case IDEActions.GoToDefinition: 454 case IDEActions.GetCompletionSuggestions: 455 case IDEActions.GetDocComments: 456 case IDEActions.GetParenCompletion: 457 case IDEActions.DebugToggleBreakpoint: 458 case IDEActions.DebugEnableBreakpoint: 459 case IDEActions.DebugDisableBreakpoint: 460 if (isDSourceFile) 461 a.state = ACTION_STATE_ENABLED; 462 else 463 a.state = ACTION_STATE_DISABLE; 464 return true; 465 case IDEActions.FileSaveAs: 466 a.state = ACTION_STATE_ENABLED; 467 return true; 468 case IDEActions.FileSave: 469 if (_content.modified) 470 a.state = ACTION_STATE_ENABLED; 471 else 472 a.state = ACTION_STATE_DISABLE; 473 return true; 474 default: 475 return super.handleActionStateRequest(a); 476 } 477 } 478 479 /// override to handle mouse hover timeout in text 480 override protected void onHoverTimeout(Point pt, TextPosition pos) { 481 // override to do something useful on hover timeout 482 Log.d("onHoverTimeout ", pos); 483 if (!isDSourceFile) 484 return; 485 editorTool.getDocComments(this, pos, delegate(string[]results) { 486 showDocCommentsPopup(results, pt); 487 }); 488 } 489 490 PopupWidget _docsPopup; 491 void showDocCommentsPopup(string[] comments, Point pt = Point(-1, -1)) { 492 if (comments.length == 0) 493 return; 494 if (pt.x < 0 || pt.y < 0) { 495 pt = textPosToClient(_caretPos).topLeft; 496 pt.x += left + _leftPaneWidth; 497 pt.y += top; 498 } 499 dchar[] text; 500 int lineCount = 0; 501 foreach(s; comments) { 502 int lineStart = 0; 503 for (int i = 0; i <= s.length; i++) { 504 if (i == s.length || (i < s.length - 1 && s[i] == '\\' && s[i + 1] == 'n')) { 505 if (i > lineStart) { 506 if (text.length) 507 text ~= "\n"d; 508 text ~= toUTF32(s[lineStart .. i]); 509 lineCount++; 510 } 511 if (i < s.length) 512 i++; 513 lineStart = i + 1; 514 } 515 } 516 } 517 if (lineCount > _numVisibleLines / 4) 518 lineCount = _numVisibleLines / 4; 519 if (lineCount < 1) 520 lineCount = 1; 521 // TODO 522 EditBox widget = new EditBox("docComments"); 523 widget.styleId = "POPUP_MENU"; 524 widget.readOnly = true; 525 //TextWidget widget = new TextWidget("docComments"); 526 //widget.maxLines = lineCount * 2; 527 //widget.text = "Test popup"d; //text.dup; 528 widget.text = text.dup; 529 530 Point bestSize = widget.fullContentSizeWithBorders(); 531 //widget.layoutHeight = lineCount * widget.fontSize; 532 if (bestSize.y > height / 3) 533 bestSize.y = height / 3; 534 if (bestSize.x > width * 3 / 4) 535 bestSize.x = width * 3 / 4; 536 widget.minHeight = bestSize.y; //max((lineCount + 1) * widget.fontSize, bestSize.y); 537 widget.maxHeight = bestSize.y; 538 539 widget.maxWidth = bestSize.x; //width * 3 / 4; 540 widget.minWidth = bestSize.x; //width / 8; 541 // widget.layoutWidth = width / 3; 542 widget.hscrollbarMode = ScrollBarMode.Auto; 543 widget.vscrollbarMode = ScrollBarMode.Auto; 544 uint pos = PopupAlign.Above; 545 if (pt.y < top + height / 4) 546 pos = PopupAlign.Below; 547 if (_docsPopup) { 548 _docsPopup.close(); 549 _docsPopup = null; 550 } 551 _docsPopup = window.showPopup(widget, this, PopupAlign.Point | pos, pt.x, pt.y); 552 //popup.setFocus(); 553 _docsPopup.popupClosed = delegate(PopupWidget source) { 554 Log.d("Closed Docs popup"); 555 _docsPopup = null; 556 //setFocus(); 557 }; 558 _docsPopup.flags = PopupFlags.CloseOnClickOutside | PopupFlags.CloseOnMouseMoveOutside; 559 invalidate(); 560 window.update(); 561 } 562 563 protected CompletionPopupMenu _completionPopupMenu; 564 protected PopupWidget _completionPopup; 565 566 dstring identPrefixUnderCursor() { 567 dstring line = _content[_caretPos.line]; 568 if (_caretPos.pos > line.length) 569 return null; 570 int end = _caretPos.pos; 571 int start = end; 572 while (start >= 0) { 573 dchar prevChar = start > 0 ? line[start - 1] : 0; 574 if (!isIdentChar(prevChar)) 575 break; 576 start--; 577 } 578 if (start >= end) 579 return null; 580 return line[start .. end].dup; 581 } 582 583 void closeCompletionPopup(CompletionPopupMenu completion) { 584 if (!_completionPopup || _completionPopupMenu !is completion) 585 return; 586 _completionPopup.close(); 587 _completionPopup = null; 588 _completionPopupMenu = null; 589 } 590 591 void showCompletionPopup(dstring[] suggestions, string[] icons) { 592 593 if(suggestions.length == 0) { 594 setFocus(); 595 return; 596 } 597 598 if (suggestions.length == 1) { 599 insertCompletion(suggestions[0]); 600 return; 601 } 602 603 dstring prefix = identPrefixUnderCursor(); 604 _completionPopupMenu = new CompletionPopupMenu(this, suggestions, icons, prefix); 605 int yOffset = font.height; 606 _completionPopup = window.showPopup(_completionPopupMenu, this, PopupAlign.Point | PopupAlign.Right, 607 textPosToClient(_caretPos).left + left + _leftPaneWidth, 608 textPosToClient(_caretPos).top + top + margins.top + yOffset); 609 _completionPopup.setFocus(); 610 _completionPopup.popupClosed = delegate(PopupWidget source) { 611 setFocus(); 612 _completionPopup = null; 613 }; 614 _completionPopup.flags = PopupFlags.CloseOnClickOutside; 615 616 Log.d("Showing popup at ", textPosToClient(_caretPos).left, " ", textPosToClient(_caretPos).top); 617 window.update(); 618 } 619 620 protected ulong _completionTimerId; 621 protected enum COMPLETION_TIMER_MS = 700; 622 protected void startCompletionTimer() { 623 if (_completionTimerId) { 624 cancelTimer(_completionTimerId); 625 } 626 _completionTimerId = setTimer(COMPLETION_TIMER_MS); 627 } 628 protected void cancelCompletionTimer() { 629 if (_completionTimerId) { 630 cancelTimer(_completionTimerId); 631 _completionTimerId = 0; 632 } 633 } 634 /// handle timer; return true to repeat timer event after next interval, false cancel timer 635 override bool onTimer(ulong id) { 636 if (id == _completionTimerId) { 637 _completionTimerId = 0; 638 if (!_completionPopup) 639 window.dispatchAction(ACTION_GET_COMPLETIONS, this); 640 } 641 return super.onTimer(id); 642 } 643 644 /// override to handle focus changes 645 override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) { 646 if (!focused) 647 cancelCompletionTimer(); 648 super.handleFocusChange(focused, receivedFocusFromKeyboard); 649 } 650 651 protected uint _lastKeyDownCode; 652 protected uint _periodKeyCode; 653 /// handle keys: support autocompletion after . press with delay 654 override bool onKeyEvent(KeyEvent event) { 655 if (event.action == KeyAction.KeyDown) 656 _lastKeyDownCode = event.keyCode; 657 if (event.action == KeyAction.Text && event.noModifiers && event.text==".") { 658 _periodKeyCode = _lastKeyDownCode; 659 startCompletionTimer(); 660 } else { 661 if (event.action == KeyAction.KeyUp && (event.text == "." || event.keyCode == KeyCode.KEY_PERIOD || event.keyCode == _periodKeyCode)) { 662 // keep completion timer 663 } else { 664 // cancel completion timer 665 cancelCompletionTimer(); 666 } 667 } 668 return super.onKeyEvent(event); 669 } 670 671 } 672 673 /// returns true if character is valid ident character 674 bool isIdentChar(dchar ch) { 675 return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_'; 676 } 677 678 /// returns true if all characters are valid ident chars 679 bool isIdentText(dstring s) { 680 foreach(ch; s) 681 if (!isIdentChar(ch)) 682 return false; 683 return true; 684 } 685 686 class CompletionPopupMenu : PopupMenu { 687 protected dstring _initialPrefix; 688 protected dstring _prefix; 689 protected dstring[] _suggestions; 690 protected string[] _icons; 691 protected MenuItem _items; 692 protected DSourceEdit _editor; 693 this(DSourceEdit editor, dstring[] suggestions, string[] icons, dstring initialPrefix) { 694 _initialPrefix = initialPrefix; 695 _prefix = initialPrefix.dup; 696 _editor = editor; 697 _suggestions = suggestions; 698 _icons = icons; 699 _items = updateItems(); 700 super(_items); 701 menuItemAction = _editor; 702 maxHeight(400); 703 selectItem(0); 704 } 705 MenuItem updateItems() { 706 MenuItem res = new MenuItem(); 707 foreach(int i, dstring suggestion ; _suggestions) { 708 if (_prefix.length && !suggestion.startsWith(_prefix)) 709 continue; 710 string iconId; 711 if (i < _icons.length) 712 iconId = _icons[i]; 713 auto action = new Action(IDEActions.InsertCompletion, suggestion); 714 action.iconId = iconId; 715 res.add(action); 716 } 717 res.updateActionState(_editor); 718 return res; 719 } 720 /// handle keys 721 override bool onKeyEvent(KeyEvent event) { 722 if (event.action == KeyAction.Text) { 723 _prefix ~= event.text; 724 MenuItem newItems = updateItems(); 725 if (newItems.subitemCount == 0) { 726 // no matches anymore 727 _editor.onKeyEvent(event); 728 _editor.closeCompletionPopup(this); 729 return true; 730 } else { 731 _editor.onKeyEvent(event); 732 menuItems = newItems; 733 selectItem(0); 734 return true; 735 } 736 } else if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.BACK && event.noModifiers) { 737 if (_prefix.length > _initialPrefix.length) { 738 _prefix.length = _prefix.length - 1; 739 MenuItem newItems = updateItems(); 740 _editor.onKeyEvent(event); 741 menuItems = newItems; 742 selectItem(0); 743 } else { 744 _editor.onKeyEvent(event); 745 _editor.closeCompletionPopup(this); 746 } 747 return true; 748 } else if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.RETURN) { 749 } else if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.SPACE) { 750 } 751 return super.onKeyEvent(event); 752 } 753 }