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