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 }