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 }