1 module dlangide.ui.searchPanel;
2 
3 
4 import dlangui;
5 
6 import dlangide.ui.frame;
7 import dlangide.ui.wspanel;
8 import dlangide.workspace.workspace;
9 import dlangide.workspace.project;
10 
11 import std..string;
12 import std.conv;
13 
14 // parallel search is disabled to fix #178
15 //version = PARALLEL_SEARCH;
16 
17 interface SearchResultClickHandler {
18     bool onSearchResultClick(int line);
19 }
20 
21 //LogWidget with highlighting for search results.
22 class SearchLogWidget : LogWidget {
23 
24     //Sends which line was clicked.
25     Signal!SearchResultClickHandler searchResultClickHandler;
26 
27     this(string ID){
28         super(ID);
29         scrollLock = false;
30         onThemeChanged();
31     }
32 
33     protected uint _filenameColor = 0x0000C0;
34     protected uint _errorColor = 0xFF0000;
35     protected uint _warningColor = 0x606000;
36     protected uint _deprecationColor = 0x802040;
37 
38     /// handle theme change: e.g. reload some themed resources
39     override void onThemeChanged() {
40         super.onThemeChanged();
41         _filenameColor = style.customColor("build_log_filename_color", 0x0000C0);
42         _errorColor = style.customColor("build_log_error_color", 0xFF0000);
43         _warningColor = style.customColor("build_log_warning_color", 0x606000);
44         _deprecationColor = style.customColor("build_log_deprecation_color", 0x802040);
45     }
46 
47     override protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) {
48         uint defColor = textColor;
49         uint flags = 0;
50         if (buf.length < txt.length)
51             buf.length = txt.length;
52             
53         //Highlights the filename
54         if(txt.startsWith("Matches in ")) {
55             CustomCharProps[] colors = buf[0..txt.length];
56             uint cl = defColor;
57             flags = 0;
58             for (int i = 0; i < txt.length; i++) {
59                 dstring rest = txt[i..$];
60                 if(i == 11) {
61                     cl = _filenameColor;
62                     flags = TextFlag.Underline;
63                 }
64                 colors[i].color = cl;
65                 colors[i].textFlags = flags;
66             }
67             return colors;
68         } else { //Highlight line and column
69             CustomCharProps[] colors = buf[0..txt.length];
70             uint cl = _filenameColor;
71             flags = 0;
72             int foundHighlightStart = 0;
73             int foundHighlightEnd = 0;
74             bool textStarted = false;
75             for (int i = 0; i < txt.length; i++) {
76                 dstring rest = txt[i..$];
77                 if (rest.startsWith(" -->"d)) {
78                     cl = _warningColor;
79                     flags = 0;
80                 }
81                 if(i == 4) {
82                     cl = _errorColor;
83                 }
84 
85                 if (textStarted && _textToHighlight.length > 0) {
86                     if (rest.startsWith(_textToHighlight)) {
87                         foundHighlightStart = i;
88                         foundHighlightEnd = i + cast(int)_textToHighlight.length;
89                     }
90                     if (i >= foundHighlightStart && i < foundHighlightEnd) {
91                         flags = TextFlag.Underline;
92                         cl = _deprecationColor;
93                     } else {
94                         flags = 0;
95                         cl = defColor;
96                     }
97                 }
98 
99                 colors[i].color = cl;
100                 colors[i].textFlags = flags;
101 
102                 //Colors to apply in following iterations of the loop.
103                 if(!textStarted && rest.startsWith("]")) {
104                     cl = defColor;
105                     flags = 0;
106                     textStarted = true;
107                 }
108             }
109             return colors;
110         }
111     }
112     
113     override bool onMouseEvent(MouseEvent event) {
114         bool res = super.onMouseEvent(event);
115         if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
116             int line = _caretPos.line;
117             if (searchResultClickHandler.assigned) {
118                 searchResultClickHandler(line);
119                 return true;
120             }
121         }
122         return res;
123     }
124     
125     override bool onKeyEvent(KeyEvent event) {
126         if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.RETURN) {
127             int line = _caretPos.line;
128             if (searchResultClickHandler.assigned) {
129                 searchResultClickHandler(line);
130                 return true;
131             }
132         }
133         return super.onKeyEvent(event);
134     }
135 }
136 
137 
138 struct SearchMatch {
139     int line;
140     long col;
141     dstring lineContent;
142 }
143 
144 struct SearchMatchList {
145     string filename;
146     SearchMatch[] matches;
147 }
148 
149 class SearchWidget : TabWidget {
150     HorizontalLayout _layout;
151     EditLine _findText;
152     SearchLogWidget _resultLog;
153     int _resultLogMatchIndex;
154     ComboBox _searchScope;
155     ImageCheckButton _cbCaseSensitive;
156     ImageCheckButton _cbWholeWords;
157 
158     protected IDEFrame _frame;
159     protected SearchMatchList[] _matchedList;
160 
161     //Sets focus on result;
162     void focus() {
163         _findText.setFocus();
164         _findText.handleAction(new Action(EditorActions.SelectAll));
165     }
166     bool onFindButtonPressed(Widget source) {
167         dstring txt = _findText.text;
168         if (txt.length > 0) {
169             findText(txt);
170             _resultLog.setFocus();
171         }
172         return true;
173     }
174     
175     public void setSearchText(dstring txt){
176         _findText.text = txt;
177     }
178 
179     this(string ID, IDEFrame frame) {
180         super(ID);
181         _frame = frame;
182         
183         layoutHeight(FILL_PARENT);
184         
185         //Remove title, more button
186         removeAllChildren();
187         
188         _layout = new HorizontalLayout();
189         _layout.addChild(new TextWidget("FindLabel", "Find: "d));
190         _layout.layoutWidth = FILL_PARENT;
191 
192         _findText = new EditLine();
193         _findText.padding(Rect(5,4,50,4));
194         _findText.layoutWidth = FILL_PARENT;
195         // to handle Enter key press in editor
196         _findText.enterKey = delegate (EditWidgetBase editor) {
197             return onFindButtonPressed(this);
198         };
199         _layout.addChild(_findText);
200         
201         auto goButton = new ImageButton("findTextButton", "edit-find");
202         goButton.click = &onFindButtonPressed;
203         _layout.addChild(goButton);
204         
205         _layout.addChild(new HSpacer);
206 
207         _searchScope = new ComboBox("searchScope", ["File"d, "Project"d, "Dependencies"d, "Everywhere"d]);
208         _searchScope.selectedItemIndex = 0;
209         _layout.addChild(_searchScope);
210 
211         _cbCaseSensitive = new ImageCheckButton("cbCaseSensitive", "find_case_sensitive");
212         _cbCaseSensitive.tooltipText = "EDIT_FIND_CASE_SENSITIVE";
213         _cbCaseSensitive.styleId = "TOOLBAR_BUTTON";
214         _cbCaseSensitive.checked = true;
215         _layout.addChild(_cbCaseSensitive);
216 
217         _cbWholeWords = new ImageCheckButton("cbWholeWords", "find_whole_words");
218         _cbWholeWords.tooltipText = "EDIT_FIND_WHOLE_WORDS";
219         _cbWholeWords.styleId = "TOOLBAR_BUTTON";
220         _layout.addChild(_cbWholeWords);
221 
222         addChild(_layout);
223 
224         _resultLog = new SearchLogWidget("SearchLogWidget");
225         _resultLog.searchResultClickHandler = &onMatchClick;
226         _resultLog.layoutHeight(FILL_PARENT);
227         addChild(_resultLog);
228     }
229     
230     //Recursively search for text in projectItem
231     void searchInProject(ProjectItem project, dstring text) {
232         if (project.isFolder == true) {
233             ProjectFolder projFolder = cast(ProjectFolder) project;
234             import std.parallelism;
235             for (int i = 0; i < projFolder.childCount; i++) {
236                 version (PARALLEL_SEARCH)
237                     taskPool.put(task(&searchInProject, projFolder.child(i), text));
238                 else
239                     searchInProject(projFolder.child(i), text);
240             }
241         }
242         else {
243             Log.d("Searching in: " ~ project.filename);
244             SearchMatchList match = findMatches(project.filename, text);
245             if(match.matches.length > 0) {
246                 synchronized {
247                     _matchedList ~= match;
248                     invalidate(); //Widget must updated with new matches
249                 }
250             }
251         }
252     }
253     
254     bool findText(dstring source) {
255         Log.d("Finding " ~ source);
256         
257         _resultLog.setTextToHighlight(""d, 0);
258         _resultLog.text = ""d;
259 
260         if (currentWorkspace is null)
261             return false;
262 
263         _matchedList = [];
264         _resultLogMatchIndex = 0;
265         
266         import std.parallelism; //for taskpool.
267         
268         switch (_searchScope.text) {
269             case "File":
270                 if (_frame.currentEditor) {
271                     SearchMatchList match = findMatches(_frame.currentEditor.filename, source);
272                     if(match.matches.length > 0)
273                         _matchedList ~= match;
274                 }
275                 break;
276             case "Project":
277                foreach(Project project; _frame._wsPanel.workspace.projects) {
278                     if(!project.isDependency) {
279                         version (PARALLEL_SEARCH)
280                             taskPool.put(task(&searchInProject, project.items, source));
281                         else
282                             searchInProject(project.items, source);
283                     }
284                }
285                break;
286             case "Dependencies":
287                foreach(Project project; _frame._wsPanel.workspace.projects) {
288                     if(project.isDependency) {
289                         version (PARALLEL_SEARCH)
290                             taskPool.put(task(&searchInProject, project.items, source));
291                         else
292                             searchInProject(project.items, source);
293                     }
294                }
295                break;
296             case "Everywhere":
297                foreach(Project project; _frame._wsPanel.workspace.projects) {
298                    version (PARALLEL_SEARCH)
299                        taskPool.put(task(&searchInProject, project.items, source));
300                    else
301                        searchInProject(project.items, source);
302                }
303                break;
304             default:
305                 assert(0);
306         }
307         _resultLog.setTextToHighlight(source, TextSearchFlag.CaseSensitive);
308         return true;
309     }
310     
311     override void onDraw(DrawBuf buf) {
312         //Check if there are new matches to display
313         if(_resultLogMatchIndex < _matchedList.length) {
314             for(; _resultLogMatchIndex < _matchedList.length; _resultLogMatchIndex++) {
315                 SearchMatchList matchList = _matchedList[_resultLogMatchIndex];
316                 _resultLog.appendText("Matches in "d ~ to!dstring(matchList.filename) ~ '\n');
317                 foreach(SearchMatch match; matchList.matches) {
318                     _resultLog.appendText(" --> ["d ~ to!dstring(match.line+1) ~ ":"d ~ to!dstring(match.col) ~ "]" ~ match.lineContent ~"\n"d);
319                 }
320             }
321         }
322         super.onDraw(buf);
323     }
324 
325     void checkSearchMode() {
326         if (!_frame.currentEditor && _searchScope.selectedItemIndex == 0)
327             _searchScope.selectedItemIndex = 1;
328     }
329 
330     uint makeSearchFlags() {
331         uint res = 0;
332         if (_cbCaseSensitive.checked)
333             res |= TextSearchFlag.CaseSensitive;
334         if (_cbWholeWords.checked)
335             res |= TextSearchFlag.WholeWords;
336         return res;
337     }
338 
339     //Find the match/matchList that corrosponds to the line in _resultLog
340     bool onMatchClick(int line) {
341         line++;
342         foreach(matchList; _matchedList){
343             line--;
344             if (line == 0) {
345                 if (_frame.openSourceFile(matchList.filename)) {
346                     _frame.currentEditor.setTextToHighlight(_findText.text, makeSearchFlags);
347                     _frame.currentEditor.setFocus();
348                 }
349                 return true;
350             }
351             foreach(match; matchList.matches) {
352                 line--;
353                 if (line == 0) {
354                     if (_frame.openSourceFile(matchList.filename)) {
355                         _frame.currentEditor.setCaretPos(match.line, to!int(match.col));
356                         _frame.currentEditor.setTextToHighlight(_findText.text, makeSearchFlags);
357                         _frame.currentEditor.setFocus();
358                     }
359                     return true;
360                 }
361             }
362         }
363         return false;
364     }
365 }
366 
367 SearchMatchList findMatches(in string filename, in dstring searchString) {
368     EditableContent content = new EditableContent(true);
369     content.load(filename);
370     SearchMatchList match;
371     match.filename = filename;
372 
373     foreach(int lineIndex, dstring line; content.lines) {
374         auto colIndex = line.indexOf(searchString);
375         
376         if (colIndex != -1) {
377             match.matches ~= SearchMatch(lineIndex, colIndex, line);
378         }
379     }
380     return match;  
381 }