-
+
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");
});