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