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