diff --git a/build/lib/policies.js b/build/lib/policies.js index 3e2a10df350..dbddfa9d106 100644 --- a/build/lib/policies.js +++ b/build/lib/policies.js @@ -452,8 +452,12 @@ async function getTranslations() { } const version = await getLatestStableVersion(updateUrl); const languageIds = Object.keys(Languages); - return await Promise.all(languageIds.map(languageId => getNLS(resourceUrlTemplate, languageId, version) + const result = await Promise.allSettled(languageIds.map(languageId => getNLS(resourceUrlTemplate, languageId, version) + .catch(err => { console.warn(`Missing translation: ${languageId}@${version}`); return Promise.reject(err); }) .then(languageTranslations => ({ languageId, languageTranslations })))); + return result + .filter((r) => r.status === 'fulfilled') + .map(r => r.value); } async function main() { const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]); diff --git a/build/lib/policies.ts b/build/lib/policies.ts index 62ea4d561e5..f56aec1a6a9 100644 --- a/build/lib/policies.ts +++ b/build/lib/policies.ts @@ -585,7 +585,8 @@ const Languages = { }; type LanguageTranslations = { [moduleName: string]: { [nlsKey: string]: string } }; -type Translations = { languageId: string; languageTranslations: LanguageTranslations }[]; +type Translation = { languageId: string; languageTranslations: LanguageTranslations }; +type Translations = Translation[]; async function getLatestStableVersion(updateUrl: string) { const res = await fetch(`${updateUrl}/api/update/darwin/stable/latest`); @@ -643,10 +644,15 @@ async function getTranslations(): Promise { const version = await getLatestStableVersion(updateUrl); const languageIds = Object.keys(Languages); - return await Promise.all(languageIds.map( + const result = await Promise.allSettled(languageIds.map( languageId => getNLS(resourceUrlTemplate, languageId, version) + .catch(err => { console.warn(`Missing translation: ${languageId}@${version}`); return Promise.reject(err); }) .then(languageTranslations => ({ languageId, languageTranslations })) )); + + return result + .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') + .map(r => r.value); } async function main() { diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index ec4b4d19e62..8cee84a8d74 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -10,7 +10,7 @@ "engines": { "vscode": "^1.20.0" }, - "main": "./out/extension.node", + "main": "./out/extension", "browser": "./dist/browser/extension", "categories": [ "Programming Languages" diff --git a/extensions/markdown-language-features/src/extension.node.ts b/extensions/markdown-language-features/src/extension.ts similarity index 100% rename from extensions/markdown-language-features/src/extension.node.ts rename to extensions/markdown-language-features/src/extension.ts diff --git a/package.json b/package.json index 592494a447a..b981e0a8f65 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.70.0", - "distro": "954078b4ad7e8d2b00615cfda5d89e5de196f696", + "distro": "73c5eeb6818a9483d7a4bc2b9328223485a59de6", "author": { "name": "Microsoft Corporation" }, @@ -85,12 +85,12 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.20.0-beta.5", + "xterm": "4.20.0-beta.6", "xterm-addon-search": "0.10.0-beta.1", "xterm-addon-serialize": "0.8.0-beta.1", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.13.0-beta.2", - "xterm-headless": "4.20.0-beta.5", + "xterm-addon-webgl": "0.13.0-beta.3", + "xterm-headless": "4.20.0-beta.6", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/package.json b/remote/package.json index 8da57bba60b..16d1d06eb07 100644 --- a/remote/package.json +++ b/remote/package.json @@ -24,12 +24,12 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.20.0-beta.5", + "xterm": "4.20.0-beta.6", "xterm-addon-search": "0.10.0-beta.1", "xterm-addon-serialize": "0.8.0-beta.1", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.13.0-beta.2", - "xterm-headless": "4.20.0-beta.5", + "xterm-addon-webgl": "0.13.0-beta.3", + "xterm-headless": "4.20.0-beta.6", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index c3a32f4d4ec..9d6478ab4c7 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -11,9 +11,9 @@ "tas-client-umd": "0.1.6", "vscode-oniguruma": "1.6.1", "vscode-textmate": "7.0.1", - "xterm": "4.20.0-beta.5", + "xterm": "4.20.0-beta.6", "xterm-addon-search": "0.10.0-beta.1", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.13.0-beta.2" + "xterm-addon-webgl": "0.13.0-beta.3" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 75a41e50c82..590ab4f8c93 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -78,12 +78,12 @@ xterm-addon-unicode11@0.4.0-beta.3: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa" integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q== -xterm-addon-webgl@0.13.0-beta.2: - version "0.13.0-beta.2" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.2.tgz#f58a7a3641ad7c8ac82dd24cfb0165656ed9ac1c" - integrity sha512-98tX0BkpD402RoCO6SyikUXpzCn9/OQhlXsRmM/kRFCxMWWofStWTXzCPhN0MjIx2IdGueDjCmnShhidwihErg== +xterm-addon-webgl@0.13.0-beta.3: + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.3.tgz#2b456c3105238e64b40a30787d6335f5f6f85abb" + integrity sha512-DFGcXAolA0VTsOLIKcORxUOp/FTJdD/YiRzKVLARjgOycwVRKvW2L5Tge8Z7ysZ16sKfnV2vCXyonXYfUWozXw== -xterm@4.20.0-beta.5: - version "4.20.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.5.tgz#d707b0dcb477a554135fb767b24003fced079866" - integrity sha512-KBWfk9UPBKRy662DVGGTZEcW1becEjYvlyWbn2hLj9h2gy6Q4EEEEbggJh8I7SGwdFizl+apHQGhEOZmFCA70w== +xterm@4.20.0-beta.6: + version "4.20.0-beta.6" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.6.tgz#3ed87ba383a5cf44284098278f714df7113e3e3c" + integrity sha512-xJd6vyOuYo4Ht/hTY3DyXGIj0U6kHjr2vWQ1lRmearo3t7QKf7uqOAAfTLeWt/g1P8qe/r0DnsNTeag6vI9RVw== diff --git a/remote/yarn.lock b/remote/yarn.lock index bc084bb74dc..444fb613090 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -803,20 +803,20 @@ xterm-addon-unicode11@0.4.0-beta.3: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa" integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q== -xterm-addon-webgl@0.13.0-beta.2: - version "0.13.0-beta.2" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.2.tgz#f58a7a3641ad7c8ac82dd24cfb0165656ed9ac1c" - integrity sha512-98tX0BkpD402RoCO6SyikUXpzCn9/OQhlXsRmM/kRFCxMWWofStWTXzCPhN0MjIx2IdGueDjCmnShhidwihErg== +xterm-addon-webgl@0.13.0-beta.3: + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.3.tgz#2b456c3105238e64b40a30787d6335f5f6f85abb" + integrity sha512-DFGcXAolA0VTsOLIKcORxUOp/FTJdD/YiRzKVLARjgOycwVRKvW2L5Tge8Z7ysZ16sKfnV2vCXyonXYfUWozXw== -xterm-headless@4.20.0-beta.5: - version "4.20.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.5.tgz#edcff27eb6437d158e6aea2ed7658e783bee5641" - integrity sha512-8SnVUsuNUrQ5P0XU/9Iau3uK7Tf8q/p0KHHwkwJXVxZDIlaDH9XKSs91U9BjJJE3sJgRxH4NSiDYR3vFLSFpxw== +xterm-headless@4.20.0-beta.6: + version "4.20.0-beta.6" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.6.tgz#bd016379e9fac47e5b8870d567cdf330cf6f49fc" + integrity sha512-EV0V7pxMKI0OEcOCD+6vdXq6rBARr7dSN3PovTsZnDWg5dmvUb2eEmz6BTejJj3UVd/JXNEmEXM+tCh97rDCDg== -xterm@4.20.0-beta.5: - version "4.20.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.5.tgz#d707b0dcb477a554135fb767b24003fced079866" - integrity sha512-KBWfk9UPBKRy662DVGGTZEcW1becEjYvlyWbn2hLj9h2gy6Q4EEEEbggJh8I7SGwdFizl+apHQGhEOZmFCA70w== +xterm@4.20.0-beta.6: + version "4.20.0-beta.6" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.6.tgz#3ed87ba383a5cf44284098278f714df7113e3e3c" + integrity sha512-xJd6vyOuYo4Ht/hTY3DyXGIj0U6kHjr2vWQ1lRmearo3t7QKf7uqOAAfTLeWt/g1P8qe/r0DnsNTeag6vI9RVw== yallist@^4.0.0: version "4.0.0" diff --git a/src/vs/base/browser/ui/button/button.css b/src/vs/base/browser/ui/button/button.css index 5327fb0e1e3..66e61c3c398 100644 --- a/src/vs/base/browser/ui/button/button.css +++ b/src/vs/base/browser/ui/button/button.css @@ -42,8 +42,13 @@ outline-offset: -1px !important; } -.monaco-button-dropdown > .monaco-dropdown-button { - margin-left: 1px; +.monaco-button-dropdown .monaco-button-dropdown-separator { + padding: 4px 0; +} + +.monaco-button-dropdown .monaco-button-dropdown-separator > div { + height: 100%; + width: 1px; } .monaco-description-button { diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 3187254843b..4ae8b9d6239 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -247,6 +247,8 @@ export class ButtonWithDropdown extends Disposable implements IButton { private readonly button: Button; private readonly action: Action; private readonly dropdownButton: Button; + private readonly separatorContainer: HTMLDivElement; + private readonly separator: HTMLDivElement; readonly element: HTMLElement; private readonly _onDidClick = this._register(new Emitter()); @@ -263,6 +265,13 @@ export class ButtonWithDropdown extends Disposable implements IButton { this._register(this.button.onDidClick(e => this._onDidClick.fire(e))); this.action = this._register(new Action('primaryAction', this.button.label, undefined, true, async () => this._onDidClick.fire(undefined))); + this.separatorContainer = document.createElement('div'); + this.separatorContainer.classList.add('monaco-button-dropdown-separator'); + + this.separator = document.createElement('div'); + this.separatorContainer.appendChild(this.separator); + this.element.appendChild(this.separatorContainer); + this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true })); this.dropdownButton.element.title = localize("button dropdown more actions", 'More Actions...'); this.dropdownButton.element.classList.add('monaco-dropdown-button'); @@ -299,6 +308,10 @@ export class ButtonWithDropdown extends Disposable implements IButton { style(styles: IButtonStyles): void { this.button.style(styles); this.dropdownButton.style(styles); + + // Separator + this.separatorContainer.style.backgroundColor = styles.buttonBackground?.toString() ?? ''; + this.separator.style.backgroundColor = styles.buttonForeground?.toString() ?? ''; } focus(): void { diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index e4cbed18667..cf8798982b0 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -527,6 +527,16 @@ export class Grid extends Disposable { return this.gridview.resizeView(location, size); } + /** + * Returns whether all other {@link IView views} are at their minimum size. + * + * @param view The reference {@link IView view}. + */ + isViewSizeMaximized(view: T): boolean { + const location = this.getViewLocation(view); + return this.gridview.isViewSizeMaximized(location); + } + /** * Get the size of a {@link IView view}. * diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index e121bcc10c8..8457663d657 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -592,6 +592,10 @@ class BranchNode implements ISplitView, IDisposable { this.splitview.resizeView(index, size); } + isChildSizeMaximized(index: number): boolean { + return this.splitview.isViewSizeMaximized(index); + } + distributeViewSizes(recursive = false): void { this.splitview.distributeViewSizes(); @@ -1431,6 +1435,27 @@ export class GridView implements IDisposable { } } + /** + * Returns whether all other {@link IView views} are at their minimum size. + * + * @param location The {@link GridLocation location} of the view. + */ + isViewSizeMaximized(location: GridLocation): boolean { + const [ancestors, node] = this.getNode(location); + + if (!(node instanceof LeafNode)) { + throw new Error('Invalid location'); + } + + for (let i = 0; i < ancestors.length; i++) { + if (!ancestors[i].isChildSizeMaximized(location[i])) { + return false; + } + } + + return true; + } + /** * Distribute the size among all {@link IView views} within the entire * grid or within a single {@link SplitView}. diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 57fbf0b1544..393f3ea3fda 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -931,6 +931,23 @@ export class SplitView extends Disposable { this.state = State.Idle; } + /** + * Returns whether all other {@link IView views} are at their minimum size. + */ + isViewSizeMaximized(index: number): boolean { + if (index < 0 || index >= this.viewItems.length) { + return false; + } + + for (const item of this.viewItems) { + if (item !== this.viewItems[index] && item.size > item.minimumSize) { + return false; + } + } + + return true; + } + /** * Distribute the entire {@link SplitView} size among all {@link IView views}. */ diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts index 8fd62606254..38cd56f7d75 100644 --- a/src/vs/base/node/ps.ts +++ b/src/vs/base/node/ps.ts @@ -52,7 +52,7 @@ export function listProcesses(rootPid: number): Promise { const ISSUE_REPORTER_HINT = /--vscode-window-kind=issue-reporter/; const PROCESS_EXPLORER_HINT = /--vscode-window-kind=process-explorer/; const UTILITY_NETWORK_HINT = /--utility-sub-type=network/; - const UTILITY_EXTENSION_HOST_HINT = /--vscode-utility-kind=extensionHost/; + const UTILITY_EXTENSION_HOST_HINT = /--vscode-utility-kind=extension-host/; const WINDOWS_CRASH_REPORTER = /--crashes-directory/; const WINDOWS_PTY = /\\pipe\\winpty-control/; const WINDOWS_CONSOLE_HOST = /conhost\.exe/; diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 1a496080028..58a09cc29e5 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -450,6 +450,7 @@ class QuickPick extends QuickInput implements IQuickPi private _matchOnDescription = false; private _matchOnDetail = false; private _matchOnLabel = true; + private _matchOnLabelMode: 'fuzzy' | 'contiguous' = 'fuzzy'; private _sortByLabel = true; private _autoFocusOnList = true; private _keepScrollPosition = false; @@ -595,6 +596,15 @@ class QuickPick extends QuickInput implements IQuickPi this.update(); } + get matchOnLabelMode() { + return this._matchOnLabelMode; + } + + set matchOnLabelMode(matchOnLabelMode: 'fuzzy' | 'contiguous') { + this._matchOnLabelMode = matchOnLabelMode; + this.update(); + } + get sortByLabel() { return this._sortByLabel; } @@ -994,6 +1004,7 @@ class QuickPick extends QuickInput implements IQuickPi this.ui.list.matchOnDescription = this.matchOnDescription; this.ui.list.matchOnDetail = this.matchOnDetail; this.ui.list.matchOnLabel = this.matchOnLabel; + this.ui.list.matchOnLabelMode = this.matchOnLabelMode; this.ui.list.sortByLabel = this.sortByLabel; if (this.itemsUpdated) { this.itemsUpdated = false; diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index 2ada8ddcc60..3292370eb5a 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -17,10 +17,11 @@ import { compareAnything } from 'vs/base/common/comparers'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; import { IMatch } from 'vs/base/common/filters'; -import { matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels'; +import { IParsedLabelWithIcons, matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels'; import { KeyCode } from 'vs/base/common/keyCodes'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; +import { ltrim } from 'vs/base/common/strings'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput'; import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils'; @@ -258,6 +259,7 @@ export class QuickInputList { matchOnDescription = false; matchOnDetail = false; matchOnLabel = true; + matchOnLabelMode: 'fuzzy' | 'contiguous' = 'fuzzy'; matchOnMeta = true; sortByLabel = true; private readonly _onChangedAllVisibleChecked = new Emitter(); @@ -610,6 +612,8 @@ export class QuickInputList { this.list.layout(); return false; } + + const queryWithWhitespace = query; query = query.trim(); // Reset filtering @@ -628,7 +632,12 @@ export class QuickInputList { else { let currentSeparator: IQuickPickSeparator | undefined; this.elements.forEach(element => { - const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel))) : undefined; + let labelHighlights: IMatch[] | undefined; + if (this.matchOnLabelMode === 'fuzzy') { + labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel))) : undefined; + } else { + labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesContiguousIconAware(queryWithWhitespace, parseLabelWithIcons(element.saneLabel))) : undefined; + } const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDescription || ''))) : undefined; const detailHighlights = this.matchOnDetail ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDetail || ''))) : undefined; const metaHighlights = this.matchOnMeta ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneMeta || ''))) : undefined; @@ -726,6 +735,43 @@ export class QuickInputList { } } +export function matchesContiguousIconAware(query: string, target: IParsedLabelWithIcons): IMatch[] | null { + + const { text, iconOffsets } = target; + + // Return early if there are no icon markers in the word to match against + if (!iconOffsets || iconOffsets.length === 0) { + return matchesContiguous(query, text); + } + + // Trim the word to match against because it could have leading + // whitespace now if the word started with an icon + const wordToMatchAgainstWithoutIconsTrimmed = ltrim(text, ' '); + const leadingWhitespaceOffset = text.length - wordToMatchAgainstWithoutIconsTrimmed.length; + + // match on value without icon + const matches = matchesContiguous(query, wordToMatchAgainstWithoutIconsTrimmed); + + // Map matches back to offsets with icon and trimming + if (matches) { + for (const match of matches) { + const iconOffset = iconOffsets[match.start + leadingWhitespaceOffset] /* icon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */; + match.start += iconOffset; + match.end += iconOffset; + } + } + + return matches; +} + +function matchesContiguous(word: string, wordToMatchAgainst: string): IMatch[] | null { + const matchIndex = wordToMatchAgainst.toLowerCase().indexOf(word.toLowerCase()); + if (matchIndex !== -1) { + return [{ start: matchIndex, end: matchIndex + word.length }]; + } + return null; +} + function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: string): number { const labelHighlightsA = elementA.labelHighlights || []; diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index bf9979e00b1..34824941387 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -292,6 +292,13 @@ export interface IQuickPick extends IQuickInput { matchOnLabel: boolean; + /** + * The mode to filter label with. Fuzzy will use fuzzy searching and + * contiguous will make filter entries that do not contain the exact string + * (including whitespace). This defaults to `'fuzzy'`. + */ + matchOnLabelMode: 'fuzzy' | 'contiguous'; + sortByLabel: boolean; autoFocusOnList: boolean; diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 3a979de3229..896d06ec812 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -852,12 +852,12 @@ suite('Map', () => { for (const item of keys) { tst.set(item, true); - assert.ok(tst._isBalanced()); + assert.ok(tst._isBalanced(), `SET${item}|${keys.map(String).join()}`); } for (const item of keys) { tst.delete(item); - assert.ok(tst._isBalanced()); + assert.ok(tst._isBalanced(), `DEL${item}|${keys.map(String).join()}`); } } }); diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index 761c4eee60f..aaa31ee4ce4 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -5,11 +5,12 @@ import * as dom from 'vs/base/browser/dom'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { ICodeEditorOpenHandler, ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IContentDecorationRenderOptions, IDecorationRenderOptions, IThemeDecorationRenderOptions, isThemeColor } from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, IModelDecorationOverviewRulerOptions, InjectedTextOptions, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -42,6 +43,7 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC protected _globalStyleSheet: GlobalStyleSheet | null; private readonly _decorationOptionProviders = new Map(); private readonly _editorStyleSheets = new Map(); + private readonly _codeEditorOpenHandlers = new LinkedList(); constructor( @IThemeService private readonly _themeService: IThemeService, @@ -247,7 +249,21 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC } abstract getActiveCodeEditor(): ICodeEditor | null; - abstract openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise; + + async openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { + for (const handler of this._codeEditorOpenHandlers) { + const candidate = await handler(input, source, sideBySide); + if (candidate !== null) { + return candidate; + } + } + return null; + } + + registerCodeEditorOpenHandler(handler: ICodeEditorOpenHandler): IDisposable { + const rm = this._codeEditorOpenHandlers.unshift(handler); + return toDisposable(rm); + } } export class ModelTransientSettingWatcher { diff --git a/src/vs/editor/browser/services/codeEditorService.ts b/src/vs/editor/browser/services/codeEditorService.ts index b56596939a8..40d7947efcd 100644 --- a/src/vs/editor/browser/services/codeEditorService.ts +++ b/src/vs/editor/browser/services/codeEditorService.ts @@ -10,6 +10,7 @@ import { IModelDecorationOptions, ITextModel } from 'vs/editor/common/model'; import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; +import { IDisposable } from 'vs/base/common/lifecycle'; export const ICodeEditorService = createDecorator('codeEditorService'); @@ -53,4 +54,9 @@ export interface ICodeEditorService { getActiveCodeEditor(): ICodeEditor | null; openCodeEditor(input: ITextResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise; + registerCodeEditorOpenHandler(handler: ICodeEditorOpenHandler): IDisposable; +} + +export interface ICodeEditorOpenHandler { + (input: ITextResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise; } diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditorService.ts b/src/vs/editor/standalone/browser/standaloneCodeEditorService.ts index af167ba519d..2f9e648d52d 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditorService.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditorService.ts @@ -13,7 +13,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IResourceEditorInput, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -31,6 +31,13 @@ export class StandaloneCodeEditorService extends AbstractCodeEditorService { this.onCodeEditorRemove(() => this._checkContextKey()); this._editorIsOpen = contextKeyService.createKey('editorIsOpen', false); this._activeCodeEditor = null; + + this.registerCodeEditorOpenHandler(async (input, source, sideBySide) => { + if (!source) { + return null; + } + return this.doOpenEditor(source, input); + }); } private _checkContextKey(): void { @@ -52,13 +59,6 @@ export class StandaloneCodeEditorService extends AbstractCodeEditorService { return this._activeCodeEditor; } - public openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { - if (!source) { - return Promise.resolve(null); - } - - return Promise.resolve(this.doOpenEditor(source, input)); - } private doOpenEditor(editor: ICodeEditor, input: ITextResourceEditorInput): ICodeEditor | null { const model = this.findModel(editor, input.resource); diff --git a/src/vs/editor/test/browser/editorTestServices.ts b/src/vs/editor/test/browser/editorTestServices.ts index 6eb1863c26e..3aad1a86547 100644 --- a/src/vs/editor/test/browser/editorTestServices.ts +++ b/src/vs/editor/test/browser/editorTestServices.ts @@ -22,7 +22,7 @@ export class TestCodeEditorService extends AbstractCodeEditorService { return null; } public lastInput?: IResourceEditorInput; - openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { + override openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { this.lastInput = input; return Promise.resolve(null); } diff --git a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts index 027748ea8c1..832a28daaa0 100644 --- a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts +++ b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts @@ -324,7 +324,7 @@ class UtilityExtensionHostProcess extends Disposable { const modulePath = FileAccess.asFileUri('bootstrap-fork.js', require).fsPath; const args: string[] = ['--type=extensionHost', '--skipWorkspaceStorageLock']; const execArgv: string[] = opts.execArgv || []; - execArgv.push(`--vscode-utility-kind=extensionHost`); + execArgv.push(`--vscode-utility-kind=extension-host`); const env: { [key: string]: any } = { ...opts.env }; // Make sure all values are strings, otherwise the process will not start diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 9fcfaaae521..94ac8223e87 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -443,11 +443,11 @@ export interface IShellLaunchConfig { /** * A string including ANSI escape sequences that will be written to the terminal emulator - * _before_ the terminal process has launched, a trailing \n is added at the end of the string. - * This allows for example the terminal instance to display a styled message as the first line - * of the terminal. Use \x1b over \033 or \e for the escape control character. + * _before_ the terminal process has launched, when a string is specified, a trailing \n is + * added at the end. This allows for example the terminal instance to display a styled message + * as the first line of the terminal. Use \x1b over \033 or \e for the escape control character. */ - initialText?: string; + initialText?: string | { text: string; trailingNewLine: boolean }; /** * Custom PTY/pseudoterminal process to use. diff --git a/src/vs/platform/terminal/common/terminalEnvironment.ts b/src/vs/platform/terminal/common/terminalEnvironment.ts index 66b5ad31a3f..1d24a24f60d 100644 --- a/src/vs/platform/terminal/common/terminalEnvironment.ts +++ b/src/vs/platform/terminal/common/terminalEnvironment.ts @@ -12,3 +12,26 @@ export function escapeNonWindowsPath(path: string): string { newPath = newPath.replace(bannedChars, ''); return `'${newPath}'`; } + +/** + * Collapses the user's home directory into `~` if it exists within the path, this gives a shorter + * path that is more suitable within the context of a terminal. + */ +export function collapseTildePath(path: string | undefined, userHome: string | undefined, separator: string): string { + if (!path) { + return ''; + } + if (!userHome) { + return path; + } + // Trim the trailing separator from the end if it exists + if (userHome.match(/[\/\\]$/)) { + userHome = userHome.slice(0, userHome.length - 1); + } + const normalizedPath = path.replace(/\\/g, '/').toLowerCase(); + const normalizedUserHome = userHome.replace(/\\/g, '/').toLowerCase(); + if (!normalizedPath.includes(normalizedUserHome)) { + return path; + } + return `~${separator}${path.slice(userHome.length + 1)}`; +} diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index bdecef41367..c45d75bbb12 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -190,7 +190,7 @@ export class PtyService extends Disposable implements IPtyService { executableEnv, options }; - const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, processLaunchOptions, unicodeVersion, this._reconnectConstants, this._logService, isReviving ? shellLaunchConfig.initialText : undefined, rawReviveBuffer, shellLaunchConfig.icon, shellLaunchConfig.color, shellLaunchConfig.name, shellLaunchConfig.fixedDimensions); + const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, processLaunchOptions, unicodeVersion, this._reconnectConstants, this._logService, isReviving && typeof shellLaunchConfig.initialText === 'string' ? shellLaunchConfig.initialText : undefined, rawReviveBuffer, shellLaunchConfig.icon, shellLaunchConfig.color, shellLaunchConfig.name, shellLaunchConfig.fixedDimensions); process.onDidChangeProperty(property => this._onDidChangeProperty.fire({ id, property })); process.onProcessExit(event => { persistentProcess.dispose(); diff --git a/src/vs/platform/terminal/test/common/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/common/terminalEnvironment.test.ts new file mode 100644 index 00000000000..2c58f9ec1fd --- /dev/null +++ b/src/vs/platform/terminal/test/common/terminalEnvironment.test.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { strictEqual } from 'assert'; +import { collapseTildePath } from 'vs/platform/terminal/common/terminalEnvironment'; + +suite('terminalEnvironment', () => { + suite('collapseTildePath', () => { + test('should return empty string for a falsy path', () => { + strictEqual(collapseTildePath('', '/foo', '/'), ''); + strictEqual(collapseTildePath(undefined, '/foo', '/'), ''); + }); + test('should return path for a falsy user home', () => { + strictEqual(collapseTildePath('/foo', '', '/'), '/foo'); + strictEqual(collapseTildePath('/foo', undefined, '/'), '/foo'); + }); + test('should not collapse when user home isn\'t present', () => { + strictEqual(collapseTildePath('/foo', '/bar', '/'), '/foo'); + strictEqual(collapseTildePath('C:\\foo', 'C:\\bar', '\\'), 'C:\\foo'); + }); + test('should collapse with Windows separators', () => { + strictEqual(collapseTildePath('C:\\foo\\bar', 'C:\\foo', '\\'), '~\\bar'); + strictEqual(collapseTildePath('C:\\foo\\bar', 'C:\\foo\\', '\\'), '~\\bar'); + strictEqual(collapseTildePath('C:\\foo\\bar\\baz', 'C:\\foo\\', '\\'), '~\\bar\\baz'); + strictEqual(collapseTildePath('C:\\foo\\bar\\baz', 'C:\\foo', '\\'), '~\\bar\\baz'); + }); + test('should collapse mixed case with Windows separators', () => { + strictEqual(collapseTildePath('c:\\foo\\bar', 'C:\\foo', '\\'), '~\\bar'); + strictEqual(collapseTildePath('C:\\foo\\bar\\baz', 'c:\\foo', '\\'), '~\\bar\\baz'); + }); + test('should collapse with Posix separators', () => { + strictEqual(collapseTildePath('/foo/bar', '/foo', '/'), '~/bar'); + strictEqual(collapseTildePath('/foo/bar', '/foo/', '/'), '~/bar'); + strictEqual(collapseTildePath('/foo/bar/baz', '/foo', '/'), '~/bar/baz'); + strictEqual(collapseTildePath('/foo/bar/baz', '/foo/', '/'), '~/bar/baz'); + }); + }); +}); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index ec0a973bf60..6973141914f 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -184,7 +184,7 @@ export interface IUserDataSyncStoreClient { delete(resource: ServerResource, ref: string | null): Promise; getAllRefs(resource: ServerResource): Promise; - resolveContent(resource: ServerResource, ref: string): Promise; + resolveContent(resource: ServerResource, ref: string, headers?: IHeaders): Promise; } export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 427babbd4f6..2b7bd79e2b8 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -244,13 +244,13 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ })); } - async resolveContent(resource: ServerResource, ref: string): Promise { + async resolveContent(resource: ServerResource, ref: string, headers: IHeaders = {}): Promise { if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, ref).toString(); - const headers: IHeaders = {}; + headers = { ...headers }; headers['Cache-Control'] = 'no-cache'; const context = await this.request(url, { type: 'GET', headers }, [], CancellationToken.None); diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 22791e7c00f..6703baae100 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -135,8 +135,6 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito readonly titleHeight: IEditorGroupTitleHeight; - readonly isMinimized: boolean; - readonly disposed: boolean; setActive(isActive: boolean): void; diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index bf39a77a241..c93f52e60c6 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -573,24 +573,31 @@ abstract class AbstractCloseAllAction extends Action { override async run(): Promise { // Depending on the editor and auto save configuration, - // split dirty editors into buckets + // split editors into buckets for handling confirmation const dirtyEditorsWithDefaultConfirm = new Set(); const dirtyAutoSaveOnFocusChangeEditors = new Set(); const dirtyAutoSaveOnWindowChangeEditors = new Set(); - const dirtyEditorsWithCustomConfirm = new Map>(); + const editorsWithCustomConfirm = new Map>(); for (const { editor, groupId } of this.editorService.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: this.excludeSticky })) { - if (!editor.isDirty() || editor.isSaving()) { - continue; // only interested in dirty editors that are not in the process of saving + let confirmClose = false; + if (editor.closeHandler) { + confirmClose = editor.closeHandler.showConfirm(); // custom handling of confirmation on close + } else { + confirmClose = editor.isDirty() && !editor.isSaving(); // default confirm only when dirty and not saving + } + + if (!confirmClose) { + continue; } // Editor has custom confirm implementation - if (typeof editor.confirm === 'function') { - let customEditorsToConfirm = dirtyEditorsWithCustomConfirm.get(editor.typeId); + if (typeof editor.closeHandler?.confirm === 'function') { + let customEditorsToConfirm = editorsWithCustomConfirm.get(editor.typeId); if (!customEditorsToConfirm) { customEditorsToConfirm = new Set(); - dirtyEditorsWithCustomConfirm.set(editor.typeId, customEditorsToConfirm); + editorsWithCustomConfirm.set(editor.typeId, customEditorsToConfirm); } customEditorsToConfirm.add({ editor, groupId }); @@ -619,7 +626,7 @@ abstract class AbstractCloseAllAction extends Action { if (dirtyEditorsWithDefaultConfirm.size > 0) { const editors = Array.from(dirtyEditorsWithDefaultConfirm.values()); - await this.revealDirtyEditors(editors); // help user make a decision by revealing editors + await this.revealEditorsToConfirm(editors); // help user make a decision by revealing editors const confirmation = await this.fileDialogService.showSaveConfirm(editors.map(({ editor }) => { if (editor instanceof SideBySideEditorInput) { @@ -642,12 +649,12 @@ abstract class AbstractCloseAllAction extends Action { } // 2.) Show custom confirm based dialog - for (const [, editorIdentifiers] of dirtyEditorsWithCustomConfirm) { + for (const [, editorIdentifiers] of editorsWithCustomConfirm) { const editors = Array.from(editorIdentifiers.values()); - await this.revealDirtyEditors(editors); // help user make a decision by revealing editors + await this.revealEditorsToConfirm(editors); // help user make a decision by revealing editors - const confirmation = await firstOrDefault(editors)?.editor.confirm?.(editors); + const confirmation = await firstOrDefault(editors)?.editor.closeHandler?.confirm?.(editors); if (typeof confirmation === 'number') { switch (confirmation) { case ConfirmResult.CANCEL: @@ -683,7 +690,7 @@ abstract class AbstractCloseAllAction extends Action { return this.doCloseAll(); } - private async revealDirtyEditors(editors: ReadonlyArray): Promise { + private async revealEditorsToConfirm(editors: ReadonlyArray): Promise { try { const handledGroups = new Set(); for (const { editor, groupId } of editors) { @@ -1016,7 +1023,7 @@ export class MinimizeOtherGroupsAction extends Action { } override async run(): Promise { - this.editorGroupService.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); + this.editorGroupService.arrangeGroups(GroupsArrangement.MAXIMIZE); } } @@ -1067,7 +1074,7 @@ export class MaximizeGroupAction extends Action { if (this.editorService.activeEditor) { this.layoutService.setPartHidden(true, Parts.SIDEBAR_PART); this.layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); - this.editorGroupService.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); + this.editorGroupService.arrangeGroups(GroupsArrangement.MAXIMIZE); } } } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 21b5d732107..bdeec719d93 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -781,14 +781,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.titleAreaControl.getHeight(); } - get isMinimized(): boolean { - if (!this.dimension) { - return false; - } - - return this.dimension.width === this.minimumWidth || this.dimension.height === this.minimumHeight; - } - notifyIndexChanged(newIndex: number): void { if (this._index !== newIndex) { this._index = newIndex; @@ -1322,16 +1314,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region closeEditor() async closeEditor(editor: EditorInput | undefined = this.activeEditor || undefined, options?: ICloseEditorOptions): Promise { - return this.doCloseEditorWithDirtyHandling(editor, options); + return this.doCloseEditorWithConfirmationHandling(editor, options); } - private async doCloseEditorWithDirtyHandling(editor: EditorInput | undefined = this.activeEditor || undefined, options?: ICloseEditorOptions, internalOptions?: IInternalEditorCloseOptions): Promise { + private async doCloseEditorWithConfirmationHandling(editor: EditorInput | undefined = this.activeEditor || undefined, options?: ICloseEditorOptions, internalOptions?: IInternalEditorCloseOptions): Promise { if (!editor) { return false; } - // Check for dirty and veto - const veto = await this.handleDirtyClosing([editor]); + // Check for confirmation and veto + const veto = await this.handleCloseConfirmation([editor]); if (veto) { return false; } @@ -1461,7 +1453,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.closeEditor(editor, internalOptions?.context)?.editorIndex; } - private async handleDirtyClosing(editors: EditorInput[]): Promise { + private async handleCloseConfirmation(editors: EditorInput[]): Promise { if (!editors.length) { return false; // no veto } @@ -1470,15 +1462,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // To prevent multiple confirmation dialogs from showing up one after the other // we check if a pending confirmation is currently showing and if so, join that - let handleDirtyClosingPromise = this.mapEditorToPendingConfirmation.get(editor); - if (!handleDirtyClosingPromise) { - handleDirtyClosingPromise = this.doHandleDirtyClosing(editor); - this.mapEditorToPendingConfirmation.set(editor, handleDirtyClosingPromise); + let handleCloseConfirmationPromise = this.mapEditorToPendingConfirmation.get(editor); + if (!handleCloseConfirmationPromise) { + handleCloseConfirmationPromise = this.doHandleCloseConfirmation(editor); + this.mapEditorToPendingConfirmation.set(editor, handleCloseConfirmationPromise); } let veto: boolean; try { - veto = await handleDirtyClosingPromise; + veto = await handleCloseConfirmationPromise; } finally { this.mapEditorToPendingConfirmation.delete(editor); } @@ -1489,12 +1481,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Otherwise continue with the remainders - return this.handleDirtyClosing(editors); + return this.handleCloseConfirmation(editors); } - private async doHandleDirtyClosing(editor: EditorInput, options?: { skipAutoSave: boolean }): Promise { - if (!editor.isDirty() || editor.isSaving()) { - return false; // editor must be dirty and not saving + private async doHandleCloseConfirmation(editor: EditorInput, options?: { skipAutoSave: boolean }): Promise { + if (!this.shouldConfirmClose(editor)) { + return false; // no veto } if (editor instanceof SideBySideEditorInput && this.model.contains(editor.primary)) { @@ -1531,10 +1523,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // on auto-save configuration. // However, make sure to respect `skipAutoSave` option in case the automated // save fails which would result in the editor never closing. + // Also, we only do this if no custom confirmation handling is implemented. let confirmation = ConfirmResult.CANCEL; let saveReason = SaveReason.EXPLICIT; let autoSave = false; - if (!editor.hasCapability(EditorInputCapabilities.Untitled) && !options?.skipAutoSave) { + if (!editor.hasCapability(EditorInputCapabilities.Untitled) && !options?.skipAutoSave && !editor.closeHandler) { // Auto-save on focus change: save, because a dialog would steal focus // (see https://github.com/microsoft/vscode/issues/108752) @@ -1554,15 +1547,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } - // No auto-save on focus change: ask user + // No auto-save on focus change or custom confirmation handler: ask user if (!autoSave) { - // Switch to editor that we want to handle and confirm to save/revert + // Switch to editor that we want to handle for confirmation await this.doOpenEditor(editor); // Let editor handle confirmation if implemented - if (typeof editor.confirm === 'function') { - confirmation = await editor.confirm(); + if (typeof editor.closeHandler?.confirm === 'function') { + confirmation = await editor.closeHandler.confirm(); } // Show a file specific confirmation @@ -1578,11 +1571,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } - // It could be that the editor saved meanwhile or is saving, so we check + // It could be that the editor's choice of confirmation has changed + // given the check for confirmation is long running, so we check // again to see if anything needs to happen before closing for good. - // This can happen for example if autoSave: onFocusChange is configured + // This can happen for example if `autoSave: onFocusChange` is configured // so that the save happens when the dialog opens. - if (!editor.isDirty() || editor.isSaving()) { + if (!this.shouldConfirmClose(editor)) { return confirmation === ConfirmResult.CANCEL ? true : false; } @@ -1595,7 +1589,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // we handle the dirty editor again but this time ensuring to // show the confirm dialog // (see https://github.com/microsoft/vscode/issues/108752) - return this.doHandleDirtyClosing(editor, { skipAutoSave: true }); + return this.doHandleCloseConfirmation(editor, { skipAutoSave: true }); } return editor.isDirty(); // veto if still dirty @@ -1621,6 +1615,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } + private shouldConfirmClose(editor: EditorInput): boolean { + if (editor.closeHandler) { + return editor.closeHandler.showConfirm(); // custom handling of confirmation on close + } + + return editor.isDirty() && !editor.isSaving(); // editor must be dirty and not saving + } + //#endregion //#region closeEditors() @@ -1632,8 +1634,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const editors = this.doGetEditorsToClose(args); - // Check for dirty and veto - const veto = await this.handleDirtyClosing(editors.slice(0)); + // Check for confirmation and veto + const veto = await this.handleCloseConfirmation(editors.slice(0)); if (veto) { return false; } @@ -1714,8 +1716,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return true; } - // Check for dirty and veto - const veto = await this.handleDirtyClosing(this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options)); + // Check for confirmation and veto + const veto = await this.handleCloseConfirmation(this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options)); if (veto) { return false; } @@ -1795,7 +1797,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.doCloseEditor(editor, false, { context: EditorCloseContext.REPLACE }); closed = true; } else { - closed = await this.doCloseEditorWithDirtyHandling(editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE }); + closed = await this.doCloseEditorWithConfirmationHandling(editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE }); } if (!closed) { @@ -1815,7 +1817,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (activeReplacement.forceReplaceDirty) { this.doCloseEditor(activeReplacement.editor, false, { context: EditorCloseContext.REPLACE }); } else { - await this.doCloseEditorWithDirtyHandling(activeReplacement.editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE }); + await this.doCloseEditorWithConfirmationHandling(activeReplacement.editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE }); } } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index cf162aa9dae..da027276bd8 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -362,32 +362,22 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro case GroupsArrangement.EVEN: this.gridWidget.distributeViewSizes(); break; - case GroupsArrangement.MINIMIZE_OTHERS: + case GroupsArrangement.MAXIMIZE: this.gridWidget.maximizeViewSize(target); break; case GroupsArrangement.TOGGLE: if (this.isGroupMaximized(target)) { this.arrangeGroups(GroupsArrangement.EVEN); } else { - this.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); + this.arrangeGroups(GroupsArrangement.MAXIMIZE); } break; } } - private isGroupMaximized(targetGroup: IEditorGroupView): boolean { - for (const group of this.groups) { - if (group === targetGroup) { - continue; // ignore target group - } - - if (!group.isMinimized) { - return false; // target cannot be maximized if one group is not minimized - } - } - - return true; + isGroupMaximized(targetGroup: IEditorGroupView): boolean { + return this.gridWidget.isViewSizeMaximized(targetGroup); } setGroupOrientation(orientation: GroupOrientation): void { @@ -609,7 +599,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro if (this.gridWidget) { const viewSize = this.gridWidget.getViewSize(group); if (viewSize.width === group.minimumWidth || viewSize.height === group.minimumHeight) { - this.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS, group); + this.arrangeGroups(GroupsArrangement.MAXIMIZE, group); } } } diff --git a/src/vs/workbench/common/editor/editorInput.ts b/src/vs/workbench/common/editor/editorInput.ts index 023e15af1af..86b53423c60 100644 --- a/src/vs/workbench/common/editor/editorInput.ts +++ b/src/vs/workbench/common/editor/editorInput.ts @@ -11,6 +11,31 @@ import { EditorInputCapabilities, Verbosity, GroupIdentifier, ISaveOptions, IRev import { isEqual } from 'vs/base/common/resources'; import { ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +export interface IEditorCloseHandler { + + /** + * If `true`, will call into the `confirm` method to ask for confirmation + * before closing the editor. + */ + showConfirm(): boolean; + + /** + * Allows an editor to control what should happen when the editor + * (or a list of editor of the same kind) is being closed. + * + * By default a file specific dialog will open if the editor is + * dirty and not in the process of saving. + * + * If the editor is not dealing with files or another condition + * should be used besides dirty state, this method should be + * implemented to show a different dialog. + * + * @param editors if more than one editor is closed, will pass in + * each editor of the same kind to be able to show a combined dialog. + */ + confirm(editors?: ReadonlyArray): Promise; +} + /** * Editor inputs are lightweight objects that can be passed to the workbench API to open inside the editor part. * Each editor input is mapped to an editor that is capable of opening it through the Platform facade. @@ -45,6 +70,12 @@ export abstract class EditorInput extends AbstractEditorInput { private disposed: boolean = false; + /** + * Optional: subclasses can override to implement + * custom confirmation on close behavior. + */ + readonly closeHandler?: IEditorCloseHandler; + /** * Unique type identifier for this input. Every editor input of the * same class should share the same type identifier. The type identifier @@ -168,20 +199,6 @@ export abstract class EditorInput extends AbstractEditorInput { return null; } - /** - * Optional: if this method is implemented, allows an editor to - * control what should happen when the editor (or a list of editors - * of the same kind) is dirty and there is an intent to close it. - * - * By default a file specific dialog will open. If the editor is - * not dealing with files, this method should be implemented to - * show a different dialog. - * - * @param editors if more than one editor is closed, will pass in - * each editor of the same kind to be able to show a combined dialog. - */ - confirm?(editors?: ReadonlyArray): Promise; - /** * Saves the editor. The provided groupId helps implementors * to e.g. preserve view state of the editor and re-open it diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index 922bf591bf7..5f44dc67e26 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -403,8 +403,7 @@ class DocumentSymbolsOutlineCreator implements IOutlineCreator void; constructor( - @IOutlineService outlineService: IOutlineService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IOutlineService outlineService: IOutlineService ) { const reg = outlineService.registerOutlineCreator(this); this.dispose = () => reg.dispose(); @@ -427,7 +426,7 @@ class DocumentSymbolsOutlineCreator implements IOutlineCreator accessor.get(IInstantiationService).createInstance(DocumentSymbolsOutline, editor!, target, firstLoadBarrier)); await firstLoadBarrier.wait(); return result; } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index deb0cb486bc..12f620df779 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -85,7 +85,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo super(); if (this.environmentService.editSessionId !== undefined) { - void this.applyEditSession(this.environmentService.editSessionId).finally(() => this.environmentService.editSessionId = undefined); + void this.resumeEditSession(this.environmentService.editSessionId).finally(() => this.environmentService.editSessionId = undefined); } this.configurationService.onDidChangeConfiguration((e) => { @@ -132,7 +132,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo this.registerContinueEditSessionAction(); - this.registerApplyLatestEditSessionAction(); + this.registerResumeLatestEditSessionAction(); this.registerStoreLatestEditSessionAction(); this.registerContinueInLocalFolderAction(); @@ -171,9 +171,9 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo })); } - private registerApplyLatestEditSessionAction(): void { + private registerResumeLatestEditSessionAction(): void { const that = this; - this._register(registerAction2(class ApplyLatestEditSessionAction extends Action2 { + this._register(registerAction2(class ResumeLatestEditSessionAction extends Action2 { constructor() { super({ id: 'workbench.experimental.editSessions.actions.resumeLatest', @@ -186,8 +186,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo async run(accessor: ServicesAccessor): Promise { await that.progressService.withProgress({ location: ProgressLocation.Notification, - title: localize('applying edit session', 'Applying edit session...') - }, async () => await that.applyEditSession()); + title: localize('resuming edit session', 'Resuming edit session...') + }, async () => await that.resumeEditSession()); } })); } @@ -213,26 +213,24 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo })); } - async applyEditSession(ref?: string): Promise { - if (ref !== undefined) { - this.logService.info(`Applying edit session with ref ${ref}.`); - } + async resumeEditSession(ref?: string): Promise { + this.logService.info(ref !== undefined ? `Resuming edit session with ref ${ref}...` : 'Resuming edit session...'); const data = await this.editSessionsWorkbenchService.read(ref); if (!data) { if (ref === undefined) { - this.notificationService.info(localize('no edit session', 'There are no edit sessions to apply.')); + this.notificationService.info(localize('no edit session', 'There are no edit sessions to resume.')); } else { - this.notificationService.warn(localize('no edit session content for ref', 'Could not apply edit session contents for ID {0}.', ref)); + this.notificationService.warn(localize('no edit session content for ref', 'Could not resume edit session contents for ID {0}.', ref)); } - this.logService.info(`Aborting applying edit session as no edit session content is available to be applied from ref ${ref}.`); + this.logService.info(`Aborting resuming edit session as no edit session content is available to be applied from ref ${ref}.`); return; } const editSession = data.editSession; ref = data.ref; if (editSession.version > EditSessionSchemaVersion) { - this.notificationService.error(localize('client too old', "Please upgrade to a newer version of {0} to apply this edit session.", this.productService.nameLong)); + this.notificationService.error(localize('client too old', "Please upgrade to a newer version of {0} to resume this edit session.", this.productService.nameLong)); return; } @@ -266,7 +264,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo if (hasLocalUncommittedChanges) { // TODO@joyceerhl Provide the option to diff files which would be overwritten by edit session contents const result = await this.dialogService.confirm({ - message: localize('apply edit session warning', 'Applying your edit session may overwrite your existing uncommitted changes. Do you want to proceed?'), + message: localize('resume edit session warning', 'Resuming your edit session may overwrite your existing uncommitted changes. Do you want to proceed?'), type: 'warning', title: EDIT_SESSION_SYNC_CATEGORY.value }); @@ -287,8 +285,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo await this.editSessionsWorkbenchService.delete(ref); this.logService.info(`Deleted edit session with ref ${ref}.`); } catch (ex) { - this.logService.error('Failed to apply edit session, reason: ', (ex as Error).toString()); - this.notificationService.error(localize('apply failed', "Failed to apply your edit session.")); + this.logService.error('Failed to resume edit session, reason: ', (ex as Error).toString()); + this.notificationService.error(localize('resume failed', "Failed to resume your edit session.")); } } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts index af25b5ee15a..b63b550ab35 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts @@ -14,11 +14,13 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IRequestService } from 'vs/platform/request/common/request'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IAuthenticationProvider } from 'vs/platform/userDataSync/common/userDataSync'; +import { createSyncHeaders, IAuthenticationProvider } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsWorkbenchService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { generateUuid } from 'vs/base/common/uuid'; type ExistingSession = IQuickPickItem & { session: AuthenticationSession & { providerId: string } }; type AuthenticationProviderOption = IQuickPickItem & { provider: IAuthenticationProvider }; @@ -47,6 +49,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes @IProductService private readonly productService: IProductService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IRequestService private readonly requestService: IRequestService, + @IDialogService private readonly dialogService: IDialogService, ) { super(); @@ -73,7 +76,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes throw new Error('Please sign in to store your edit session.'); } - return this.storeClient!.write('editSessions', JSON.stringify(editSession), null); + return this.storeClient!.write('editSessions', JSON.stringify(editSession), null, createSyncHeaders(generateUuid())); } /** @@ -89,11 +92,12 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } let content: string | undefined | null; + const headers = createSyncHeaders(generateUuid()); try { if (ref !== undefined) { - content = await this.storeClient?.resolveContent('editSessions', ref); + content = await this.storeClient?.resolveContent('editSessions', ref, headers); } else { - const result = await this.storeClient?.read('editSessions', null); + const result = await this.storeClient?.read('editSessions', null, headers); content = result?.content; ref = result?.ref; } @@ -160,7 +164,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes const existing = await this.getExistingSession(); if (existing !== undefined) { this.logService.trace(`Found existing authentication session with ID ${existingSessionId}`); - this.#authenticationInfo = { sessionId: existing.session.id, token: existing.session.accessToken, providerId: existing.session.providerId }; + this.#authenticationInfo = { sessionId: existing.session.id, token: existing.session.idToken ?? existing.session.accessToken, providerId: existing.session.providerId }; this.storeClient.setAuthToken(this.#authenticationInfo.token, this.#authenticationInfo.providerId); return true; } @@ -169,7 +173,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes // Ask the user to pick a preferred account const session = await this.getAccountPreference(); if (session !== undefined) { - this.#authenticationInfo = { sessionId: session.id, token: session.accessToken, providerId: session.providerId }; + this.#authenticationInfo = { sessionId: session.id, token: session.idToken ?? session.accessToken, providerId: session.providerId }; this.storeClient.setAuthToken(this.#authenticationInfo.token, this.#authenticationInfo.providerId); this.existingSessionId = session.id; this.logService.trace(`Saving authentication session preference for ID ${session.id}.`); @@ -350,8 +354,19 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes }); } - run() { - that.clearAuthenticationPreference(); + async run() { + const result = await that.dialogService.confirm({ + type: 'info', + message: localize('sign out of edit sessions clear data prompt', 'Do you want to sign out of edit sessions?'), + checkbox: { label: localize('delete all edit sessions', 'Delete all stored edit sessions from the cloud.') }, + primaryButton: localize('clear data confirm', 'Yes'), + }); + if (result.confirmed) { + if (result.checkboxChecked) { + that.storeClient?.delete('editSessions', null); + } + that.clearAuthenticationPreference(); + } } })); } diff --git a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts index b7caca6f077..07917f285e5 100644 --- a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts +++ b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts @@ -112,8 +112,8 @@ suite('Edit session sync', () => { // Create root folder await fileService.createFolder(folderUri); - // Apply edit session - await editSessionsContribution.applyEditSession(); + // Resume edit session + await editSessionsContribution.resumeEditSession(); // Verify edit session was correctly applied assert.equal((await fileService.readFile(fileUri)).value.toString(), fileContents); diff --git a/src/vs/workbench/contrib/localization/browser/localeService.ts b/src/vs/workbench/contrib/localization/browser/localeService.ts index 4768a1752b4..dc8138589bf 100644 --- a/src/vs/workbench/contrib/localization/browser/localeService.ts +++ b/src/vs/workbench/contrib/localization/browser/localeService.ts @@ -33,7 +33,7 @@ export class WebLocaleService implements ILocaleService { const restartDialog = await this.dialogService.confirm({ type: 'info', - message: localize('relaunchDisplayLanguageMessage', "{0} needs to reload to change the display language", this.productService.nameLong), + message: localize('relaunchDisplayLanguageMessage', "To change the display language, {0} needs to reload", this.productService.nameLong), detail: localize('relaunchDisplayLanguageDetail', "Press the reload button to refresh the page and set the display language to {0}.", languagePackItem.label), primaryButton: localize({ key: 'reload', comment: ['&& denotes a mnemonic character'] }, "&&Reload"), }); @@ -52,7 +52,7 @@ export class WebLocaleService implements ILocaleService { const restartDialog = await this.dialogService.confirm({ type: 'info', - message: localize('clearDisplayLanguageMessage', "{0} needs to reload to change the display language", this.productService.nameLong), + message: localize('clearDisplayLanguageMessage', "To change the display language, {0} needs to reload", this.productService.nameLong), detail: localize('clearDisplayLanguageDetail', "Press the reload button to refresh the page and use your browser's language."), primaryButton: localize({ key: 'reload', comment: ['&& denotes a mnemonic character'] }, "&&Reload"), }); diff --git a/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts b/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts index 59b10dc5fcf..4c0da0bc253 100644 --- a/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts +++ b/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts @@ -130,7 +130,7 @@ export class NativeLocaleService implements ILocaleService { private async showRestartDialog(languageName: string) { const restartDialog = await this.dialogService.confirm({ type: 'info', - message: localize('restartDisplayLanguageMessage', "{0} needs to restart to change the display language", this.productService.nameLong), + message: localize('restartDisplayLanguageMessage', "To change the display language, {0} needs to restart", this.productService.nameLong), detail: localize( 'restartDisplayLanguageDetail', "Press the restart button to restart {0} and set the display language to {1}.", diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index 8dc07e6463f..09206f43520 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -8,11 +8,13 @@ import { registerAction2 } from 'vs/platform/actions/common/actions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; -import { CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextConflict, GoToPreviousConflict, OpenMergeEditor, ToggleActiveConflictInput1, ToggleActiveConflictInput2, SetColumnLayout, SetMixedLayout, OpenBaseFile } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; +import { CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextConflict, GoToPreviousConflict, OpenBaseFile, OpenMergeEditor, SetColumnLayout, SetMixedLayout, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; import { MergeEditorCopyContentsToJSON, MergeEditorOpenContents } from 'vs/workbench/contrib/mergeEditor/browser/commands/devCommands'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; -import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; +import { MergeEditor, MergeEditorOpenHandlerContribution } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { MergeEditorSerializer } from './mergeEditorSerializer'; Registry.as(EditorExtensions.EditorPane).registerEditorPane( @@ -47,3 +49,8 @@ registerAction2(ToggleActiveConflictInput2); registerAction2(CompareInput1WithBaseCommand); registerAction2(CompareInput2WithBaseCommand); + + +Registry + .as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(MergeEditorOpenHandlerContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index 053c7309754..4f29941dc13 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -14,15 +14,13 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor'; -import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorInput, IEditorCloseHandler } from 'vs/workbench/common/editor/editorInput'; import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { EditorWorkerServiceDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ILanguageSupport, ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { assertType } from 'vs/base/common/types'; -import { Event } from 'vs/base/common/event'; +import { autorun } from 'vs/base/common/observable'; export class MergeEditorInputData { constructor( @@ -39,7 +37,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements private _model?: MergeEditorModel; private _outTextModel?: ITextFileEditorModel; - private _ignoreUnhandledConflictsForDirtyState?: true; + + override closeHandler: MergeEditorCloseHandler | undefined; constructor( public readonly base: URI, @@ -48,8 +47,6 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements public readonly result: URI, @IInstantiationService private readonly _instaService: IInstantiationService, @ITextModelService private readonly _textModelService: ITextModelService, - @IDialogService private readonly _dialogService: IDialogService, - @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService, @IEditorService editorService: IEditorService, @ITextFileService textFileService: ITextFileService, @ILabelService labelService: ILabelService, @@ -118,6 +115,13 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements }, ); + // set/unset the closeHandler whenever unhandled conflicts are detected + const closeHandler = this._instaService.createInstance(MergeEditorCloseHandler, this._model); + this._store.add(autorun('closeHandler', reader => { + const value = this._model!.hasUnhandledConflicts.read(reader); + this.closeHandler = value ? closeHandler : undefined; + })); + await this._model.onInitialized; this._store.add(this._model); @@ -125,11 +129,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements this._store.add(input1); this._store.add(input2); this._store.add(result); - - this._store.add(Event.fromObservable(this._model.hasUnhandledConflicts)(() => this._onDidChangeDirty.fire(undefined))); } - this._ignoreUnhandledConflictsForDirtyState = undefined; return this._model; } @@ -146,66 +147,57 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements // ---- FileEditorInput override isDirty(): boolean { - const textModelDirty = Boolean(this._outTextModel?.isDirty()); - if (textModelDirty) { - // text model dirty -> 3wm is dirty - return true; - } - if (!this._ignoreUnhandledConflictsForDirtyState) { - // unhandled conflicts -> 3wm is dirty UNLESS we explicitly set this input - // to ignore unhandled conflicts for the dirty-state. This happens only - // after confirming to ignore unhandled changes - return Boolean(this._model && this._model.hasUnhandledConflicts.get()); - } - return false; + return Boolean(this._outTextModel?.isDirty()); } - override async confirm(editors?: ReadonlyArray): Promise { + setLanguageId(languageId: string, _setExplicitly?: boolean): void { + this._model?.setLanguageId(languageId); + } - const inputs: MergeEditorInput[] = [this]; - if (editors) { - for (const { editor } of editors) { - if (editor instanceof MergeEditorInput) { - inputs.push(editor); - } - } - } + // implement get/set languageId + // implement get/set encoding +} - const inputsWithUnhandledConflicts = inputs +class MergeEditorCloseHandler implements IEditorCloseHandler { + + private _ignoreUnhandledConflicts: boolean = false; + + constructor( + private readonly _model: MergeEditorModel, + @IDialogService private readonly _dialogService: IDialogService, + ) { } + + showConfirm(): boolean { + // unhandled conflicts -> 3wm asks to confirm UNLESS we explicitly set this input + // to ignore unhandled conflicts. This happens only after confirming to ignore unhandled changes + return !this._ignoreUnhandledConflicts && this._model.hasUnhandledConflicts.get(); + } + + async confirm(editors?: readonly IEditorIdentifier[] | undefined): Promise { + + const handler: MergeEditorCloseHandler[] = [this]; + editors?.forEach(candidate => candidate.editor.closeHandler instanceof MergeEditorCloseHandler && handler.push(candidate.editor.closeHandler)); + + const inputsWithUnhandledConflicts = handler .filter(input => input._model && input._model.hasUnhandledConflicts.get()); if (inputsWithUnhandledConflicts.length === 0) { + // shouldn't happen return ConfirmResult.SAVE; } - const actions: string[] = []; + const actions: string[] = [ + localize('unhandledConflicts.ignore', "Continue with Conflicts"), + localize('unhandledConflicts.discard', "Discard Merge Changes"), + localize('unhandledConflicts.cancel', "Cancel"), + ]; const options = { - cancelId: 0, - detail: inputs.length > 1 - ? localize('unhandledConflicts.detailN', 'Merge conflicts in {0} editors will remain unhandled.', inputs.length) + cancelId: 2, + detail: handler.length > 1 + ? localize('unhandledConflicts.detailN', 'Merge conflicts in {0} editors will remain unhandled.', handler.length) : localize('unhandledConflicts.detail1', 'Merge conflicts in this editor will remain unhandled.') }; - const isAnyAutoSave = this._filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF; - if (!isAnyAutoSave) { - // manual-save: FYI and discard - actions.push( - localize('unhandledConflicts.manualSaveIgnore', "Save and Continue with Conflicts"), // 0 - localize('unhandledConflicts.discard', "Discard Merge Changes"), // 1 - localize('unhandledConflicts.manualSaveNoSave', "Don't Save"), // 2 - ); - - } else { - // auto-save: only FYI - actions.push( - localize('unhandledConflicts.ignore', "Continue with Conflicts"), // 0 - localize('unhandledConflicts.discard', "Discard Merge Changes"), // 1 - ); - } - - actions.push(localize('unhandledConflicts.cancel', "Cancel")); - options.cancelId = actions.length - 1; - const { choice } = await this._dialogService.show( Severity.Info, localize('unhandledConflicts.msg', 'Do you want to continue with unhandled conflicts?'), // 1 @@ -220,8 +212,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements // save or revert: in both cases we tell the inputs to ignore unhandled conflicts // for the dirty state computation. - for (const input of inputs) { - input._ignoreUnhandledConflictsForDirtyState = true; + for (const input of handler) { + input._ignoreUnhandledConflicts = true; } if (choice === 0) { @@ -230,7 +222,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements } else if (choice === 1) { // discard: undo all changes and save original (pre-merge) state - for (const input of inputs) { + for (const input of handler) { input._discardMergeChanges(); } return ConfirmResult.SAVE; @@ -242,8 +234,6 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements } private _discardMergeChanges(): void { - assertType(this._model !== undefined); - const chunks: string[] = []; while (true) { const chunk = this._model.resultSnapshot.read(); @@ -254,11 +244,4 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements } this._model.result.setValue(chunks.join()); } - - setLanguageId(languageId: string, _setExplicitly?: boolean): void { - this._model?.setLanguageId(languageId); - } - - // implement get/set languageId - // implement get/set encoding } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 544e07332cb..abd5efbe468 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -11,12 +11,13 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Color } from 'vs/base/common/color'; import { BugIndicatingError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { autorunWithStore, IObservable } from 'vs/base/common/observable'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/mergeEditor'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; @@ -26,7 +27,7 @@ import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/men import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorOptions, ITextEditorOptions, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -35,7 +36,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { AbstractTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorInputWithOptions, EditorResourceAccessor, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; @@ -46,7 +47,6 @@ import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/v import { ctxBaseResourceScheme, ctxIsMergeEditor, ctxMergeEditorLayout, MergeEditorLayoutTypes } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import './colors'; import { InputCodeEditorView } from './editors/inputCodeEditorView'; @@ -86,9 +86,9 @@ export class MergeEditor extends AbstractTextEditor { private readonly _sessionDisposables = new DisposableStore(); private _grid!: Grid; - private readonly input1View = this._register(this.instantiation.createInstance(InputCodeEditorView, 1)); - private readonly input2View = this._register(this.instantiation.createInstance(InputCodeEditorView, 2)); - private readonly inputResultView = this._register(this.instantiation.createInstance(ResultCodeEditorView)); + private readonly input1View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 1)); + private readonly input2View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 2)); + private readonly inputResultView = this._register(this.instantiationService.createInstance(ResultCodeEditorView)); private readonly _layoutMode: MergeEditorLayout; private readonly _ctxIsMergeEditor: IContextKey; @@ -103,7 +103,7 @@ export class MergeEditor extends AbstractTextEditor { } constructor( - @IInstantiationService private readonly instantiation: IInstantiationService, + @IInstantiationService instantiation: IInstantiationService, @ILabelService private readonly _labelService: ILabelService, @IMenuService private readonly _menuService: IMenuService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -115,7 +115,6 @@ export class MergeEditor extends AbstractTextEditor { @IEditorService editorService: IEditorService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IFileService fileService: IFileService, - @IEditorResolverService private readonly _editorResolverService: IEditorResolverService, ) { super(MergeEditor.ID, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); @@ -186,7 +185,7 @@ export class MergeEditor extends AbstractTextEditor { createAndFillInActionBarActions(toolbarMenu, { renderShortTitle: true, shouldForwardArgs: true }, actions); if (actions.length > 0) { const [first] = actions; - const acceptBtn = this.instantiation.createInstance(FloatingClickWidget, this.inputResultView.editor, first.label, first.id); + const acceptBtn = this.instantiationService.createInstance(FloatingClickWidget, this.inputResultView.editor, first.label, first.id); toolbarMenuDisposables.add(acceptBtn.onClick(() => first.run(this.inputResultView.editor.getModel()?.uri))); toolbarMenuDisposables.add(acceptBtn); acceptBtn.render(); @@ -296,7 +295,6 @@ export class MergeEditor extends AbstractTextEditor { await super.setInput(input, options, context, token); this._sessionDisposables.clear(); - this._toggleEditorOverwrite(true); const model = await input.resolve(); this._model = model; @@ -309,16 +307,17 @@ export class MergeEditor extends AbstractTextEditor { this._ctxBaseResourceScheme.set(model.base.uri.scheme); const viewState = this.loadEditorViewState(input, context); - this._applyViewState(viewState); - - this._sessionDisposables.add(thenIfNotDisposed(model.onInitialized, () => { - const firstConflict = model.modifiedBaseRanges.get().find(r => r.isConflicting); - if (!firstConflict) { - return; - } - - this.input1View.editor.revealLineInCenter(firstConflict.input1Range.startLineNumber); - })); + if (viewState) { + this._applyViewState(viewState); + } else { + this._sessionDisposables.add(thenIfNotDisposed(model.onInitialized, () => { + const firstConflict = model.modifiedBaseRanges.get().find(r => r.isConflicting); + if (!firstConflict) { + return; + } + this.input1View.editor.revealLineInCenter(firstConflict.input1Range.startLineNumber); + })); + } this._sessionDisposables.add(autorunWithStore((reader, store) => { @@ -373,7 +372,6 @@ export class MergeEditor extends AbstractTextEditor { super.clearInput(); this._sessionDisposables.clear(); - this._toggleEditorOverwrite(false); for (const { editor } of [this.input1View, this.input2View, this.inputResultView]) { editor.setModel(null); @@ -405,39 +403,6 @@ export class MergeEditor extends AbstractTextEditor { } this._ctxIsMergeEditor.set(visible); - this._toggleEditorOverwrite(visible); - } - - private readonly _editorOverrideHandle = this._store.add(new MutableDisposable()); - - private _toggleEditorOverwrite(haveIt: boolean) { - if (!haveIt) { - this._editorOverrideHandle.clear(); - return; - } - // this is RATHER UGLY. I dynamically register an editor for THIS (editor,input) so that - // navigating within the merge editor works, e.g navigating from the outline or breakcrumps - // or revealing a definition, reference etc - // TODO@jrieken @bpasero @lramos15 - const input = this.input; - if (input instanceof MergeEditorInput) { - this._editorOverrideHandle.value = this._editorResolverService.registerEditor( - `${input.result.scheme}:${input.result.fsPath}`, - { - id: `${this.getId()}/fake`, - label: this.input?.getName()!, - priority: RegisteredEditorPriority.exclusive - }, - {}, - (candidate): EditorInputWithOptions => { - const resource = EditorResourceAccessor.getCanonicalUri(candidate); - if (!isEqual(resource, this.model?.result.uri)) { - throw new Error(`Expected to be called WITH ${input.result.toString()}`); - } - return { editor: input }; - } - ); - } } // ---- interact with "outside world" via`getControl`, `scopedContextKeyService`: we only expose the result-editor keep the others internal @@ -506,6 +471,37 @@ export class MergeEditor extends AbstractTextEditor { } } +export class MergeEditorOpenHandlerContribution extends Disposable { + + constructor( + @IEditorService private readonly _editorService: IEditorService, + @ICodeEditorService codeEditorService: ICodeEditorService, + ) { + super(); + this._store.add(codeEditorService.registerCodeEditorOpenHandler(this.openCodeEditorFromMergeEditor.bind(this))); + } + + private async openCodeEditorFromMergeEditor(input: ITextResourceEditorInput, _source: ICodeEditor | null, sideBySide?: boolean | undefined): Promise { + const activePane = this._editorService.activeEditorPane; + if (!sideBySide + && input.options + && activePane instanceof MergeEditor + && activePane.getControl() + && activePane.input instanceof MergeEditorInput + && isEqual(input.resource, activePane.input.result) + ) { + // Special: stay inside the merge editor when it is active and when the input + // targets the result editor of the merge editor. + const targetEditor = activePane.getControl()!; + applyTextEditorOptions(input.options, targetEditor, ScrollType.Smooth); + return targetEditor; + } + + // cannot handle this + return null; + } +} + type IMergeEditorViewState = ICodeEditorViewState & { readonly input1State?: ICodeEditorViewState; readonly input2State?: ICodeEditorViewState; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index ed23a5f2632..a75b4184bd2 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -41,6 +41,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; const OpenInEditorCommandId = 'search.action.openInEditor'; const OpenNewEditorToSideCommandId = 'search.action.openNewEditorToSide'; const FocusQueryEditorWidgetCommandId = 'search.action.focusQueryEditorWidget'; +const FocusQueryEditorFilesToIncludeCommandId = 'search.action.focusFilesToInclude'; +const FocusQueryEditorFilesToExcludeCommandId = 'search.action.focusFilesToExclude'; const ToggleSearchEditorCaseSensitiveCommandId = 'toggleSearchEditorCaseSensitive'; const ToggleSearchEditorWholeWordCommandId = 'toggleSearchEditorWholeWord'; @@ -374,6 +376,44 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: FocusQueryEditorFilesToIncludeCommandId, + title: { value: localize('search.action.focusFilesToInclude', "Focus Search Editor Files to Include"), original: 'Focus Search Editor Files to Include' }, + category, + f1: true, + precondition: SearchEditorConstants.InSearchEditor, + }); + } + async run(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + (editorService.activeEditorPane as SearchEditor).focusFilesToIncludeInput(); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: FocusQueryEditorFilesToExcludeCommandId, + title: { value: localize('search.action.focusFilesToExclude', "Focus Search Editor Files to Exclude"), original: 'Focus Search Editor Files to Exclude' }, + category, + f1: true, + precondition: SearchEditorConstants.InSearchEditor, + }); + } + async run(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + (editorService.activeEditorPane as SearchEditor).focusFilesToExcludeInput(); + } + } +}); + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 380f8dbcc8f..54be4156b6e 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -275,6 +275,20 @@ export class SearchEditor extends AbstractTextCodeEditor this.queryEditorWidget.searchInput.focus(); } + focusFilesToIncludeInput() { + if (!this.showingIncludesExcludes) { + this.toggleIncludesExcludes(true); + } + this.inputPatternIncludes.focus(); + } + + focusFilesToExcludeInput() { + if (!this.showingIncludesExcludes) { + this.toggleIncludesExcludes(true); + } + this.inputPatternExcludes.focus(); + } + focusNextInput() { if (this.queryEditorWidget.searchInputHasFocus()) { if (this.showingIncludesExcludes) { diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 754a84163c1..7cfab363b6d 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -1157,7 +1157,10 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { }, 'Executing task: {0}', commandLine), { excludeLeadingNewLine: true }) + this.taskShellIntegrationOutputSequence; } } else { - shellLaunchConfig.initialText = this.taskShellIntegrationStartSequence + this.taskShellIntegrationOutputSequence; + shellLaunchConfig.initialText = { + text: this.taskShellIntegrationStartSequence + this.taskShellIntegrationOutputSequence, + trailingNewLine: false + }; } } else { const commandExecutable = (task.command.runtime !== RuntimeType.CustomExecution) ? CommandString.value(command) : undefined; @@ -1197,7 +1200,10 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { }, 'Executing task: {0}', `${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)}`), { excludeLeadingNewLine: true }) + this.taskShellIntegrationOutputSequence; } } else { - shellLaunchConfig.initialText = this.taskShellIntegrationStartSequence + this.taskShellIntegrationOutputSequence; + shellLaunchConfig.initialText = { + text: this.taskShellIntegrationStartSequence + this.taskShellIntegrationOutputSequence, + trailingNewLine: false + }; } } diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index 88498fbfb9a..a2f4afa1400 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -110,23 +110,22 @@ __vsc_precmd() { } __vsc_preexec() { - if [ "$__vsc_in_command_execution" = "0" ]; then - __vsc_initialized=1 - __vsc_in_command_execution="1" - if [[ ! "$BASH_COMMAND" =~ ^__vsc_prompt* ]]; then - __vsc_current_command=$BASH_COMMAND - else - __vsc_current_command="" - fi - __vsc_command_output_start + __vsc_initialized=1 + if [[ ! "$BASH_COMMAND" =~ ^__vsc_prompt* ]]; then + __vsc_current_command=$BASH_COMMAND + else + __vsc_current_command="" fi + __vsc_command_output_start } # Debug trapping/preexec inspired by starship (ISC) if [[ -n "${bash_preexec_imported:-}" ]]; then __vsc_preexec_only() { - __vsc_status="$?" - __vsc_preexec + if [ "$__vsc_in_command_execution" = "0" ]; then + __vsc_in_command_execution="1" + __vsc_preexec + fi } precmd_functions+=(__vsc_prompt_cmd) preexec_functions+=(__vsc_preexec_only) @@ -134,15 +133,19 @@ else __vsc_dbg_trap="$(trap -p DEBUG | cut -d' ' -f3 | tr -d \')" if [[ -z "$__vsc_dbg_trap" ]]; then __vsc_preexec_only() { - __vsc_status="$?" - __vsc_preexec + if [ "$__vsc_in_command_execution" = "0" ]; then + __vsc_in_command_execution="1" + __vsc_preexec + fi } trap '__vsc_preexec_only "$_"' DEBUG elif [[ "$__vsc_dbg_trap" != '__vsc_preexec "$_"' && "$__vsc_dbg_trap" != '__vsc_preexec_all "$_"' ]]; then __vsc_preexec_all() { - __vsc_status="$?" - builtin eval ${__vsc_dbg_trap} - __vsc_preexec + if [ "$__vsc_in_command_execution" = "0" ]; then + __vsc_in_command_execution="1" + builtin eval ${__vsc_dbg_trap} + __vsc_preexec + fi } trap '__vsc_preexec_all "$_"' DEBUG fi @@ -151,6 +154,7 @@ fi __vsc_update_prompt __vsc_prompt_cmd_original() { + __vsc_status="$?" if [[ ${IFS+set} ]]; then __vsc_original_ifs="$IFS" fi diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts index 839209a2cca..55032336cd5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts @@ -9,7 +9,7 @@ import { dispose, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { EditorInputCapabilities, IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorInput, IEditorCloseHandler } from 'vs/workbench/common/editor/editorInput'; import { ITerminalInstance, ITerminalInstanceService, terminalEditorId } from 'vs/workbench/contrib/terminal/browser/terminal'; import { getColorClass, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -23,21 +23,22 @@ import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/termin import { ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { Emitter } from 'vs/base/common/event'; -export class TerminalEditorInput extends EditorInput { - - protected readonly _onDidRequestAttach = this._register(new Emitter()); - readonly onDidRequestAttach = this._onDidRequestAttach.event; +export class TerminalEditorInput extends EditorInput implements IEditorCloseHandler { static readonly ID = 'workbench.editors.terminal'; + override readonly closeHandler = this; + private _isDetached = false; private _isShuttingDown = false; private _isReverted = false; private _copyLaunchConfig?: IShellLaunchConfig; private _terminalEditorFocusContextKey: IContextKey; - private _group: IEditorGroup | undefined; + protected readonly _onDidRequestAttach = this._register(new Emitter()); + readonly onDidRequestAttach = this._onDidRequestAttach.event; + setGroup(group: IEditorGroup | undefined) { this._group = group; } @@ -64,13 +65,6 @@ export class TerminalEditorInput extends EditorInput { } this._terminalInstance = instance; this._setupInstanceListeners(); - - // Refresh dirty state when the confirm on kill setting is changed - this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalSettingId.ConfirmOnKill)) { - this._onDidChangeDirty.fire(); - } - }); } override copy(): EditorInput { @@ -95,7 +89,7 @@ export class TerminalEditorInput extends EditorInput { return this._isDetached ? undefined : this._terminalInstance; } - override isDirty(): boolean { + showConfirm(): boolean { if (this._isReverted) { return false; } @@ -106,7 +100,7 @@ export class TerminalEditorInput extends EditorInput { return false; } - override async confirm(terminals?: ReadonlyArray): Promise { + async confirm(terminals?: ReadonlyArray): Promise { const { choice } = await this._dialogService.show( Severity.Warning, localize('confirmDirtyTerminal.message', "Do you want to terminate running processes?"), @@ -148,12 +142,6 @@ export class TerminalEditorInput extends EditorInput { this._terminalEditorFocusContextKey = TerminalContextKeys.editorFocus.bindTo(_contextKeyService); - // Refresh dirty state when the confirm on kill setting is changed - this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalSettingId.ConfirmOnKill)) { - this._onDidChangeDirty.fire(); - } - }); if (_terminalInstance) { this._setupInstanceListeners(); } @@ -180,7 +168,6 @@ export class TerminalEditorInput extends EditorInput { instance.onIconChanged(() => this._onDidChangeLabel.fire()), instance.onDidFocus(() => this._terminalEditorFocusContextKey.set(true)), instance.onDidBlur(() => this._terminalEditorFocusContextKey.reset()), - instance.onDidChangeHasChildProcesses(() => this._onDidChangeDirty.fire()), instance.statusList.onDidChangePrimaryStatus(() => this._onDidChangeLabel.fire()) ]; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index c1bbe018ea2..d6573103a76 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -47,7 +47,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { TerminalCapabilityStoreMultiplexer } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; import { IProcessDataEvent, IProcessPropertyMap, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, PosixShellType, ProcessPropertyType, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalSettingId, TerminalShellType, TitleEventSource, WindowsShellType } from 'vs/platform/terminal/common/terminal'; -import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; +import { escapeNonWindowsPath, collapseTildePath } from 'vs/platform/terminal/common/terminalEnvironment'; import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -357,6 +357,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private readonly _terminalHasFixedWidth: IContextKey, private readonly _terminalShellTypeContextKey: IContextKey, private readonly _terminalAltBufferActiveContextKey: IContextKey, + private readonly _terminalInRunCommandPicker: IContextKey, private readonly _configHelper: TerminalConfigHelper, private _shellLaunchConfig: IShellLaunchConfig, resource: URI | undefined, @@ -700,7 +701,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Write initial text, deferring onLineFeed listener when applicable to avoid firing // onLineData events containing initialText if (this._shellLaunchConfig.initialText) { - this.xterm.raw.writeln(this._shellLaunchConfig.initialText, () => { + this._writeInitialText(this.xterm, () => { lineDataEventAddon.onLineData(e => this._onLineData.fire(e)); }); } else { @@ -823,7 +824,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._linkManager.openRecentLink(type); } - async runRecent(type: 'command' | 'cwd'): Promise { + async runRecent(type: 'command' | 'cwd', filterMode?: 'fuzzy' | 'contiguous', value?: string): Promise { if (!this.xterm) { return; } @@ -853,7 +854,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (label.length === 0 || commandMap.has(label)) { continue; } - let description = `${entry.cwd}`; + let description = collapseTildePath(entry.cwd, this._userHome, this._processManager?.os === OperatingSystem.Windows ? '\\' : '/'); if (entry.exitCode) { // Since you cannot get the last command's exit code on pwsh, just whether it failed // or not, -1 is treated specially as simply failed @@ -970,44 +971,67 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } const outputProvider = this._instantiationService.createInstance(TerminalOutputProvider); const quickPick = this._quickInputService.createQuickPick(); - quickPick.items = items; + const originalItems = items; + quickPick.items = [...originalItems]; quickPick.sortByLabel = false; quickPick.placeholder = placeholder; - return new Promise(r => { - quickPick.onDidTriggerItemButton(async e => { - if (e.button === removeFromCommandHistoryButton) { - if (type === 'command') { - this._instantiationService.invokeFunction(getCommandHistory)?.remove(e.item.label); - } else { - this._instantiationService.invokeFunction(getDirectoryHistory)?.remove(e.item.label); - } + quickPick.customButton = true; + quickPick.matchOnLabelMode = filterMode || 'contiguous'; + if (filterMode === 'fuzzy') { + quickPick.customLabel = nls.localize('terminal.contiguousSearch', 'Use Contiguous Search'); + quickPick.onDidCustom(() => { + quickPick.hide(); + this.runRecent(type, 'contiguous', quickPick.value); + }); + } else { + quickPick.customLabel = nls.localize('terminal.fuzzySearch', 'Use Fuzzy Search'); + quickPick.onDidCustom(() => { + quickPick.hide(); + this.runRecent(type, 'fuzzy', quickPick.value); + }); + } + quickPick.onDidTriggerItemButton(async e => { + if (e.button === removeFromCommandHistoryButton) { + if (type === 'command') { + this._instantiationService.invokeFunction(getCommandHistory)?.remove(e.item.label); } else { - const selectedCommand = (e.item as Item).command; - const output = selectedCommand?.getOutput(); - if (output && selectedCommand?.command) { - const textContent = await outputProvider.provideTextContent(URI.from( - { - scheme: TerminalOutputProvider.scheme, - path: `${selectedCommand.command}... ${fromNow(selectedCommand.timestamp, true)}`, - fragment: output, - query: `terminal-output-${selectedCommand.timestamp}-${this.instanceId}` - })); - if (textContent) { - await this._editorService.openEditor({ - resource: textContent.uri - }); - } + this._instantiationService.invokeFunction(getDirectoryHistory)?.remove(e.item.label); + } + } else { + const selectedCommand = (e.item as Item).command; + const output = selectedCommand?.getOutput(); + if (output && selectedCommand?.command) { + const textContent = await outputProvider.provideTextContent(URI.from( + { + scheme: TerminalOutputProvider.scheme, + path: `${selectedCommand.command}... ${fromNow(selectedCommand.timestamp, true)}`, + fragment: output, + query: `terminal-output-${selectedCommand.timestamp}-${this.instanceId}` + })); + if (textContent) { + await this._editorService.openEditor({ + resource: textContent.uri + }); } } - quickPick.hide(); - }); - quickPick.onDidAccept(() => { - const result = quickPick.activeItems[0]; - this.sendText(type === 'cwd' ? `cd ${result.label}` : result.label, !quickPick.keyMods.alt); - quickPick.hide(); - }); + } + quickPick.hide(); + }); + quickPick.onDidAccept(() => { + const result = quickPick.activeItems[0]; + this.sendText(type === 'cwd' ? `cd ${result.label}` : result.label, !quickPick.keyMods.alt); + quickPick.hide(); + }); + if (value) { + quickPick.value = value; + } + return new Promise(r => { quickPick.show(); - quickPick.onDidHide(() => r()); + this._terminalInRunCommandPicker.set(true); + quickPick.onDidHide(() => { + this._terminalInRunCommandPicker.set(false); + r(); + }); }); } @@ -1441,9 +1465,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } async sendText(text: string, addNewLine: boolean): Promise { + // Apply bracketed paste sequences if the terminal has the mode enabled, this will prevent + // the text from triggering keybindings https://github.com/microsoft/vscode/issues/153592 + if (this.xterm?.raw.modes.bracketedPasteMode) { + text = `\x1b[200~${text}\x1b[201~`; + } + // Normalize line endings to 'enter' press. text = text.replace(/\r?\n/g, '\r'); - if (addNewLine && text.substr(text.length - 1) !== '\r') { + if (addNewLine && text[text.length - 1] !== '\r') { text += '\r'; } @@ -1684,7 +1714,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const parsedExitResult = parseExitResult(exitCodeOrError, this.shellLaunchConfig, this._processManager.processState, this._initialCwd); - if (this._usedShellIntegrationInjection && (this._processManager.processState === ProcessState.KilledDuringLaunch || this._processManager.processState === ProcessState.KilledByProcess)) { + if (this._usedShellIntegrationInjection && this._processManager.processState === ProcessState.KilledDuringLaunch && parsedExitResult?.code !== 0) { this._relaunchWithShellIntegrationDisabled(parsedExitResult?.message); this._onExit.fire(exitCodeOrError); return; @@ -1807,29 +1837,49 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + private _writeInitialText(xterm: XtermTerminal, callback?: () => void): void { + if (!this._shellLaunchConfig.initialText) { + callback?.(); + return; + } + const text = typeof this._shellLaunchConfig.initialText === 'string' + ? this._shellLaunchConfig.initialText + : this._shellLaunchConfig.initialText?.text; + if (typeof this._shellLaunchConfig.initialText === 'string') { + xterm.raw.writeln(text, callback); + } else { + if (this._shellLaunchConfig.initialText.trailingNewLine) { + xterm.raw.writeln(text, callback); + } else { + xterm.raw.write(text, callback); + } + } + } + async reuseTerminal(shell: IShellLaunchConfig, reset: boolean = false): Promise { // Unsubscribe any key listener we may have. this._pressAnyKeyToCloseListener?.dispose(); this._pressAnyKeyToCloseListener = undefined; - if (this.xterm) { + const xterm = this.xterm; + if (xterm) { if (!reset) { // Ensure new processes' output starts at start of new line - await new Promise(r => this.xterm!.raw.write('\n\x1b[G', r)); + await new Promise(r => xterm.raw.write('\n\x1b[G', r)); } // Print initialText if specified if (shell.initialText) { - await new Promise(r => this.xterm!.raw.writeln(shell.initialText!, r)); + await new Promise(r => this._writeInitialText(xterm, r)); } // Clean up waitOnExit state if (this._isExiting && this._shellLaunchConfig.waitOnExit) { - this.xterm.raw.options.disableStdin = false; + xterm.raw.options.disableStdin = false; this._isExiting = false; } if (reset) { - this.xterm.clearDecorations(); + xterm.clearDecorations(); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index c9da9557b27..a7732ab7c62 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -23,6 +23,7 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst private _terminalHasFixedWidth: IContextKey; private _terminalShellTypeContextKey: IContextKey; private _terminalAltBufferActiveContextKey: IContextKey; + private _terminalInRunCommandPicker: IContextKey; private _configHelper: TerminalConfigHelper; private readonly _onDidCreateInstance = new Emitter(); @@ -37,6 +38,7 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst this._terminalHasFixedWidth = TerminalContextKeys.terminalHasFixedWidth.bindTo(this._contextKeyService); this._terminalShellTypeContextKey = TerminalContextKeys.shellType.bindTo(this._contextKeyService); this._terminalAltBufferActiveContextKey = TerminalContextKeys.altBufferActive.bindTo(this._contextKeyService); + this._terminalInRunCommandPicker = TerminalContextKeys.inTerminalRunCommandPicker.bindTo(this._contextKeyService); this._configHelper = _instantiationService.createInstance(TerminalConfigHelper); } @@ -49,6 +51,7 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst this._terminalHasFixedWidth, this._terminalShellTypeContextKey, this._terminalAltBufferActiveContextKey, + this._terminalInRunCommandPicker, this._configHelper, shellLaunchConfig, resource diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 5183542cb14..c6e6700e280 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -417,6 +417,7 @@ export function setupTerminalMenus(): void { order: 2, when: ContextKeyExpr.and( ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), + ContextKeyExpr.notEquals(`config.${TerminalSettingId.TabsHideCondition}`, 'never'), ContextKeyExpr.or( ContextKeyExpr.not(`config.${TerminalSettingId.TabsEnabled}`), ContextKeyExpr.and( @@ -451,6 +452,7 @@ export function setupTerminalMenus(): void { order: 3, when: ContextKeyExpr.and( ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), + ContextKeyExpr.notEquals(`config.${TerminalSettingId.TabsHideCondition}`, 'never'), ContextKeyExpr.or( ContextKeyExpr.not(`config.${TerminalSettingId.TabsEnabled}`), ContextKeyExpr.and( diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 72f1792fc71..1d292d45536 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -31,6 +31,7 @@ export const enum TerminalContextKeyStrings { TabsSingularSelection = 'terminalTabsSingularSelection', SplitTerminal = 'terminalSplitTerminal', ShellType = 'terminalShellType', + InTerminalRunCommandPicker = 'inTerminalRunCommandPicker', } export namespace TerminalContextKeys { @@ -119,4 +120,7 @@ export namespace TerminalContextKeys { /** Whether the focused tab's terminal is a split terminal. */ export const splitTerminal = new RawContextKey(TerminalContextKeyStrings.SplitTerminal, false, localize('isSplitTerminalContextKey', "Whether the focused tab's terminal is a split terminal.")); + + /** Whether the terminal run command picker is currently open. */ + export const inTerminalRunCommandPicker = new RawContextKey(TerminalContextKeyStrings.InTerminalRunCommandPicker, false, localize('inTerminalRunCommandPickerContextKey', "Whether the terminal run command picker is currently open.")); } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 52d57770b21..e9d136fdc9f 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -35,7 +35,7 @@ const testFilterDescriptions: { [K in TestFilterTerm]: string } = { export class TestingExplorerFilter extends BaseActionViewItem { private input!: SuggestEnabledInputWithHistory; private wrapper!: HTMLDivElement; - private readonly history: StoredValue = this.instantiationService.createInstance(StoredValue, { + private readonly history: StoredValue<{ values: string[]; lastValue: string } | string[]> = this.instantiationService.createInstance(StoredValue, { key: 'testing.filterHistory2', scope: StorageScope.WORKSPACE, target: StorageTarget.USER @@ -65,9 +65,12 @@ export class TestingExplorerFilter extends BaseActionViewItem { const wrapper = this.wrapper = dom.$('.testing-filter-wrapper'); container.appendChild(wrapper); - const history = this.history.get([]); - if (history.length) { - this.state.setText(history[history.length - 1]); + let history = this.history.get({ lastValue: '', values: [] }); + if (history instanceof Array) { + history = { lastValue: '', values: history }; + } + if (history.lastValue) { + this.state.setText(history.lastValue); } const input = this.input = this._register(this.instantiationService.createInstance(ContextScopedSuggestEnabledInputWithHistory, { @@ -94,7 +97,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { value: this.state.text.value, placeholderText: localize('testExplorerFilter', "Filter (e.g. text, !exclude, @tag)"), }, - history + history: history.values })); this._register(attachSuggestEnabledInputBoxStyler(input, this.themeService)); @@ -145,12 +148,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { * Persists changes to the input history. */ public saveState() { - const history = this.input.getHistory(); - if (history.length) { - this.history.store(history); - } else { - this.history.delete(); - } + this.history.store({ lastValue: this.input.getValue(), values: this.input.getHistory() }); } /** diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 38363be6614..f5878699fdf 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -56,6 +56,7 @@ import { PolicyChannelClient } from 'vs/platform/policy/common/policyIpc'; import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; export class DesktopMain extends Disposable { @@ -184,6 +185,9 @@ export class DesktopMain extends Disposable { if (logService.getLevel() === LogLevel.Trace) { logService.trace('workbench#open(): with configuration', safeStringify(this.configuration)); } + if (process.sandboxed) { + logService.info('Electron sandbox mode is enabled!'); + } // Shared Process const sharedProcessService = new SharedProcessService(this.configuration.windowId, logService); diff --git a/src/vs/workbench/services/editor/browser/codeEditorService.ts b/src/vs/workbench/services/editor/browser/codeEditorService.ts index 0aaf3980f4f..492e5077def 100644 --- a/src/vs/workbench/services/editor/browser/codeEditorService.ts +++ b/src/vs/workbench/services/editor/browser/codeEditorService.ts @@ -24,6 +24,9 @@ export class CodeEditorService extends AbstractCodeEditorService { @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(themeService); + + this.registerCodeEditorOpenHandler(this.doOpenCodeEditor.bind(this)); + this.registerCodeEditorOpenHandler(this.doOpenCodeEditorFromDiff.bind(this)); } getActiveCodeEditor(): ICodeEditor | null { @@ -44,7 +47,7 @@ export class CodeEditorService extends AbstractCodeEditorService { return null; } - async openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { + private async doOpenCodeEditorFromDiff(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { // Special case: If the active editor is a diff editor and the request to open originates and // targets the modified side of it, we just apply the request there to prevent opening the modified @@ -66,10 +69,10 @@ export class CodeEditorService extends AbstractCodeEditorService { return targetEditor; } - // Open using our normal editor service - return this.doOpenCodeEditor(input, source, sideBySide); + return null; } + // Open using our normal editor service private async doOpenCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { // Special case: we want to detect the request to open an editor that diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index aae30a8d46e..2e19d0d0cbf 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -47,7 +47,7 @@ export const enum GroupsArrangement { * Make the current active group consume the maximum * amount of space possible. */ - MINIMIZE_OTHERS, + MAXIMIZE, /** * Size all groups evenly. diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index e4da27e5ebc..f193e7d69b0 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -1661,7 +1661,7 @@ suite('EditorService', () => { editor = await service.openEditor(input2, { pinned: true, activation: EditorActivation.ACTIVATE }, sideGroup); assert.strictEqual(part.activeGroup, sideGroup); - part.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); + part.arrangeGroups(GroupsArrangement.MAXIMIZE); editor = await service.openEditor(input1, { pinned: true, preserveFocus: true, activation: EditorActivation.RESTORE }, rootGroup); assert.strictEqual(part.activeGroup, sideGroup); }); @@ -1681,13 +1681,13 @@ suite('EditorService', () => { assert.strictEqual(part.activeGroup, sideGroup); assert.notStrictEqual(rootGroup, sideGroup); - part.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS, part.activeGroup); + part.arrangeGroups(GroupsArrangement.MAXIMIZE, part.activeGroup); await rootGroup.closeEditor(input2); assert.strictEqual(part.activeGroup, sideGroup); - assert.strictEqual(rootGroup.isMinimized, true); - assert.strictEqual(part.activeGroup.isMinimized, false); + assert(!part.isGroupMaximized(rootGroup)); + assert(part.isGroupMaximized(part.activeGroup)); }); test('active editor change / visible editor change events', async function () { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index f7e9543fb3f..1a2209c0e43 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -853,7 +853,6 @@ export class TestEditorGroupView implements IEditorGroupView { titleHeight!: IEditorGroupTitleHeight; isEmpty = true; - isMinimized = false; onWillDispose: Event = Event.None; onDidModelChange: Event = Event.None; diff --git a/yarn.lock b/yarn.lock index 9c823e24f88..307f8ca2f24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12112,20 +12112,20 @@ xterm-addon-unicode11@0.4.0-beta.3: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa" integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q== -xterm-addon-webgl@0.13.0-beta.2: - version "0.13.0-beta.2" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.2.tgz#f58a7a3641ad7c8ac82dd24cfb0165656ed9ac1c" - integrity sha512-98tX0BkpD402RoCO6SyikUXpzCn9/OQhlXsRmM/kRFCxMWWofStWTXzCPhN0MjIx2IdGueDjCmnShhidwihErg== +xterm-addon-webgl@0.13.0-beta.3: + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.3.tgz#2b456c3105238e64b40a30787d6335f5f6f85abb" + integrity sha512-DFGcXAolA0VTsOLIKcORxUOp/FTJdD/YiRzKVLARjgOycwVRKvW2L5Tge8Z7ysZ16sKfnV2vCXyonXYfUWozXw== -xterm-headless@4.20.0-beta.5: - version "4.20.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.5.tgz#edcff27eb6437d158e6aea2ed7658e783bee5641" - integrity sha512-8SnVUsuNUrQ5P0XU/9Iau3uK7Tf8q/p0KHHwkwJXVxZDIlaDH9XKSs91U9BjJJE3sJgRxH4NSiDYR3vFLSFpxw== +xterm-headless@4.20.0-beta.6: + version "4.20.0-beta.6" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.6.tgz#bd016379e9fac47e5b8870d567cdf330cf6f49fc" + integrity sha512-EV0V7pxMKI0OEcOCD+6vdXq6rBARr7dSN3PovTsZnDWg5dmvUb2eEmz6BTejJj3UVd/JXNEmEXM+tCh97rDCDg== -xterm@4.20.0-beta.5: - version "4.20.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.5.tgz#d707b0dcb477a554135fb767b24003fced079866" - integrity sha512-KBWfk9UPBKRy662DVGGTZEcW1becEjYvlyWbn2hLj9h2gy6Q4EEEEbggJh8I7SGwdFizl+apHQGhEOZmFCA70w== +xterm@4.20.0-beta.6: + version "4.20.0-beta.6" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.6.tgz#3ed87ba383a5cf44284098278f714df7113e3e3c" + integrity sha512-xJd6vyOuYo4Ht/hTY3DyXGIj0U6kHjr2vWQ1lRmearo3t7QKf7uqOAAfTLeWt/g1P8qe/r0DnsNTeag6vI9RVw== y18n@^3.2.1: version "3.2.2"