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