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