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 }