diff --git a/src/dlangide/ui/commands.d b/src/dlangide/ui/commands.d index f1b68ba..72979e1 100644 --- a/src/dlangide/ui/commands.d +++ b/src/dlangide/ui/commands.d @@ -44,6 +44,7 @@ enum IDEActions : int { GoToDefinition, GetCompletionSuggestions, InsertCompletion, + FindText, } __gshared static this() { @@ -101,3 +102,4 @@ const Action ACTION_ADD_TO_CURRENT_WORKSPACE = new Action(IDEActions.AddToCurren const Action ACTION_GO_TO_DEFINITION = (new Action(IDEActions.GoToDefinition, "GO_TO_DEFINITION"c, ""c, KeyCode.KEY_G, KeyFlag.Control)).addAccelerator(KeyCode.F12, 0).disableByDefault(); const Action ACTION_GET_COMPLETIONS = (new Action(IDEActions.GetCompletionSuggestions, "SHOW_COMPLETIONS"c, ""c, KeyCode.KEY_G, KeyFlag.Control|KeyFlag.Shift)).addAccelerator(KeyCode.SPACE, KeyFlag.Control).disableByDefault(); +const Action ACTION_FIND_TEXT = (new Action(IDEActions.FindText, "FIND_TEXT"c, ""c, KeyCode.KEY_F, KeyFlag.Control)); diff --git a/src/dlangide/ui/frame.d b/src/dlangide/ui/frame.d index d0a7f13..23a06df 100644 --- a/src/dlangide/ui/frame.d +++ b/src/dlangide/ui/frame.d @@ -385,7 +385,7 @@ class IDEFrame : AppFrame { MenuItem editItem = new MenuItem(new Action(2, "MENU_EDIT")); editItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, - ACTION_EDIT_CUT, ACTION_EDIT_UNDO, ACTION_EDIT_REDO); + ACTION_EDIT_CUT, ACTION_EDIT_UNDO, ACTION_EDIT_REDO, ACTION_FIND_TEXT); MenuItem editItemAdvanced = new MenuItem(new Action(221, "MENU_EDIT_ADVANCED")); editItemAdvanced.add(ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT, ACTION_EDIT_TOGGLE_BLOCK_COMMENT, ACTION_GO_TO_DEFINITION, ACTION_GET_COMPLETIONS); editItem.add(editItemAdvanced); @@ -626,6 +626,17 @@ class IDEFrame : AppFrame { case IDEActions.EditPreferences: showPreferences(); return true; + case IDEActions.FindText: + Log.d("Opening Search Field"); + import dlangide.ui.searchPanel; + int searchPanelIndex = _logPanel.getTabs.tabIndex("search"); + if(searchPanelIndex == -1) { + SearchWidget searchPanel = new SearchWidget("search", this); + _logPanel.getTabs.addTab( searchPanel, "Search"d, null, true); + } + _logPanel.getTabs.selectTab("search"); + //TODO: Focus search field + return true; default: return super.handleAction(a); } diff --git a/src/dlangide/ui/outputpanel.d b/src/dlangide/ui/outputpanel.d index f997429..5f1efa0 100644 --- a/src/dlangide/ui/outputpanel.d +++ b/src/dlangide/ui/outputpanel.d @@ -3,16 +3,19 @@ module dlangide.ui.outputpanel; import dlangui; import dlangide.workspace.workspace; import dlangide.workspace.project; +import dlangide.ui.frame; import std.utf; import std.regex; import std.algorithm : startsWith; +import std.string; /// event listener to navigate by error/warning position interface CompilerLogIssueClickHandler { bool onCompilerLogIssueClick(dstring filename, int line, int column); } + /// Log widget with parsing of compiler output class CompilerLogWidget : LogWidget { @@ -113,20 +116,42 @@ class OutputPanel : DockWindow { protected CompilerLogWidget _logWidget; + TabWidget _tabs; + + @property TabWidget getTabs() { return _tabs;} + this(string id) { + _showCloseButton = false; + dockAlignment = DockAlignment.Bottom; super(id); - _caption.text = "Output"d; - dockAlignment = DockAlignment.Bottom; - } + + } override protected Widget createBodyWidget() { + _tabs = new TabWidget("OutputPanelTabs"); + _tabs.setStyles(STYLE_DOCK_HOST_BODY, STYLE_TAB_UP_DARK, STYLE_TAB_UP_BUTTON_DARK, STYLE_TAB_UP_BUTTON_DARK_TEXT); + _logWidget = new CompilerLogWidget("logwidget"); _logWidget.readOnly = true; - _logWidget.layoutHeight(FILL_PARENT).layoutHeight(FILL_PARENT); + _logWidget.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); _logWidget.compilerLogIssueClickHandler = &onIssueClick; - return _logWidget; + + _tabs.addTab(_logWidget, "Compiler Log"d); + _tabs.selectTab("logwidget"); + + return _tabs; } + override protected void init() { + + styleId = STYLE_DOCK_WINDOW; + _bodyWidget = createBodyWidget(); + _bodyWidget.styleId = STYLE_DOCK_WINDOW_BODY; + addChild(_bodyWidget); + } + + //TODO: Refactor OutputPanel to expose CompilerLogWidget + void appendText(string category, dstring msg) { _logWidget.appendText(msg); } @@ -159,4 +184,4 @@ class OutputPanel : DockWindow { return true; } -} +} \ No newline at end of file diff --git a/src/dlangide/ui/searchPanel.d b/src/dlangide/ui/searchPanel.d new file mode 100644 index 0000000..b3445e9 --- /dev/null +++ b/src/dlangide/ui/searchPanel.d @@ -0,0 +1,249 @@ +module dlangide.ui.searchPanel; + + +import dlangui; + +import dlangide.ui.frame; +import dlangide.ui.wspanel; +import dlangide.workspace.workspace; +import dlangide.workspace.project; + +import std.string; +import std.conv; + +interface SearchResultClickHandler { + bool onSearchResultClick(int line); +} + +//LogWidget with highlighting for search results. +class SearchLogWidget : LogWidget { + + //Sends which line was clicked. + Signal!SearchResultClickHandler searchResultClickHandler; + + this(string ID){ + super(ID); + scrollLock = false; + } + + override protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) { + uint defColor = textColor; + const uint filenameColor = 0x0000C0; + const uint errorColor = 0xFF0000; + const uint warningColor = 0x606000; + const uint deprecationColor = 0x802040; + uint flags = 0; + if (buf.length < txt.length) + buf.length = txt.length; + + //Highlights the filename + if(txt.startsWith("Matches in ")) { + CustomCharProps[] colors = buf[0..txt.length]; + uint cl = defColor; + flags = 0; + for (int i = 0; i < txt.length; i++) { + dstring rest = txt[i..$]; + if(i == 11) { + cl = filenameColor; + flags = TextFlag.Underline; + } + colors[i].color = cl; + colors[i].textFlags = flags; + } + return colors; + } + //Highlight line and collumn + else { + CustomCharProps[] colors = buf[0..txt.length]; + uint cl = filenameColor; + flags = TextFlag.Underline; + for (int i = 0; i < txt.length; i++) { + dstring rest = txt[i..$]; + if (rest.startsWith(" -->"d)) { + cl = warningColor; + flags = 0; + } + if(i == 4) { + cl = errorColor; + flags = TextFlag.Underline; + } + + colors[i].color = cl; + colors[i].textFlags = flags; + + //Colors to apply in following iterations of the loop. + if(rest.startsWith("]")) { + cl = defColor; + flags = 0; + } + } + return colors; + } + } + + override bool onMouseEvent(MouseEvent event) { + super.onMouseEvent(event); + if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) { + int line = _caretPos.line; + if (searchResultClickHandler.assigned) { + searchResultClickHandler(line); + return true; + } + } + return false; + } +} + +class SearchWidget : TabWidget { + HorizontalLayout _layout; + EditLine _findText; + SearchLogWidget _resultLog; + ComboBox _searchScope; + + protected IDEFrame _frame; + protected SearchMatchList[] _matchedList; + + struct SearchMatch { + int line; + long col; + dstring lineContent; + } + + struct SearchMatchList { + string filename; + SearchMatch[] matches; + } + + this(string ID, IDEFrame frame) { + super(ID); + _frame = frame; + + layoutHeight(FILL_PARENT); + + //Remove title, more button + removeAllChildren(); + + _layout = new HorizontalLayout(); + _layout.addChild(new TextWidget("FindLabel", "Find: "d)); + + _findText = new EditLine(); + _findText.padding(Rect(5,4,50,4)); + _findText.layoutWidth(400); + _layout.addChild(_findText); + + auto goButton = new ImageButton("findTextButton", "edit-redo"); + goButton.addOnClickListener( delegate(Widget) { + findText(_findText.text); + return true; + }); + _layout.addChild(goButton); + + _searchScope = new ComboBox("searchScope", ["File"d, "Project"d, "Dependencies"d, "Everywhere"d]); + _searchScope.selectedItemIndex = 0; + _layout.addChild(_searchScope); + addChild(_layout); + + _resultLog = new SearchLogWidget("SearchLogWidget"); + _resultLog.searchResultClickHandler = &onMatchClick; + _resultLog.layoutHeight(FILL_PARENT); + addChild(_resultLog); + } + + //Recursively search for text in projectItem + void searchInProject(ProjectItem project, dstring text) { + if (project.isFolder == true) { + ProjectFolder projFolder = cast(ProjectFolder) project; + for (int i = 0; i < projFolder.childCount; i++) { + searchInProject(projFolder.child(i), text); + } + } + else { + Log.d("Searching in: " ~ project.filename); + EditableContent content = new EditableContent(true); + content.load(project.filename); + SearchMatchList match; + match.filename = project.filename; + + foreach(int lineIndex, dstring line; content.lines) { + auto colIndex = line.indexOf(text); + if (colIndex != -1) { + match.matches ~= SearchMatch(lineIndex, colIndex, line); + } + } + + if(match.matches.length > 0) { + _matchedList ~= match; + } + } + } + + bool findText(dstring source) { + Log.d("Finding " ~ source); + + _resultLog.text = ""d; + _matchedList = []; + + switch (_searchScope.text) { + case "File": //File + auto currentFileItem = _frame._wsPanel.workspace.findSourceFileItem(_frame.currentEditor.filename); + if(currentFileItem !is null) + searchInProject(currentFileItem, source); + break; + case "Project": //Project + foreach(Project project; _frame._wsPanel.workspace.projects) { + if(!project.isDependency) + searchInProject(project.items, source); + } + break; + case "Dependencies": //Dependencies + foreach(Project project; _frame._wsPanel.workspace.projects) { + if(project.isDependency) + searchInProject(project.items, source); + } + break; + case "Everywhere": //Everywhere + foreach(Project project; _frame._wsPanel.workspace.projects) { + searchInProject(project.items, source); + } + break; + default: + assert(0); + } + + if (_matchedList.length == 0) { + _resultLog.appendText(to!dstring("No matches found.\n")); + } + else { + foreach(SearchMatchList fileMatchList; _matchedList) { + _resultLog.appendText("Matches in "d ~ to!dstring(fileMatchList.filename) ~ '\n'); + foreach(SearchMatch match; fileMatchList.matches) { + _resultLog.appendText(" --> ["d ~ to!dstring(match.line+1) ~ ":"d ~ to!dstring(match.col) ~ "]" ~ match.lineContent ~"\n"d); + } + } + } + return true; + } + + //Find the match/matchList that corrosponds to the line in _resultLog + bool onMatchClick(int line) { + line++; + foreach(matchList; _matchedList){ + line--; + if (line == 0) { + _frame.openSourceFile(matchList.filename); + _frame.currentEditor.setFocus(); + return true; + } + foreach(match; matchList.matches) { + line--; + if (line == 0) { + _frame.openSourceFile(matchList.filename); + _frame.currentEditor.setCaretPos(match.line, to!int(match.col)); + _frame.currentEditor.setFocus(); + return true; + } + } + } + return false; + } +} diff --git a/src/dlangide/workspace/project.d b/src/dlangide/workspace/project.d index 5dc712f..0faf0d6 100644 --- a/src/dlangide/workspace/project.d +++ b/src/dlangide/workspace/project.d @@ -63,7 +63,7 @@ class ProjectItem { } /// returns true if item is folder - @property bool isFolder() { + @property const bool isFolder() { return false; } /// returns child object count @@ -84,7 +84,7 @@ class ProjectFolder : ProjectItem { super(filename); } - @property override bool isFolder() { + @property override const bool isFolder() { return true; } @property override int childCount() { diff --git a/views/res/i18n/en.ini b/views/res/i18n/en.ini index 68922e4..59b8987 100644 --- a/views/res/i18n/en.ini +++ b/views/res/i18n/en.ini @@ -56,9 +56,11 @@ MENU_WINDOW_CLOSE_ALL_DOCUMENTS=Close All Documents MENU_HELP=&HELP MENU_HELP_VIEW_HELP=&View help MENU_HELP_ABOUT=&About +MENU_NAVIGATE=NAVIGATE + GO_TO_DEFINITION=Go To Definition SHOW_COMPLETIONS=Get Autocompletions -MENU_NAVIGATE=NAVIGATE +FIND_TEXT=Find text TAB_LONG_LIST=Long list