Merge branch 'main' into proposal-130231

This commit is contained in:
Daniel Imms
2022-07-10 07:40:11 -07:00
committed by GitHub
60 changed files with 771 additions and 416 deletions
+5 -1
View File
@@ -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()]);
+8 -2
View File
@@ -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<Translations> {
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<Translation> => r.status === 'fulfilled')
.map(r => r.value);
}
async function main() {
@@ -10,7 +10,7 @@
"engines": {
"vscode": "^1.20.0"
},
"main": "./out/extension.node",
"main": "./out/extension",
"browser": "./dist/browser/extension",
"categories": [
"Programming Languages"
+4 -4
View File
@@ -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"
},
+3 -3
View File
@@ -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"
},
+2 -2
View File
@@ -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"
}
}
+8 -8
View File
@@ -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==
+12 -12
View File
@@ -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"
+7 -2
View File
@@ -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 {
+13
View File
@@ -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<Event | undefined>());
@@ -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 {
+10
View File
@@ -527,6 +527,16 @@ export class Grid<T extends IView = IView> 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}.
*
+25
View File
@@ -592,6 +592,10 @@ class BranchNode implements ISplitView<ILayoutContext>, 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}.
@@ -931,6 +931,23 @@ export class SplitView<TLayoutContext = undefined> 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}.
*/
+1 -1
View File
@@ -52,7 +52,7 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> {
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/;
@@ -450,6 +450,7 @@ class QuickPick<T extends IQuickPickItem> 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<T extends IQuickPickItem> 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<T extends IQuickPickItem> 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;
@@ -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<boolean>();
@@ -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 || [];
@@ -292,6 +292,13 @@ export interface IQuickPick<T extends IQuickPickItem> 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;
+2 -2
View File
@@ -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()}`);
}
}
});
@@ -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<string, IModelDecorationOptionsProvider>();
private readonly _editorStyleSheets = new Map<string, RefCountedStyleSheet>();
private readonly _codeEditorOpenHandlers = new LinkedList<ICodeEditorOpenHandler>();
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<ICodeEditor | null>;
async openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
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 {
@@ -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<ICodeEditorService>('codeEditorService');
@@ -53,4 +54,9 @@ export interface ICodeEditorService {
getActiveCodeEditor(): ICodeEditor | null;
openCodeEditor(input: ITextResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null>;
registerCodeEditorOpenHandler(handler: ICodeEditorOpenHandler): IDisposable;
}
export interface ICodeEditorOpenHandler {
(input: ITextResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null>;
}
@@ -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<ICodeEditor | null> {
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);
@@ -22,7 +22,7 @@ export class TestCodeEditorService extends AbstractCodeEditorService {
return null;
}
public lastInput?: IResourceEditorInput;
openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
override openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
this.lastInput = input;
return Promise.resolve(null);
}
@@ -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
+4 -4
View File
@@ -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.
@@ -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)}`;
}
+1 -1
View File
@@ -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();
@@ -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');
});
});
});
@@ -184,7 +184,7 @@ export interface IUserDataSyncStoreClient {
delete(resource: ServerResource, ref: string | null): Promise<void>;
getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]>;
resolveContent(resource: ServerResource, ref: string): Promise<string | null>;
resolveContent(resource: ServerResource, ref: string, headers?: IHeaders): Promise<string | null>;
}
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
@@ -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<string | null> {
async resolveContent(resource: ServerResource, ref: string, headers: IHeaders = {}): Promise<string | null> {
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);
@@ -135,8 +135,6 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito
readonly titleHeight: IEditorGroupTitleHeight;
readonly isMinimized: boolean;
readonly disposed: boolean;
setActive(isActive: boolean): void;
@@ -573,24 +573,31 @@ abstract class AbstractCloseAllAction extends Action {
override async run(): Promise<void> {
// Depending on the editor and auto save configuration,
// split dirty editors into buckets
// split editors into buckets for handling confirmation
const dirtyEditorsWithDefaultConfirm = new Set<IEditorIdentifier>();
const dirtyAutoSaveOnFocusChangeEditors = new Set<IEditorIdentifier>();
const dirtyAutoSaveOnWindowChangeEditors = new Set<IEditorIdentifier>();
const dirtyEditorsWithCustomConfirm = new Map<string /* typeId */, Set<IEditorIdentifier>>();
const editorsWithCustomConfirm = new Map<string /* typeId */, Set<IEditorIdentifier>>();
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<IEditorIdentifier>): Promise<void> {
private async revealEditorsToConfirm(editors: ReadonlyArray<IEditorIdentifier>): Promise<void> {
try {
const handledGroups = new Set<GroupIdentifier>();
for (const { editor, groupId } of editors) {
@@ -1016,7 +1023,7 @@ export class MinimizeOtherGroupsAction extends Action {
}
override async run(): Promise<void> {
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);
}
}
}
@@ -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<boolean> {
return this.doCloseEditorWithDirtyHandling(editor, options);
return this.doCloseEditorWithConfirmationHandling(editor, options);
}
private async doCloseEditorWithDirtyHandling(editor: EditorInput | undefined = this.activeEditor || undefined, options?: ICloseEditorOptions, internalOptions?: IInternalEditorCloseOptions): Promise<boolean> {
private async doCloseEditorWithConfirmationHandling(editor: EditorInput | undefined = this.activeEditor || undefined, options?: ICloseEditorOptions, internalOptions?: IInternalEditorCloseOptions): Promise<boolean> {
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<boolean /* veto */> {
private async handleCloseConfirmation(editors: EditorInput[]): Promise<boolean /* veto */> {
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<boolean /* veto */> {
if (!editor.isDirty() || editor.isSaving()) {
return false; // editor must be dirty and not saving
private async doHandleCloseConfirmation(editor: EditorInput, options?: { skipAutoSave: boolean }): Promise<boolean /* veto */> {
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 });
}
}
@@ -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);
}
}
}
+31 -14
View File
@@ -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<IEditorIdentifier>): Promise<ConfirmResult>;
}
/**
* 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<IEditorIdentifier>): Promise<ConfirmResult>;
/**
* Saves the editor. The provided groupId helps implementors
* to e.g. preserve view state of the editor and re-open it
@@ -403,8 +403,7 @@ class DocumentSymbolsOutlineCreator implements IOutlineCreator<IEditorPane, Docu
readonly dispose: () => 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<IEditorPane, Docu
return undefined;
}
const firstLoadBarrier = new Barrier();
const result = this._instantiationService.createInstance(DocumentSymbolsOutline, editor, target, firstLoadBarrier);
const result = editor.invokeWithinContext(accessor => accessor.get(IInstantiationService).createInstance(DocumentSymbolsOutline, editor!, target, firstLoadBarrier));
await firstLoadBarrier.wait();
return result;
}
@@ -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<void> {
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<void> {
if (ref !== undefined) {
this.logService.info(`Applying edit session with ref ${ref}.`);
}
async resumeEditSession(ref?: string): Promise<void> {
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."));
}
}
@@ -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();
}
}
}));
}
@@ -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);
@@ -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"),
});
@@ -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}.",
@@ -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<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
@@ -47,3 +49,8 @@ registerAction2(ToggleActiveConflictInput2);
registerAction2(CompareInput1WithBaseCommand);
registerAction2(CompareInput2WithBaseCommand);
Registry
.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(MergeEditorOpenHandlerContribution, LifecyclePhase.Restored);
@@ -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<IEditorIdentifier>): Promise<ConfirmResult> {
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<ConfirmResult> {
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
}
@@ -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<IMergeEditorViewState> {
private readonly _sessionDisposables = new DisposableStore();
private _grid!: Grid<IView>;
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<boolean>;
@@ -103,7 +103,7 @@ export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> {
}
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<IMergeEditorViewState> {
@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<IMergeEditorViewState> {
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<IMergeEditorViewState> {
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<IMergeEditorViewState> {
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<IMergeEditorViewState> {
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<IMergeEditorViewState> {
}
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<IMergeEditorViewState> {
}
}
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<ICodeEditor | null> {
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 = <ICodeEditor>activePane.getControl()!;
applyTextEditorOptions(input.options, targetEditor, ScrollType.Smooth);
return targetEditor;
}
// cannot handle this
return null;
}
}
type IMergeEditorViewState = ICodeEditorViewState & {
readonly input1State?: ICodeEditorViewState;
readonly input2State?: ICodeEditorViewState;
@@ -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({
@@ -275,6 +275,20 @@ export class SearchEditor extends AbstractTextCodeEditor<SearchEditorViewState>
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) {
@@ -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
};
}
}
@@ -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
@@ -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<ITerminalInstance>());
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<boolean>;
private _group: IEditorGroup | undefined;
protected readonly _onDidRequestAttach = this._register(new Emitter<ITerminalInstance>());
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<IEditorIdentifier>): Promise<ConfirmResult> {
async confirm(terminals?: ReadonlyArray<IEditorIdentifier>): Promise<ConfirmResult> {
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())
];
@@ -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<boolean>,
private readonly _terminalShellTypeContextKey: IContextKey<string>,
private readonly _terminalAltBufferActiveContextKey: IContextKey<boolean>,
private readonly _terminalInRunCommandPicker: IContextKey<boolean>,
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<void> {
async runRecent(type: 'command' | 'cwd', filterMode?: 'fuzzy' | 'contiguous', value?: string): Promise<void> {
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<void>(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<void>(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<void> {
// 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<void> {
// 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<void>(r => this.xterm!.raw.write('\n\x1b[G', r));
await new Promise<void>(r => xterm.raw.write('\n\x1b[G', r));
}
// Print initialText if specified
if (shell.initialText) {
await new Promise<void>(r => this.xterm!.raw.writeln(shell.initialText!, r));
await new Promise<void>(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();
}
}
@@ -23,6 +23,7 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst
private _terminalHasFixedWidth: IContextKey<boolean>;
private _terminalShellTypeContextKey: IContextKey<string>;
private _terminalAltBufferActiveContextKey: IContextKey<boolean>;
private _terminalInRunCommandPicker: IContextKey<boolean>;
private _configHelper: TerminalConfigHelper;
private readonly _onDidCreateInstance = new Emitter<ITerminalInstance>();
@@ -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
@@ -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(
@@ -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<boolean>(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<boolean>(TerminalContextKeyStrings.InTerminalRunCommandPicker, false, localize('inTerminalRunCommandPickerContextKey', "Whether the terminal run command picker is currently open."));
}
@@ -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<string[]> = 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() });
}
/**
@@ -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);
@@ -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<ICodeEditor | null> {
private async doOpenCodeEditorFromDiff(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
// 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<ICodeEditor | null> {
// Special case: we want to detect the request to open an editor that
@@ -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.
@@ -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 () {
@@ -853,7 +853,6 @@ export class TestEditorGroupView implements IEditorGroupView {
titleHeight!: IEditorGroupTitleHeight;
isEmpty = true;
isMinimized = false;
onWillDispose: Event<void> = Event.None;
onDidModelChange: Event<IGroupModelChangeEvent> = Event.None;
+12 -12
View File
@@ -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"