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     protected bool onEditorAction(const Action action) {
180         if (action.id == EditorActions.InsertNewLine) {
181             return onFindButtonPressed(this);
182         }
183         return false;
184     }
185 
186     this(string ID, IDEFrame frame) {
187         super(ID);
188         _frame = frame;
189         
190         layoutHeight(FILL_PARENT);
191         
192         //Remove title, more button
193         removeAllChildren();
194         
195         _layout = new HorizontalLayout();
196         _layout.addChild(new TextWidget("FindLabel", "Find: "d));
197         _layout.layoutWidth = FILL_PARENT;
198 
199         _findText = new EditLine();
200         _findText.padding(Rect(5,4,50,4));
201         _findText.layoutWidth = FILL_PARENT;
202         _findText.editorAction = &onEditorAction; // to handle Enter key press in editor
203         _layout.addChild(_findText);
204         
205         auto goButton = new ImageButton("findTextButton", "edit-find");
206         goButton.click = &onFindButtonPressed;
207         _layout.addChild(goButton);
208         
209         _layout.addChild(new HSpacer);
210 
211         _searchScope = new ComboBox("searchScope", ["File"d, "Project"d, "Dependencies"d, "Everywhere"d]);
212         _searchScope.selectedItemIndex = 0;
213         _layout.addChild(_searchScope);
214 
215         _cbCaseSensitive = new ImageCheckButton("cbCaseSensitive", "find_case_sensitive");
216         _cbCaseSensitive.tooltipText = "EDIT_FIND_CASE_SENSITIVE";
217         _cbCaseSensitive.styleId = "TOOLBAR_BUTTON";
218         _cbCaseSensitive.checked = true;
219         _layout.addChild(_cbCaseSensitive);
220 
221         _cbWholeWords = new ImageCheckButton("cbWholeWords", "find_whole_words");
222         _cbWholeWords.tooltipText = "EDIT_FIND_WHOLE_WORDS";
223         _cbWholeWords.styleId = "TOOLBAR_BUTTON";
224         _layout.addChild(_cbWholeWords);
225 
226         addChild(_layout);
227 
228         _resultLog = new SearchLogWidget("SearchLogWidget");
229         _resultLog.searchResultClickHandler = &onMatchClick;
230         _resultLog.layoutHeight(FILL_PARENT);
231         addChild(_resultLog);
232     }
233     
234     //Recursively search for text in projectItem
235     void searchInProject(ProjectItem project, dstring text) {
236         if (project.isFolder == true) {
237             ProjectFolder projFolder = cast(ProjectFolder) project;
238             import std.parallelism;
239             for (int i = 0; i < projFolder.childCount; i++) {
240                 version (PARALLEL_SEARCH)
241                     taskPool.put(task(&searchInProject, projFolder.child(i), text));
242                 else
243                     searchInProject(projFolder.child(i), text);
244             }
245         }
246         else {
247             Log.d("Searching in: " ~ project.filename);
248             SearchMatchList match = findMatches(project.filename, text);
249             if(match.matches.length > 0) {
250                 synchronized {
251                     _matchedList ~= match;
252                     invalidate(); //Widget must updated with new matches
253                 }
254             }
255         }
256     }
257     
258     bool findText(dstring source) {
259         Log.d("Finding " ~ source);
260         
261         _resultLog.setTextToHighlight(""d, 0);
262         _resultLog.text = ""d;
263 
264         if (currentWorkspace is null)
265             return false;
266 
267         _matchedList = [];
268         _resultLogMatchIndex = 0;
269         
270         import std.parallelism; //for taskpool.
271         
272         switch (_searchScope.text) {
273             case "File":
274                 if (_frame.currentEditor) {
275                     SearchMatchList match = findMatches(_frame.currentEditor.filename, source);
276                     if(match.matches.length > 0)
277                         _matchedList ~= match;
278                 }
279                 break;
280             case "Project":
281                foreach(Project project; _frame._wsPanel.workspace.projects) {
282                     if(!project.isDependency) {
283                         version (PARALLEL_SEARCH)
284                             taskPool.put(task(&searchInProject, project.items, source));
285                         else
286                             searchInProject(project.items, source);
287                     }
288                }
289                break;
290             case "Dependencies":
291                foreach(Project project; _frame._wsPanel.workspace.projects) {
292                     if(project.isDependency) {
293                         version (PARALLEL_SEARCH)
294                             taskPool.put(task(&searchInProject, project.items, source));
295                         else
296                             searchInProject(project.items, source);
297                     }
298                }
299                break;
300             case "Everywhere":
301                foreach(Project project; _frame._wsPanel.workspace.projects) {
302                    version (PARALLEL_SEARCH)
303                        taskPool.put(task(&searchInProject, project.items, source));
304                    else
305                        searchInProject(project.items, source);
306                }
307                break;
308             default:
309                 assert(0);
310         }
311         _resultLog.setTextToHighlight(source, TextSearchFlag.CaseSensitive);
312         return true;
313     }
314     
315     override void onDraw(DrawBuf buf) {
316         //Check if there are new matches to display
317         if(_resultLogMatchIndex < _matchedList.length) {
318             for(; _resultLogMatchIndex < _matchedList.length; _resultLogMatchIndex++) {
319                 SearchMatchList matchList = _matchedList[_resultLogMatchIndex];
320                 _resultLog.appendText("Matches in "d ~ to!dstring(matchList.filename) ~ '\n');
321                 foreach(SearchMatch match; matchList.matches) {
322                     _resultLog.appendText(" --> ["d ~ to!dstring(match.line+1) ~ ":"d ~ to!dstring(match.col) ~ "]" ~ match.lineContent ~"\n"d);
323                 }
324             }
325         }
326         super.onDraw(buf);
327     }
328 
329     void checkSearchMode() {
330         if (!_frame.currentEditor && _searchScope.selectedItemIndex == 0)
331             _searchScope.selectedItemIndex = 1;
332     }
333 
334     uint makeSearchFlags() {
335         uint res = 0;
336         if (_cbCaseSensitive.checked)
337             res |= TextSearchFlag.CaseSensitive;
338         if (_cbWholeWords.checked)
339             res |= TextSearchFlag.WholeWords;
340         return res;
341     }
342 
343     //Find the match/matchList that corrosponds to the line in _resultLog
344     bool onMatchClick(int line) {
345         line++;
346         foreach(matchList; _matchedList){
347             line--;
348             if (line == 0) {
349                 if (_frame.openSourceFile(matchList.filename)) {
350                     _frame.currentEditor.setTextToHighlight(_findText.text, makeSearchFlags);
351                     _frame.currentEditor.setFocus();
352                 }
353                 return true;
354             }
355             foreach(match; matchList.matches) {
356                 line--;
357                 if (line == 0) {
358                     if (_frame.openSourceFile(matchList.filename)) {
359                         _frame.currentEditor.setCaretPos(match.line, to!int(match.col));
360                         _frame.currentEditor.setTextToHighlight(_findText.text, makeSearchFlags);
361                         _frame.currentEditor.setFocus();
362                     }
363                     return true;
364                 }
365             }
366         }
367         return false;
368     }
369 }
370 
371 SearchMatchList findMatches(in string filename, in dstring searchString) {
372     EditableContent content = new EditableContent(true);
373     content.load(filename);
374     SearchMatchList match;
375     match.filename = filename;
376 
377     foreach(int lineIndex, dstring line; content.lines) {
378         auto colIndex = line.indexOf(searchString);
379         
380         if (colIndex != -1) {
381             match.matches ~= SearchMatch(lineIndex, colIndex, line);
382         }
383     }
384     return match;  
385 }