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 }