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 : toUTF32;
28 import dlangui.core.types : toUTF8;
29 
30 interface BreakpointListChangeListener {
31     void onBreakpointListChanged(ProjectSourceFile sourceFile, Breakpoint[] breakpoints);
32 }
33 
34 interface BookmarkListChangeListener {
35     void onBookmarkListChanged(ProjectSourceFile sourceFile, EditorBookmark[] bookmarks);
36 }
37 
38 /// DIDE source file editor
39 class DSourceEdit : SourceEdit, EditableContentMarksChangeListener {
40     this(string ID) {
41         super(ID);
42         static if (BACKEND_GUI) {
43             styleId = null;
44             backgroundColor = style.customColor("edit_background");
45         }
46         onThemeChanged();
47         //setTokenHightlightColor(TokenCategory.Identifier, 0x206000);  // no colors
48         MenuItem editPopupItem = new MenuItem(null);
49         editPopupItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_EDIT_UNDO, 
50                           ACTION_EDIT_REDO, ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT,
51                           ACTION_GET_COMPLETIONS, ACTION_GO_TO_DEFINITION, ACTION_DEBUG_TOGGLE_BREAKPOINT);
52         popupMenu = editPopupItem;
53         showIcons = true;
54         //showFolding = true;
55         showWhiteSpaceMarks = true;
56         showTabPositionMarks = true;
57         content.marksChanged = this;
58     }
59 
60     this() {
61         this("SRCEDIT");
62     }
63 
64     ~this() {
65         if (_editorTool) {
66             destroy(_editorTool);
67             _editorTool = null;
68         }
69     }
70 
71     Signal!BreakpointListChangeListener breakpointListChanged;
72     Signal!BookmarkListChangeListener bookmarkListChanged;
73 
74     /// handle theme change: e.g. reload some themed resources
75     override void onThemeChanged() {
76         static if (BACKEND_GUI) backgroundColor = style.customColor("edit_background");
77         setTokenHightlightColor(TokenCategory.Comment, style.customColor("syntax_highlight_comment")); // green
78         setTokenHightlightColor(TokenCategory.Keyword, style.customColor("syntax_highlight_keyword")); // blue
79         setTokenHightlightColor(TokenCategory.Integer, style.customColor("syntax_highlight_integer", 0x000000));
80         setTokenHightlightColor(TokenCategory.Float, style.customColor("syntax_highlight_float", 0x000000));
81         setTokenHightlightColor(TokenCategory.String, style.customColor("syntax_highlight_string"));  // brown
82         setTokenHightlightColor(TokenCategory.Identifier, style.customColor("syntax_highlight_ident"));
83         setTokenHightlightColor(TokenCategory.Character, style.customColor("syntax_highlight_character"));  // brown
84         setTokenHightlightColor(TokenCategory.Error, style.customColor("syntax_highlight_error"));  // red
85         setTokenHightlightColor(TokenCategory.Comment_Documentation, style.customColor("syntax_highlight_comment_documentation"));
86 
87         super.onThemeChanged();
88     }
89 
90     protected IDESettings _settings;
91     @property DSourceEdit settings(IDESettings s) {
92         _settings = s;
93         return this;
94     }
95     @property IDESettings settings() {
96         return _settings;
97     }
98     void applySettings() {
99         if (!_settings)
100             return;
101         tabSize = _settings.tabSize;
102         useSpacesForTabs = _settings.useSpacesForTabs;
103         smartIndents = _settings.smartIndents;
104         smartIndentsAfterPaste = _settings.smartIndentsAfterPaste;
105         showWhiteSpaceMarks = _settings.showWhiteSpaceMarks;
106         showTabPositionMarks = _settings.showTabPositionMarks;
107         string face = _settings.editorFontFace;
108         if (face == "Default")
109             face = null;
110         else if (face)
111             face ~= ",";
112         face ~= DEFAULT_SOURCE_EDIT_FONT_FACES;
113         fontFace = face;
114     }
115 
116     protected EditorTool _editorTool;
117     @property EditorTool editorTool() { return _editorTool; }
118     @property EditorTool editorTool(EditorTool tool) { 
119         if (_editorTool && _editorTool !is tool) {
120             destroy(_editorTool);
121             _editorTool = null;
122         }
123         return _editorTool = tool; 
124     };
125 
126     protected ProjectSourceFile _projectSourceFile;
127     @property ProjectSourceFile projectSourceFile() { return _projectSourceFile; }
128     /// load by filename
129     override bool load(string fn) {
130         _projectSourceFile = null;
131         bool res = super.load(fn);
132         setSyntaxSupport();
133         return res;
134     }
135 
136     @property bool isDSourceFile() {
137         return filename.endsWith(".d") || filename.endsWith(".dd") || filename.endsWith(".dd") ||
138                filename.endsWith(".di") || filename.endsWith(".dh") || filename.endsWith(".ddoc");
139     }
140 
141     @property bool isJsonFile() {
142         return filename.endsWith(".json") || filename.endsWith(".JSON");
143     }
144 
145     @property bool isDMLFile() {
146         return filename.endsWith(".dml") || filename.endsWith(".DML");
147     }
148 
149     @property bool isXMLFile() {
150         return filename.endsWith(".xml") || filename.endsWith(".XML");
151     }
152 
153     override protected MenuItem getLeftPaneIconsPopupMenu(int line) {
154         MenuItem menu = super.getLeftPaneIconsPopupMenu(line);
155         if (isDSourceFile) {
156             Action action = ACTION_DEBUG_TOGGLE_BREAKPOINT.clone();
157             action.longParam = line;
158             action.objectParam = this;
159             menu.add(action);
160             action = ACTION_DEBUG_ENABLE_BREAKPOINT.clone();
161             action.longParam = line;
162             action.objectParam = this;
163             menu.add(action);
164             action = ACTION_DEBUG_DISABLE_BREAKPOINT.clone();
165             action.longParam = line;
166             action.objectParam = this;
167             menu.add(action);
168         }
169         return menu;
170     }
171 
172     uint _executionLineHighlightColor = BACKEND_GUI ? 0x808080FF : 0x000080;
173     int _executionLine = -1;
174     @property int executionLine() { return _executionLine; }
175     @property void executionLine(int line) {
176         if (line == _executionLine)
177             return;
178         _executionLine = line;
179         if (_executionLine >= 0) {
180             setCaretPos(_executionLine, 0, true);
181         }
182         invalidate();
183     }
184     /// override to custom highlight of line background
185     override protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) {
186         if (lineIndex == _executionLine) {
187             buf.fillRect(visibleRect, _executionLineHighlightColor);
188         }
189         super.drawLineBackground(buf, lineIndex, lineRect, visibleRect);
190     }
191 
192     void setSyntaxSupport() {
193         if (isDSourceFile) {
194             content.syntaxSupport = new SimpleDSyntaxSupport(filename);
195         } else if (isJsonFile) {
196             content.syntaxSupport = new DMLSyntaxSupport(filename);
197         } else if (isDMLFile) {
198             content.syntaxSupport = new DMLSyntaxSupport(filename);
199         } else {
200             content.syntaxSupport = null;
201         }
202     }
203 
204     /// returns project import paths - if file from project is opened in current editor
205     string[] importPaths() {
206         if (_projectSourceFile)
207             return _projectSourceFile.project.importPaths;
208         return null;
209     }
210 
211     /// load by project item
212     bool load(ProjectSourceFile f) {
213         if (!load(f.filename)) {
214             _projectSourceFile = null;
215             return false;
216         }
217         _projectSourceFile = f;
218         setSyntaxSupport();
219         return true;
220     }
221 
222     /// save to the same file
223     bool save() {
224         return _content.save();
225     }
226 
227     void insertCompletion(dstring completionText) {
228         TextRange range;
229         TextPosition p = caretPos;
230         range.start = range.end = p;
231         dstring lineText = content.line(p.line);
232         dchar prevChar = p.pos > 0 ? lineText[p.pos - 1] : 0;
233         dchar nextChar = p.pos < lineText.length ? lineText[p.pos] : 0;
234         if (isIdentMiddleChar(prevChar)) {
235             while(range.start.pos > 0 && isIdentMiddleChar(lineText[range.start.pos - 1]))
236                 range.start.pos--;
237             if (isIdentMiddleChar(nextChar)) {
238                 while(range.end.pos < lineText.length && isIdentMiddleChar(lineText[range.end.pos]))
239                     range.end.pos++;
240             }
241         }
242         EditOperation edit = new EditOperation(EditAction.Replace, range, completionText);
243         _content.performOperation(edit, this);
244         setFocus();
245     }
246 
247     /// override to handle specific actions
248     override bool handleAction(const Action a) {
249         import ddc.lexer.tokenizer;
250         if (a) {
251             switch (a.id) {
252                 case IDEActions.FileSave:
253                     save();
254                     return true;
255                 case IDEActions.InsertCompletion:
256                     insertCompletion(a.label);
257                     return true;
258                 case IDEActions.DebugToggleBreakpoint:
259                 case IDEActions.DebugEnableBreakpoint:
260                 case IDEActions.DebugDisableBreakpoint:
261                     handleBreakpointAction(a);
262                     return true;
263                 case EditorActions.ToggleBookmark:
264                     super.handleAction(a);
265                     notifyBookmarkListChanged();
266                     return true;
267                 default:
268                     break;
269             }
270         }
271         return super.handleAction(a);
272     }
273 
274     /// Handle Ctrl + Left mouse click on text
275     override protected void onControlClick() {
276         window.dispatchAction(ACTION_GO_TO_DEFINITION);
277     }
278 
279 
280     /// left button click on icons panel: toggle breakpoint
281     override protected bool handleLeftPaneIconsMouseClick(MouseEvent event, Rect rc, int line) {
282         if (event.button == MouseButton.Left) {
283             LineIcon icon = content.lineIcons.findByLineAndType(line, LineIconType.breakpoint);
284             if (icon)
285                 removeBreakpoint(line, icon);
286             else
287                 addBreakpoint(line);
288             return true;
289         }
290         return super.handleLeftPaneIconsMouseClick(event, rc, line);
291     }
292 
293     protected void addBreakpoint(int line) {
294         import std.path;
295         Breakpoint bp = new Breakpoint();
296         bp.file = baseName(filename);
297         bp.line = line + 1;
298         bp.fullFilePath = filename;
299         if (projectSourceFile) {
300             bp.projectName = toUTF8(projectSourceFile.project.name);
301             bp.projectFilePath = projectSourceFile.project.absoluteToRelativePath(filename);
302         }
303         LineIcon icon = new LineIcon(LineIconType.breakpoint, line, bp);
304         content.lineIcons.add(icon);
305         notifyBreakpointListChanged();
306     }
307 
308     protected void removeBreakpoint(int line, LineIcon icon) {
309         content.lineIcons.remove(icon);
310         notifyBreakpointListChanged();
311     }
312 
313     void setBreakpointList(Breakpoint[] breakpoints) {
314         // remove all existing breakpoints
315         content.lineIcons.removeByType(LineIconType.breakpoint);
316         // add new breakpoints
317         foreach(bp; breakpoints) {
318             LineIcon icon = new LineIcon(LineIconType.breakpoint, bp.line - 1, bp);
319             content.lineIcons.add(icon);
320         }
321     }
322 
323     Breakpoint[] getBreakpointList() {
324         LineIcon[] icons = content.lineIcons.findByType(LineIconType.breakpoint);
325         Breakpoint[] breakpoints;
326         foreach(icon; icons) {
327             Breakpoint bp = cast(Breakpoint)icon.objectParam;
328             if (bp)
329                 breakpoints ~= bp;
330         }
331         return breakpoints;
332     }
333 
334     void setBookmarkList(EditorBookmark[] bookmarks) {
335         // remove all existing breakpoints
336         content.lineIcons.removeByType(LineIconType.bookmark);
337         // add new breakpoints
338         foreach(bp; bookmarks) {
339             LineIcon icon = new LineIcon(LineIconType.bookmark, bp.line - 1);
340             content.lineIcons.add(icon);
341         }
342     }
343 
344     EditorBookmark[] getBookmarkList() {
345         import std.path;
346         LineIcon[] icons = content.lineIcons.findByType(LineIconType.bookmark);
347         EditorBookmark[] bookmarks;
348         if (projectSourceFile) {
349             foreach(icon; icons) {
350                 EditorBookmark bp = new EditorBookmark();
351                 bp.line = icon.line + 1;
352                 bp.file = baseName(filename);
353                 bp.projectName = projectSourceFile.project.name8;
354                 bp.fullFilePath = filename;
355                 bp.projectFilePath = projectSourceFile.project.absoluteToRelativePath(filename);
356                 bookmarks ~= bp;
357             }
358         }
359         return bookmarks;
360     }
361 
362     protected void onMarksChange(EditableContent content, LineIcon[] movedMarks, LineIcon[] removedMarks) {
363         bool changed = false;
364         bool bookmarkChanged = false;
365         foreach(moved; movedMarks) {
366             if (moved.type == LineIconType.breakpoint) {
367                 Breakpoint bp = cast(Breakpoint)moved.objectParam;
368                 if (bp) {
369                     // update Breakpoint line
370                     bp.line = moved.line + 1;
371                     changed = true;
372                 }
373             } else if (moved.type == LineIconType.bookmark) {
374                 EditorBookmark bp = cast(EditorBookmark)moved.objectParam;
375                 if (bp) {
376                     // update Breakpoint line
377                     bp.line = moved.line + 1;
378                     bookmarkChanged = true;
379                 }
380             }
381         }
382         foreach(removed; removedMarks) {
383             if (removed.type == LineIconType.breakpoint) {
384                 Breakpoint bp = cast(Breakpoint)removed.objectParam;
385                 if (bp) {
386                     changed = true;
387                 }
388             } else if (removed.type == LineIconType.bookmark) {
389                 EditorBookmark bp = cast(EditorBookmark)removed.objectParam;
390                 if (bp) {
391                     bookmarkChanged = true;
392                 }
393             }
394         }
395         if (changed)
396             notifyBreakpointListChanged();
397         if (bookmarkChanged)
398             notifyBookmarkListChanged();
399     }
400 
401     protected void notifyBreakpointListChanged() {
402         if (projectSourceFile) {
403             if (breakpointListChanged.assigned)
404                 breakpointListChanged(projectSourceFile, getBreakpointList());
405         }
406     }
407 
408     protected void notifyBookmarkListChanged() {
409         if (projectSourceFile) {
410             if (bookmarkListChanged.assigned)
411                 bookmarkListChanged(projectSourceFile, getBookmarkList());
412         }
413     }
414 
415     protected void handleBreakpointAction(const Action a) {
416         int line = a.longParam >= 0 ? cast(int)a.longParam : caretPos.line;
417         LineIcon icon = content.lineIcons.findByLineAndType(line, LineIconType.breakpoint);
418         switch(a.id) {
419             case IDEActions.DebugToggleBreakpoint:
420                 if (icon)
421                     removeBreakpoint(line, icon);
422                 else
423                     addBreakpoint(line);
424                 break;
425             case IDEActions.DebugEnableBreakpoint:
426                 break;
427             case IDEActions.DebugDisableBreakpoint:
428                 break;
429             default:
430                 break;
431         }
432     }
433 
434     /// override to handle specific actions state (e.g. change enabled state for supported actions)
435     override bool handleActionStateRequest(const Action a) {
436         switch (a.id) {
437             case IDEActions.GoToDefinition:
438             case IDEActions.GetCompletionSuggestions:
439             case IDEActions.GetDocComments:
440             case IDEActions.GetParenCompletion:
441             case IDEActions.DebugToggleBreakpoint:
442             case IDEActions.DebugEnableBreakpoint:
443             case IDEActions.DebugDisableBreakpoint:
444                 if (isDSourceFile)
445                     a.state = ACTION_STATE_ENABLED;
446                 else
447                     a.state = ACTION_STATE_DISABLE;
448                 return true;
449             default:
450                 return super.handleActionStateRequest(a);
451         }
452     }
453 
454     /// override to handle mouse hover timeout in text
455     override protected void onHoverTimeout(Point pt, TextPosition pos) {
456         // override to do something useful on hover timeout
457         Log.d("onHoverTimeout ", pos);
458         if (!isDSourceFile)
459             return;
460         editorTool.getDocComments(this, pos, delegate(string[]results) {
461             showDocCommentsPopup(results, pt);
462         });
463     }
464 
465     PopupWidget _docsPopup;
466     void showDocCommentsPopup(string[] comments, Point pt = Point(-1, -1)) {
467         if (comments.length == 0)
468             return;
469         if (pt.x < 0 || pt.y < 0) {
470             pt = textPosToClient(_caretPos).topLeft;
471             pt.x += left + _leftPaneWidth;
472             pt.y += top;
473         }
474         dchar[] text;
475         int lineCount = 0;
476         foreach(s; comments) {
477             int lineStart = 0;
478             for (int i = 0; i <= s.length; i++) {
479                 if (i == s.length || (i < s.length - 1 && s[i] == '\\' && s[i + 1] == 'n')) {
480                     if (i > lineStart) {
481                         if (text.length)
482                             text ~= "\n"d;
483                         text ~= toUTF32(s[lineStart .. i]);
484                         lineCount++;
485                     }
486                     if (i < s.length)
487                         i++;
488                     lineStart = i + 1;
489                 }
490             }
491         }
492         if (lineCount > _numVisibleLines / 4)
493             lineCount = _numVisibleLines / 4;
494         if (lineCount < 1)
495             lineCount = 1;
496         // TODO
497         EditBox widget = new EditBox("docComments");
498         widget.readOnly = true;
499         //TextWidget widget = new TextWidget("docComments");
500         //widget.maxLines = lineCount * 2;
501         //widget.text = "Test popup"d; //text.dup;
502         widget.text = text.dup;
503         //widget.layoutHeight = lineCount * widget.fontSize;
504         widget.minHeight = (lineCount + 1) * widget.fontSize;
505         widget.maxWidth = width * 3 / 4;
506         widget.minWidth = width / 8;
507        // widget.layoutWidth = width / 3;
508         widget.styleId = "POPUP_MENU";
509         widget.hscrollbarMode = ScrollBarMode.Auto;
510         widget.vscrollbarMode = ScrollBarMode.Auto;
511         uint pos = PopupAlign.Above;
512         if (pt.y < top + height / 4)
513             pos = PopupAlign.Below;
514         if (_docsPopup) {
515             _docsPopup.close();
516             _docsPopup = null;
517         }
518         _docsPopup = window.showPopup(widget, this, PopupAlign.Point | pos, pt.x, pt.y);
519         //popup.setFocus();
520         _docsPopup.popupClosed = delegate(PopupWidget source) {
521             Log.d("Closed Docs popup");
522             _docsPopup = null;
523             //setFocus(); 
524         };
525         _docsPopup.flags = PopupFlags.CloseOnClickOutside | PopupFlags.CloseOnMouseMoveOutside;
526         invalidate();
527         window.update();
528     }
529 
530     protected CompletionPopupMenu _completionPopupMenu;
531     protected PopupWidget _completionPopup;
532 
533     dstring identPrefixUnderCursor() {
534         dstring line = _content[_caretPos.line];
535         if (_caretPos.pos > line.length)
536             return null;
537         int end = _caretPos.pos;
538         int start = end;
539         while (start >= 0) {
540             dchar prevChar = start > 0 ? line[start - 1] : 0;
541             if (!isIdentChar(prevChar))
542                 break;
543             start--;
544         }
545         if (start >= end)
546             return null;
547         return line[start .. end].dup;
548     }
549 
550     void closeCompletionPopup(CompletionPopupMenu completion) {
551         if (!_completionPopup || _completionPopupMenu !is completion)
552             return;
553         _completionPopup.close();
554         _completionPopup = null;
555         _completionPopupMenu = null;
556     }
557 
558     void showCompletionPopup(dstring[] suggestions, string[] icons) {
559 
560         if(suggestions.length == 0) {
561             setFocus();
562             return;
563         }
564 
565         if (suggestions.length == 1) {
566             insertCompletion(suggestions[0]);
567             return;
568         }
569 
570         dstring prefix = identPrefixUnderCursor();
571         _completionPopupMenu = new CompletionPopupMenu(this, suggestions, icons, prefix);
572         int yOffset = font.height;
573         _completionPopup = window.showPopup(_completionPopupMenu, this, PopupAlign.Point | PopupAlign.Right,
574                                              textPosToClient(_caretPos).left + left + _leftPaneWidth,
575                                              textPosToClient(_caretPos).top + top + margins.top + yOffset);
576         _completionPopup.setFocus();
577         _completionPopup.popupClosed = delegate(PopupWidget source) { 
578             setFocus();
579             _completionPopup = null;
580         };
581         _completionPopup.flags = PopupFlags.CloseOnClickOutside;
582 
583         Log.d("Showing popup at ", textPosToClient(_caretPos).left, " ", textPosToClient(_caretPos).top);
584         window.update();
585     }
586 
587     protected ulong _completionTimerId;
588     protected enum COMPLETION_TIMER_MS = 700;
589     protected void startCompletionTimer() {
590         if (_completionTimerId) {
591             cancelTimer(_completionTimerId);
592         }
593         _completionTimerId = setTimer(COMPLETION_TIMER_MS);
594     }
595     protected void cancelCompletionTimer() {
596         if (_completionTimerId) {
597             cancelTimer(_completionTimerId);
598             _completionTimerId = 0;
599         }
600     }
601     /// handle timer; return true to repeat timer event after next interval, false cancel timer
602     override bool onTimer(ulong id) {
603         if (id == _completionTimerId) {
604             _completionTimerId = 0;
605             if (!_completionPopup)
606                 window.dispatchAction(ACTION_GET_COMPLETIONS, this);
607         }
608         return super.onTimer(id);
609     }
610 
611     /// override to handle focus changes
612     override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) {
613         if (!focused)
614             cancelCompletionTimer();
615         super.handleFocusChange(focused, receivedFocusFromKeyboard);
616     }
617 
618     protected uint _lastKeyDownCode;
619     protected uint _periodKeyCode;
620     /// handle keys: support autocompletion after . press with delay
621     override bool onKeyEvent(KeyEvent event) {
622         if (event.action == KeyAction.KeyDown)
623             _lastKeyDownCode = event.keyCode;
624         if (event.action == KeyAction.Text && event.noModifiers && event.text==".") {
625             _periodKeyCode = _lastKeyDownCode;
626             startCompletionTimer();
627         } else {
628             if (event.action == KeyAction.KeyUp && (event.text == "." || event.keyCode == KeyCode.KEY_PERIOD || event.keyCode == _periodKeyCode)) {
629                 // keep completion timer
630             } else {
631                 // cancel completion timer
632                 cancelCompletionTimer();
633             }
634         }
635         return super.onKeyEvent(event);
636     }
637 
638 }
639 
640 /// returns true if character is valid ident character
641 bool isIdentChar(dchar ch) {
642     return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_';
643 }
644 
645 /// returns true if all characters are valid ident chars
646 bool isIdentText(dstring s) {
647     foreach(ch; s)
648         if (!isIdentChar(ch))
649             return false;
650     return true;
651 }
652 
653 class CompletionPopupMenu : PopupMenu {
654     protected dstring _initialPrefix;
655     protected dstring _prefix;
656     protected dstring[] _suggestions;
657     protected string[] _icons;
658     protected MenuItem _items;
659     protected DSourceEdit _editor;
660     this(DSourceEdit editor, dstring[] suggestions, string[] icons, dstring initialPrefix) {
661         _initialPrefix = initialPrefix;
662         _prefix = initialPrefix.dup;
663         _editor = editor;
664         _suggestions = suggestions;
665         _icons = icons;
666         _items = updateItems();
667         super(_items);
668         menuItemAction = _editor;
669         maxHeight(400);
670         selectItem(0);
671     }
672     MenuItem updateItems() {
673         MenuItem res = new MenuItem();
674         foreach(int i, dstring suggestion ; _suggestions) {
675             if (_prefix.length && !suggestion.startsWith(_prefix))
676                 continue;
677             string iconId;
678             if (i < _icons.length)
679                 iconId = _icons[i];
680             auto action = new Action(IDEActions.InsertCompletion, suggestion);
681             action.iconId = iconId;
682             res.add(action);
683         }
684         res.updateActionState(_editor);
685         return res;
686     }
687     /// handle keys
688     override bool onKeyEvent(KeyEvent event) {
689         if (event.action == KeyAction.Text) {
690             _prefix ~= event.text;
691             MenuItem newItems = updateItems();
692             if (newItems.subitemCount == 0) {
693                 // no matches anymore
694                 _editor.onKeyEvent(event);
695                 _editor.closeCompletionPopup(this);
696                 return true;
697             } else {
698                 _editor.onKeyEvent(event);
699                 menuItems = newItems;
700                 selectItem(0);
701                 return true;
702             }
703         } else if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.BACK && event.noModifiers) {
704             if (_prefix.length > _initialPrefix.length) {
705                 _prefix.length = _prefix.length - 1;
706                 MenuItem newItems = updateItems();
707                 _editor.onKeyEvent(event);
708                 menuItems = newItems;
709                 selectItem(0);
710             } else {
711                 _editor.onKeyEvent(event);
712                 _editor.closeCompletionPopup(this);
713             }
714             return true;
715         } else if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.RETURN) {
716         } else if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.SPACE) {
717         }
718         return super.onKeyEvent(event);
719     }
720 }