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