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 }