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         _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         super.onThemeChanged();
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 	protected bool onEditorAction(const Action action) {
175 		if (action.id == EditorActions.InsertNewLine) {
176 			return onFindButtonPressed(this);
177 		}
178 		return false;
179 	}
180 
181 	this(string ID, IDEFrame frame) {
182 		super(ID);
183 		_frame = frame;
184         
185         layoutHeight(FILL_PARENT);
186         
187 		//Remove title, more button
188 		removeAllChildren();
189 		
190 		_layout = new HorizontalLayout();
191 		_layout.addChild(new TextWidget("FindLabel", "Find: "d));
192 		
193 		_findText = new EditLine();
194 		_findText.padding(Rect(5,4,50,4));
195 		_findText.layoutWidth(400);
196 		_findText.editorAction = &onEditorAction; // to handle Enter key press in editor
197 		_layout.addChild(_findText);
198 		
199 		auto goButton = new ImageButton("findTextButton", "edit-find");
200 		goButton.click = &onFindButtonPressed;
201 		_layout.addChild(goButton);
202         
203         _searchScope = new ComboBox("searchScope", ["File"d, "Project"d, "Dependencies"d, "Everywhere"d]);
204         _searchScope.selectedItemIndex = 0;
205         _layout.addChild(_searchScope);
206 		addChild(_layout);
207 
208 		_resultLog = new SearchLogWidget("SearchLogWidget");
209         _resultLog.searchResultClickHandler = &onMatchClick;
210 		_resultLog.layoutHeight(FILL_PARENT);
211         addChild(_resultLog);
212 	}
213     
214     //Recursively search for text in projectItem
215     void searchInProject(ProjectItem project, dstring text) {
216         if (project.isFolder == true) {
217         	ProjectFolder projFolder = cast(ProjectFolder) project;
218         	import std.parallelism;
219 	        for (int i = 0; i < projFolder.childCount; i++) {
220                     taskPool.put(task(&searchInProject, projFolder.child(i), text));   
221 	        }
222         }
223         else {
224             Log.d("Searching in: " ~ project.filename);
225             SearchMatchList match = findMatches(project.filename, text);
226             if(match.matches.length > 0) {
227                 synchronized {
228                     _matchedList ~= match;
229                     invalidate(); //Widget must updated with new matches
230                 }
231             }
232         }
233     }
234 	
235 	bool findText(dstring source) {
236         Log.d("Finding " ~ source);
237         
238         _resultLog.textToHighlight = ""d;
239         _resultLog.text = ""d;
240         _matchedList = [];
241         _resultLogMatchIndex = 0;
242         
243         import std.parallelism; //for taskpool.
244         
245         switch (_searchScope.text) {
246             case "File":
247                 SearchMatchList match = findMatches(_frame.currentEditor.filename, source);
248                 if(match.matches.length > 0)
249                     _matchedList ~= match;
250                 break;
251             case "Project":
252                foreach(Project project; _frame._wsPanel.workspace.projects) {
253                     if(!project.isDependency)
254                         taskPool.put(task(&searchInProject, project.items, source));
255                }
256                break;
257             case "Dependencies":
258                foreach(Project project; _frame._wsPanel.workspace.projects) {
259                     if(project.isDependency)
260                         taskPool.put(task(&searchInProject, project.items, source));
261                }
262                break;
263             case "Everywhere":
264                foreach(Project project; _frame._wsPanel.workspace.projects) {
265                     taskPool.put(task(&searchInProject, project.items, source));
266                }
267                break;
268             default:
269                 assert(0);
270         }
271         _resultLog.textToHighlight = source;
272 		return true;
273 	}
274     
275     override void onDraw(DrawBuf buf) {
276         //Check if there are new matches to display
277         if(_resultLogMatchIndex < _matchedList.length) {
278             for(; _resultLogMatchIndex < _matchedList.length; _resultLogMatchIndex++) {
279                 SearchMatchList matchList = _matchedList[_resultLogMatchIndex];
280                 _resultLog.appendText("Matches in "d ~ to!dstring(matchList.filename) ~ '\n');
281     			foreach(SearchMatch match; matchList.matches) {
282                     _resultLog.appendText(" --> ["d ~ to!dstring(match.line+1) ~ ":"d ~ to!dstring(match.col) ~ "]" ~ match.lineContent ~"\n"d);
283     			}
284     		}
285         }
286         super.onDraw(buf);
287     }
288     
289     //Find the match/matchList that corrosponds to the line in _resultLog
290     bool onMatchClick(int line) {
291         line++;
292         foreach(matchList; _matchedList){
293         	line--;
294         	if (line == 0) {
295         		_frame.openSourceFile(matchList.filename);
296         		_frame.currentEditor.setFocus();
297         		return true;
298         	}
299             foreach(match; matchList.matches) {
300             	line--;
301             	if (line == 0) {
302             		_frame.openSourceFile(matchList.filename);
303             		_frame.currentEditor.setCaretPos(match.line, to!int(match.col));
304             		_frame.currentEditor.setFocus();
305             		return true;
306             	}
307             }
308         }
309         return false;
310     }
311 }
312 
313 SearchMatchList findMatches(in string filename, in dstring searchString) {
314     EditableContent content = new EditableContent(true);
315     content.load(filename);
316     SearchMatchList match;
317     match.filename = filename;
318 
319     foreach(int lineIndex, dstring line; content.lines) {
320 		auto colIndex = line.indexOf(searchString);
321         
322 		if (colIndex != -1) {
323 			match.matches ~= SearchMatch(lineIndex, colIndex, line);
324 		}
325 	}
326     return match;  
327 }