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 }