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 : toUTF8, toUTF32; 28 29 interface BreakpointListChangeListener { 30 void onBreakpointListChanged(ProjectSourceFile sourceFile, Breakpoint[] breakpoints); 31 } 32 33 interface BookmarkListChangeListener { 34 void onBookmarkListChanged(ProjectSourceFile sourceFile, EditorBookmark[] bookmarks); 35 } 36 37 /// DIDE source file editor 38 class DSourceEdit : SourceEdit, EditableContentMarksChangeListener { 39 this(string ID) { 40 super(ID); 41 static if (BACKEND_GUI) { 42 styleId = null; 43 backgroundColor = style.customColor("edit_background"); 44 } 45 onThemeChanged(); 46 //setTokenHightlightColor(TokenCategory.Identifier, 0x206000); // no colors 47 MenuItem editPopupItem = new MenuItem(null); 48 editPopupItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_EDIT_UNDO, 49 ACTION_EDIT_REDO, ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT, 50 ACTION_GET_COMPLETIONS, ACTION_GO_TO_DEFINITION, ACTION_DEBUG_TOGGLE_BREAKPOINT); 51 popupMenu = editPopupItem; 52 showIcons = true; 53 //showFolding = true; 54 showWhiteSpaceMarks = true; 55 showTabPositionMarks = true; 56 content.marksChanged = this; 57 } 58 59 this() { 60 this("SRCEDIT"); 61 } 62 63 Signal!BreakpointListChangeListener breakpointListChanged; 64 Signal!BookmarkListChangeListener bookmarkListChanged; 65 66 /// handle theme change: e.g. reload some themed resources 67 override void onThemeChanged() { 68 static if (BACKEND_GUI) backgroundColor = style.customColor("edit_background"); 69 setTokenHightlightColor(TokenCategory.Comment, style.customColor("syntax_highlight_comment")); // green 70 setTokenHightlightColor(TokenCategory.Keyword, style.customColor("syntax_highlight_keyword")); // blue 71 setTokenHightlightColor(TokenCategory.Integer, style.customColor("syntax_highlight_integer", 0x000000)); 72 setTokenHightlightColor(TokenCategory.Float, style.customColor("syntax_highlight_float", 0x000000)); 73 setTokenHightlightColor(TokenCategory.String, style.customColor("syntax_highlight_string")); // brown 74 setTokenHightlightColor(TokenCategory.Identifier, style.customColor("syntax_highlight_ident")); 75 setTokenHightlightColor(TokenCategory.Character, style.customColor("syntax_highlight_character")); // brown 76 setTokenHightlightColor(TokenCategory.Error, style.customColor("syntax_highlight_error")); // red 77 setTokenHightlightColor(TokenCategory.Comment_Documentation, style.customColor("syntax_highlight_comment_documentation")); 78 79 super.onThemeChanged(); 80 } 81 82 protected IDESettings _settings; 83 @property DSourceEdit settings(IDESettings s) { 84 _settings = s; 85 return this; 86 } 87 @property IDESettings settings() { 88 return _settings; 89 } 90 void applySettings() { 91 if (!_settings) 92 return; 93 tabSize = _settings.tabSize; 94 useSpacesForTabs = _settings.useSpacesForTabs; 95 smartIndents = _settings.smartIndents; 96 smartIndentsAfterPaste = _settings.smartIndentsAfterPaste; 97 showWhiteSpaceMarks = _settings.showWhiteSpaceMarks; 98 showTabPositionMarks = _settings.showTabPositionMarks; 99 } 100 101 protected EditorTool _editorTool; 102 @property EditorTool editorTool() { return _editorTool; } 103 @property EditorTool editorTool(EditorTool tool) { return _editorTool = tool; }; 104 105 protected ProjectSourceFile _projectSourceFile; 106 @property ProjectSourceFile projectSourceFile() { return _projectSourceFile; } 107 /// load by filename 108 override bool load(string fn) { 109 _projectSourceFile = null; 110 bool res = super.load(fn); 111 setSyntaxSupport(); 112 return res; 113 } 114 115 @property bool isDSourceFile() { 116 return filename.endsWith(".d") || filename.endsWith(".dd") || filename.endsWith(".dd") || 117 filename.endsWith(".di") || filename.endsWith(".dh") || filename.endsWith(".ddoc"); 118 } 119 120 @property bool isJsonFile() { 121 return filename.endsWith(".json") || filename.endsWith(".JSON"); 122 } 123 124 @property bool isDMLFile() { 125 return filename.endsWith(".dml") || filename.endsWith(".DML"); 126 } 127 128 @property bool isXMLFile() { 129 return filename.endsWith(".xml") || filename.endsWith(".XML"); 130 } 131 132 override protected MenuItem getLeftPaneIconsPopupMenu(int line) { 133 MenuItem menu = super.getLeftPaneIconsPopupMenu(line); 134 if (isDSourceFile) { 135 Action action = ACTION_DEBUG_TOGGLE_BREAKPOINT.clone(); 136 action.longParam = line; 137 action.objectParam = this; 138 menu.add(action); 139 action = ACTION_DEBUG_ENABLE_BREAKPOINT.clone(); 140 action.longParam = line; 141 action.objectParam = this; 142 menu.add(action); 143 action = ACTION_DEBUG_DISABLE_BREAKPOINT.clone(); 144 action.longParam = line; 145 action.objectParam = this; 146 menu.add(action); 147 } 148 return menu; 149 } 150 151 uint _executionLineHighlightColor = BACKEND_GUI ? 0x808080FF : 0x000080; 152 int _executionLine = -1; 153 @property int executionLine() { return _executionLine; } 154 @property void executionLine(int line) { 155 if (line == _executionLine) 156 return; 157 _executionLine = line; 158 if (_executionLine >= 0) { 159 setCaretPos(_executionLine, 0, true); 160 } 161 invalidate(); 162 } 163 /// override to custom highlight of line background 164 override protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 165 if (lineIndex == _executionLine) { 166 buf.fillRect(visibleRect, _executionLineHighlightColor); 167 } 168 super.drawLineBackground(buf, lineIndex, lineRect, visibleRect); 169 } 170 171 void setSyntaxSupport() { 172 if (isDSourceFile) { 173 content.syntaxSupport = new SimpleDSyntaxSupport(filename); 174 } else if (isJsonFile) { 175 content.syntaxSupport = new DMLSyntaxSupport(filename); 176 } else if (isDMLFile) { 177 content.syntaxSupport = new DMLSyntaxSupport(filename); 178 } else { 179 content.syntaxSupport = null; 180 } 181 } 182 183 /// returns project import paths - if file from project is opened in current editor 184 string[] importPaths() { 185 if (_projectSourceFile) 186 return _projectSourceFile.project.importPaths; 187 return null; 188 } 189 190 /// load by project item 191 bool load(ProjectSourceFile f) { 192 if (!load(f.filename)) { 193 _projectSourceFile = null; 194 return false; 195 } 196 _projectSourceFile = f; 197 setSyntaxSupport(); 198 return true; 199 } 200 201 /// save to the same file 202 bool save() { 203 return _content.save(); 204 } 205 206 void insertCompletion(dstring completionText) { 207 TextRange range; 208 TextPosition p = caretPos; 209 range.start = range.end = p; 210 dstring lineText = content.line(p.line); 211 dchar prevChar = p.pos > 0 ? lineText[p.pos - 1] : 0; 212 dchar nextChar = p.pos < lineText.length ? lineText[p.pos] : 0; 213 if (isIdentMiddleChar(prevChar)) { 214 while(range.start.pos > 0 && isIdentMiddleChar(lineText[range.start.pos - 1])) 215 range.start.pos--; 216 if (isIdentMiddleChar(nextChar)) { 217 while(range.end.pos < lineText.length && isIdentMiddleChar(lineText[range.end.pos])) 218 range.end.pos++; 219 } 220 } 221 EditOperation edit = new EditOperation(EditAction.Replace, range, completionText); 222 _content.performOperation(edit, this); 223 setFocus(); 224 } 225 226 /// override to handle specific actions 227 override bool handleAction(const Action a) { 228 import ddc.lexer.tokenizer; 229 if (a) { 230 switch (a.id) { 231 case IDEActions.FileSave: 232 save(); 233 return true; 234 case IDEActions.InsertCompletion: 235 insertCompletion(a.label); 236 return true; 237 case IDEActions.DebugToggleBreakpoint: 238 case IDEActions.DebugEnableBreakpoint: 239 case IDEActions.DebugDisableBreakpoint: 240 handleBreakpointAction(a); 241 return true; 242 case EditorActions.ToggleBookmark: 243 super.handleAction(a); 244 notifyBookmarkListChanged(); 245 return true; 246 default: 247 break; 248 } 249 } 250 return super.handleAction(a); 251 } 252 253 /// Handle Ctrl + Left mouse click on text 254 override protected void onControlClick() { 255 window.dispatchAction(ACTION_GO_TO_DEFINITION); 256 } 257 258 259 /// left button click on icons panel: toggle breakpoint 260 override protected bool handleLeftPaneIconsMouseClick(MouseEvent event, Rect rc, int line) { 261 if (event.button == MouseButton.Left) { 262 LineIcon icon = content.lineIcons.findByLineAndType(line, LineIconType.breakpoint); 263 if (icon) 264 removeBreakpoint(line, icon); 265 else 266 addBreakpoint(line); 267 return true; 268 } 269 return super.handleLeftPaneIconsMouseClick(event, rc, line); 270 } 271 272 protected void addBreakpoint(int line) { 273 import std.path; 274 Breakpoint bp = new Breakpoint(); 275 bp.file = baseName(filename); 276 bp.line = line + 1; 277 bp.fullFilePath = filename; 278 if (projectSourceFile) { 279 bp.projectName = toUTF8(projectSourceFile.project.name); 280 bp.projectFilePath = projectSourceFile.project.absoluteToRelativePath(filename); 281 } 282 LineIcon icon = new LineIcon(LineIconType.breakpoint, line, bp); 283 content.lineIcons.add(icon); 284 notifyBreakpointListChanged(); 285 } 286 287 protected void removeBreakpoint(int line, LineIcon icon) { 288 content.lineIcons.remove(icon); 289 notifyBreakpointListChanged(); 290 } 291 292 void setBreakpointList(Breakpoint[] breakpoints) { 293 // remove all existing breakpoints 294 content.lineIcons.removeByType(LineIconType.breakpoint); 295 // add new breakpoints 296 foreach(bp; breakpoints) { 297 LineIcon icon = new LineIcon(LineIconType.breakpoint, bp.line - 1, bp); 298 content.lineIcons.add(icon); 299 } 300 } 301 302 Breakpoint[] getBreakpointList() { 303 LineIcon[] icons = content.lineIcons.findByType(LineIconType.breakpoint); 304 Breakpoint[] breakpoints; 305 foreach(icon; icons) { 306 Breakpoint bp = cast(Breakpoint)icon.objectParam; 307 if (bp) 308 breakpoints ~= bp; 309 } 310 return breakpoints; 311 } 312 313 void setBookmarkList(EditorBookmark[] bookmarks) { 314 // remove all existing breakpoints 315 content.lineIcons.removeByType(LineIconType.bookmark); 316 // add new breakpoints 317 foreach(bp; bookmarks) { 318 LineIcon icon = new LineIcon(LineIconType.bookmark, bp.line - 1); 319 content.lineIcons.add(icon); 320 } 321 } 322 323 EditorBookmark[] getBookmarkList() { 324 import std.path; 325 LineIcon[] icons = content.lineIcons.findByType(LineIconType.bookmark); 326 EditorBookmark[] bookmarks; 327 if (projectSourceFile) { 328 foreach(icon; icons) { 329 EditorBookmark bp = new EditorBookmark(); 330 bp.line = icon.line + 1; 331 bp.file = baseName(filename); 332 bp.projectName = projectSourceFile.project.name8; 333 bp.fullFilePath = filename; 334 bp.projectFilePath = projectSourceFile.project.absoluteToRelativePath(filename); 335 bookmarks ~= bp; 336 } 337 } 338 return bookmarks; 339 } 340 341 protected void onMarksChange(EditableContent content, LineIcon[] movedMarks, LineIcon[] removedMarks) { 342 bool changed = false; 343 bool bookmarkChanged = false; 344 foreach(moved; movedMarks) { 345 if (moved.type == LineIconType.breakpoint) { 346 Breakpoint bp = cast(Breakpoint)moved.objectParam; 347 if (bp) { 348 // update Breakpoint line 349 bp.line = moved.line + 1; 350 changed = true; 351 } 352 } else if (moved.type == LineIconType.bookmark) { 353 EditorBookmark bp = cast(EditorBookmark)moved.objectParam; 354 if (bp) { 355 // update Breakpoint line 356 bp.line = moved.line + 1; 357 bookmarkChanged = true; 358 } 359 } 360 } 361 foreach(removed; removedMarks) { 362 if (removed.type == LineIconType.breakpoint) { 363 Breakpoint bp = cast(Breakpoint)removed.objectParam; 364 if (bp) { 365 changed = true; 366 } 367 } else if (removed.type == LineIconType.bookmark) { 368 EditorBookmark bp = cast(EditorBookmark)removed.objectParam; 369 if (bp) { 370 bookmarkChanged = true; 371 } 372 } 373 } 374 if (changed) 375 notifyBreakpointListChanged(); 376 if (bookmarkChanged) 377 notifyBookmarkListChanged(); 378 } 379 380 protected void notifyBreakpointListChanged() { 381 if (projectSourceFile) { 382 if (breakpointListChanged.assigned) 383 breakpointListChanged(projectSourceFile, getBreakpointList()); 384 } 385 } 386 387 protected void notifyBookmarkListChanged() { 388 if (projectSourceFile) { 389 if (bookmarkListChanged.assigned) 390 bookmarkListChanged(projectSourceFile, getBookmarkList()); 391 } 392 } 393 394 protected void handleBreakpointAction(const Action a) { 395 int line = a.longParam >= 0 ? cast(int)a.longParam : caretPos.line; 396 LineIcon icon = content.lineIcons.findByLineAndType(line, LineIconType.breakpoint); 397 switch(a.id) { 398 case IDEActions.DebugToggleBreakpoint: 399 if (icon) 400 removeBreakpoint(line, icon); 401 else 402 addBreakpoint(line); 403 break; 404 case IDEActions.DebugEnableBreakpoint: 405 break; 406 case IDEActions.DebugDisableBreakpoint: 407 break; 408 default: 409 break; 410 } 411 } 412 413 /// override to handle specific actions state (e.g. change enabled state for supported actions) 414 override bool handleActionStateRequest(const Action a) { 415 switch (a.id) { 416 case IDEActions.GoToDefinition: 417 case IDEActions.GetCompletionSuggestions: 418 case IDEActions.GetDocComments: 419 case IDEActions.GetParenCompletion: 420 case IDEActions.DebugToggleBreakpoint: 421 case IDEActions.DebugEnableBreakpoint: 422 case IDEActions.DebugDisableBreakpoint: 423 if (isDSourceFile) 424 a.state = ACTION_STATE_ENABLED; 425 else 426 a.state = ACTION_STATE_DISABLE; 427 return true; 428 default: 429 return super.handleActionStateRequest(a); 430 } 431 } 432 433 /// override to handle mouse hover timeout in text 434 override protected void onHoverTimeout(Point pt, TextPosition pos) { 435 // override to do something useful on hover timeout 436 Log.d("onHoverTimeout ", pos); 437 if (!isDSourceFile) 438 return; 439 editorTool.getDocComments(this, pos, delegate(string[]results) { 440 showDocCommentsPopup(results, pt); 441 }); 442 } 443 444 PopupWidget _docsPopup; 445 void showDocCommentsPopup(string[] comments, Point pt = Point(-1, -1)) { 446 if (comments.length == 0) 447 return; 448 if (pt.x < 0 || pt.y < 0) { 449 pt = textPosToClient(_caretPos).topLeft; 450 pt.x += left + _leftPaneWidth; 451 pt.y += top; 452 } 453 dchar[] text; 454 int lineCount = 0; 455 foreach(s; comments) { 456 int lineStart = 0; 457 for (int i = 0; i <= s.length; i++) { 458 if (i == s.length || (i < s.length - 1 && s[i] == '\\' && s[i + 1] == 'n')) { 459 if (i > lineStart) { 460 if (text.length) 461 text ~= "\n"d; 462 text ~= toUTF32(s[lineStart .. i]); 463 lineCount++; 464 } 465 if (i < s.length) 466 i++; 467 lineStart = i + 1; 468 } 469 } 470 } 471 if (lineCount > _numVisibleLines / 4) 472 lineCount = _numVisibleLines / 4; 473 if (lineCount < 1) 474 lineCount = 1; 475 // TODO 476 EditBox widget = new EditBox("docComments"); 477 widget.readOnly = true; 478 //TextWidget widget = new TextWidget("docComments"); 479 //widget.maxLines = lineCount * 2; 480 //widget.text = "Test popup"d; //text.dup; 481 widget.text = text.dup; 482 //widget.layoutHeight = lineCount * widget.fontSize; 483 widget.minHeight = (lineCount + 1) * widget.fontSize; 484 widget.maxWidth = width * 3 / 4; 485 widget.minWidth = width / 8; 486 // widget.layoutWidth = width / 3; 487 widget.styleId = "POPUP_MENU"; 488 widget.hscrollbarMode = ScrollBarMode.Auto; 489 widget.vscrollbarMode = ScrollBarMode.Auto; 490 uint pos = PopupAlign.Above; 491 if (pt.y < top + height / 4) 492 pos = PopupAlign.Below; 493 if (_docsPopup) { 494 _docsPopup.close(); 495 _docsPopup = null; 496 } 497 _docsPopup = window.showPopup(widget, this, PopupAlign.Point | pos, pt.x, pt.y); 498 //popup.setFocus(); 499 _docsPopup.popupClosed = delegate(PopupWidget source) { 500 Log.d("Closed Docs popup"); 501 _docsPopup = null; 502 //setFocus(); 503 }; 504 _docsPopup.flags = PopupFlags.CloseOnClickOutside | PopupFlags.CloseOnMouseMoveOutside; 505 invalidate(); 506 window.update(); 507 } 508 509 void showCompletionPopup(dstring[] suggestions, string[] icons) { 510 511 if(suggestions.length == 0) { 512 setFocus(); 513 return; 514 } 515 516 if (suggestions.length == 1) { 517 insertCompletion(suggestions[0]); 518 return; 519 } 520 521 MenuItem completionPopupItems = new MenuItem(null); 522 //Add all the suggestions. 523 foreach(int i, dstring suggestion ; suggestions) { 524 string iconId; 525 if (i < icons.length) 526 iconId = icons[i]; 527 auto action = new Action(IDEActions.InsertCompletion, suggestion); 528 action.iconId = iconId; 529 completionPopupItems.add(action); 530 } 531 completionPopupItems.updateActionState(this); 532 533 PopupMenu popupMenu = new PopupMenu(completionPopupItems); 534 popupMenu.menuItemAction = this; 535 popupMenu.maxHeight(400); 536 popupMenu.selectItem(0); 537 538 PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, 539 textPosToClient(_caretPos).left + left + _leftPaneWidth, 540 textPosToClient(_caretPos).top + top + margins.top); 541 popup.setFocus(); 542 popup.popupClosed = delegate(PopupWidget source) { setFocus(); }; 543 popup.flags = PopupFlags.CloseOnClickOutside; 544 545 Log.d("Showing popup at ", textPosToClient(_caretPos).left, " ", textPosToClient(_caretPos).top); 546 window.update(); 547 } 548 549 }