diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index e5b864be48..e3a0e2ce91 100644 --- a/docs/API-Reference/command/Commands.md +++ b/docs/API-Reference/command/Commands.md @@ -992,6 +992,12 @@ Performs a mixed reset ## CMD\_GIT\_TOGGLE\_PANEL Toggles the git panel +**Kind**: global variable + + +## CMD\_CUSTOM\_SNIPPETS\_PANEL +Toggles the custom snippets panel + **Kind**: global variable diff --git a/docs/API-Reference/document/Document.md b/docs/API-Reference/document/Document.md index 4f4b14cba7..17368b1391 100644 --- a/docs/API-Reference/document/Document.md +++ b/docs/API-Reference/document/Document.md @@ -27,6 +27,7 @@ const Document = brackets.getModule("document/Document") * [.replaceRange(text, start, end, origin)](#Document+replaceRange) * [.getRange(start, end)](#Document+getRange) ⇒ string * [.getLine(Zero-based)](#Document+getLine) ⇒ string + * [.posFromIndex(index)](#Document+posFromIndex) ⇒ Object * [.batchOperation(doOperation)](#Document+batchOperation) * [.notifySaved()](#Document+notifySaved) * [.adjustPosForChange(pos, textLines, start, end)](#Document+adjustPosForChange) ⇒ Object @@ -240,6 +241,19 @@ Returns the text of the given line (excluding any line ending characters) | --- | --- | --- | | Zero-based | number | line number | + + +### document.posFromIndex(index) ⇒ Object +Given a character index within the document text (assuming \n newlines), +returns the corresponding {line, ch} position. Works whether or not +a master editor is attached. + +**Kind**: instance method of [Document](#Document) + +| Param | Type | Description | +| --- | --- | --- | +| index | number | Zero-based character offset | + ### document.batchOperation(doOperation) diff --git a/docs/API-Reference/search/SearchResultsView.md b/docs/API-Reference/search/SearchResultsView.md index d78dcac237..a26874c555 100644 --- a/docs/API-Reference/search/SearchResultsView.md +++ b/docs/API-Reference/search/SearchResultsView.md @@ -21,7 +21,7 @@ Dispatches the following events_ ### new Handles the search results panel. Dispatches the following events: replaceBatch - when the Replace button is clicked. - close - when the panel is closed.(model, panelID, panelName, type) + close - when the panel is closed.(model, panelID, panelName, type, [title]) | Param | Type | Description | | --- | --- | --- | @@ -29,4 +29,5 @@ Dispatches the following events: | panelID | string | The CSS ID to use for the panel. | | panelName | string | The name to use for the panel, as passed to WorkspaceManager.createBottomPanel(). | | type | string | type to identify if it is reference search or string match serach | +| [title] | string | Display title for the panel tab. | diff --git a/docs/API-Reference/view/PanelView.md b/docs/API-Reference/view/PanelView.md index c4663e07c7..4bce903cdc 100644 --- a/docs/API-Reference/view/PanelView.md +++ b/docs/API-Reference/view/PanelView.md @@ -9,7 +9,7 @@ const PanelView = brackets.getModule("view/PanelView") **Kind**: global class * [Panel](#Panel) - * [new Panel($panel)](#new_Panel_new) + * [new Panel($panel, id, [title])](#new_Panel_new) * [.$panel](#Panel+$panel) : jQueryObject * [.isVisible()](#Panel+isVisible) ⇒ boolean * [.registerCanBeShownHandler(canShowHandlerFn)](#Panel+registerCanBeShownHandler) ⇒ boolean @@ -17,17 +17,21 @@ const PanelView = brackets.getModule("view/PanelView") * [.show()](#Panel+show) * [.hide()](#Panel+hide) * [.setVisible(visible)](#Panel+setVisible) + * [.setTitle(newTitle)](#Panel+setTitle) + * [.destroy()](#Panel+destroy) * [.getPanelType()](#Panel+getPanelType) ⇒ string -### new Panel($panel) +### new Panel($panel, id, [title]) Represents a panel below the editor area (a child of ".content"). | Param | Type | Description | | --- | --- | --- | | $panel | jQueryObject | The entire panel, including any chrome, already in the DOM. | +| id | string | Unique panel identifier. | +| [title] | string | Optional display title for the tab bar. | @@ -84,12 +88,66 @@ Sets the panel's visibility state | --- | --- | --- | | visible | boolean | true to show, false to hide | + + +### panel.setTitle(newTitle) +Updates the display title shown in the tab bar for this panel. + +**Kind**: instance method of [Panel](#Panel) + +| Param | Type | Description | +| --- | --- | --- | +| newTitle | string | The new title to display. | + + + +### panel.destroy() +Destroys the panel, removing it from the tab bar, internal maps, and the DOM. +After calling this, the Panel instance should not be reused. + +**Kind**: instance method of [Panel](#Panel) ### panel.getPanelType() ⇒ string gets the Panel's type **Kind**: instance method of [Panel](#Panel) + + +## \_panelMap : Object.<string, Panel> +Maps panel ID to Panel instance + +**Kind**: global variable + + +## \_$container : jQueryObject +The single container wrapping all bottom panels + +**Kind**: global variable + + +## \_$tabBar : jQueryObject +The tab bar inside the container + +**Kind**: global variable + + +## \_$tabsOverflow : jQueryObject +Scrollable area holding the tab elements + +**Kind**: global variable + + +## \_openIds : Array.<string> +Ordered list of currently open (tabbed) panel IDs + +**Kind**: global variable + + +## \_activeId : string \| null +The panel ID of the currently visible (active) tab + +**Kind**: global variable ## EVENT\_PANEL\_HIDDEN : string @@ -108,3 +166,30 @@ Event when panel is shown type for bottom panel **Kind**: global constant + + +## init($container, $tabBar, $tabsOverflow) +Initializes the PanelView module with references to the bottom panel container DOM elements. +Called by WorkspaceManager during htmlReady. + +**Kind**: global function + +| Param | Type | Description | +| --- | --- | --- | +| $container | jQueryObject | The bottom panel container element. | +| $tabBar | jQueryObject | The tab bar element inside the container. | +| $tabsOverflow | jQueryObject | The scrollable area holding tab elements. | + + + +## getOpenBottomPanelIDs() ⇒ Array.<string> +Returns a copy of the currently open bottom panel IDs in tab order. + +**Kind**: global function + + +## hideAllOpenPanels() ⇒ Array.<string> +Hides every open bottom panel tab in a single batch + +**Kind**: global function +**Returns**: Array.<string> - The IDs of panels that were open (useful for restoring later). diff --git a/docs/API-Reference/view/WorkspaceManager.md b/docs/API-Reference/view/WorkspaceManager.md index 8d39dbbaaf..57d4d627e2 100644 --- a/docs/API-Reference/view/WorkspaceManager.md +++ b/docs/API-Reference/view/WorkspaceManager.md @@ -22,10 +22,14 @@ Events: * [.PANEL_TYPE_BOTTOM_PANEL](#module_view/WorkspaceManager.PANEL_TYPE_BOTTOM_PANEL) : string * [.PANEL_TYPE_PLUGIN_PANEL](#module_view/WorkspaceManager.PANEL_TYPE_PLUGIN_PANEL) : string * _inner_ + * [.$bottomPanelContainer](#module_view/WorkspaceManager..$bottomPanelContainer) : jQueryObject + * [.$statusBarPanelToggle](#module_view/WorkspaceManager..$statusBarPanelToggle) : jQueryObject + * [._statusBarToggleInProgress](#module_view/WorkspaceManager.._statusBarToggleInProgress) : boolean * [.EVENT_WORKSPACE_UPDATE_LAYOUT](#module_view/WorkspaceManager..EVENT_WORKSPACE_UPDATE_LAYOUT) * [.EVENT_WORKSPACE_PANEL_SHOWN](#module_view/WorkspaceManager..EVENT_WORKSPACE_PANEL_SHOWN) * [.EVENT_WORKSPACE_PANEL_HIDDEN](#module_view/WorkspaceManager..EVENT_WORKSPACE_PANEL_HIDDEN) - * [.createBottomPanel(id, $panel, [minSize])](#module_view/WorkspaceManager..createBottomPanel) ⇒ Panel + * [.createBottomPanel(id, $panel, [minSize], [title])](#module_view/WorkspaceManager..createBottomPanel) ⇒ Panel + * [.destroyBottomPanel(id)](#module_view/WorkspaceManager..destroyBottomPanel) * [.createPluginPanel(id, $panel, [minSize], $toolbarIcon, [initialSize])](#module_view/WorkspaceManager..createPluginPanel) ⇒ Panel * [.getAllPanelIDs()](#module_view/WorkspaceManager..getAllPanelIDs) ⇒ Array * [.getPanelForID(panelID)](#module_view/WorkspaceManager..getPanelForID) ⇒ Object @@ -47,6 +51,24 @@ Constant representing the type of bottom panel Constant representing the type of plugin panel **Kind**: static property of [view/WorkspaceManager](#module_view/WorkspaceManager) + + +### view/WorkspaceManager.$bottomPanelContainer : jQueryObject +The single container wrapping all bottom panels + +**Kind**: inner property of [view/WorkspaceManager](#module_view/WorkspaceManager) + + +### view/WorkspaceManager.$statusBarPanelToggle : jQueryObject +Chevron toggle in the status bar + +**Kind**: inner property of [view/WorkspaceManager](#module_view/WorkspaceManager) + + +### view/WorkspaceManager.\_statusBarToggleInProgress : boolean +True while the status bar toggle button is handling a click + +**Kind**: inner property of [view/WorkspaceManager](#module_view/WorkspaceManager) ### view/WorkspaceManager.EVENT\_WORKSPACE\_UPDATE\_LAYOUT @@ -67,7 +89,7 @@ Event triggered when a panel is hidden. **Kind**: inner constant of [view/WorkspaceManager](#module_view/WorkspaceManager) -### view/WorkspaceManager.createBottomPanel(id, $panel, [minSize]) ⇒ Panel +### view/WorkspaceManager.createBottomPanel(id, $panel, [minSize], [title]) ⇒ Panel Creates a new resizable panel beneath the editor area and above the status bar footer. Panel is initially invisible. The panel's size & visibility are automatically saved & restored as a view-state preference. @@ -77,7 +99,20 @@ The panel's size & visibility are automatically saved & restored as a view-state | --- | --- | --- | | id | string | Unique id for this panel. Use package-style naming, e.g. "myextension.feature.panelname" | | $panel | jQueryObject | DOM content to use as the panel. Need not be in the document yet. Must have an id attribute, for use as a preferences key. | -| [minSize] | number | Minimum height of panel in px. | +| [minSize] | number | @deprecated No longer used. Pass `undefined`. | +| [title] | string | Display title shown in the bottom panel tab bar. | + + + +### view/WorkspaceManager.destroyBottomPanel(id) +Destroys a bottom panel, removing it from internal registries, the tab bar, and the DOM. +After calling this, the panel ID is no longer valid and the Panel instance should not be reused. + +**Kind**: inner method of [view/WorkspaceManager](#module_view/WorkspaceManager) + +| Param | Type | Description | +| --- | --- | --- | +| id | string | The panel ID that was passed to createBottomPanel. | diff --git a/src/brackets.js b/src/brackets.js index a4c486b0f1..172467f66c 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -135,6 +135,7 @@ define(function (require, exports, module) { require("utils/NodeUtils"); require("utils/ColorUtils"); require("view/ThemeManager"); + require("view/DefaultPanelView"); require("thirdparty/lodash"); require("language/XMLUtils"); require("language/JSONUtils"); diff --git a/src/command/Commands.js b/src/command/Commands.js index dbd94cb219..95a47fcf71 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -541,6 +541,9 @@ define(function (require, exports, module) { /** Toggles the git panel */ exports.CMD_GIT_TOGGLE_PANEL = "git-toggle-panel"; + /** Toggles the custom snippets panel */ + exports.CMD_CUSTOM_SNIPPETS_PANEL = "custom_snippets"; + /** Goes to next git change */ exports.CMD_GIT_GOTO_NEXT_CHANGE = "git-gotoNextChange"; diff --git a/src/extensions/default/DebugCommands/testBuilder.js b/src/extensions/default/DebugCommands/testBuilder.js index 38e58ddf0e..0bf8508f57 100644 --- a/src/extensions/default/DebugCommands/testBuilder.js +++ b/src/extensions/default/DebugCommands/testBuilder.js @@ -38,7 +38,7 @@ define(function (require, exports, module) { function toggleTestBuilder() { if(!$panel){ $panel = $(panelHTML); - builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100); + builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100, "Test Builder"); builderPanel.hide(); _setupPanel().then(()=>{ builderPanel.setVisible(!builderPanel.isVisible()); @@ -49,23 +49,19 @@ define(function (require, exports, module) { } const panelHTML = `
-
-
-
Test Builder
+
+
-
+
- - × -
+ +
-
`; @@ -161,7 +157,6 @@ define(function (require, exports, module) { builderEditor && builderEditor.updateLayout(); }).observe($panel[0]); - $panel.find(".close").click(toggleTestBuilder); $panel.find(".save-test-builder").click(saveFile); $panel.find(".run-test-builder").click(()=>{ runTests(); @@ -177,7 +172,7 @@ define(function (require, exports, module) { return; } $panel = $(panelHTML); - builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100); + builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100, "Test Builder"); builderPanel.hide(); _setupPanel(); }); diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js index 02a34cfcdc..3a90cac8e9 100644 --- a/src/extensions/default/Git/src/Panel.js +++ b/src/extensions/default/Git/src/Panel.js @@ -1212,7 +1212,7 @@ define(function (require, exports) { return; } const mainToolbarWidth = $mainToolbar.width(); - let overFlowWidth = 565; + let overFlowWidth = 540; const breakpoints = [ { width: overFlowWidth, className: "hide-when-small" }, { width: 400, className: "hide-when-x-small" } @@ -1240,13 +1240,12 @@ define(function (require, exports) { var $panelHtml = $(panelHtml); $panelHtml.find(".git-available, .git-not-available").hide(); - gitPanel = WorkspaceManager.createBottomPanel("main-git.panel", $panelHtml, 100); + gitPanel = WorkspaceManager.createBottomPanel("main-git.panel", $panelHtml, 100, Strings.GIT_PANEL_TITLE); $gitPanel = gitPanel.$panel; const resizeObserver = new ResizeObserver(_panelResized); resizeObserver.observe($gitPanel[0]); $mainToolbar = $gitPanel.find(".mainToolbar"); $gitPanel - .on("click", ".close", toggle) .on("click", ".check-all", function () { if ($(this).is(":checked")) { return Git.stageAll().then(function () { @@ -1502,6 +1501,17 @@ define(function (require, exports) { handleGitCommit(lastCommitMessage[ProjectManager.getProjectRoot().fullPath], false, COMMIT_MODE.DEFAULT); }); + // When the panel tab is closed externally (e.g. via the × button), + // update the toolbar icon and menu checked state to stay in sync. + WorkspaceManager.on(WorkspaceManager.EVENT_WORKSPACE_PANEL_HIDDEN, function (event, panelID) { + if (panelID === "main-git.panel" && gitPanel) { + Main.$icon.toggleClass("on", false); + Main.$icon.toggleClass("selected-button", false); + CommandManager.get(Constants.CMD_GIT_TOGGLE_PANEL).setChecked(false); + Preferences.set("panelEnabled", false); + } + }); + exports.init = init; exports.refresh = refresh; exports.toggle = toggle; diff --git a/src/extensions/default/Git/styles/git-styles.less b/src/extensions/default/Git/styles/git-styles.less index 8075032b8b..0cf885384b 100644 --- a/src/extensions/default/Git/styles/git-styles.less +++ b/src/extensions/default/Git/styles/git-styles.less @@ -1013,17 +1013,12 @@ .toolbar { overflow: visible; - .close { - position: absolute; - top: 22px; - margin-top: -10px; - } } .git-more-options-btn { position: absolute; - right: 25px; - top: 8px; - padding: 4px 8px 2px 8px; + right: 8px; + top: 5px; + padding: 4px 9px 2px 8px; opacity: .7; .dark & { opacity: .5; @@ -1070,7 +1065,11 @@ .btn-group { line-height: 1; button { - height: 26px; + height: 22px; + margin-top: 2px; + margin-bottom: 2px; + padding-top: 2px; + padding-bottom: 2px; } } } @@ -1086,7 +1085,7 @@ } .git-right-icons { position:absolute; - right: 55px; + right: 32px; top: 5px; } .octicon:not(:only-child) { diff --git a/src/extensions/default/Git/templates/git-panel.html b/src/extensions/default/Git/templates/git-panel.html index 94f2d4099a..b0133c2c87 100644 --- a/src/extensions/default/Git/templates/git-panel.html +++ b/src/extensions/default/Git/templates/git-panel.html @@ -69,8 +69,7 @@
- × -
+
diff --git a/src/extensionsIntegrated/CustomSnippets/UIHelper.js b/src/extensionsIntegrated/CustomSnippets/UIHelper.js index 545214da0b..8a4f59b6f6 100644 --- a/src/extensionsIntegrated/CustomSnippets/UIHelper.js +++ b/src/extensionsIntegrated/CustomSnippets/UIHelper.js @@ -21,9 +21,11 @@ /* eslint-disable no-invalid-this */ define(function (require, exports, module) { const StringUtils = require("utils/StringUtils"); - const Global = require("./global"); const Strings = require("strings"); + /** @type {Object} Reference to the panel instance, set via init() */ + let _panel; + /** * this is a generic function to show error messages for input fields * @@ -113,7 +115,6 @@ define(function (require, exports, module) { const $backToListMenuBtn = $("#back-to-list-menu-btn"); const $addNewSnippetBtn = $("#add-new-snippet-btn"); const $filterSnippetsPanel = $("#filter-snippets-panel"); - const $toolbarTitle = $(".toolbar-title"); $addSnippetMenu.removeClass("hidden"); $snippetListMenu.addClass("hidden"); @@ -122,7 +123,9 @@ define(function (require, exports, module) { $addNewSnippetBtn.addClass("hidden"); $filterSnippetsPanel.addClass("hidden"); - $toolbarTitle.html(`${Strings.CUSTOM_SNIPPETS_ADD_PANEL_TITLE} `); + if (_panel) { + _panel.setTitle(Strings.CUSTOM_SNIPPETS_ADD_PANEL_TITLE); + } } /** @@ -137,7 +140,6 @@ define(function (require, exports, module) { const $backToListMenuBtn = $("#back-to-list-menu-btn"); const $addNewSnippetBtn = $("#add-new-snippet-btn"); const $filterSnippetsPanel = $("#filter-snippets-panel"); - const $toolbarTitle = $(".toolbar-title"); $addSnippetMenu.addClass("hidden"); $editSnippetMenu.addClass("hidden"); @@ -147,12 +149,9 @@ define(function (require, exports, module) { $addNewSnippetBtn.removeClass("hidden"); $filterSnippetsPanel.removeClass("hidden"); - // add the snippet count in the toolbar (the no. of snippets added) - const snippetCount = Global.SnippetHintsList.length; - const countText = snippetCount > 0 ? `(${snippetCount})` : ""; - $toolbarTitle.html( - `${Strings.CUSTOM_SNIPPETS_PANEL_TITLE} ${countText}` - ); + if (_panel) { + _panel.setTitle(Strings.CUSTOM_SNIPPETS_PANEL_TITLE); + } $("#filter-snippets-input").val(""); } @@ -167,7 +166,6 @@ define(function (require, exports, module) { const $backToListMenuBtn = $("#back-to-list-menu-btn"); const $addNewSnippetBtn = $("#add-new-snippet-btn"); const $filterSnippetsPanel = $("#filter-snippets-panel"); - const $toolbarTitle = $(".toolbar-title"); $editSnippetMenu.removeClass("hidden"); $snippetListMenu.addClass("hidden"); @@ -176,8 +174,9 @@ define(function (require, exports, module) { $addNewSnippetBtn.addClass("hidden"); $filterSnippetsPanel.addClass("hidden"); - // Update toolbar title - $toolbarTitle.html(`${Strings.CUSTOM_SNIPPETS_EDIT_PANEL_TITLE} `); + if (_panel) { + _panel.setTitle(Strings.CUSTOM_SNIPPETS_EDIT_PANEL_TITLE); + } } /** @@ -213,18 +212,24 @@ define(function (require, exports, module) { } /** - * Initializes the toolbar title for the list view - * This is called when the panel is first opened to ensure the snippet count is displayed + * Resets the tab title back to the default list view title. + * Called when the panel is first opened or toggled visible. */ function initializeListViewToolbarTitle() { - const $toolbarTitle = $(".toolbar-title"); - const snippetCount = Global.SnippetHintsList.length; - const countText = snippetCount > 0 ? `(${snippetCount})` : ""; - $toolbarTitle.html( - `${Strings.CUSTOM_SNIPPETS_PANEL_TITLE} ${countText}` - ); + if (_panel) { + _panel.setTitle(Strings.CUSTOM_SNIPPETS_PANEL_TITLE); + } + } + + /** + * Sets the panel reference so UIHelper can update the tab title. + * @param {Object} panel The Panel instance returned by WorkspaceManager.createBottomPanel + */ + function init(panel) { + _panel = panel; } + exports.init = init; exports.showEmptySnippetMessage = showEmptySnippetMessage; exports.showSnippetsList = showSnippetsList; exports.clearSnippetsList = clearSnippetsList; diff --git a/src/extensionsIntegrated/CustomSnippets/helper.js b/src/extensionsIntegrated/CustomSnippets/helper.js index f68dfe6294..43713dcda5 100644 --- a/src/extensionsIntegrated/CustomSnippets/helper.js +++ b/src/extensionsIntegrated/CustomSnippets/helper.js @@ -569,20 +569,6 @@ define(function (require, exports, module) { $("#edit-file-extn-box").val(""); } - /** - * Updates the snippets count which is displayed in the toolbar at the left side - * @private - */ - function updateSnippetsCount() { - const count = Global.SnippetHintsList.length; - const $countSpan = $("#snippets-count"); - if (count > 0) { - $countSpan.text(`(${count})`); - } else { - $countSpan.text(""); - } - } - /** * validates and sanitizes file extension input * @@ -932,7 +918,6 @@ define(function (require, exports, module) { exports.isSnippetSupportedInFile = isSnippetSupportedInFile; exports.hasExactMatchingSnippet = hasExactMatchingSnippet; exports.getMatchingSnippets = getMatchingSnippets; - exports.updateSnippetsCount = updateSnippetsCount; exports.sanitizeFileExtensionInput = sanitizeFileExtensionInput; exports.handleFileExtensionInput = handleFileExtensionInput; exports.handleFileExtensionKeypress = handleFileExtensionKeypress; diff --git a/src/extensionsIntegrated/CustomSnippets/htmlContent/snippets-panel.html b/src/extensionsIntegrated/CustomSnippets/htmlContent/snippets-panel.html index f1b5e3c0e2..b6312b8c4d 100644 --- a/src/extensionsIntegrated/CustomSnippets/htmlContent/snippets-panel.html +++ b/src/extensionsIntegrated/CustomSnippets/htmlContent/snippets-panel.html @@ -1,10 +1,10 @@
- {{Strings.CUSTOM_SNIPPETS_PANEL_TITLE}}
diff --git a/src/extensionsIntegrated/CustomSnippets/main.js b/src/extensionsIntegrated/CustomSnippets/main.js index 5e2b1b3d0f..b66ed5df63 100644 --- a/src/extensionsIntegrated/CustomSnippets/main.js +++ b/src/extensionsIntegrated/CustomSnippets/main.js @@ -58,7 +58,9 @@ define(function (require, exports, module) { * @private */ function _createPanel() { - customSnippetsPanel = WorkspaceManager.createBottomPanel(PANEL_ID, $snippetsPanel, PANEL_MIN_SIZE); + customSnippetsPanel = WorkspaceManager.createBottomPanel(PANEL_ID, $snippetsPanel, PANEL_MIN_SIZE, + Strings.CUSTOM_SNIPPETS_PANEL_TITLE); + UIHelper.init(customSnippetsPanel); customSnippetsPanel.show(); // also register the handlers @@ -129,7 +131,6 @@ define(function (require, exports, module) { * @private */ function _registerHandlers() { - const $closePanelBtn = $("#close-custom-snippets-panel-btn"); const $saveCustomSnippetBtn = $("#save-custom-snippet-btn"); const $cancelCustomSnippetBtn = $("#cancel-custom-snippet-btn"); const $abbrInput = $("#abbr-box"); @@ -161,10 +162,6 @@ define(function (require, exports, module) { SnippetsList.showSnippetsList(); }); - $closePanelBtn.on("click", function () { - _hidePanel(); - }); - $saveCustomSnippetBtn.on("click", function () { Driver.handleSaveBtnClick(); }); @@ -258,6 +255,14 @@ define(function (require, exports, module) { }); } + // When the panel tab is closed externally (e.g. via the × button), + // update the menu checked state to stay in sync. + WorkspaceManager.on(WorkspaceManager.EVENT_WORKSPACE_PANEL_HIDDEN, function (event, panelID) { + if (panelID === PANEL_ID && customSnippetsPanel) { + CommandManager.get(MY_COMMAND_ID).setChecked(false); + } + }); + AppInit.appReady(function () { CommandManager.register(MENU_ITEM_NAME, MY_COMMAND_ID, showCustomSnippetsPanel); // Render template with localized strings diff --git a/src/extensionsIntegrated/CustomSnippets/snippetsList.js b/src/extensionsIntegrated/CustomSnippets/snippetsList.js index 6215842986..de45f3d95f 100644 --- a/src/extensionsIntegrated/CustomSnippets/snippetsList.js +++ b/src/extensionsIntegrated/CustomSnippets/snippetsList.js @@ -180,8 +180,6 @@ define(function (require, exports, module) { console.error("failed to delete custom snippet correctly:", error); }); - // update the snippets count in toolbar - Helper.updateSnippetsCount(); // Refresh the entire list to properly handle filtering showSnippetsList(); } diff --git a/src/extensionsIntegrated/DisplayShortcuts/main.js b/src/extensionsIntegrated/DisplayShortcuts/main.js index 857ddf79ce..0b8f49678c 100644 --- a/src/extensionsIntegrated/DisplayShortcuts/main.js +++ b/src/extensionsIntegrated/DisplayShortcuts/main.js @@ -478,7 +478,8 @@ define(function (require, exports, module) { // AppInit.htmlReady() has already executed before extensions are loaded // so, for now, we need to call this ourself - panel = WorkspaceManager.createBottomPanel(TOGGLE_SHORTCUTS_ID, $(s), 300); + panel = WorkspaceManager.createBottomPanel(TOGGLE_SHORTCUTS_ID, $(s), 300, + Strings.KEYBOARD_SHORTCUT_PANEL_TITLE); panel.hide(); $shortcutsPanel = $("#shortcuts-panel"); @@ -505,10 +506,6 @@ define(function (require, exports, module) { } }); - $shortcutsPanel.find(".close").click(function () { - CommandManager.execute(TOGGLE_SHORTCUTS_ID); - }); - $shortcutsPanel.find(".reset-to-default").click(function () { Dialogs.showConfirmDialog( Strings.KEYBOARD_SHORTCUT_RESET_DIALOG_TITLE, @@ -541,5 +538,15 @@ define(function (require, exports, module) { KeyBindingManager.on(KeyBindingManager.EVENT_KEY_BINDING_REMOVED, _updateKeyBindings); KeyBindingManager.on(KeyBindingManager.EVENT_NEW_PRESET, _updatePresets); KeyBindingManager.on(KeyBindingManager.EVENT_PRESET_CHANGED, _updatePresets); + + // When the panel tab is closed externally (e.g. via the × button), + // update the menu checked state and clean up resources. + WorkspaceManager.on(WorkspaceManager.EVENT_WORKSPACE_PANEL_HIDDEN, function (event, panelID) { + if (panelID === TOGGLE_SHORTCUTS_ID && panel) { + destroyKeyList(); + _clearSortingEventHandlers(); + CommandManager.get(TOGGLE_SHORTCUTS_ID).setChecked(false); + } + }); }); }); diff --git a/src/extensionsIntegrated/DisplayShortcuts/templates/bottom-panel.html b/src/extensionsIntegrated/DisplayShortcuts/templates/bottom-panel.html index 4762ac7851..a9f10e7fda 100644 --- a/src/extensionsIntegrated/DisplayShortcuts/templates/bottom-panel.html +++ b/src/extensionsIntegrated/DisplayShortcuts/templates/bottom-panel.html @@ -1,7 +1,5 @@
- {{KEYBOARD_SHORTCUT_PANEL_TITLE}} - × diff --git a/src/extensionsIntegrated/NoDistractions/main.js b/src/extensionsIntegrated/NoDistractions/main.js index e0da1645eb..55f8d27a94 100644 --- a/src/extensionsIntegrated/NoDistractions/main.js +++ b/src/extensionsIntegrated/NoDistractions/main.js @@ -87,15 +87,22 @@ define(function (require, exports, module) { * hide all open panels */ function _hidePanelsIfRequired() { - var panelIDs = WorkspaceManager.getAllPanelIDs(); _previouslyOpenPanelIDs = []; - panelIDs.forEach(function (panelID) { - var panel = WorkspaceManager.getPanelForID(panelID); + + // Batch-hide all open bottom panel tabs in one pass, avoiding O(n) + // intermediate tab activations and layout recalcs. + let hiddenBottomPanels = WorkspaceManager.hideAllOpenBottomPanels(); + _previouslyOpenPanelIDs = hiddenBottomPanels; + + // Hide any remaining visible panels (e.g. plugin side-panels) + let panelIDs = WorkspaceManager.getAllPanelIDs(); + for (let i = 0; i < panelIDs.length; i++) { + let panel = WorkspaceManager.getPanelForID(panelIDs[i]); if (panel && panel.isVisible()) { panel.hide(); - _previouslyOpenPanelIDs.push(panelID); + _previouslyOpenPanelIDs.push(panelIDs[i]); } - }); + } } /** diff --git a/src/features/FindReferencesManager.js b/src/features/FindReferencesManager.js index 0c71269278..2cb213c871 100644 --- a/src/features/FindReferencesManager.js +++ b/src/features/FindReferencesManager.js @@ -194,7 +194,8 @@ define(function (require, exports, module) { searchModel, "reference-in-files-results", "reference-in-files.results", - "reference" + "reference", + Strings.REFERENCES_PANEL_TITLE ); if(_resultsView) { _resultsView diff --git a/src/htmlContent/problems-panel.html b/src/htmlContent/problems-panel.html index 7ad8e75ddf..34ca3fa58e 100644 --- a/src/htmlContent/problems-panel.html +++ b/src/htmlContent/problems-panel.html @@ -2,7 +2,6 @@
- ×
-
+
diff --git a/src/htmlContent/search-panel.html b/src/htmlContent/search-panel.html index e297478baa..4f81e7af5b 100644 --- a/src/htmlContent/search-panel.html +++ b/src/htmlContent/search-panel.html @@ -1,10 +1,8 @@
- ×
-
- +
diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js index 8cbb15d89f..027cfd1ad9 100644 --- a/src/language/CodeInspection.js +++ b/src/language/CodeInspection.js @@ -111,7 +111,6 @@ define(function (require, exports, module) { * @private */ const PREF_ENABLED = "enabled", - PREF_COLLAPSED = "collapsed", PREF_ASYNC_TIMEOUT = "asyncTimeout", PREF_PREFER_PROVIDERS = "prefer", PREF_PREFERRED_ONLY = "usePreferredOnly"; @@ -120,19 +119,11 @@ define(function (require, exports, module) { /** * When disabled, the errors panel is closed and the status bar icon is grayed out. - * Takes precedence over _collapsed. * @private * @type {boolean} */ var _enabled = false; - /** - * When collapsed, the errors panel is closed but the status bar icon is kept up to date. - * @private - * @type {boolean} - */ - var _collapsed = false; - /** * @private * @type {$.Element} @@ -467,7 +458,7 @@ define(function (require, exports, module) { .addClass(CODE_INSPECTION_GUTTER); $marker.click(function (){ editor.setCursorPos(line, ch); - toggleCollapsed(false); + _showProblemsPanel(); scrollToProblem(line); }); $marker.find('span') @@ -601,7 +592,7 @@ define(function (require, exports, module) { // shouldnt open the problems panel return; } - toggleCollapsed(false); + _showProblemsPanel(); scrollToProblem(pos.line); // todo strobe effect }); @@ -711,21 +702,24 @@ define(function (require, exports, module) { const scrollPositionMap = new Map(); function _noProviderReturnedResults(currentDoc, fullFilePath) { - // No provider for current file + // No provider for current file — update content but never hide the panel. _hasErrors = false; _currentPromise = null; - updatePanelTitleAndStatusBar(0, [], false, - fullFilePath ? path.basename(fullFilePath) : Strings.ERRORS_NO_FILE); - if(problemsPanel){ - problemsPanel.hide(); - } + + let message; const language = currentDoc && LanguageManager.getLanguageForPath(currentDoc.file.fullPath); if (language) { - StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-disabled", - StringUtils.format(Strings.NO_LINT_AVAILABLE, language.getName())); + message = StringUtils.format(Strings.NO_LINT_AVAILABLE, language.getName()); } else { - StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-disabled", Strings.NOTHING_TO_LINT); + message = Strings.NOTHING_TO_LINT; } + + // Update panel content to show the "no linter" message + $problemsPanel.find(".title").text(message); + $problemsPanelTable.empty(); + $fixAllBtn.addClass("forced-hidden"); + + StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-disabled", message); setGotoEnabled(false); } @@ -742,7 +736,11 @@ define(function (require, exports, module) { if (!_enabled) { _hasErrors = false; _currentPromise = null; - problemsPanel.hide(); + // Update status bar to show linting is disabled, but do NOT hide the panel. + // The panel content will be cleared and the status bar updated. + $problemsPanel.find(".title").text(Strings.LINT_DISABLED); + $problemsPanelTable.empty(); + $fixAllBtn.addClass("forced-hidden"); StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-disabled", Strings.LINT_DISABLED); setGotoEnabled(false); return; @@ -794,13 +792,16 @@ define(function (require, exports, module) { _hasErrors = Boolean(errors); if (!errors) { - problemsPanel.hide(); - + // No errors found — update panel content but never hide the panel. var message = Strings.NO_ERRORS_MULTIPLE_PROVIDER; if (providerList.length === 1) { message = StringUtils.format(Strings.NO_ERRORS, providerList[0].name); } + $problemsPanel.find(".title").text(message); + $problemsPanelTable.empty(); + $fixAllBtn.addClass("forced-hidden"); + StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-valid", message); setGotoEnabled(false); @@ -859,10 +860,6 @@ define(function (require, exports, module) { .empty() .append(html); // otherwise scroll pos from previous contents is remembered - if (!_collapsed) { - problemsPanel.show(); - } - updatePanelTitleAndStatusBar(numProblems, providersReportingProblems, aborted, path.basename(fullFilePath)); setGotoEnabled(true); @@ -1038,7 +1035,14 @@ define(function (require, exports, module) { } function toggleProblems() { - toggleCollapsed(); + if (!problemsPanel) { + return; + } + if (problemsPanel.isVisible()) { + problemsPanel.hide(); + } else { + problemsPanel.show(); + } } let lastRunTime; @@ -1060,34 +1064,14 @@ define(function (require, exports, module) { }); /** - * Toggle the collapsed state for the panel. This explicitly collapses the panel (as opposed to - * the auto collapse due to files with no errors & filetypes with no provider). When explicitly - * collapsed, the panel will not reopen automatically on switch files or save. + * Show the problems panel. Used by gutter marker clicks and QuickView + * clicks to ensure the panel is visible when the user interacts with + * an error indicator. * @private - * @param {?boolean} collapsed Collapsed state. If omitted, the state is toggled. - * @param {?boolean} doNotSave true if the preference should not be saved to user settings. This is generally for events triggered by project-level settings. */ - function toggleCollapsed(collapsed, doNotSave) { - if (collapsed === undefined) { - collapsed = !_collapsed; - } - - if (collapsed === _collapsed) { - return; - } - - _collapsed = collapsed; - if (!doNotSave) { - prefs.set(PREF_COLLAPSED, _collapsed); - prefs.save(); - } - - if (_collapsed) { - problemsPanel.hide(); - } else { - if (_hasErrors) { - problemsPanel.show(); - } + function _showProblemsPanel() { + if (problemsPanel && !problemsPanel.isVisible()) { + problemsPanel.show(); } } @@ -1183,13 +1167,6 @@ define(function (require, exports, module) { toggleEnabled(prefs.get(PREF_ENABLED), true); }); - prefs.definePreference(PREF_COLLAPSED, "boolean", false, { - description: Strings.DESCRIPTION_LINTING_COLLAPSED - }) - .on("change", function (e, data) { - toggleCollapsed(prefs.get(PREF_COLLAPSED), true); - }); - prefs.definePreference(PREF_ASYNC_TIMEOUT, "number", 10000, { description: Strings.DESCRIPTION_ASYNC_TIMEOUT }); @@ -1263,7 +1240,7 @@ define(function (require, exports, module) { Editor.registerGutter(CODE_INSPECTION_GUTTER, CODE_INSPECTION_GUTTER_PRIORITY); // Create bottom panel to list error details var panelHtml = Mustache.render(PanelTemplate, Strings); - problemsPanel = WorkspaceManager.createBottomPanel("errors", $(panelHtml), 100); + problemsPanel = WorkspaceManager.createBottomPanel("errors", $(panelHtml), 100, Strings.CMD_VIEW_TOGGLE_PROBLEMS); $problemsPanel = $("#problems-panel"); $fixAllBtn = $problemsPanel.find(".problems-fix-all-btn"); $fixAllBtn.click(()=>{ @@ -1354,25 +1331,17 @@ define(function (require, exports, module) { } }); - $("#problems-panel .close").click(function () { - toggleCollapsed(true); - MainViewManager.focusActivePane(); - }); - // Status bar indicator - icon & tooltip updated by run() var statusIconHtml = Mustache.render("
 
", Strings); StatusBar.addIndicator(INDICATOR_ID, $(statusIconHtml), true, "", "", "status-indent"); $("#status-inspection").click(function () { - // Clicking indicator toggles error panel, if any errors in current file - if (_hasErrors) { - toggleCollapsed(); - } + // Clicking indicator always toggles the problems panel + toggleProblems(); }); // Set initial UI state toggleEnabled(prefs.get(PREF_ENABLED), true); - toggleCollapsed(prefs.get(PREF_COLLAPSED), true); QuickViewManager.registerQuickViewProvider({ getQuickView, diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index f6b9db8f8d..4ee48d7c63 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1249,6 +1249,15 @@ define({ "REFERENCES_IN_FILES": "references", "REFERENCE_IN_FILES": "reference", "REFERENCES_NO_RESULTS": "No References available for current cursor position", + "REFERENCES_PANEL_TITLE": "References", + "SEARCH_RESULTS_PANEL_TITLE": "Search Results", + "BOTTOM_PANEL_HIDE": "Hide Panel", + "BOTTOM_PANEL_SHOW": "Show Bottom Panel", + "BOTTOM_PANEL_HIDE_TOGGLE": "Hide Bottom Panel", + "BOTTOM_PANEL_DEFAULT_TITLE": "Quick Access", + "BOTTOM_PANEL_DEFAULT_HEADING": "Open a Panel", + "BOTTOM_PANEL_MAXIMIZE": "Maximize Panel", + "BOTTOM_PANEL_RESTORE": "Restore Panel Size", "CMD_FIND_DOCUMENT_SYMBOLS": "Find Document Symbols", "CMD_FIND_PROJECT_SYMBOLS": "Find Project Symbols", @@ -1396,6 +1405,7 @@ define({ "BUTTON_CANCEL": "Cancel", "CHECKOUT_COMMIT": "Checkout", "CHECKOUT_COMMIT_DETAIL": "Commit Message: {0}
Commit hash: {1}", + "GIT_PANEL_TITLE": "Git", "GIT_CLONE": "Clone", "BUTTON_CLOSE": "Close", "BUTTON_COMMIT": "Commit", @@ -1696,6 +1706,7 @@ define({ "CUSTOM_SNIPPETS_ADD_PANEL_TITLE": "Add Snippet", "CUSTOM_SNIPPETS_EDIT_PANEL_TITLE": "Edit Snippet", "CUSTOM_SNIPPETS_ADD_NEW_TITLE": "Add new snippet", + "CUSTOM_SNIPPETS_ADD_BTN_LABEL": "Add", "CUSTOM_SNIPPETS_BACK_TO_LIST_TITLE": "Back to snippets list", "CUSTOM_SNIPPETS_BACK": "Back", "CUSTOM_SNIPPETS_FILTER_PLACEHOLDER": "Filter...", diff --git a/src/search/FindInFilesUI.js b/src/search/FindInFilesUI.js index 6a02cd7e19..8ce87f25a4 100644 --- a/src/search/FindInFilesUI.js +++ b/src/search/FindInFilesUI.js @@ -111,7 +111,7 @@ define(function (require, exports, module) { } } else { - _resultsView.close(); + _resultsView.showNoResults(); if (_findBar) { var showMessage = false; @@ -536,7 +536,8 @@ define(function (require, exports, module) { // Initialize items dependent on HTML DOM AppInit.htmlReady(function () { var model = FindInFiles.searchModel; - _resultsView = new SearchResultsView(model, "find-in-files-results", "find-in-files.results"); + _resultsView = new SearchResultsView(model, "find-in-files-results", "find-in-files.results", + undefined, Strings.SEARCH_RESULTS_PANEL_TITLE); _resultsView .on("replaceBatch", function () { _finishReplaceBatch(model); diff --git a/src/search/SearchResultsView.js b/src/search/SearchResultsView.js index 54751a67e3..e79a86116b 100644 --- a/src/search/SearchResultsView.js +++ b/src/search/SearchResultsView.js @@ -76,12 +76,13 @@ define(function (require, exports, module) { * @param {string} panelID The CSS ID to use for the panel. * @param {string} panelName The name to use for the panel, as passed to WorkspaceManager.createBottomPanel(). * @param {string} type type to identify if it is reference search or string match serach + * @param {string=} title Display title for the panel tab. */ - function SearchResultsView(model, panelID, panelName, type) { + function SearchResultsView(model, panelID, panelName, type, title) { const self = this; let panelHtml = Mustache.render(searchPanelTemplate, {panelID: panelID}); - this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100); + this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100, title); this._$summary = this._panel.$panel.find(".title"); this._$table = this._panel.$panel.find(".table-container"); this._$previewEditor = this._panel.$panel.find(".search-editor-preview"); @@ -96,12 +97,10 @@ define(function (require, exports, module) { }).observe(this._panel.$panel[0]); function _showPanelIfResultsAvailable(_e, shownPanelID) { - if(self._model.numMatches === 0){ - self._panel.hide(); - } - if(shownPanelID === self._panel.panelID && !self._model.isReplace){ - // If it is replace, _handleModelChange will close the find bar as we dont - // do replace if there is a model change. So we wont enter this flow if it is a replace operation + if (shownPanelID === self._panel.panelID && self._model.numMatches > 0 && !self._model.isReplace) { + // Refresh results when the tab is re-activated (they may have changed + // while the panel was in a background tab). Skip when numMatches is 0 + // so the "no results" state isn't disturbed. self._handleModelChange(); } } @@ -328,12 +327,6 @@ define(function (require, exports, module) { var self = this; this._panel.$panel .off(".searchResults") // Remove the old events - .on("dblclick.searchResults", ".toolbar", function() { - self._panel.hide(); - }) - .on("click.searchResults", ".close", function () { - self._panel.hide(); - }) // The link to go the first page .on("click.searchResults", ".first-page:not(.disabled)", function () { self._currentStart = 0; @@ -530,18 +523,18 @@ define(function (require, exports, module) { let self = this; let count = self._model.countFilesMatches(), lastIndex = self._getLastIndex(count.matches), - typeStr = (count.matches > 1) ? Strings.FIND_IN_FILES_MATCHES : Strings.FIND_IN_FILES_MATCH, + typeStr = (count.matches !== 1) ? Strings.FIND_IN_FILES_MATCHES : Strings.FIND_IN_FILES_MATCH, filesStr, summary; if(this._searchResultsType === "reference") { - typeStr = (count.matches > 1) ? Strings.REFERENCES_IN_FILES : Strings.REFERENCE_IN_FILES; + typeStr = (count.matches !== 1) ? Strings.REFERENCES_IN_FILES : Strings.REFERENCE_IN_FILES; } filesStr = StringUtils.format( Strings.FIND_NUM_FILES, count.files, - (count.files > 1 ? Strings.FIND_IN_FILES_FILES : Strings.FIND_IN_FILES_FILE) + (count.files !== 1 ? Strings.FIND_IN_FILES_FILES : Strings.FIND_IN_FILES_FILE) ); // This text contains some formatting, so all the strings are assumed to be already escaped @@ -576,8 +569,17 @@ define(function (require, exports, module) { * Shows the current set of results. */ SearchResultsView.prototype._render = function () { + let count = this._model.countFilesMatches(); + if (count.matches === 0 && this._model.queryInfo) { + // Only redirect to showNoResults() when the model has a valid query + // (i.e. this is a real "no results" state, not a transient clear). + this.showNoResults(); + return; + } + + this._panel.$panel.removeClass("search-no-results"); + let searchItems, match, i, item, multiLine, - count = this._model.countFilesMatches(), searchFiles = this._model.prioritizeOpenFile(this._initialFilePath), lastIndex = this._getLastIndex(count.matches), matchesCounter = 0, @@ -823,12 +825,43 @@ define(function (require, exports, module) { this._model.on("change.SearchResultsView", this._handleModelChange.bind(this)); }; + /** + * Opens the panel and displays a "no results" message instead of closing it. + * Keeps the tab visible so the user gets clear feedback without jarring tab switches. + * @param {string=} message Optional message to display. Defaults to Strings.FIND_NO_RESULTS. + */ + SearchResultsView.prototype.showNoResults = function (message) { + this._currentStart = 0; + this._$selectedRow = null; + this._allChecked = false; + + if (this._timeoutID) { + window.clearTimeout(this._timeoutID); + this._timeoutID = null; + } + + this._$table.empty(); + this._closePreviewEditor(); + + this._panel.$panel.addClass("search-no-results"); + this._showSummary(); + this._$table.append( + $('
').text(message || Strings.FIND_NO_RESULTS) + ); + + this._panel.$panel.off(".searchResults"); + this._model.off("change.SearchResultsView"); + + this._panel.show(); + }; + /** * Hides the Search Results Panel and unregisters listeners. */ SearchResultsView.prototype.close = function () { if (this._panel && this._panel.isVisible()) { this._$table.empty(); + this._panel.$panel.removeClass("search-no-results"); this._panel.hide(); this._panel.$panel.off(".searchResults"); this._model.off("change.SearchResultsView"); diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less new file mode 100644 index 0000000000..03d38b9ab3 --- /dev/null +++ b/src/styles/Extn-BottomPanelTabs.less @@ -0,0 +1,317 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/* Bottom panel tab bar — switches between tabbed bottom panels. + * Visual style mirrors the file tab bar (Extn-TabBar.less) for consistency. */ + +#bottom-panel-container { + background-color: @bc-panel-bg; + border-top: 1px solid @bc-panel-border; + display: flex; + flex-direction: column; + + .dark & { + background-color: @dark-bc-panel-bg; + border-top: 1px solid @dark-bc-panel-border; + } + + .bottom-panel { + display: none !important; + flex: 1; + min-height: 0; + border-top: none; + height: auto !important; + + &.active-bottom-panel { + display: flex !important; + flex-direction: column; + } + + .toolbar { + box-shadow: none; + } + + .resizable-content { + flex: 1; + min-height: 0; + height: auto !important; + } + } +} + +#bottom-panel-tab-bar { + display: flex; + align-items: center; + height: 2rem; + min-height: 2rem; + background-color: #f5f5f5; + border-bottom: 1px solid #d6d6d6; + overflow: hidden; + user-select: none; + + .dark & { + background-color: #222222; + border-bottom: 1px solid #333; + } +} + +.bottom-panel-tabs-overflow { + flex: 1; + display: flex; + overflow-x: auto; + overflow-y: hidden; + height: 100%; + + /* Hide scrollbar but allow scrolling */ + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; + scrollbar-width: none; +} + +.bottom-panel-tab { + display: inline-flex; + align-items: center; + padding: 0 0.4rem 0 0.8rem; + height: 100%; + cursor: pointer; + position: relative; + flex: 0 0 auto; + min-width: fit-content; + color: #555; + background-color: #f1f1f1; + border-right: 1px solid rgba(0, 0, 0, 0.05); + font-size: 1rem; + font-weight: 500; + letter-spacing: 0.5px; + white-space: nowrap; + transition: color 0.12s ease-out, background-color 0.12s ease-out; + + .dark & { + color: #aaa; + background-color: #292929; + border-right: 1px solid rgba(255, 255, 255, 0.05); + } + + &:hover { + background-color: #e0e0e0; + + .dark & { + background-color: #3b3a3a; + } + } + + &.active { + color: #333; + background-color: #fff; + + .dark & { + color: #dedede; + background-color: #1D1F21; + } + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 0.1rem; + background-color: #0078D7; + + .dark & { + background-color: #75BEFF; + } + } + } +} + +.bottom-panel-tab-title { + pointer-events: none; +} + +.bottom-panel-tab-close-btn { + margin-left: 0.55rem; + border-radius: 3px; + cursor: pointer; + color: #999; + font-size: 1.25rem; + font-weight: 500; + padding: 0 4px; + line-height: 1; + opacity: 0; + transition: opacity 0.12s ease, color 0.12s ease, background-color 0.12s ease; + + .dark & { + color: #666; + } + + .bottom-panel-tab:hover & { + opacity: 1; + color: #666; + + .dark & { + color: #888; + } + } + + .bottom-panel-tab.active & { + opacity: 1; + color: #666; + + .dark & { + color: #888; + } + } + + &:hover { + opacity: 1; + color: #333; + background-color: rgba(0, 0, 0, 0.1); + + .dark & { + color: #fff; + background-color: rgba(255, 255, 255, 0.12); + } + } +} + +.bottom-panel-tab-bar-actions { + display: flex; + align-items: center; + height: 100%; + margin-left: auto; + margin-right: 6px; + gap: 6px; + flex: 0 0 auto; +} + +.bottom-panel-maximize-btn, +.bottom-panel-hide-btn { + display: flex; + align-items: center; + justify-content: center; + width: 1.9rem; + height: 2rem; + cursor: pointer; + color: #666; + font-size: 0.88rem; + transition: color 0.12s ease, background-color 0.12s ease; + + .dark & { + color: #aaa; + } + + &:hover { + background-color: #e0e0e0; + color: #333; + + .dark & { + background-color: #333; + color: #eee; + } + } +} + +.default-panel-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 16px; + gap: 12px; + user-select: none; +} + +.default-panel-heading { + font-size: 14px; + letter-spacing: 0.6px; + word-spacing: 1px; + font-weight: 500; + color: #555; + margin-bottom: 4px; + + .dark & { + color: #bbb; + } +} + +.default-panel-buttons { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; +} + +.default-panel-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + width: 120px; + height: 72px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + background: rgba(0, 0, 0, 0.03); + color: #444; + cursor: pointer; + transition: background-color 0.15s, border-color 0.15s; + + i { + font-size: 20px; + } + + .default-panel-btn-label { + font-size: 11px; + text-align: center; + line-height: 1.2; + max-width: 110px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &:hover { + background: rgba(0, 0, 0, 0.07); + border-color: rgba(0, 0, 0, 0.25); + } + + &:active { + background: rgba(0, 0, 0, 0.12); + } + + .dark & { + border-color: rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.05); + color: #ccc; + + &:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.3); + } + + &:active { + background: rgba(255, 255, 255, 0.15); + } + } +} diff --git a/src/styles/Extn-CustomSnippets.less b/src/styles/Extn-CustomSnippets.less index 57e6b32966..78aadd400e 100644 --- a/src/styles/Extn-CustomSnippets.less +++ b/src/styles/Extn-CustomSnippets.less @@ -27,42 +27,30 @@ align-items: center; gap: 10px; flex-wrap: wrap; + margin-left: 4px; } .buttons { gap: 4px; flex-wrap: wrap; align-items: center; + margin: 0; } } -.toolbar-title { - color: @bc-text; - font-size: 15px; - font-weight: 500; - - .dark & { - color: @dark-bc-text; - } -} - -.snippets-count { - color: @bc-text; - font-size: 15px; - font-weight: 500; - - .dark & { - color: @dark-bc-text; - } -} .custom-snippet-btn button { - height: 26px; + height: 21px; border-radius: 4px; background-color: @bc-btn-bg; color: @bc-text; border: 1px solid @bc-btn-border; box-shadow: inset 0 1px @bc-highlight; + display: flex; + align-items: center; + gap: 4px; + padding: 0 8px; + font-size: 12px; .dark & { background-color: @dark-bc-btn-bg; @@ -72,11 +60,6 @@ } } -.custom-snippet-btn button { - display: flex; - align-items: center; -} - .custom-snippet-btn .back-btn-left-icon { position: relative; top: 0.6px; @@ -93,14 +76,6 @@ top: 0.6px; } -#custom-snippets-panel .close { - padding-top: 2.5px; -} - -#custom-snippets-panel .close:hover { - cursor: pointer; -} - .filter-snippets-panel { display: inline-block; } @@ -113,8 +88,11 @@ height: 14px; min-width: 120px; margin-bottom: 0; - margin-top: -4px; - margin-right: 30px; + margin-right: 4px; +} + +#custom-snippets-list { + height: 100%; } #custom-snippets-list.hidden { diff --git a/src/styles/Extn-DisplayShortcuts.less b/src/styles/Extn-DisplayShortcuts.less index 77765ffbf0..a326162b06 100644 --- a/src/styles/Extn-DisplayShortcuts.less +++ b/src/styles/Extn-DisplayShortcuts.less @@ -106,15 +106,15 @@ } #shortcuts-panel .toolbar { - display: block; - padding-right: 28px; + display: flex; + justify-content: flex-end; + padding-right: 4px; } #shortcuts-panel .toolbar button.reset-to-default, #shortcuts-panel .toolbar button.presetPicker, #shortcuts-panel .toolbar .filter { - float: right; - margin: 4px 10px; + margin: 1px 8px 1px 6px; padding-top: 2px; padding-bottom: 2px; } diff --git a/src/styles/brackets.less b/src/styles/brackets.less index a0f0c46faa..72fbe70317 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -47,6 +47,7 @@ @import "Extn-CustomSnippets.less"; @import "Extn-CollapseFolders.less"; @import "Extn-SidebarTabs.less"; +@import "Extn-BottomPanelTabs.less"; @import "Extn-AIChatPanel.less"; @import "UserProfile.less"; @import "phoenix-pro.less"; @@ -647,6 +648,14 @@ a, img { animation: brightenFade 2s ease-in-out; } +#status-panel-toggle { + cursor: pointer; +} + +#status-panel-toggle.flash { + animation: brightenFade 800ms ease-in-out; +} + @keyframes brightenFade { 0% { background-color: transparent; @@ -1899,11 +1908,14 @@ a, img { /* Find in Files results panel - temporary UI, to be replaced with a richer search feature later */ +.search-results .toolbar { + padding: 5px 8px; +} .search-results .title { .sane-box-model; - padding-right: 20px; + margin-left: 4px; width: 100%; - line-height: 25px; + line-height: 26px; .flex-box; .contracting-col { @@ -1969,6 +1981,31 @@ a, img { } } +.search-results.search-no-results { + .table-container { + width: 100% !important; + } + .search-editor-preview { + display: none !important; + } +} + +.search-no-results-message { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: #888; + font-size: 15px; + letter-spacing: 0.5px; + word-spacing: 1px; + user-select: none; + + .dark & { + color: #999; + } +} + .search-results .disclosure-triangle, #problems-panel .disclosure-triangle { .expand-collapse-triangle(); @@ -2365,6 +2402,7 @@ a, img { .search-input-container { display: inline-flex; + vertical-align: top; } .filter-container{ margin-left: -11px; @@ -3032,6 +3070,12 @@ textarea.exclusions-editor { #problems-panel { .user-select(text); // allow selecting error messages for easy web searching + .toolbar { + padding: 9px 8px; + } + .title { + margin-left: 4px; + } .line { text-align: right; // make line number line up with editor line numbers } diff --git a/src/view/DefaultPanelView.js b/src/view/DefaultPanelView.js new file mode 100644 index 0000000000..353fc5b105 --- /dev/null +++ b/src/view/DefaultPanelView.js @@ -0,0 +1,186 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/** + * DefaultPanelView - A launcher panel shown in the bottom panel area when no + * other panels are open. Provides quick-access buttons for common panels + * (Problems, Find in Files, Git, Custom Snippets, Keyboard Shortcuts) and a + * link to the documentation. + * + * @module view/DefaultPanelView + */ +define(function (require, exports, module) { + + const AppInit = require("utils/AppInit"), + Commands = require("command/Commands"), + CommandManager = require("command/CommandManager"), + Strings = require("strings"), + WorkspaceManager = require("view/WorkspaceManager"), + PanelView = require("view/PanelView"); + + /** + * Descriptors for each launcher button. + */ + const _panelButtons = [ + { + id: "problems", + icon: "fa-solid fa-triangle-exclamation", + label: Strings.CMD_VIEW_TOGGLE_PROBLEMS || "Problems", + commandID: Commands.VIEW_TOGGLE_PROBLEMS + }, + { + id: "search", + icon: "fa-solid fa-magnifying-glass", + label: Strings.CMD_FIND_IN_FILES || "Find in Files", + commandID: Commands.CMD_FIND_IN_FILES + }, + { + id: "git", + icon: "fa-solid fa-code-branch", + label: Strings.GIT_PANEL_TITLE || "Git", + commandID: Commands.CMD_GIT_TOGGLE_PANEL + }, + { + id: "snippets", + icon: "fa-solid fa-code", + label: Strings.CUSTOM_SNIPPETS_PANEL_TITLE || "Custom Snippets", + commandID: Commands.CMD_CUSTOM_SNIPPETS_PANEL + }, + { + id: "shortcuts", + icon: "fa-solid fa-keyboard", + label: Strings.KEYBOARD_SHORTCUT_PANEL_TITLE || "Keyboard Shortcuts", + commandID: Commands.HELP_TOGGLE_SHORTCUTS_PANEL + } + ]; + + /** @type {Panel} The default panel instance */ + let _panel; + + /** @type {jQueryObject} The panel DOM element */ + let _$panel; + + /** + * Build the panel DOM. + * @return {jQueryObject} + * @private + */ + function _buildPanelHTML() { + let $panel = $('
'); + let $content = $('
'); + let $heading = $('
') + .text(Strings.BOTTOM_PANEL_DEFAULT_HEADING); + $content.append($heading); + + let $buttonsRow = $('
'); + + _panelButtons.forEach(function (btn) { + let $button = $('') + .attr("data-command", btn.commandID) + .attr("data-btn-id", btn.id) + .attr("title", btn.label); + let $icon = $('').addClass(btn.icon); + let $label = $('').text(btn.label); + $button.append($icon).append($label); + $buttonsRow.append($button); + }); + + $content.append($buttonsRow); + + $panel.append($content); + return $panel; + } + + /** + * Check whether Git is available for the current project. + * The Git extension hides its toolbar icon with the "forced-hidden" class + * when Git is not available (no binary, not a repo, extension disabled, etc.). + * @return {boolean} + * @private + */ + function _isGitAvailable() { + const $gitIcon = $("#git-toolbar-icon"); + return $gitIcon.length > 0 && !$gitIcon.hasClass("forced-hidden"); + } + + /** + * Show or hide buttons based on current state. + * The Problems button is always shown since the panel now displays + * meaningful content regardless of error state. + * @private + */ + function _updateButtonVisibility() { + if (!_$panel) { + return; + } + _$panel.find('.default-panel-btn[data-btn-id="git"]').toggle(_isGitAvailable()); + } + + /** + * Set up MutationObservers on the Git toolbar icon so that + * button visibility updates live. + * @private + */ + function _observeStateChanges() { + // Watch Git toolbar icon for class changes (forced-hidden added/removed) + const gitIcon = document.getElementById("git-toolbar-icon"); + if (gitIcon) { + const gitObserver = new MutationObserver(_updateButtonVisibility); + gitObserver.observe(gitIcon, {attributes: true, attributeFilter: ["class"]}); + } + } + + /** + * Initialise the default panel. Called once at appReady. + * @private + */ + function _init() { + _$panel = _buildPanelHTML(); + _panel = WorkspaceManager.createBottomPanel( + WorkspaceManager.DEFAULT_PANEL_ID, + _$panel, + undefined, + Strings.BOTTOM_PANEL_DEFAULT_TITLE + ); + + // Button click handler: execute the command to open the target panel. + // The auto-hide listener (EVENT_PANEL_SHOWN) will close the default panel. + _$panel.on("click", ".default-panel-btn", function () { + let commandID = $(this).attr("data-command"); + if (commandID) { + CommandManager.execute(commandID); + } + }); + + // Auto-hide when any other panel is shown. + // hide() is a no-op if the panel is already closed, so no guard needed. + PanelView.on(PanelView.EVENT_PANEL_SHOWN, function (event, panelID) { + if (panelID !== WorkspaceManager.DEFAULT_PANEL_ID) { + _panel.hide(); + } else { + _updateButtonVisibility(); + } + }); + + // Initial visibility update and set up live observers + _updateButtonVisibility(); + _observeStateChanges(); + } + + AppInit.appReady(_init); +}); diff --git a/src/view/PanelView.js b/src/view/PanelView.js index d3c6e266fa..d6bb60b606 100644 --- a/src/view/PanelView.js +++ b/src/view/PanelView.js @@ -26,8 +26,9 @@ define(function (require, exports, module) { const EventDispatcher = require("utils/EventDispatcher"), - Resizer = require("utils/Resizer"); - + Resizer = require("utils/Resizer"), + Strings = require("strings"); + /** * Event when panel is hidden * @type {string} @@ -49,15 +50,200 @@ define(function (require, exports, module) { */ const PANEL_TYPE_BOTTOM_PANEL = 'bottomPanel'; + // --- Module-level tab state --- + + /** @type {Object.} Maps panel ID to Panel instance */ + let _panelMap = {}; + + /** @type {jQueryObject} The single container wrapping all bottom panels */ + let _$container; + + /** @type {jQueryObject} The tab bar inside the container */ + let _$tabBar; + + /** @type {jQueryObject} Scrollable area holding the tab elements */ + let _$tabsOverflow; + + /** @type {string[]} Ordered list of currently open (tabbed) panel IDs */ + let _openIds = []; + + /** @type {string|null} The panel ID of the currently visible (active) tab */ + let _activeId = null; + + /** @type {boolean} Whether the bottom panel is currently maximized */ + let _isMaximized = false; + + /** + * Pixel threshold for detecting near-maximize state during resize. + * If the editor holder height is within this many pixels of zero, the + * panel is treated as maximized. Keeps the maximize icon responsive + * during drag without being overly sensitive. + * @const + * @type {number} + */ + const MAXIMIZE_THRESHOLD = 2; + + /** + * Minimum panel height (matches Resizer minSize) used as a floor + * when computing a sensible restore height. + * @const + * @type {number} + */ + const MIN_PANEL_HEIGHT = 200; + + /** @type {number|null} The panel height before maximize, for restore */ + let _preMaximizeHeight = null; + + /** @type {jQueryObject} The editor holder element, passed from WorkspaceManager */ + let _$editorHolder = null; + + /** @type {function} recomputeLayout callback from WorkspaceManager */ + let _recomputeLayout = null; + + // --- Tab helper functions --- + + /** + * Resolve the display title for a bottom panel tab. + * Uses the explicit title if provided, then checks for a .toolbar .title + * DOM element in the panel, and finally derives a name from the panel id. + * @param {string} id The panel registration ID + * @param {jQueryObject} $panel The panel's jQuery element + * @param {string=} title Explicit title passed to createBottomPanel + * @return {string} + * @private + */ + function _getPanelTitle(id, $panel, title) { + if (title) { + return title; + } + let $titleEl = $panel.find(".toolbar .title"); + if ($titleEl.length && $.trim($titleEl.text())) { + return $.trim($titleEl.text()); + } + let label = id.replace(new RegExp("[-_.]", "g"), " ").split(" ")[0]; + return label.charAt(0).toUpperCase() + label.slice(1); + } + + /** + * Full rebuild of the tab bar DOM from _openIds. + * Call this when tabs are added, removed, or renamed. + * @private + */ + function _updateBottomPanelTabBar() { + if (!_$tabsOverflow) { + return; + } + _$tabsOverflow.empty(); + + _openIds.forEach(function (panelId) { + let panel = _panelMap[panelId]; + if (!panel) { + return; + } + let title = panel._tabTitle || _getPanelTitle(panelId, panel.$panel); + let isActive = (panelId === _activeId); + let $tab = $('
') + .toggleClass('active', isActive) + .attr('data-panel-id', panelId); + $tab.append($('').text(title)); + $tab.append($('×').attr('title', Strings.CLOSE)); + _$tabsOverflow.append($tab); + }); + } + + /** + * Swap the .active class on the tab bar without rebuilding the DOM. + * @private + */ + function _updateActiveTabHighlight() { + if (!_$tabBar) { + return; + } + _$tabBar.find(".bottom-panel-tab").each(function () { + let $tab = $(this); + if ($tab.data("panel-id") === _activeId) { + $tab.addClass("active"); + } else { + $tab.removeClass("active"); + } + }); + } + + /** + * Append a single tab to the tab bar for the given panel. + * Use instead of _updateBottomPanelTabBar() when adding one tab. + * @param {string} panelId + * @private + */ + function _addTabToBar(panelId) { + if (!_$tabsOverflow) { + return; + } + let panel = _panelMap[panelId]; + if (!panel) { + return; + } + let title = panel._tabTitle || _getPanelTitle(panelId, panel.$panel); + let isActive = (panelId === _activeId); + let $tab = $('
') + .toggleClass('active', isActive) + .attr('data-panel-id', panelId); + $tab.append($('').text(title)); + $tab.append($('×').attr('title', Strings.CLOSE)); + _$tabsOverflow.append($tab); + } + + /** + * Remove a single tab from the tab bar by panel ID. + * Use instead of _updateBottomPanelTabBar() when removing one tab. + * @param {string} panelId + * @private + */ + function _removeTabFromBar(panelId) { + if (!_$tabsOverflow) { + return; + } + _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + panelId + '"]').remove(); + } + + /** + * Switch the active tab to the given panel. Does not show/hide the container. + * @param {string} panelId + * @private + */ + function _switchToTab(panelId) { + if (_activeId === panelId) { + return; + } + // Remove active class from current + if (_activeId) { + let prevPanel = _panelMap[_activeId]; + if (prevPanel) { + prevPanel.$panel.removeClass("active-bottom-panel"); + } + } + // Set new active + _activeId = panelId; + let newPanel = _panelMap[panelId]; + if (newPanel) { + newPanel.$panel.addClass("active-bottom-panel"); + } + _updateActiveTabHighlight(); + } + /** * Represents a panel below the editor area (a child of ".content"). * @constructor * @param {!jQueryObject} $panel The entire panel, including any chrome, already in the DOM. + * @param {string} id Unique panel identifier. + * @param {string=} title Optional display title for the tab bar. */ - function Panel($panel, id) { + function Panel($panel, id, title) { this.$panel = $panel; this.panelID = id; + this._tabTitle = _getPanelTitle(id, $panel, title); + _panelMap[id] = this; } /** @@ -71,7 +257,7 @@ define(function (require, exports, module) { * @return {boolean} true if visible, false if not */ Panel.prototype.isVisible = function () { - return this.$panel.is(":visible"); + return (_activeId === this.panelID) && _$container && _$container.is(":visible"); }; /** @@ -104,19 +290,83 @@ define(function (require, exports, module) { * Shows the panel */ Panel.prototype.show = function () { - if(!this.isVisible() && this.canBeShown()){ - Resizer.show(this.$panel[0]); - exports.trigger(EVENT_PANEL_SHOWN, this.panelID); + if (!this.canBeShown() || !_$container) { + return; + } + let panelId = this.panelID; + let isOpen = _openIds.indexOf(panelId) !== -1; + let isActive = (_activeId === panelId); + + if (isOpen && isActive) { + // Already open and active — just ensure container is visible + if (!_$container.is(":visible")) { + Resizer.show(_$container[0]); + exports.trigger(EVENT_PANEL_SHOWN, panelId); + } + return; } + if (isOpen && !isActive) { + // Open but not active - switch tab and ensure container is visible + _switchToTab(panelId); + if (!_$container.is(":visible")) { + Resizer.show(_$container[0]); + } + exports.trigger(EVENT_PANEL_SHOWN, panelId); + return; + } + // Not open: add to open set + _openIds.push(panelId); + + // Show container if it was hidden + if (!_$container.is(":visible")) { + Resizer.show(_$container[0]); + } + + _switchToTab(panelId); + _addTabToBar(panelId); + exports.trigger(EVENT_PANEL_SHOWN, panelId); }; /** * Hides the panel */ Panel.prototype.hide = function () { - if(this.isVisible()){ - Resizer.hide(this.$panel[0]); - exports.trigger(EVENT_PANEL_HIDDEN, this.panelID); + let panelId = this.panelID; + let idx = _openIds.indexOf(panelId); + if (idx === -1) { + // Not open - no-op + return; + } + + // Remove from open set + _openIds.splice(idx, 1); + this.$panel.removeClass("active-bottom-panel"); + + let wasActive = (_activeId === panelId); + let activatedId = null; + + if (wasActive && _openIds.length > 0) { + let nextIdx = Math.min(idx, _openIds.length - 1); + activatedId = _openIds[nextIdx]; + _activeId = null; // clear so _switchToTab runs + _switchToTab(activatedId); + } else if (wasActive) { + // No more tabs - hide the container + _activeId = null; + if (_$container) { + restoreIfMaximized(); + Resizer.hide(_$container[0]); + } + } + + _removeTabFromBar(panelId); + + // Always fire HIDDEN for the closed panel first + exports.trigger(EVENT_PANEL_HIDDEN, panelId); + + // Then fire SHOWN for the newly activated tab, if any + if (activatedId) { + exports.trigger(EVENT_PANEL_SHOWN, activatedId); } }; @@ -132,6 +382,30 @@ define(function (require, exports, module) { } }; + /** + * Updates the display title shown in the tab bar for this panel. + * @param {string} newTitle The new title to display. + */ + Panel.prototype.setTitle = function (newTitle) { + this._tabTitle = newTitle; + if (_$tabsOverflow) { + _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + this.panelID + '"] .bottom-panel-tab-title') + .text(newTitle); + } + }; + + /** + * Destroys the panel, removing it from the tab bar, internal maps, and the DOM. + * After calling this, the Panel instance should not be reused. + */ + Panel.prototype.destroy = function () { + if (_openIds.indexOf(this.panelID) !== -1) { + this.hide(); + } + delete _panelMap[this.panelID]; + this.$panel.remove(); + }; + /** * gets the Panel's type * @return {string} @@ -140,10 +414,286 @@ define(function (require, exports, module) { return PANEL_TYPE_BOTTOM_PANEL; }; + /** + * Initializes the PanelView module with references to the bottom panel container DOM elements. + * Called by WorkspaceManager during htmlReady. + * @param {jQueryObject} $container The bottom panel container element. + * @param {jQueryObject} $tabBar The tab bar element inside the container. + * @param {jQueryObject} $tabsOverflow The scrollable area holding tab elements. + * @param {jQueryObject} $editorHolder The editor holder element (for maximize height calculation). + * @param {function} recomputeLayoutFn Callback to trigger workspace layout recomputation. + */ + function init($container, $tabBar, $tabsOverflow, $editorHolder, recomputeLayoutFn) { + _$container = $container; + _$tabBar = $tabBar; + _$tabsOverflow = $tabsOverflow; + _$editorHolder = $editorHolder; + _recomputeLayout = recomputeLayoutFn; + + // Tab bar click handlers + _$tabBar.on("click", ".bottom-panel-tab-close-btn", function (e) { + e.stopPropagation(); + let panelId = $(this).closest(".bottom-panel-tab").data("panel-id"); + if (panelId) { + let panel = _panelMap[panelId]; + if (panel) { + panel.hide(); + } + } + }); + + _$tabBar.on("click", ".bottom-panel-tab", function (e) { + let panelId = $(this).data("panel-id"); + if (panelId && panelId !== _activeId) { + let panel = _panelMap[panelId]; + if (panel) { + panel.show(); + } + } + }); + + // Hide-panel button collapses the container but keeps tabs intact + _$tabBar.on("click", ".bottom-panel-hide-btn", function (e) { + e.stopPropagation(); + if (_$container.is(":visible")) { + restoreIfMaximized(); + Resizer.hide(_$container[0]); + } + }); + + // Maximize/restore toggle button + _$tabBar.on("click", ".bottom-panel-maximize-btn", function (e) { + e.stopPropagation(); + _toggleMaximize(); + }); + + // Double-click on tab bar toggles maximize (exclude action buttons) + _$tabBar.on("dblclick", function (e) { + if ($(e.target).closest(".bottom-panel-tab-close-btn, .bottom-panel-hide-btn, .bottom-panel-maximize-btn").length) { + return; + } + _toggleMaximize(); + }); + } + + /** + * Toggle maximize/restore of the bottom panel. + * @private + */ + function _toggleMaximize() { + if (!_$container || !_$container.is(":visible")) { + return; + } + if (_isMaximized) { + _restorePanel(); + } else { + _maximizePanel(); + } + } + + /** + * Maximize the bottom panel to fill all available vertical space. + * @private + */ + function _maximizePanel() { + _preMaximizeHeight = _$container.height(); + let maxHeight = _$editorHolder.height() + _$container.height(); + _$container.height(maxHeight); + _isMaximized = true; + _updateMaximizeButton(); + if (_recomputeLayout) { + _recomputeLayout(); + } + } + + /** + * Compute a sensible panel height for restore when the saved height is + * missing or indistinguishable from the maximized height. + * Returns roughly one-third of the total available space, floored at + * MIN_PANEL_HEIGHT so the panel never restores too small. + * @return {number} + * @private + */ + function _getDefaultRestoreHeight() { + let totalHeight = (_$editorHolder ? _$editorHolder.height() : 0) + + (_$container ? _$container.height() : 0); + return Math.max(MIN_PANEL_HEIGHT, Math.round(totalHeight / 3)); + } + + /** + * Return true if the given height is effectively the same as the + * maximized height (within MAXIMIZE_THRESHOLD). + * @param {number} height + * @return {boolean} + * @private + */ + function _isNearMaxHeight(height) { + let totalHeight = (_$editorHolder ? _$editorHolder.height() : 0) + + (_$container ? _$container.height() : 0); + return (totalHeight - height) <= MAXIMIZE_THRESHOLD; + } + + /** + * Restore the bottom panel to its pre-maximize height. + * If the saved height is missing (e.g. maximize was triggered by + * drag-to-max) or was essentially the same as the maximized height, + * a sensible default (≈ 1/3 of available space) is used instead so + * that the restore feels like a visible change. + * @private + */ + function _restorePanel() { + let restoreHeight; + if (_preMaximizeHeight !== null && !_isNearMaxHeight(_preMaximizeHeight)) { + restoreHeight = _preMaximizeHeight; + } else { + restoreHeight = _getDefaultRestoreHeight(); + } + _$container.height(restoreHeight); + _isMaximized = false; + _preMaximizeHeight = null; + _updateMaximizeButton(); + if (_recomputeLayout) { + _recomputeLayout(); + } + } + + /** + * Update the maximize button icon and tooltip based on current state. + * @private + */ + function _updateMaximizeButton() { + if (!_$tabBar) { + return; + } + let $btn = _$tabBar.find(".bottom-panel-maximize-btn"); + let $icon = $btn.find("i"); + if (_isMaximized) { + $icon.removeClass("fa-expand") + .addClass("fa-compress"); + $btn.attr("title", Strings.BOTTOM_PANEL_RESTORE); + } else { + $icon.removeClass("fa-compress") + .addClass("fa-expand"); + $btn.attr("title", Strings.BOTTOM_PANEL_MAXIMIZE); + } + } + + /** + * Exit maximize state without resizing (for external callers like drag-resize). + * Clears internal maximize state and resets the button icon. + */ + function exitMaximizeOnResize() { + if (!_isMaximized) { + return; + } + _isMaximized = false; + _preMaximizeHeight = null; + _updateMaximizeButton(); + } + + /** + * Enter maximize state during a drag-resize that reaches the maximum + * height. No pre-maximize height is stored because the user arrived + * here via continuous dragging; a sensible default will be computed if + * they later click the Restore button. + */ + function enterMaximizeOnResize() { + if (_isMaximized) { + return; + } + _isMaximized = true; + _preMaximizeHeight = null; + _updateMaximizeButton(); + } + + /** + * Restore the container's CSS height to the pre-maximize value and clear maximize state. + * Must be called BEFORE Resizer.hide() so the Resizer reads the correct height. + * If not maximized, this is a no-op. + * When the saved height is near-max or unknown, a sensible default is used. + */ + function restoreIfMaximized() { + if (!_isMaximized) { + return; + } + if (_preMaximizeHeight !== null && !_isNearMaxHeight(_preMaximizeHeight)) { + _$container.height(_preMaximizeHeight); + } else { + _$container.height(_getDefaultRestoreHeight()); + } + _isMaximized = false; + _preMaximizeHeight = null; + _updateMaximizeButton(); + } + + /** + * Returns true if the bottom panel is currently maximized. + * @return {boolean} + */ + function isMaximized() { + return _isMaximized; + } + + /** + * Returns a copy of the currently open bottom panel IDs in tab order. + * @return {string[]} + */ + function getOpenBottomPanelIDs() { + return _openIds.slice(); + } + + /** + * Hides every open bottom panel tab in a single batch + * @return {string[]} The IDs of panels that were open (useful for restoring later). + */ + function hideAllOpenPanels() { + if (_openIds.length === 0) { + return []; + } + let closedIds = _openIds.slice(); + + // Remove visual active state from every panel + for (let i = 0; i < closedIds.length; i++) { + let panel = _panelMap[closedIds[i]]; + if (panel) { + panel.$panel.removeClass("active-bottom-panel"); + } + } + + // Clear internal state BEFORE hiding the container so the + // panelCollapsed handler sees an empty _openIds and doesn't + // redundantly update the stacks. + _openIds = []; + _activeId = null; + + if (_$container && _$container.is(":visible")) { + restoreIfMaximized(); + Resizer.hide(_$container[0]); + } + + _updateBottomPanelTabBar(); + + // Fire one EVENT_PANEL_HIDDEN per panel for stack tracking. + // No intermediate EVENT_PANEL_SHOWN events are emitted. + for (let i = 0; i < closedIds.length; i++) { + exports.trigger(EVENT_PANEL_HIDDEN, closedIds[i]); + } + + return closedIds; + } + EventDispatcher.makeEventDispatcher(exports); // Public API exports.Panel = Panel; + exports.init = init; + exports.getOpenBottomPanelIDs = getOpenBottomPanelIDs; + exports.hideAllOpenPanels = hideAllOpenPanels; + exports.exitMaximizeOnResize = exitMaximizeOnResize; + exports.enterMaximizeOnResize = enterMaximizeOnResize; + exports.restoreIfMaximized = restoreIfMaximized; + exports.isMaximized = isMaximized; + exports.MAXIMIZE_THRESHOLD = MAXIMIZE_THRESHOLD; //events exports.EVENT_PANEL_HIDDEN = EVENT_PANEL_HIDDEN; exports.EVENT_PANEL_SHOWN = EVENT_PANEL_SHOWN; diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index c56bbb4d95..86875bcca2 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -40,11 +40,20 @@ define(function (require, exports, module) { EventDispatcher = require("utils/EventDispatcher"), KeyBindingManager = require("command/KeyBindingManager"), Resizer = require("utils/Resizer"), + AnimationUtils = require("utils/AnimationUtils"), + Strings = require("strings"), PluginPanelView = require("view/PluginPanelView"), PanelView = require("view/PanelView"), EditorManager = require("editor/EditorManager"), KeyEvent = require("utils/KeyEvent"); + /** + * Panel ID for the default launcher panel shown when no other panels are open. + * @const + * @private + */ + const DEFAULT_PANEL_ID = "workspace.defaultPanel"; + /** * Event triggered when the workspace layout updates. @@ -120,9 +129,16 @@ define(function (require, exports, module) { */ var windowResizing = false; - let lastHiddenBottomPanelStack = [], - lastShownBottomPanelStack = []; + let lastShownBottomPanelStack = []; + + /** @type {jQueryObject} The single container wrapping all bottom panels */ + let $bottomPanelContainer; + + /** @type {jQueryObject} Chevron toggle in the status bar */ + let $statusBarPanelToggle; + /** @type {boolean} True while the status bar toggle button is handling a click */ + let _statusBarToggleInProgress = false; /** * Calculates the available height for the full-size Editor (or the no-editor placeholder), @@ -198,6 +214,13 @@ define(function (require, exports, module) { // FIXME (issue #4564) Workaround https://github.com/codemirror/CodeMirror/issues/1787 triggerUpdateLayout(); + // Re-apply maximize height when the window resizes so the panel stays maximized + if (PanelView.isMaximized() && $bottomPanelContainer && $bottomPanelContainer.is(":visible")) { + let maxHeight = $editorHolder.height() + $bottomPanelContainer.height(); + $bottomPanelContainer.height(maxHeight); + triggerUpdateLayout(); + } + if (!windowResizing) { windowResizing = true; @@ -226,7 +249,6 @@ define(function (require, exports, module) { }); } - /** * Creates a new resizable panel beneath the editor area and above the status bar footer. Panel is initially invisible. * The panel's size & visibility are automatically saved & restored as a view-state preference. @@ -234,24 +256,36 @@ define(function (require, exports, module) { * @param {!string} id Unique id for this panel. Use package-style naming, e.g. "myextension.feature.panelname" * @param {!jQueryObject} $panel DOM content to use as the panel. Need not be in the document yet. Must have an id * attribute, for use as a preferences key. - * @param {number=} minSize Minimum height of panel in px. + * @param {number=} minSize @deprecated No longer used. Pass `undefined`. + * @param {string=} title Display title shown in the bottom panel tab bar. * @return {!Panel} */ - function createBottomPanel(id, $panel, minSize) { - $panel.insertBefore("#status-bar"); + function createBottomPanel(id, $panel, minSize, title) { + $bottomPanelContainer.append($panel); $panel.hide(); - updateResizeLimits(); // initialize panel's max size - - let bottomPanel = new PanelView.Panel($panel, id); + updateResizeLimits(); + let bottomPanel = new PanelView.Panel($panel, id, title); panelIDMap[id] = bottomPanel; - - Resizer.makeResizable($panel[0], Resizer.DIRECTION_VERTICAL, Resizer.POSITION_TOP, minSize, - false, undefined, true); - listenToResize($panel); - return bottomPanel; } + /** + * Destroys a bottom panel, removing it from internal registries, the tab bar, and the DOM. + * After calling this, the panel ID is no longer valid and the Panel instance should not be reused. + * + * @param {!string} id The panel ID that was passed to createBottomPanel. + */ + function destroyBottomPanel(id) { + let panel = panelIDMap[id]; + if (!panel) { + return; + } + if (typeof panel.destroy === 'function') { + panel.destroy(); + } + delete panelIDMap[id]; + } + /** * Creates a new resizable plugin panel associated with the given toolbar icon. Panel is initially invisible. * The panel's size & visibility are automatically saved & restored. Only one panel can be associated with a @@ -324,6 +358,101 @@ define(function (require, exports, module) { $mainPluginPanel = $("#main-plugin-panel"); $pluginIconsBar = $("#plugin-icons-bar"); + // --- Create the bottom panel tabbed container --- + $bottomPanelContainer = $('
'); + let $bottomPanelTabBar = $('
'); + let $bottomPanelTabsOverflow = $('
'); + let $tabBarActions = $('
'); + $tabBarActions.append( + $('') + .attr('title', Strings.BOTTOM_PANEL_MAXIMIZE) + ); + $tabBarActions.append( + $('') + .attr('title', Strings.BOTTOM_PANEL_HIDE) + ); + $bottomPanelTabBar.append($bottomPanelTabsOverflow); + $bottomPanelTabBar.append($tabBarActions); + $bottomPanelContainer.append($bottomPanelTabBar); + $bottomPanelContainer.insertBefore("#status-bar"); + $bottomPanelContainer.hide(); + + // Initialize PanelView with container DOM references and tab bar click handlers + PanelView.init($bottomPanelContainer, $bottomPanelTabBar, $bottomPanelTabsOverflow, + $editorHolder, recomputeLayout); + + // Create status bar chevron toggle for bottom panel + $statusBarPanelToggle = $('
') + .attr('title', Strings.BOTTOM_PANEL_SHOW); + $("#status-indicators").prepend($statusBarPanelToggle); + + $statusBarPanelToggle.on("click", function () { + _statusBarToggleInProgress = true; + if ($bottomPanelContainer.is(":visible")) { + PanelView.restoreIfMaximized(); + Resizer.hide($bottomPanelContainer[0]); + triggerUpdateLayout(); + } else if (PanelView.getOpenBottomPanelIDs().length > 0) { + Resizer.show($bottomPanelContainer[0]); + triggerUpdateLayout(); + } else { + _showDefaultPanel(); + } + _statusBarToggleInProgress = false; + }); + + // Make the container resizable (not individual panels) + Resizer.makeResizable($bottomPanelContainer[0], Resizer.DIRECTION_VERTICAL, Resizer.POSITION_TOP, + 200, false, undefined, true); + listenToResize($bottomPanelContainer); + + // Track maximize state live during drag-resize so the icon updates + // immediately rather than waiting for mouseup. panelResizeUpdate fires + // after listenToResize's handler has already called triggerUpdateLayout(), + // so $editorHolder height is up-to-date at this point. + $bottomPanelContainer.on("panelResizeUpdate panelResizeEnd", function () { + let editorHeight = $editorHolder.height(); + if (PanelView.isMaximized() && editorHeight > PanelView.MAXIMIZE_THRESHOLD) { + PanelView.exitMaximizeOnResize(); + } else if (!PanelView.isMaximized() && editorHeight <= PanelView.MAXIMIZE_THRESHOLD) { + PanelView.enterMaximizeOnResize(); + } + }); + + $bottomPanelContainer.on("panelCollapsed", function () { + PanelView.exitMaximizeOnResize(); + $statusBarPanelToggle.find("i") + .removeClass("fa-chevron-down") + .addClass("fa-chevron-up"); + $statusBarPanelToggle.attr("title", Strings.BOTTOM_PANEL_SHOW); + if (!_statusBarToggleInProgress) { + AnimationUtils.animateUsingClass($statusBarPanelToggle[0], "flash", 800); + } + // When the container collapses while tabs are still open, clear the + // shown stack so the Escape-key handler doesn't silently close tabs + // that the user can't even see. + let openIds = PanelView.getOpenBottomPanelIDs(); + for (let i = 0; i < openIds.length; i++) { + lastShownBottomPanelStack = lastShownBottomPanelStack.filter(item => item !== openIds[i]); + } + }); + + $bottomPanelContainer.on("panelExpanded", function () { + $statusBarPanelToggle.find("i") + .removeClass("fa-chevron-up") + .addClass("fa-chevron-down"); + $statusBarPanelToggle.attr("title", Strings.BOTTOM_PANEL_HIDE_TOGGLE); + if (!_statusBarToggleInProgress) { + AnimationUtils.animateUsingClass($statusBarPanelToggle[0], "flash", 800); + } + // When the container re-expands, add the open panels to the shown stack. + let openIds = PanelView.getOpenBottomPanelIDs(); + for (let i = 0; i < openIds.length; i++) { + lastShownBottomPanelStack = lastShownBottomPanelStack.filter(item => item !== openIds[i]); + lastShownBottomPanelStack.push(openIds[i]); + } + }); + // Sidebar is a special case: it isn't a Panel, and is not created dynamically. Need to explicitly // listen for resize here. listenToResize($("#sidebar")); @@ -351,16 +480,15 @@ define(function (require, exports, module) { EventDispatcher.makeEventDispatcher(exports); PanelView.on(PanelView.EVENT_PANEL_SHOWN, (event, panelID)=>{ - lastHiddenBottomPanelStack = lastHiddenBottomPanelStack.filter(item => item !== panelID); lastShownBottomPanelStack = lastShownBottomPanelStack.filter(item => item !== panelID); lastShownBottomPanelStack.push(panelID); exports.trigger(EVENT_WORKSPACE_PANEL_SHOWN, panelID); + triggerUpdateLayout(); }); PanelView.on(PanelView.EVENT_PANEL_HIDDEN, (event, panelID)=>{ - lastHiddenBottomPanelStack = lastHiddenBottomPanelStack.filter(item => item !== panelID); - lastHiddenBottomPanelStack.push(panelID); lastShownBottomPanelStack = lastShownBottomPanelStack.filter(item => item !== panelID); exports.trigger(EVENT_WORKSPACE_PANEL_HIDDEN, panelID); + triggerUpdateLayout(); }); let currentlyShownPanel = null, @@ -492,44 +620,29 @@ define(function (require, exports, module) { return false; } - function _showLastHiddenPanelIfPossible() { - while(lastHiddenBottomPanelStack.length > 0){ - let panelToShow = getPanelForID(lastHiddenBottomPanelStack.pop()); - if(panelToShow.canBeShown()){ - panelToShow.show(); - return true; - } + /** + * Shows the default launcher panel when no other panels are open. + * @private + */ + function _showDefaultPanel() { + let defaultPanel = panelIDMap[DEFAULT_PANEL_ID]; + if (defaultPanel) { + defaultPanel.show(); } - return false; } function _handleEscapeKey() { - let allPanelsIDs = getAllPanelIDs(); - // first we see if there is any least recently shown panel - if(lastShownBottomPanelStack.length > 0){ - let panelToHide = getPanelForID(lastShownBottomPanelStack.pop()); - panelToHide.hide(); + // Collapse the entire bottom panel container, keeping all tabs intact + if ($bottomPanelContainer && $bottomPanelContainer.is(":visible")) { + PanelView.restoreIfMaximized(); + Resizer.hide($bottomPanelContainer[0]); + triggerUpdateLayout(); return true; } - // if not, see if there is any open panels that are not yet tracked in the least recently used stacks. - for(let panelID of allPanelsIDs){ - let panel = getPanelForID(panelID); - if(panel.getPanelType() === PanelView.PANEL_TYPE_BOTTOM_PANEL && panel.isVisible()){ - panel.hide(); - lastHiddenBottomPanelStack.push(panelID); - return true; - } - } - // no panels hidden, we will toggle the last hidden panel with succeeding escape key presses - return _showLastHiddenPanelIfPossible(); - } - - function _handleShiftEscapeKey() { - // show hidden panels one by one - return _showLastHiddenPanelIfPossible(); + return false; } - // pressing escape when focused on editor will toggle the last opened bottom panel + // pressing escape when focused on editor will hide the bottom panel container function _handleKeydown(event) { if(event.keyCode !== KeyEvent.DOM_VK_ESCAPE || KeyBindingManager.isInOverlayMode()){ return; @@ -553,9 +666,7 @@ define(function (require, exports, module) { return; } - if (event.keyCode === KeyEvent.DOM_VK_ESCAPE && event.shiftKey) { - _handleShiftEscapeKey(); - } else if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) { + if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) { _handleEscapeKey(); } @@ -566,18 +677,22 @@ define(function (require, exports, module) { // Define public API exports.createBottomPanel = createBottomPanel; + exports.destroyBottomPanel = destroyBottomPanel; exports.createPluginPanel = createPluginPanel; exports.isPanelVisible = isPanelVisible; exports.setPluginPanelWidth = setPluginPanelWidth; exports.recomputeLayout = recomputeLayout; exports.getAllPanelIDs = getAllPanelIDs; exports.getPanelForID = getPanelForID; + exports.getOpenBottomPanelIDs = PanelView.getOpenBottomPanelIDs; + exports.hideAllOpenBottomPanels = PanelView.hideAllOpenPanels; exports.addEscapeKeyEventHandler = addEscapeKeyEventHandler; exports.removeEscapeKeyEventHandler = removeEscapeKeyEventHandler; exports._setMockDOM = _setMockDOM; exports.EVENT_WORKSPACE_UPDATE_LAYOUT = EVENT_WORKSPACE_UPDATE_LAYOUT; exports.EVENT_WORKSPACE_PANEL_SHOWN = EVENT_WORKSPACE_PANEL_SHOWN; exports.EVENT_WORKSPACE_PANEL_HIDDEN = EVENT_WORKSPACE_PANEL_HIDDEN; + exports.DEFAULT_PANEL_ID = DEFAULT_PANEL_ID; /** * Constant representing the type of bottom panel diff --git a/test/spec/CodeInspection-fix-integ-test.js b/test/spec/CodeInspection-fix-integ-test.js index bad689bde2..a812d7c4c0 100644 --- a/test/spec/CodeInspection-fix-integ-test.js +++ b/test/spec/CodeInspection-fix-integ-test.js @@ -135,6 +135,10 @@ define(function (require, exports, module) { async function _openProjectFile(fileName) { await awaitsForDone(SpecRunnerUtils.openProjectFiles([fileName]), "opening "+ fileName); + + if (!$("#problems-panel").is(":visible")) { + CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); + } } it("should run test linter when a vbscript document opens show fix buttons in the panel", async function () { diff --git a/test/spec/CodeInspection-integ-test.js b/test/spec/CodeInspection-integ-test.js index a7535399a8..b6b10679e1 100644 --- a/test/spec/CodeInspection-integ-test.js +++ b/test/spec/CodeInspection-integ-test.js @@ -477,6 +477,10 @@ define(function (require, exports, module) { beforeEach(function () { CodeInspection._unregisterAll(); CodeInspection.toggleEnabled(true); + // Ensure problems panel starts hidden for each test + if ($("#problems-panel").is(":visible")) { + CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); + } }); // Utility to create an async provider where the testcase can control when each async result resolves @@ -512,7 +516,7 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000); - expect($("#problems-panel").is(":visible")).toBe(true); + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); var $statusBar = $("#status-inspection"); expect($statusBar.is(":visible")).toBe(true); }); @@ -523,7 +527,7 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000); - expect($("#problems-panel").is(":visible")).toBe(true); + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); let marks = EditorManager.getActiveEditor().getAllMarks("codeInspector"); expect(marks.length).toBe(1); expect(marks[0].className).toBe("editor-text-fragment-warn"); @@ -535,7 +539,8 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000); - expect($("#problems-panel").is(":visible")).toBe(true); + expect($("#status-inspection").hasClass("inspection-errors") || + $("#status-inspection").hasClass("inspection-repair")).toBe(true); let marks = EditorManager.getActiveEditor().getGutterMarker(1, CodeInspection.CODE_INSPECTION_GUTTER); expect(marks.title).toBe('Some errors here and there at column: 4'); marks = $(marks); @@ -594,7 +599,7 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file"); - expect($("#problems-panel").is(":visible")).toBe(true); + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); let marks = $(EditorManager.getActiveEditor() .getGutterMarker(1, CodeInspection.CODE_INSPECTION_GUTTER)); expect(marks.find('span').hasClass('line-icon-problem_type_info')).toBeTrue(); @@ -633,7 +638,7 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file"); - expect($("#problems-panel").is(":visible")).toBe(true); + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); let marks = EditorManager.getActiveEditor().getAllMarks("codeInspector"); expect(marks.length).toBe(numMarksExpected); @@ -716,7 +721,8 @@ define(function (require, exports, module) { // Finish new (current) linting session - verify results are shown asyncProvider.futures[noErrorsJS][1].resolve(failLintResult()); await awaits(100); - expect($("#problems-panel").is(":visible")).toBe(true); + + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); }); it("should ignore async results from previous run in same file - finishing reverse order", async function () { @@ -740,12 +746,14 @@ define(function (require, exports, module) { // Finish new (current) linting session - verify results are shown asyncProvider.futures[noErrorsJS][1].resolve(failLintResult()); await awaits(100); - expect($("#problems-panel").is(":visible")).toBe(true); + + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); // Finish old (stale) linting session - verify results don't replace current results asyncProvider.futures[noErrorsJS][0].resolve(successfulLintResult()); await awaits(100); - expect($("#problems-panel").is(":visible")).toBe(true); + // Status bar should still show errors (stale success result should be ignored) + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); }); it("should ignore async results after linting disabled", async function () { @@ -792,7 +800,7 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000); - expect($("#problems-panel").is(":visible")).toBe(true); + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); var $statusBar = $("#status-inspection"); expect($statusBar.is(":visible")).toBe(true); @@ -866,6 +874,9 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000); + expect($("#status-inspection").hasClass("inspection-errors") || + $("#status-inspection").hasClass("inspection-repair")).toBe(true); + CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); expect($("#problems-panel").is(":visible")).toBe(true); expect($(".inspector-section").is(":visible")).toBeFalsy(); } @@ -890,6 +901,8 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000); + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); + CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); expect($("#problems-panel").is(":visible")).toBe(true); var $inspectorSections = $(".inspector-section"); @@ -904,17 +917,18 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file"); - toggleJSLintResults(false); toggleJSLintResults(true); + toggleJSLintResults(false); }); - it("status icon should not toggle Errors panel when no errors present", async function () { + it("status icon should toggle Errors panel even when no errors present", async function () { var codeInspector = createCodeInspector("javascript linter", successfulLintResult()); CodeInspection.register("javascript", codeInspector); await awaitsForDone(SpecRunnerUtils.openProjectFiles(["no-errors.js"]), "open test file"); - toggleJSLintResults(false); + // Status bar click always toggles the panel regardless of error state + toggleJSLintResults(true); toggleJSLintResults(false); }); @@ -1307,8 +1321,7 @@ define(function (require, exports, module) { await awaits(prefs.get(CodeInspection._PREF_ASYNC_TIMEOUT) + 20); - var $problemsPanel = $("#problems-panel"); - expect($problemsPanel.is(":visible")).toBe(true); + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); var $problemsPanelTitle = $("#problems-panel .title").text(); expect($problemsPanelTitle).toBe(StringUtils.format(Strings.SINGLE_ERROR, "SlowAsyncLinter", "errors.js")); @@ -1337,8 +1350,7 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file"); - var $problemsPanel = $("#problems-panel"); - expect($problemsPanel.is(":visible")).toBe(true); + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); var $problemsPanelTitle = $("#problems-panel .title").text(); expect($problemsPanelTitle).toBe(StringUtils.format(Strings.SINGLE_ERROR, providerName, "errors.js")); @@ -1363,8 +1375,7 @@ define(function (require, exports, module) { await awaitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file"); - var $problemsPanel = $("#problems-panel"); - expect($problemsPanel.is(":visible")).toBe(true); + expect($("#status-inspection").hasClass("inspection-errors")).toBe(true); var $problemsPanelTitle = $("#problems-panel .title").text(); expect($problemsPanelTitle).toBe(StringUtils.format(Strings.SINGLE_ERROR, providerName, "errors.js")); diff --git a/test/spec/Extn-ESLint-integ-test.js b/test/spec/Extn-ESLint-integ-test.js index 5a07682304..9f21716bac 100644 --- a/test/spec/Extn-ESLint-integ-test.js +++ b/test/spec/Extn-ESLint-integ-test.js @@ -85,9 +85,22 @@ define(function (require, exports, module) { } async function _waitForProblemsPanelVisible(visible) { - await awaitsFor(()=>{ - return $("#problems-panel").is(":visible") === visible; - }, "Problems panel to be visible", 15000); + if (visible) { + // Wait for lint to detect errors, then ensure panel is shown + await awaitsFor(()=>{ + return $("#status-inspection").hasClass("inspection-errors") || + $("#status-inspection").hasClass("inspection-repair"); + }, "Lint errors to be detected", 15000); + if (!$("#problems-panel").is(":visible")) { + CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); + } + } else { + // Wait for no-errors state in the status bar + await awaitsFor(()=>{ + return !$("#status-inspection").hasClass("inspection-errors") && + !$("#status-inspection").hasClass("inspection-repair"); + }, "No lint errors detected", 15000); + } } async function _openSimpleES6Project() { diff --git a/test/spec/Extn-HTMLCodeHints-Lint-integ-test.js b/test/spec/Extn-HTMLCodeHints-Lint-integ-test.js index c09f9ddea0..3764f5855d 100644 --- a/test/spec/Extn-HTMLCodeHints-Lint-integ-test.js +++ b/test/spec/Extn-HTMLCodeHints-Lint-integ-test.js @@ -85,9 +85,22 @@ define(function (require, exports, module) { } async function _waitForProblemsPanelVisible(visible) { - await awaitsFor(()=>{ - return $("#problems-panel").is(":visible") === visible; - }, "Problems panel to be visible"); + if (visible) { + // Wait for lint to detect errors, then ensure panel is shown + await awaitsFor(()=>{ + return $("#status-inspection").hasClass("inspection-errors") || + $("#status-inspection").hasClass("inspection-repair"); + }, "Lint errors to be detected"); + if (!$("#problems-panel").is(":visible")) { + CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); + } + } else { + // Wait for no-errors state in the status bar + await awaitsFor(()=>{ + return !$("#status-inspection").hasClass("inspection-errors") && + !$("#status-inspection").hasClass("inspection-repair"); + }, "No lint errors detected"); + } } async function _siwtchFilesTo(destinationFile) { @@ -99,6 +112,7 @@ define(function (require, exports, module) { it("should show html lint error with no config file", async function () { await _openProjectFile("simple1.html"); + await _waitForProblemsPanelVisible(true); await awaitsFor(()=>{ return $("#problems-panel").text().includes( " is missing required \"lang\" attribute (element-required-attributes)"); diff --git a/test/spec/Extn-JSHint-integ-test.js b/test/spec/Extn-JSHint-integ-test.js index 943bdbf339..4a3b4abb39 100644 --- a/test/spec/Extn-JSHint-integ-test.js +++ b/test/spec/Extn-JSHint-integ-test.js @@ -30,7 +30,9 @@ define(function (require, exports, module) { let testProjectsFolder = SpecRunnerUtils.getTestPath("/spec/JSHintExtensionTest-files/"), testWindow, $, - CodeInspection; + CodeInspection, + CommandManager, + Commands; var toggleJSLintResults = function () { $("#status-inspection").triggerHandler("click"); @@ -41,6 +43,8 @@ define(function (require, exports, module) { // Load module instances from brackets.test $ = testWindow.$; CodeInspection = testWindow.brackets.test.CodeInspection; + CommandManager = testWindow.brackets.test.CommandManager; + Commands = testWindow.brackets.test.Commands; CodeInspection.toggleEnabled(true); await awaitsFor(()=>testWindow._JsHintExtensionReadyToIntegTest, "JsHint extension to be loaded", 10000); @@ -49,12 +53,20 @@ define(function (require, exports, module) { afterAll(async function () { testWindow = null; $ = null; + CommandManager = null; + Commands = null; await SpecRunnerUtils.closeTestWindow(); }, 30000); it("status icon should toggle Errors panel when errors present", async function () { await SpecRunnerUtils.loadProjectInTestWindow(testProjectsFolder + "valid-config-error"); await awaitsForDone(SpecRunnerUtils.openProjectFiles(["es8.js"]), "open test file with error"); + await awaitsFor(()=>{ + return $("#status-inspection").hasClass("inspection-errors"); + }, "Lint errors to be detected"); + if (!$("#problems-panel").is(":visible")) { + CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); + } await awaitsFor(()=>{ return $("#problems-panel").is(":visible"); }, "Problems panel to be visible"); @@ -73,6 +85,12 @@ define(function (require, exports, module) { it("should show errors if invalid .jshintrc detected", async function () { await SpecRunnerUtils.loadProjectInTestWindow(testProjectsFolder + "invalid-config"); await awaitsForDone(SpecRunnerUtils.openProjectFiles(["no-errors.js"]), "open test file"); + await awaitsFor(()=>{ + return $("#status-inspection").hasClass("inspection-errors"); + }, "Lint errors to be detected"); + if (!$("#problems-panel").is(":visible")) { + CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); + } await awaitsFor(()=>{ return $("#problems-panel").is(":visible"); }, "Problems panel to be visible"); @@ -85,14 +103,14 @@ define(function (require, exports, module) { await awaits(100); await awaitsFor(()=>{ - return !$("#problems-panel").is(":visible"); - }, "Problems panel to be hidden"); + return $("#status-inspection").hasClass("inspection-valid"); + }, "No lint errors for es6.js"); // using es8 async feature in es6 jshint mode should have errors in problems panel await awaitsForDone(SpecRunnerUtils.openProjectFiles(["es8.js"]), "open test file es8.js"); await awaitsFor(()=>{ - return $("#problems-panel").is(":visible"); - }, "Problems panel to be visible"); + return $("#status-inspection").hasClass("inspection-errors"); + }, "Lint errors detected for es8.js"); }); it("should extend valid es6 .jshintrc in project", async function () { @@ -102,19 +120,25 @@ define(function (require, exports, module) { await awaits(100); await awaitsFor(()=>{ - return !$("#problems-panel").is(":visible"); - }, "Problems panel to be hidden"); + return $("#status-inspection").hasClass("inspection-valid"); + }, "No lint errors for es6.js"); // using es8 async feature in es6 jshint mode should have errors in problems panel await awaitsForDone(SpecRunnerUtils.openProjectFiles(["es8.js"]), "open test file es8.js"); await awaitsFor(()=>{ - return $("#problems-panel").is(":visible"); - }, "Problems panel to be visible"); + return $("#status-inspection").hasClass("inspection-errors"); + }, "Lint errors detected for es8.js"); }); it("should show errors if invalid .jshintrc extend file detected", async function () { await SpecRunnerUtils.loadProjectInTestWindow(testProjectsFolder + "invalid-config-extend"); await awaitsForDone(SpecRunnerUtils.openProjectFiles(["no-errors.js"]), "open test file"); + await awaitsFor(()=>{ + return $("#status-inspection").hasClass("inspection-errors"); + }, "Lint errors to be detected"); + if (!$("#problems-panel").is(":visible")) { + CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); + } await awaitsFor(()=>{ return $("#problems-panel").is(":visible"); }, "Problems panel to be visible"); diff --git a/test/spec/FileFilters-integ-test.js b/test/spec/FileFilters-integ-test.js index c86f6c9f17..34ba8017d6 100644 --- a/test/spec/FileFilters-integ-test.js +++ b/test/spec/FileFilters-integ-test.js @@ -86,9 +86,12 @@ define(function (require, exports, module) { }, "search bar open"); } - function closeSearchBar() { + async function closeSearchBar() { let $searchField = $(".modal-bar #find-group textarea"); SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", $searchField[0]); + await awaitsFor(function () { + return $(".modal-bar").length === 0; + }, "search bar close"); } async function executeSearch(searchString) { @@ -250,12 +253,15 @@ define(function (require, exports, module) { // Error message displayed expect($modalBar.find(".scope-group div.error-filter").is(":visible")).toBeTruthy(); - // Search panel not showing - expect($("#find-in-files-results").is(":visible")).toBeFalsy(); + // Search panel shows "no results" state + expect($("#find-in-files-results").is(":visible")).toBeTruthy(); // Close search bar let $searchField = $modalBar.find("#find-group textarea"); - await SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", $searchField[0]); + SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", $searchField[0]); + await awaitsFor(function () { + return $(".modal-bar").length === 0; + }, "search bar close"); }, 30000); it("should respect filter when editing code", async function () { diff --git a/test/spec/FindInFiles-integ-test.js b/test/spec/FindInFiles-integ-test.js index 7e7547dfbe..1aa74322f7 100644 --- a/test/spec/FindInFiles-integ-test.js +++ b/test/spec/FindInFiles-integ-test.js @@ -403,7 +403,7 @@ define(function (require, exports, module) { expect($(".modal-bar").length).toBe(1); }); - it("should keep dialog and not show panel when there are no results", async function () { + it("should keep dialog and show no-results panel when there are no results", async function () { var filePath = testPath + "/bar.txt", fileEntry = FileSystem.getFileForPath(filePath); @@ -424,7 +424,8 @@ define(function (require, exports, module) { } expect(resultFound).toBe(false); - expect($("#find-in-files-results").is(":visible")).toBeFalsy(); + expect($("#find-in-files-results").is(":visible")).toBeTruthy(); + expect($("#find-in-files-results").hasClass("search-no-results")).toBeTrue(); expect($(".modal-bar").length).toBe(1); // Close search bar diff --git a/test/spec/MainViewManager-integ-test.js b/test/spec/MainViewManager-integ-test.js index 1d9d96e36a..df02bb4448 100644 --- a/test/spec/MainViewManager-integ-test.js +++ b/test/spec/MainViewManager-integ-test.js @@ -905,7 +905,7 @@ define(function (require, exports, module) { _testPanelDoesntToggleBlocked(); }); - async function _testPanelToggleSuccess() { + async function _testPanelHideSuccess() { panel1.show(); expect(panel1.isVisible()).toBeTrue(); @@ -915,12 +915,10 @@ define(function (require, exports, module) { SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); expect(panel1.isVisible()).toBeFalse(); - SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); - expect(panel1.isVisible()).toBeTrue(); } - it("should bottom panel toggle visibility on escape on focus in code editor", async function () { - await _testPanelToggleSuccess(); + it("should escape hide bottom panel when editor is focused", async function () { + await _testPanelHideSuccess(); }); it("should bottom panel not toggle visibility on escape if modal dialog shown", async function () { @@ -994,7 +992,7 @@ define(function (require, exports, module) { it("should dismiss panel if EscapeKeyEventHandler returns false", async function () { function escapeHandler() {return false;} expect(WorkspaceManager.addEscapeKeyEventHandler("x", escapeHandler)).toBeTrue(); - await _testPanelToggleSuccess(); + await _testPanelHideSuccess(); // cleanup expect(WorkspaceManager.removeEscapeKeyEventHandler("x")).toBeTrue(); @@ -1031,7 +1029,7 @@ define(function (require, exports, module) { expect(panel1.isVisible()).toBeTrue(); }); - it("should escape close bottom panel one by one", async function () { + it("should escape collapse entire bottom panel container", async function () { panel1.show(); expect(panel1.isVisible()).toBeTrue(); panel2.show(); @@ -1041,15 +1039,12 @@ define(function (require, exports, module) { promise = MainViewManager._open(MainViewManager.FIRST_PANE, FileSystem.getFileForPath(testPath + "/test.js")); await awaitsForDone(promise, "MainViewManager.doOpen"); - SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); - expect(panel2.isVisible()).toBeFalse(); - expect(panel1.isVisible()).toBeTrue(); SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); expect(panel2.isVisible()).toBeFalse(); expect(panel1.isVisible()).toBeFalse(); }); - it("should escape toggle bottom panel if there is only one shown", async function () { + it("should not toggle bottom panel back on subsequent escape", async function () { panel1.show(); expect(panel1.isVisible()).toBeTrue(); @@ -1060,14 +1055,13 @@ define(function (require, exports, module) { SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); expect(panel1.isVisible()).toBeFalse(); SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); - expect(panel1.isVisible()).toBeTrue(); - SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); expect(panel1.isVisible()).toBeFalse(); }); - it("should show closed bottom panel one by one in LRU order if shift-escape is pressed", async function () { - panel1.show(); panel2.show(); - panel1.hide(); panel2.hide(); + it("should shift-escape also collapse bottom panel", async function () { + panel1.show(); + expect(panel1.isVisible()).toBeTrue(); + expect(MainViewManager.getActivePaneId()).toEqual("first-pane"); promise = MainViewManager._open(MainViewManager.FIRST_PANE, FileSystem.getFileForPath(testPath + "/test.js")); await awaitsForDone(promise, "MainViewManager.doOpen"); @@ -1075,26 +1069,11 @@ define(function (require, exports, module) { SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0], { shiftKey: true }); - expect(panel2.isVisible()).toBeTrue(); expect(panel1.isVisible()).toBeFalse(); - SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0], { - shiftKey: true - }); - expect(panel2.isVisible()).toBeTrue(); - expect(panel1.isVisible()).toBeTrue(); - SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0], { - shiftKey: true - }); - expect(panel2.isVisible()).toBeTrue(); - expect(panel1.isVisible()).toBeTrue(); - SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); - expect(panel1.isVisible()).toBeFalse(); - expect(panel2.isVisible()).toBeTrue(); }); - it("should show closed bottom panel only if it can be shown if shift-escape is pressed", async function () { - panel1.show(); panel2.show(); - panel1.hide(); panel2.hide(); + it("should shift-escape collapse bottom panel regardless of canBeShown", async function () { + panel1.show(); panel2.registerCanBeShownHandler(function () { return false; }); @@ -1106,19 +1085,12 @@ define(function (require, exports, module) { SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0], { shiftKey: true }); - expect(panel2.isVisible()).toBeFalse(); - expect(panel1.isVisible()).toBeTrue(); - SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0], { - shiftKey: true - }); - expect(panel2.isVisible()).toBeFalse(); - expect(panel1.isVisible()).toBeTrue(); + expect(panel1.isVisible()).toBeFalse(); panel2.registerCanBeShownHandler(null); }); - it("should toggle closed bottom panel only if it can be shown if escape is pressed", async function () { - panel1.show(); panel2.show(); - panel1.hide(); panel2.hide(); + it("should escape collapse bottom panel regardless of canBeShown", async function () { + panel1.show(); panel2.registerCanBeShownHandler(function () { return false; }); @@ -1128,14 +1100,7 @@ define(function (require, exports, module) { await awaitsForDone(promise, "MainViewManager.doOpen"); SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); - expect(panel2.isVisible()).toBeFalse(); - expect(panel1.isVisible()).toBeTrue(); - SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); - expect(panel2.isVisible()).toBeFalse(); expect(panel1.isVisible()).toBeFalse(); - SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keydown", _$("#editor-holder")[0]); - expect(panel2.isVisible()).toBeFalse(); - expect(panel1.isVisible()).toBeTrue(); panel2.registerCanBeShownHandler(null); }); }); diff --git a/test/spec/PreferencesManager-integ-test.js b/test/spec/PreferencesManager-integ-test.js index 223576f6b9..f7c63336df 100644 --- a/test/spec/PreferencesManager-integ-test.js +++ b/test/spec/PreferencesManager-integ-test.js @@ -93,6 +93,9 @@ define(function (require, exports, module) { }); it("should show a problem when both .phcode.json and .brackets.json are present in project", async function () { + const CommandManager = testWindow.brackets.test.CommandManager; + const Commands = testWindow.brackets.test.Commands; + await SpecRunnerUtils.loadProjectInTestWindow(testPathBothPrefs); await awaitsForDone(SpecRunnerUtils.openProjectFiles(".phcode.json")); await awaitsFor(()=>{ @@ -102,19 +105,23 @@ define(function (require, exports, module) { // there will be an error in problems panel if both present await awaitsForDone(SpecRunnerUtils.openProjectFiles(".phcode.json")); await awaitsFor(()=>{ - return testWindow.$("#problems-panel").is(":visible") && - testWindow.$("#problems-panel").text().includes(Strings.ERROR_PREFS_PROJECT_LINT_MESSAGE); + return testWindow.$("#status-inspection").hasClass("inspection-errors"); + }, "lint errors detected on .phcode.json"); + if (!testWindow.$("#problems-panel").is(":visible")) { + CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); + } + await awaitsFor(()=>{ + return testWindow.$("#problems-panel").text().includes(Strings.ERROR_PREFS_PROJECT_LINT_MESSAGE); }, "problem panel on .phcode.json"); await awaitsForDone(SpecRunnerUtils.openProjectFiles("test.json")); await awaitsFor(()=>{ - return !testWindow.$("#problems-panel").is(":visible"); - }, "problem panel should not be there for normal test.json file"); + return !testWindow.$("#status-inspection").hasClass("inspection-errors"); + }, "no lint errors for normal test.json file"); await awaitsForDone(SpecRunnerUtils.openProjectFiles(".brackets.json")); await awaitsFor(()=>{ - return testWindow.$("#problems-panel").is(":visible") && - testWindow.$("#problems-panel").text().includes(Strings.ERROR_PREFS_PROJECT_LINT_MESSAGE); + return testWindow.$("#problems-panel").text().includes(Strings.ERROR_PREFS_PROJECT_LINT_MESSAGE); }, "problem panel on .brackets.json"); });