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 }