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