Merge branch 'master' into sandy081/moveProblemsToView

This commit is contained in:
Sandeep Somavarapu
2019-12-18 12:13:25 +01:00
51 changed files with 445 additions and 342 deletions
+4
View File
@@ -58,6 +58,10 @@
"name": "vs/workbench/contrib/emmet",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/experiments",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/extensions",
"project": "vscode-workbench"
@@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { QuickOpenModel, QuickOpenEntry, QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { DataSource } from 'vs/base/parts/quickopen/browser/quickOpenViewer';
@@ -28,7 +29,7 @@ suite('QuickOpen', () => {
assert.equal(entry2, model.getEntries(true)[0]);
});
test('QuickOpenDataSource', () => {
test('QuickOpenDataSource', async () => {
const model = new QuickOpenModel();
const entry1 = new QuickOpenEntry();
@@ -42,8 +43,7 @@ suite('QuickOpen', () => {
assert.equal(true, ds.hasChildren(null!, model));
assert.equal(false, ds.hasChildren(null!, entry1));
ds.getChildren(null!, model).then((children: any[]) => {
assert.equal(3, children.length);
});
const children = await ds.getChildren(null!, model);
assert.equal(3, children.length);
});
});
});
@@ -833,4 +833,4 @@ suite('Quick Open Scorer', () => {
assert.equal(scorer.prepareQuery('ModelTester.ts').containsPathSeparator, false);
assert.equal(scorer.prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true);
});
});
});
+2 -1
View File
@@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
@@ -28,4 +29,4 @@ suite('ProgressBar', () => {
bar.dispose();
});
});
});
+1
View File
@@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ok } from 'vs/base/common/assert';
@@ -6,7 +6,6 @@
import * as assert from 'assert';
import * as collections from 'vs/base/common/collections';
suite('Collections', () => {
test('forEach', () => {
+1 -1
View File
@@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as extpath from 'vs/base/common/extpath';
import * as platform from 'vs/base/common/platform';
@@ -28,7 +29,6 @@ suite('Paths', () => {
assert.equal(extpath.getRoot('http://www/'), 'http://www/');
assert.equal(extpath.getRoot('file:///foo'), 'file:///');
assert.equal(extpath.getRoot('file://foo'), '');
});
test('isUNC', () => {
+1
View File
@@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { guessMimeTypes, registerTextMime, suggestFilename } from 'vs/base/common/mime';
import { URI } from 'vs/base/common/uri';
+2
View File
@@ -2,10 +2,12 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as types from 'vs/base/common/types';
suite('Types', () => {
test('isFunction', () => {
assert(!types.isFunction(undefined));
assert(!types.isFunction(null));
@@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as os from 'os';
import * as path from 'vs/base/common/path';
@@ -44,16 +44,16 @@ suite('BackupMainService', () => {
this.workspacesJsonPath = backupWorkspacesPath;
}
public toBackupPath(arg: URI | string): string {
toBackupPath(arg: URI | string): string {
const id = arg instanceof URI ? super.getFolderHash(arg) : arg;
return path.join(this.backupHome, id);
}
public getFolderHash(folderUri: URI): string {
getFolderHash(folderUri: URI): string {
return super.getFolderHash(folderUri);
}
public toLegacyBackupPath(folderPath: string): string {
toLegacyBackupPath(folderPath: string): string {
return path.join(this.backupHome, super.getLegacyFolderHash(folderPath));
}
}
@@ -119,17 +119,16 @@ suite('BackupMainService', () => {
let service: TestBackupMainService;
let configService: TestConfigurationService;
setup(() => {
setup(async () => {
// Delete any existing backups completely and then re-create it.
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE).then(() => {
return pfs.mkdirp(backupHome);
}).then(() => {
configService = new TestConfigurationService();
service = new TestBackupMainService(backupHome, backupWorkspacesPath, configService);
await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
await pfs.mkdirp(backupHome);
return service.initialize();
});
configService = new TestConfigurationService();
service = new TestBackupMainService(backupHome, backupWorkspacesPath, configService);
return service.initialize();
});
teardown(() => {
@@ -591,71 +590,71 @@ suite('BackupMainService', () => {
});
});
test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (folder workspace)', () => {
test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (folder workspace)', async () => {
service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]);
});
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]);
});
test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (root workspace)', () => {
test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (root workspace)', async () => {
const upperFooPath = fooFile.fsPath.toUpperCase();
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath));
assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [URI.file(upperFooPath)]);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]);
});
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]);
});
suite('removeBackupPathSync', () => {
test('should remove folder workspaces from workspaces.json (folder workspace)', () => {
test('should remove folder workspaces from workspaces.json (folder workspace)', async () => {
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
service.unregisterFolderBackupSync(fooFile);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]);
service.unregisterFolderBackupSync(barFile);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => {
const json2 = <IBackupWorkspacesFormat>JSON.parse(content);
assert.deepEqual(json2.folderURIWorkspaces, []);
});
});
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]);
service.unregisterFolderBackupSync(barFile);
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json2.folderURIWorkspaces, []);
});
test('should remove folder workspaces from workspaces.json (root workspace)', () => {
test('should remove folder workspaces from workspaces.json (root workspace)', async () => {
const ws1 = toWorkspaceBackupInfo(fooFile.fsPath);
service.registerWorkspaceBackupSync(ws1);
const ws2 = toWorkspaceBackupInfo(barFile.fsPath);
service.registerWorkspaceBackupSync(ws2);
service.unregisterWorkspaceBackupSync(ws1.workspace);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]);
service.unregisterWorkspaceBackupSync(ws2.workspace);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => {
const json2 = <IBackupWorkspacesFormat>JSON.parse(content);
assert.deepEqual(json2.rootURIWorkspaces, []);
});
});
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]);
service.unregisterWorkspaceBackupSync(ws2.workspace);
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json2.rootURIWorkspaces, []);
});
test('should remove empty workspaces from workspaces.json', () => {
test('should remove empty workspaces from workspaces.json', async () => {
service.registerEmptyWindowBackupSync('foo');
service.registerEmptyWindowBackupSync('bar');
service.unregisterEmptyWindowBackupSync('foo');
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.emptyWorkspaces, ['bar']);
service.unregisterEmptyWindowBackupSync('bar');
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => {
const json2 = <IBackupWorkspacesFormat>JSON.parse(content);
assert.deepEqual(json2.emptyWorkspaces, []);
});
});
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.emptyWorkspaces, ['bar']);
service.unregisterEmptyWindowBackupSync('bar');
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json2.emptyWorkspaces, []);
});
test('should fail gracefully when removing a path that doesn\'t exist', async () => {
+3 -3
View File
@@ -33,10 +33,10 @@ export interface ITunnelService {
readonly tunnels: Promise<readonly RemoteTunnel[]>;
readonly onTunnelOpened: Event<RemoteTunnel>;
readonly onTunnelClosed: Event<number>;
readonly onTunnelClosed: Event<{ host: string, port: number }>;
openTunnel(remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
closeTunnel(remotePort: number): Promise<void>;
openTunnel(remoteHost: string | undefined, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
closeTunnel(remoteHost: string, remotePort: number): Promise<void>;
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable;
}
@@ -13,12 +13,12 @@ export class NoOpTunnelService implements ITunnelService {
public readonly tunnels: Promise<readonly RemoteTunnel[]> = Promise.resolve([]);
private _onTunnelOpened: Emitter<RemoteTunnel> = new Emitter();
public onTunnelOpened: Event<RemoteTunnel> = this._onTunnelOpened.event;
private _onTunnelClosed: Emitter<number> = new Emitter();
public onTunnelClosed: Event<number> = this._onTunnelClosed.event;
openTunnel(_remotePort: number): Promise<RemoteTunnel> | undefined {
private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter();
public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event;
openTunnel(_remoteHost: string, _remotePort: number): Promise<RemoteTunnel> | undefined {
return undefined;
}
async closeTunnel(_remotePort: number): Promise<void> {
async closeTunnel(_remoteHost: string, _remotePort: number): Promise<void> {
}
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable {
throw new Error('Method not implemented.');
@@ -89,11 +89,11 @@ export class StorageMainService extends Disposable implements IStorageMainServic
private static readonly STORAGE_NAME = 'state.vscdb';
private readonly _onDidChangeStorage: Emitter<IStorageChangeEvent> = this._register(new Emitter<IStorageChangeEvent>());
readonly onDidChangeStorage: Event<IStorageChangeEvent> = this._onDidChangeStorage.event;
private readonly _onDidChangeStorage = this._register(new Emitter<IStorageChangeEvent>());
readonly onDidChangeStorage = this._onDidChangeStorage.event;
private readonly _onWillSaveState: Emitter<void> = this._register(new Emitter<void>());
readonly onWillSaveState: Event<void> = this._onWillSaveState.event;
private readonly _onWillSaveState = this._register(new Emitter<void>());
readonly onWillSaveState = this._onWillSaveState.event;
get items(): Map<string, string> { return this.storage.items; }
@@ -28,11 +28,11 @@ suite('StorageService', () => {
function removeData(scope: StorageScope): void {
const storage = new InMemoryStorageService();
storage.store('Monaco.IDE.Core.Storage.Test.remove', 'foobar', scope);
strictEqual('foobar', storage.get('Monaco.IDE.Core.Storage.Test.remove', scope, (undefined)!));
storage.store('test.remove', 'foobar', scope);
strictEqual('foobar', storage.get('test.remove', scope, (undefined)!));
storage.remove('Monaco.IDE.Core.Storage.Test.remove', scope);
ok(!storage.get('Monaco.IDE.Core.Storage.Test.remove', scope, (undefined)!));
storage.remove('test.remove', scope);
ok(!storage.get('test.remove', scope, (undefined)!));
}
test('Get Data, Integer, Boolean (global, in-memory)', () => {
@@ -46,34 +46,34 @@ suite('StorageService', () => {
function storeData(scope: StorageScope): void {
const storage = new InMemoryStorageService();
strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, 'foobar'), 'foobar');
strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, ''), '');
strictEqual(storage.getNumber('Monaco.IDE.Core.Storage.Test.getNumber', scope, 5), 5);
strictEqual(storage.getNumber('Monaco.IDE.Core.Storage.Test.getNumber', scope, 0), 0);
strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, true), true);
strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, false), false);
strictEqual(storage.get('test.get', scope, 'foobar'), 'foobar');
strictEqual(storage.get('test.get', scope, ''), '');
strictEqual(storage.getNumber('test.getNumber', scope, 5), 5);
strictEqual(storage.getNumber('test.getNumber', scope, 0), 0);
strictEqual(storage.getBoolean('test.getBoolean', scope, true), true);
strictEqual(storage.getBoolean('test.getBoolean', scope, false), false);
storage.store('Monaco.IDE.Core.Storage.Test.get', 'foobar', scope);
strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, (undefined)!), 'foobar');
storage.store('test.get', 'foobar', scope);
strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar');
storage.store('Monaco.IDE.Core.Storage.Test.get', '', scope);
strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, (undefined)!), '');
storage.store('test.get', '', scope);
strictEqual(storage.get('test.get', scope, (undefined)!), '');
storage.store('Monaco.IDE.Core.Storage.Test.getNumber', 5, scope);
strictEqual(storage.getNumber('Monaco.IDE.Core.Storage.Test.getNumber', scope, (undefined)!), 5);
storage.store('test.getNumber', 5, scope);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5);
storage.store('Monaco.IDE.Core.Storage.Test.getNumber', 0, scope);
strictEqual(storage.getNumber('Monaco.IDE.Core.Storage.Test.getNumber', scope, (undefined)!), 0);
storage.store('test.getNumber', 0, scope);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 0);
storage.store('Monaco.IDE.Core.Storage.Test.getBoolean', true, scope);
strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, (undefined)!), true);
storage.store('test.getBoolean', true, scope);
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), true);
storage.store('Monaco.IDE.Core.Storage.Test.getBoolean', false, scope);
strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, (undefined)!), false);
storage.store('test.getBoolean', false, scope);
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), false);
strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.getDefault', scope, 'getDefault'), 'getDefault');
strictEqual(storage.getNumber('Monaco.IDE.Core.Storage.Test.getNumberDefault', scope, 5), 5);
strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBooleanDefault', scope, true), true);
strictEqual(storage.get('test.getDefault', scope, 'getDefault'), 'getDefault');
strictEqual(storage.getNumber('test.getNumberDefault', scope, 5), 5);
strictEqual(storage.getBoolean('test.getBooleanDefault', scope, true), true);
}
function uniqueStorageDir(): string {
@@ -13,14 +13,14 @@ import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { IStateService } from 'vs/platform/state/node/state';
import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window';
import { ipcMain as ipc, screen, BrowserWindow, systemPreferences, MessageBoxOptions, Display } from 'electron';
import { ipcMain as ipc, screen, BrowserWindow, systemPreferences, MessageBoxOptions, Display, app } from 'electron';
import { parseLineAndColumnAware } from 'vs/code/node/paths';
import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, IPathsToWaitFor, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest } from 'vs/platform/windows/common/windows';
import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri } from 'vs/platform/windows/node/window';
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
import { Emitter } from 'vs/base/common/event';
import product from 'vs/platform/product/common/product';
import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
@@ -160,14 +160,16 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
private readonly windowsState: IWindowsState;
private lastClosedWindowState?: IWindowState;
private shuttingDown = false;
private readonly _onWindowReady = this._register(new Emitter<ICodeWindow>());
readonly onWindowReady: CommonEvent<ICodeWindow> = this._onWindowReady.event;
readonly onWindowReady = this._onWindowReady.event;
private readonly _onWindowClose = this._register(new Emitter<number>());
readonly onWindowClose: CommonEvent<number> = this._onWindowClose.event;
readonly onWindowClose = this._onWindowClose.event;
private readonly _onWindowsCountChanged = this._register(new Emitter<IWindowsCountChangedEvent>());
readonly onWindowsCountChanged: CommonEvent<IWindowsCountChangedEvent> = this._onWindowsCountChanged.event;
readonly onWindowsCountChanged = this._onWindowsCountChanged.event;
constructor(
private readonly machineId: string,
@@ -236,6 +238,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
systemPreferences.on('high-contrast-color-scheme-changed', () => onHighContrastChange());
}
// When a window looses focus, save all windows state. This allows to
// prevent loss of window-state data when OS is restarted without properly
// shutting down the application (https://github.com/microsoft/vscode/issues/87171)
app.on('browser-window-blur', () => {
if (!this.shuttingDown) {
this.saveWindowsState();
}
});
// Handle various lifecycle events around windows
this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window));
this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown());
@@ -292,6 +303,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
// - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0)
//
private onBeforeShutdown(): void {
this.shuttingDown = true;
this.saveWindowsState();
}
private saveWindowsState(): void {
const currentWindowsState: IWindowsState = {
openedWindows: [],
lastPluginDevelopmentHostWindow: this.windowsState.lastPluginDevelopmentHostWindow,
@@ -327,8 +344,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
// Persist
const state = getWindowsStateStoreData(currentWindowsState);
this.logService.trace('onBeforeShutdown', state);
this.stateService.setItem(WindowsMainService.windowsStateStorageKey, state);
if (this.shuttingDown) {
this.logService.trace('onBeforeShutdown', state);
}
}
// See note on #onBeforeShutdown() for details how these events are flowing
+2 -1
View File
@@ -79,7 +79,8 @@ declare module 'vscode' {
export namespace workspace {
/**
* Forwards a port. Currently only works for a remote host of localhost.
* Forwards a port. If the current resolver implements RemoteAuthorityResolver:forwardPort then that will be used to make the tunnel.
* By default, makeTunnel only support localhost; however, RemoteAuthorityResolver:forwardPort can be used to support other ips.
* @param forward The `localPort` is a suggestion only. If that port is not available another will be chosen.
*/
export function makeTunnel(forward: TunnelOptions): Thenable<Tunnel>;
@@ -22,15 +22,15 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape {
}
async $openTunnel(tunnelOptions: TunnelOptions): Promise<TunnelDto | undefined> {
const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remote.port, tunnelOptions.localPort, tunnelOptions.name);
const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remote, tunnelOptions.localPort, tunnelOptions.name);
if (tunnel) {
return TunnelDto.fromServiceTunnel(tunnel);
}
return undefined;
}
async $closeTunnel(remotePort: number): Promise<void> {
return this.remoteExplorerService.close(remotePort);
async $closeTunnel(remote: { host: string, port: number }): Promise<void> {
return this.remoteExplorerService.close(remote);
}
async $registerCandidateFinder(): Promise<void> {
@@ -327,7 +327,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
@IContextMenuService contextMenuService: IContextMenuService,
@IExtensionService extensionService: IExtensionService,
) {
super(id, `${id}.state`, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
super(id, `${id}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
}
}
@@ -774,7 +774,7 @@ export interface MainThreadWindowShape extends IDisposable {
export interface MainThreadTunnelServiceShape extends IDisposable {
$openTunnel(tunnelOptions: TunnelOptions): Promise<TunnelDto | undefined>;
$closeTunnel(remotePort: number): Promise<void>;
$closeTunnel(remote: { host: string, port: number }): Promise<void>;
$registerCandidateFinder(): Promise<void>;
$setTunnelProvider(): Promise<void>;
}
@@ -1395,7 +1395,7 @@ export interface ExtHostStorageShape {
export interface ExtHostTunnelServiceShape {
$findCandidatePorts(): Promise<{ port: number, detail: string }[]>;
$findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]>;
$forwardPort(tunnelOptions: TunnelOptions): Promise<TunnelDto> | undefined;
$closeTunnel(remote: { host: string, port: number }): Promise<void>;
}
@@ -48,7 +48,7 @@ export class ExtHostTunnelService implements IExtHostTunnelService {
async makeTunnel(forward: TunnelOptions): Promise<vscode.Tunnel | undefined> {
return undefined;
}
async $findCandidatePorts(): Promise<{ port: number; detail: string; }[]> {
async $findCandidatePorts(): Promise<{ host: string, port: number; detail: string; }[]> {
return [];
}
async setForwardPortProvider(provider: vscode.RemoteAuthorityResolver | undefined): Promise<IDisposable> { return { dispose: () => { } }; }
@@ -52,7 +52,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
const tunnel = await this._proxy.$openTunnel(forward);
if (tunnel) {
const disposableTunnel: vscode.Tunnel = new ExtensionTunnel(tunnel.remote, tunnel.localAddress, () => {
return this._proxy.$closeTunnel(tunnel.remote.port);
return this._proxy.$closeTunnel(tunnel.remote);
});
this._register(disposableTunnel);
return disposableTunnel;
@@ -95,7 +95,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
this._extensionTunnels.set(tunnelOptions.remote.host, new Map());
}
this._extensionTunnels.get(tunnelOptions.remote.host)!.set(tunnelOptions.remote.port, tunnel);
this._register(tunnel.onDispose(() => this._proxy.$closeTunnel(tunnel.remote.port)));
this._register(tunnel.onDispose(() => this._proxy.$closeTunnel(tunnel.remote)));
return Promise.resolve(TunnelDto.fromApiTunnel(tunnel));
});
}
@@ -104,12 +104,12 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
}
async $findCandidatePorts(): Promise<{ port: number, detail: string }[]> {
async $findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]> {
if (!isLinux) {
return [];
}
const ports: { port: number, detail: string }[] = [];
const ports: { host: string, port: number, detail: string }[] = [];
const tcp: string = fs.readFileSync('/proc/net/tcp', 'utf8');
const tcp6: string = fs.readFileSync('/proc/net/tcp6', 'utf8');
const procSockets: string = await (new Promise(resolve => {
@@ -150,7 +150,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => {
const command = processMap[socketMap[socket].pid].cmd;
if (!command.match('.*\.vscode\-server\-[a-zA-Z]+\/bin.*') && (command.indexOf('out/vs/server/main.js') === -1)) {
ports.push({ port, detail: processMap[socketMap[socket].pid].cmd });
ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd });
}
});
@@ -234,7 +234,8 @@ export abstract class ViewPane extends Pane implements IView {
}
export interface IViewPaneContainerOptions extends IPaneViewOptions {
showHeaderInTitleWhenSingleView: boolean;
mergeViewWithContainerWhenSingleView: boolean;
donotShowContainerTitleWhenMergedWithContainer?: boolean;
}
interface IViewPaneItem {
@@ -346,14 +347,16 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
getTitle(): string {
const composite = Registry.as<ViewletRegistry>(ViewletExtensions.Viewlets).getViewlet(this.getId()) || Registry.as<PanelRegistry>(PanelExtensions.Panels).getPanel(this.getId());
let title = composite.name;
if (this.isSingleView()) {
if (this.isViewMergedWithContainer()) {
const paneItemTitle = this.paneItems[0].pane.title;
title = paneItemTitle ? `${title}: ${paneItemTitle}` : title;
if (this.options.donotShowContainerTitleWhenMergedWithContainer) {
return this.paneItems[0].pane.title;
}
return paneItemTitle ? `${composite.name}: ${paneItemTitle}` : composite.name;
}
return title;
return composite.name;
}
private showContextMenu(event: StandardMouseEvent): void {
@@ -402,7 +405,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
}
getActions(): IAction[] {
if (this.isSingleView()) {
if (this.isViewMergedWithContainer()) {
return this.paneItems[0].pane.getActions();
}
@@ -410,7 +413,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
}
getSecondaryActions(): IAction[] {
if (this.isSingleView()) {
if (this.isViewMergedWithContainer()) {
return this.paneItems[0].pane.getSecondaryActions();
}
@@ -418,7 +421,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
}
getActionViewItem(action: IAction): IActionViewItem | undefined {
if (this.isSingleView()) {
if (this.isViewMergedWithContainer()) {
return this.paneItems[0].pane.getActionViewItem(action);
}
@@ -459,14 +462,14 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
}
addPanes(panes: { pane: ViewPane, size: number, index?: number; }[]): void {
const wasSingleView = this.isSingleView();
const wasMerged = this.isViewMergedWithContainer();
for (const { pane: pane, size, index } of panes) {
this.addPane(pane, size, index);
}
this.updateViewHeaders();
if (this.isSingleView() !== wasSingleView) {
if (this.isViewMergedWithContainer() !== wasMerged) {
this.updateTitleArea();
}
}
@@ -643,7 +646,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
private addPane(pane: ViewPane, size: number, index = this.paneItems.length - 1): void {
const onDidFocus = pane.onDidFocus(() => this.lastFocusedPane = pane);
const onDidChangeTitleArea = pane.onDidChangeTitleArea(() => {
if (this.isSingleView()) {
if (this.isViewMergedWithContainer()) {
this.updateTitleArea();
}
});
@@ -668,12 +671,12 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
}
removePanes(panes: ViewPane[]): void {
const wasSingleView = this.isSingleView();
const wasMerged = this.isViewMergedWithContainer();
panes.forEach(pane => this.removePane(pane));
this.updateViewHeaders();
if (wasSingleView !== this.isSingleView()) {
if (wasMerged !== this.isViewMergedWithContainer()) {
this.updateTitleArea();
}
}
@@ -726,8 +729,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
return assertIsDefined(this.paneview).getPaneSize(pane);
}
protected updateViewHeaders(): void {
if (this.isSingleView()) {
private updateViewHeaders(): void {
if (this.isViewMergedWithContainer()) {
this.paneItems[0].pane.setExpanded(true);
this.paneItems[0].pane.headerVisible = false;
} else {
@@ -735,8 +738,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
}
}
protected isSingleView(): boolean {
if (!(this.options.showHeaderInTitleWhenSingleView && this.paneItems.length === 1)) {
private isViewMergedWithContainer(): boolean {
if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) {
return false;
}
if (!this.areExtensionsReady) {
@@ -46,7 +46,7 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer {
@IWorkspaceContextService contextService: IWorkspaceContextService
) {
super(viewletId, `${viewletId}.state`, { showHeaderInTitleWhenSingleView: false }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
super(viewletId, `${viewletId}.state`, { mergeViewWithContainerWhenSingleView: false }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
this._register(onDidChangeFilterValue(newFilterValue => {
this.filterValue = newFilterValue;
this.onFilterChanged(newFilterValue);
@@ -235,12 +235,6 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio
'description': nls.localize('settings.editor.desc', "Determines which settings editor to use by default."),
'default': 'ui',
'scope': ConfigurationScope.WINDOW
},
'workbench.enableExperiments': {
'type': 'boolean',
'description': nls.localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."),
'default': true,
'tags': ['usesOnlineServices']
}
}
});
@@ -93,7 +93,9 @@ suite('BackupModelRestorer', () => {
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
});
test('Restore backups', async () => {
test('Restore backups', async function () {
this.timeout(20000);
const backupFileService = new NodeTestBackupFileService(workspaceBackupPath);
const instantiationService = workbenchInstantiationService();
instantiationService.stub(IBackupFileService, backupFileService);
@@ -79,7 +79,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer {
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@INotificationService private readonly notificationService: INotificationService
) {
super(VIEWLET_ID, `${VIEWLET_ID}.state`, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
super(VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
this._register(this.debugService.onDidChangeState(state => this.onDebugServiceStateChange(state)));
this._register(this.debugService.onDidNewSession(() => this.updateToolBar()));
@@ -3,13 +3,31 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IExperimentService, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/browser/experimentalPrompt';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
registerSingleton(IExperimentService, ExperimentService, true);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExperimentalPrompts, LifecyclePhase.Eventually);
const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
// Configuration
registry.registerConfiguration({
...workbenchConfigurationNodeBase,
'properties': {
'workbench.enableExperiments': {
'type': 'boolean',
'description': localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."),
'default': true,
'tags': ['usesOnlineServices']
}
}
});
@@ -370,7 +370,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
@IContextMenuService contextMenuService: IContextMenuService,
@IExtensionService extensionService: IExtensionService,
) {
super(VIEWLET_ID, `${VIEWLET_ID}.state`, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
super(VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
this.searchDelayer = new Delayer(500);
this.nonEmptyWorkspaceContextKey = NonEmptyWorkspaceContext.bindTo(contextKeyService);
@@ -183,7 +183,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer {
@IExtensionService extensionService: IExtensionService
) {
super(VIEWLET_ID, ExplorerViewPaneContainer.EXPLORER_VIEWS_STATE, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
super(VIEWLET_ID, ExplorerViewPaneContainer.EXPLORER_VIEWS_STATE, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
this.viewletVisibleContextKey = ExplorerViewletVisibleContext.bindTo(contextKeyService);
@@ -7,7 +7,7 @@ import 'vs/css!./media/fileactions';
import * as nls from 'vs/nls';
import { isWindows, isWeb } from 'vs/base/common/platform';
import * as extpath from 'vs/base/common/extpath';
import { extname, basename, posix, win32 } from 'vs/base/common/path';
import { extname, basename } from 'vs/base/common/path';
import * as resources from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { toErrorMessage } from 'vs/base/common/errorMessage';
@@ -45,7 +45,6 @@ import { asDomUri, triggerDownload } from 'vs/base/browser/dom';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { ILabelService } from 'vs/platform/label/common/label';
export const NEW_FILE_COMMAND_ID = 'explorer.newFile';
export const NEW_FILE_LABEL = nls.localize('newFile', "New File");
@@ -855,7 +854,6 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole
const editorService = accessor.get(IEditorService);
const viewletService = accessor.get(IViewletService);
const notificationService = accessor.get(INotificationService);
const labelService = accessor.get(ILabelService);
await viewletService.openViewlet(VIEWLET_ID, true);
@@ -878,10 +876,7 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole
folder.addChild(newStat);
const onSuccess = (value: string): Promise<void> => {
const separator = labelService.getSeparator(folder.resource.scheme);
const resource = folder.resource.with({ path: separator === '/' ? posix.join(folder.resource.path, value) : win32.join(folder.resource.path, value) });
const createPromise = isFolder ? fileService.createFolder(resource) : textFileService.create(resource);
const createPromise = isFolder ? fileService.createFolder(resources.joinPath(folder.resource, value)) : textFileService.create(resources.joinPath(folder.resource, value));
return createPromise.then(created => {
refreshIfSeparator(value, explorerService);
return isFolder ? explorerService.select(created.resource, true)
@@ -14,7 +14,7 @@ import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/wor
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor';
import { AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files';
import { VIEWLET_ID, SortOrderConfiguration, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files';
import { VIEWLET_ID, SortOrder, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files';
import { FileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/fileEditorTracker';
import { TextFileSaveErrorHandler } from 'vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler';
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
@@ -396,8 +396,8 @@ configurationRegistry.registerConfiguration({
},
'explorer.sortOrder': {
'type': 'string',
'enum': [SortOrderConfiguration.DEFAULT, SortOrderConfiguration.MIXED, SortOrderConfiguration.FILES_FIRST, SortOrderConfiguration.TYPE, SortOrderConfiguration.MODIFIED],
'default': SortOrderConfiguration.DEFAULT,
'enum': [SortOrder.Default, SortOrder.Mixed, SortOrder.FilesFirst, SortOrder.Type, SortOrder.Modified],
'default': SortOrder.Default,
'enumDescriptions': [
nls.localize('sortOrder.default', 'Files and folders are sorted by their names, in alphabetical order. Folders are displayed before files.'),
nls.localize('sortOrder.mixed', 'Files and folders are sorted by their names, in alphabetical order. Files are interwoven with folders.'),
@@ -436,7 +436,7 @@ export class ExplorerView extends ViewPane {
}
}));
// save view state on shutdown
// save view state
this._register(this.storageService.onWillSaveState(() => {
this.storageService.store(ExplorerView.TREE_VIEW_STATE_STORAGE_KEY, JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE);
}));
@@ -14,7 +14,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { memoize } from 'vs/base/common/decorators';
import { Emitter, Event } from 'vs/base/common/event';
import { IExplorerService } from 'vs/workbench/contrib/files/common/files';
import { IExplorerService, SortOrder } from 'vs/workbench/contrib/files/common/files';
import { joinPath, isEqualOrParent, basenameOrAuthority } from 'vs/base/common/resources';
export class ExplorerModel implements IDisposable {
@@ -263,7 +263,7 @@ export class ExplorerItem {
if (!this._isDirectoryResolved) {
// Resolve metadata only when the mtime is needed since this can be expensive
// Mtime is only used when the sort order is 'modified'
const resolveMetadata = explorerService.sortOrder === 'modified';
const resolveMetadata = explorerService.sortOrder === SortOrder.Modified;
try {
const stat = await fileService.resolve(this.resource, { resolveSingleChildDescendants: true, resolveMetadata });
const resolved = ExplorerItem.create(explorerService, fileService, stat, this);
@@ -6,7 +6,7 @@
import { Event, Emitter } from 'vs/base/common/event';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IExplorerService, IFilesConfiguration, SortOrder, SortOrderConfiguration, IContextProvider } from 'vs/workbench/contrib/files/common/files';
import { IExplorerService, IFilesConfiguration, SortOrder, IContextProvider } from 'vs/workbench/contrib/files/common/files';
import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel';
import { URI } from 'vs/base/common/uri';
import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType, IResolveFileOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
@@ -187,7 +187,7 @@ export class ExplorerService implements IExplorerService {
}
// Stat needs to be resolved first and then revealed
const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.sortOrder === 'modified' };
const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.sortOrder === SortOrder.Modified };
const workspaceFolder = this.contextService.getWorkspaceFolder(resource);
if (workspaceFolder === null) {
return Promise.resolve(undefined);
@@ -361,7 +361,7 @@ export class ExplorerService implements IExplorerService {
}
// Handle updated files/folders if we sort by modified
if (this._sortOrder === SortOrderConfiguration.MODIFIED) {
if (this._sortOrder === SortOrder.Modified) {
const updated = e.getUpdated();
// Check updated: Refresh if updated file/folder part of resolved root
@@ -387,7 +387,7 @@ export class ExplorerService implements IExplorerService {
private filterToViewRelevantEvents(e: FileChangesEvent): FileChangesEvent {
return new FileChangesEvent(e.changes.filter(change => {
if (change.type === FileChangeType.UPDATED && this._sortOrder !== SortOrderConfiguration.MODIFIED) {
if (change.type === FileChangeType.UPDATED && this._sortOrder !== SortOrder.Modified) {
return false; // we only are about updated if we sort by modified time
}
@@ -133,15 +133,13 @@ export interface IFileResource {
isDirectory?: boolean;
}
export const SortOrderConfiguration = {
DEFAULT: 'default',
MIXED: 'mixed',
FILES_FIRST: 'filesFirst',
TYPE: 'type',
MODIFIED: 'modified'
};
export type SortOrder = 'default' | 'mixed' | 'filesFirst' | 'type' | 'modified';
export const enum SortOrder {
Default = 'default',
Mixed = 'mixed',
FilesFirst = 'filesFirst',
Type = 'type',
Modified = 'modified'
}
export class TextFileContentProvider extends Disposable implements ITextModelContentProvider {
private readonly fileWatcherDisposable = this._register(new MutableDisposable());
@@ -28,7 +28,6 @@ class ServiceAccessor {
}
suite('Files - FileEditorInput', () => {
let instantiationService: IInstantiationService;
let accessor: ServiceAccessor;
@@ -6,7 +6,3 @@
.customview-tree .tunnel-view-label {
flex: 1;
}
.customview-tree .tunnel-view-label .action-label.codicon {
margin-top: 4px;
}
@@ -26,7 +26,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { ActionRunner, IAction } from 'vs/base/common/actions';
import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions';
import { createAndFillInContextMenuActions, createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IRemoteExplorerService, TunnelModel } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { IRemoteExplorerService, TunnelModel, MakeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
@@ -105,13 +105,13 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel {
get forwarded(): TunnelItem[] {
return Array.from(this.model.forwarded.values()).map(tunnel => {
return new TunnelItem(TunnelType.Forwarded, tunnel.remote, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description);
return new TunnelItem(TunnelType.Forwarded, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description);
});
}
get detected(): TunnelItem[] {
return Array.from(this.model.detected.values()).map(tunnel => {
return new TunnelItem(TunnelType.Detected, tunnel.remote, tunnel.localAddress, false, tunnel.name, tunnel.description);
return new TunnelItem(TunnelType.Detected, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, false, tunnel.name, tunnel.description);
});
}
@@ -119,8 +119,9 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel {
return this.model.candidates.then(values => {
const candidates: TunnelItem[] = [];
values.forEach(value => {
if (!this.model.forwarded.has(value.port) && !this.model.detected.has(value.port)) {
candidates.push(new TunnelItem(TunnelType.Candidate, value.port, undefined, false, undefined, value.detail));
const key = MakeAddress(value.host, value.port);
if (!this.model.forwarded.has(key) && !this.model.detected.has(key)) {
candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, undefined, false, undefined, value.detail));
}
});
return candidates;
@@ -185,7 +186,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
}
private isTunnelItem(item: ITunnelGroup | ITunnelItem): item is ITunnelItem {
return !!((<ITunnelItem>item).remote);
return !!((<ITunnelItem>item).remotePort);
}
renderElement(element: ITreeNode<ITunnelGroup | ITunnelItem, ITunnelGroup | ITunnelItem>, index: number, templateData: ITunnelTemplateData): void {
@@ -196,7 +197,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
templateData.actionBar.clear();
let editableData: IEditableData | undefined;
if (this.isTunnelItem(node)) {
editableData = this.remoteExplorerService.getEditableData(node.remote);
editableData = this.remoteExplorerService.getEditableData(node.remoteHost, node.remotePort);
if (editableData) {
templateData.iconLabel.element.style.display = 'none';
this.renderInputBox(templateData.container, editableData);
@@ -204,7 +205,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
templateData.iconLabel.element.style.display = 'flex';
this.renderTunnel(node, templateData);
}
} else if ((node.tunnelType === TunnelType.Add) && (editableData = this.remoteExplorerService.getEditableData(undefined))) {
} else if ((node.tunnelType === TunnelType.Add) && (editableData = this.remoteExplorerService.getEditableData(undefined, undefined))) {
templateData.iconLabel.element.style.display = 'none';
this.renderInputBox(templateData.container, editableData);
} else {
@@ -338,7 +339,8 @@ interface ITunnelGroup {
interface ITunnelItem {
tunnelType: TunnelType;
remote: number;
remoteHost: string;
remotePort: number;
localAddress?: string;
name?: string;
closeable?: boolean;
@@ -349,7 +351,8 @@ interface ITunnelItem {
class TunnelItem implements ITunnelItem {
constructor(
public tunnelType: TunnelType,
public remote: number,
public remoteHost: string,
public remotePort: number,
public localAddress?: string,
public closeable?: boolean,
public name?: string,
@@ -359,9 +362,9 @@ class TunnelItem implements ITunnelItem {
if (this.name) {
return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name);
} else if (this.localAddress) {
return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0} to {1}", this.remote, this.localAddress);
return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0} to {1}", this.remotePort, this.localAddress);
} else {
return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} not forwarded", this.remote);
return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} not forwarded", this.remotePort);
}
}
@@ -369,7 +372,7 @@ class TunnelItem implements ITunnelItem {
if (this._description) {
return this._description;
} else if (this.name) {
return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} to {1}", this.remote, this.localAddress);
return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} to {1}", this.remotePort, this.localAddress);
}
return undefined;
}
@@ -469,12 +472,12 @@ export class TunnelPanel extends ViewPane {
this._register(Event.debounce(navigator.onDidOpenResource, (last, event) => event, 75, true)(e => {
if (e.element && (e.element.tunnelType === TunnelType.Add)) {
this.commandService.executeCommand(ForwardPortAction.ID);
this.commandService.executeCommand(ForwardPortAction.ID, 'inline add');
}
}));
this._register(this.remoteExplorerService.onDidChangeEditable(async e => {
const isEditing = !!this.remoteExplorerService.getEditableData(e);
const isEditing = !!this.remoteExplorerService.getEditableData(e.host, e.port);
if (!isEditing) {
dom.removeClass(treeContainer, 'highlight');
@@ -575,12 +578,12 @@ namespace LabelTunnelAction {
return async (accessor, arg) => {
if (arg instanceof TunnelItem) {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
remoteExplorerService.setEditable(arg.remote, {
remoteExplorerService.setEditable(arg.remoteHost, arg.remotePort, {
onFinish: (value, success) => {
if (success) {
remoteExplorerService.tunnelModel.name(arg.remote, value);
remoteExplorerService.tunnelModel.name(arg.remoteHost, arg.remotePort, value);
}
remoteExplorerService.setEditable(arg.remote, null);
remoteExplorerService.setEditable(arg.remoteHost, arg.remotePort, null);
},
validationMessage: () => null,
placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"),
@@ -595,31 +598,52 @@ namespace LabelTunnelAction {
namespace ForwardPortAction {
export const ID = 'remote.tunnel.forward';
export const LABEL = nls.localize('remote.tunnel.forward', "Forward a Port");
const forwardPrompt = nls.localize('remote.tunnel.forwardPrompt', "Port number or address (eg. 3000 or 10.10.10.10:2000).");
function parseInput(value: string): { host: string, port: number } | undefined {
const matches = value.match(/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\:|localhost:)?([0-9]+)$/);
if (!matches) {
return undefined;
}
return { host: matches[1]?.substring(0, matches[1].length - 1) || 'localhost', port: Number(matches[2]) };
}
function validateInput(value: string): string | null {
if (!parseInput(value)) {
return nls.localize('remote.tunnelsView.portNumberValid', "Port number is invalid");
}
return null;
}
export function handler(): ICommandHandler {
return async (accessor, arg) => {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
if (arg instanceof TunnelItem) {
remoteExplorerService.tunnelModel.forward(arg.remote);
remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort });
} else if (arg) {
remoteExplorerService.setEditable(undefined, undefined, {
onFinish: (value, success) => {
let parsed: { host: string, port: number } | undefined;
if (success && (parsed = parseInput(value))) {
remoteExplorerService.forward({ host: parsed.host, port: parsed.port });
}
remoteExplorerService.setEditable(undefined, undefined, null);
},
validationMessage: validateInput,
placeholder: forwardPrompt
});
} else {
const viewsService = accessor.get(IViewsService);
const quickInputService = accessor.get(IQuickInputService);
await viewsService.openView(TunnelPanel.ID, true);
remoteExplorerService.setEditable(undefined, {
onFinish: (value, success) => {
if (success) {
remoteExplorerService.tunnelModel.forward(Number(value));
}
remoteExplorerService.setEditable(undefined, null);
},
validationMessage: (value) => {
const asNumber = Number(value);
if ((value === '') || isNaN(asNumber) || (asNumber < 0) || (asNumber > 65535)) {
return nls.localize('remote.tunnelsView.portNumberValid', "Port number is invalid");
}
return null;
},
placeholder: nls.localize('remote.tunnelsView.forwardPortPlaceholder', "Port number")
const value = await quickInputService.input({
prompt: forwardPrompt,
validateInput: (value) => Promise.resolve(validateInput(value))
});
let parsed: { host: string, port: number } | undefined;
if (value && (parsed = parseInput(value))) {
remoteExplorerService.forward({ host: parsed.host, port: parsed.port });
}
}
};
}
@@ -633,7 +657,7 @@ namespace ClosePortAction {
return async (accessor, arg) => {
if (arg instanceof TunnelItem) {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
await remoteExplorerService.tunnelModel.close(arg.remote);
await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort });
}
};
}
@@ -648,9 +672,10 @@ namespace OpenPortInBrowserAction {
if (arg instanceof TunnelItem) {
const model = accessor.get(IRemoteExplorerService).tunnelModel;
const openerService = accessor.get(IOpenerService);
const tunnel = model.forwarded.has(arg.remote) ? model.forwarded.get(arg.remote) : model.detected.get(arg.remote);
const key = MakeAddress(arg.remoteHost, arg.remotePort);
const tunnel = model.forwarded.get(key) || model.detected.get(key);
let address: string | undefined;
if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remote))) {
if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) {
return openerService.open(URI.parse('http://' + address));
}
return Promise.resolve();
@@ -668,7 +693,7 @@ namespace CopyAddressAction {
if (arg instanceof TunnelItem) {
const model = accessor.get(IRemoteExplorerService).tunnelModel;
const clipboard = accessor.get(IClipboardService);
const address = model.address(arg.remote);
const address = model.address(arg.remoteHost, arg.remotePort);
if (address) {
await clipboard.writeText(address.toString());
}
@@ -116,7 +116,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
super(VIEWLET_ID, SCMViewPaneContainer.STATE_KEY, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
super(VIEWLET_ID, SCMViewPaneContainer.STATE_KEY, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
this.menus = instantiationService.createInstance(SCMMenus, undefined);
this._register(this.menus.onDidChangeTitle(this.updateTitleArea, this));
@@ -48,7 +48,7 @@ export class SearchViewPaneContainer extends ViewPaneContainer {
@IContextMenuService contextMenuService: IContextMenuService,
@IExtensionService extensionService: IExtensionService,
) {
super(VIEWLET_ID, `${VIEWLET_ID}.state`, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
super(VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService);
}
getTitle(): string {
@@ -68,7 +68,7 @@ export class WebviewPortMappingManager extends Disposable {
if (existing) {
return existing;
}
const tunnel = this.tunnelService.openTunnel(remotePort);
const tunnel = this.tunnelService.openTunnel(undefined, remotePort);
if (tunnel) {
this._tunnels.set(remotePort, tunnel);
}
+1 -1
View File
@@ -453,7 +453,7 @@ export class ElectronWindow extends Disposable {
if (options?.allowTunneling) {
const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri);
if (portMappingRequest) {
const tunnel = await this.tunnelService.openTunnel(portMappingRequest.port);
const tunnel = await this.tunnelService.openTunnel(undefined, portMappingRequest.port);
if (tunnel) {
return {
resolved: uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }),
@@ -18,24 +18,32 @@ export const IRemoteExplorerService = createDecorator<IRemoteExplorerService>('r
export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType';
export interface Tunnel {
remote: number;
remoteHost: string;
remotePort: number;
localAddress: string;
local?: number;
localPort?: number;
name?: string;
description?: string;
closeable?: boolean;
}
export function MakeAddress(host: string, port: number): string {
if (host = '127.0.0.1') {
host = 'localhost';
}
return host + ':' + port;
}
export class TunnelModel extends Disposable {
readonly forwarded: Map<number, Tunnel>;
readonly detected: Map<number, Tunnel>;
readonly forwarded: Map<string, Tunnel>;
readonly detected: Map<string, Tunnel>;
private _onForwardPort: Emitter<Tunnel> = new Emitter();
public onForwardPort: Event<Tunnel> = this._onForwardPort.event;
private _onClosePort: Emitter<number> = new Emitter();
public onClosePort: Event<number> = this._onClosePort.event;
private _onPortName: Emitter<number> = new Emitter();
public onPortName: Event<number> = this._onPortName.event;
private _candidateFinder: (() => Promise<{ port: number, detail: string }[]>) | undefined;
private _onClosePort: Emitter<{ host: string, port: number }> = new Emitter();
public onClosePort: Event<{ host: string, port: number }> = this._onClosePort.event;
private _onPortName: Emitter<{ host: string, port: number }> = new Emitter();
public onPortName: Event<{ host: string, port: number }> = this._onPortName.event;
private _candidateFinder: (() => Promise<{ host: string, port: number, detail: string }[]>) | undefined;
constructor(
@ITunnelService private readonly tunnelService: ITunnelService
@@ -45,10 +53,11 @@ export class TunnelModel extends Disposable {
this.tunnelService.tunnels.then(tunnels => {
tunnels.forEach(tunnel => {
if (tunnel.localAddress) {
this.forwarded.set(tunnel.tunnelRemotePort, {
remote: tunnel.tunnelRemotePort,
this.forwarded.set(MakeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort), {
remotePort: tunnel.tunnelRemotePort,
remoteHost: tunnel.tunnelRemoteHost,
localAddress: tunnel.localAddress,
local: tunnel.tunnelLocalPort
localPort: tunnel.tunnelLocalPort
});
}
});
@@ -56,75 +65,83 @@ export class TunnelModel extends Disposable {
this.detected = new Map();
this._register(this.tunnelService.onTunnelOpened(tunnel => {
if (!this.forwarded.has(tunnel.tunnelRemotePort) && tunnel.localAddress) {
this.forwarded.set(tunnel.tunnelRemotePort, {
remote: tunnel.tunnelRemotePort,
const key = MakeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort);
if ((!this.forwarded.has(key)) && tunnel.localAddress) {
this.forwarded.set(key, {
remoteHost: tunnel.tunnelRemoteHost,
remotePort: tunnel.tunnelRemotePort,
localAddress: tunnel.localAddress,
local: tunnel.tunnelLocalPort,
localPort: tunnel.tunnelLocalPort,
closeable: true
});
}
this._onForwardPort.fire(this.forwarded.get(tunnel.tunnelRemotePort)!);
this._onForwardPort.fire(this.forwarded.get(key)!);
}));
this._register(this.tunnelService.onTunnelClosed(remotePort => {
if (this.forwarded.has(remotePort)) {
this.forwarded.delete(remotePort);
this._onClosePort.fire(remotePort);
this._register(this.tunnelService.onTunnelClosed(address => {
const key = MakeAddress(address.host, address.port);
if (this.forwarded.has(key)) {
this.forwarded.delete(key);
this._onClosePort.fire(address);
}
}));
}
async forward(remote: number, local?: number, name?: string): Promise<RemoteTunnel | void> {
if (!this.forwarded.has(remote)) {
const tunnel = await this.tunnelService.openTunnel(remote, local);
async forward(remote: { host: string, port: number }, local?: number, name?: string): Promise<RemoteTunnel | void> {
const key = MakeAddress(remote.host, remote.port);
if (!this.forwarded.has(key)) {
const tunnel = await this.tunnelService.openTunnel(remote.host, remote.port, local);
if (tunnel && tunnel.localAddress) {
const newForward: Tunnel = {
remote: tunnel.tunnelRemotePort,
local: tunnel.tunnelLocalPort,
remoteHost: tunnel.tunnelRemoteHost,
remotePort: tunnel.tunnelRemotePort,
localPort: tunnel.tunnelLocalPort,
name: name,
closeable: true,
localAddress: tunnel.localAddress
};
this.forwarded.set(remote, newForward);
this.forwarded.set(key, newForward);
this._onForwardPort.fire(newForward);
return tunnel;
}
}
}
name(remote: number, name: string) {
if (this.forwarded.has(remote)) {
this.forwarded.get(remote)!.name = name;
this._onPortName.fire(remote);
} else if (this.detected.has(remote)) {
this.detected.get(remote)!.name = name;
this._onPortName.fire(remote);
name(host: string, port: number, name: string) {
const key = MakeAddress(host, port);
if (this.forwarded.has(key)) {
this.forwarded.get(key)!.name = name;
this._onPortName.fire({ host, port });
} else if (this.detected.has(key)) {
this.detected.get(key)!.name = name;
this._onPortName.fire({ host, port });
}
}
async close(remote: number): Promise<void> {
return this.tunnelService.closeTunnel(remote);
async close(host: string, port: number): Promise<void> {
return this.tunnelService.closeTunnel(host, port);
}
address(remote: number): string | undefined {
return (this.forwarded.get(remote) || this.detected.get(remote))?.localAddress;
address(host: string, port: number): string | undefined {
const key = MakeAddress(host, port);
return (this.forwarded.get(key) || this.detected.get(key))?.localAddress;
}
addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[]): void {
tunnels.forEach(tunnel => {
this.detected.set(tunnel.remote.port, {
remote: tunnel.remote.port,
this.detected.set(MakeAddress(tunnel.remote.host, tunnel.remote.port), {
remoteHost: tunnel.remote.host,
remotePort: tunnel.remote.port,
localAddress: tunnel.localAddress,
closeable: false
});
});
}
registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void {
registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void {
this._candidateFinder = finder;
}
get candidates(): Promise<{ port: number, detail: string }[]> {
get candidates(): Promise<{ host: string, port: number, detail: string }[]> {
if (this._candidateFinder) {
return this._candidateFinder();
}
@@ -138,13 +155,13 @@ export interface IRemoteExplorerService {
targetType: string;
readonly helpInformation: HelpInformation[];
readonly tunnelModel: TunnelModel;
onDidChangeEditable: Event<number | undefined>;
setEditable(remote: number | undefined, data: IEditableData | null): void;
getEditableData(remote: number | undefined): IEditableData | undefined;
forward(remote: number, local?: number, name?: string): Promise<RemoteTunnel | void>;
close(remote: number): Promise<void>;
onDidChangeEditable: Event<{ host: string, port: number | undefined }>;
setEditable(remoteHost: string | undefined, remotePort: number | undefined, data: IEditableData | null): void;
getEditableData(remoteHost: string | undefined, remotePort: number | undefined): IEditableData | undefined;
forward(remote: { host: string, port: number }, localPort?: number, name?: string): Promise<RemoteTunnel | void>;
close(remote: { host: string, port: number }): Promise<void>;
addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[] | undefined): void;
registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void;
registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void;
}
export interface HelpInformation {
@@ -189,9 +206,9 @@ class RemoteExplorerService implements IRemoteExplorerService {
public readonly onDidChangeTargetType: Event<string> = this._onDidChangeTargetType.event;
private _helpInformation: HelpInformation[] = [];
private _tunnelModel: TunnelModel;
private _editable: { remote: number | undefined, data: IEditableData } | undefined;
private readonly _onDidChangeEditable: Emitter<number | undefined> = new Emitter();
public readonly onDidChangeEditable: Event<number | undefined> = this._onDidChangeEditable.event;
private _editable: { remoteHost: string, remotePort: number | undefined, data: IEditableData } | undefined;
private readonly _onDidChangeEditable: Emitter<{ host: string, port: number | undefined }> = new Emitter();
public readonly onDidChangeEditable: Event<{ host: string, port: number | undefined }> = this._onDidChangeEditable.event;
constructor(
@IStorageService private readonly storageService: IStorageService,
@@ -246,12 +263,12 @@ class RemoteExplorerService implements IRemoteExplorerService {
return this._tunnelModel;
}
forward(remote: number, local?: number, name?: string): Promise<RemoteTunnel | void> {
forward(remote: { host: string, port: number }, local?: number, name?: string): Promise<RemoteTunnel | void> {
return this.tunnelModel.forward(remote, local, name);
}
close(remote: number): Promise<void> {
return this.tunnelModel.close(remote);
close(remote: { host: string, port: number }): Promise<void> {
return this.tunnelModel.close(remote.host, remote.port);
}
addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[] | undefined): void {
@@ -260,20 +277,21 @@ class RemoteExplorerService implements IRemoteExplorerService {
}
}
setEditable(remote: number | undefined, data: IEditableData | null): void {
setEditable(remoteHost: string, remotePort: number | undefined, data: IEditableData | null): void {
if (!data) {
this._editable = undefined;
} else {
this._editable = { remote, data };
this._editable = { remoteHost, remotePort, data };
}
this._onDidChangeEditable.fire(remote);
this._onDidChangeEditable.fire({ host: remoteHost, port: remotePort });
}
getEditableData(remote: number | undefined): IEditableData | undefined {
return this._editable && this._editable.remote === remote ? this._editable.data : undefined;
getEditableData(remoteHost: string | undefined, remotePort: number | undefined): IEditableData | undefined {
return (this._editable && (this._editable.remotePort === remotePort) && this._editable.remoteHost === remoteHost) ?
this._editable.data : undefined;
}
registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void {
registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void {
this.tunnelModel.registerCandidateFinder(finder);
}
@@ -103,9 +103,9 @@ export class TunnelService implements ITunnelService {
private _onTunnelOpened: Emitter<RemoteTunnel> = new Emitter();
public onTunnelOpened: Event<RemoteTunnel> = this._onTunnelOpened.event;
private _onTunnelClosed: Emitter<number> = new Emitter();
public onTunnelClosed: Event<number> = this._onTunnelClosed.event;
private readonly _tunnels = new Map</* port */ number, { refcount: number, readonly value: Promise<RemoteTunnel> }>();
private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter();
public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event;
private readonly _tunnels = new Map</*host*/ string, Map</* port */ number, { refcount: number, readonly value: Promise<RemoteTunnel> }>>();
private _tunnelProvider: ITunnelProvider | undefined;
public constructor(
@@ -130,23 +130,32 @@ export class TunnelService implements ITunnelService {
}
public get tunnels(): Promise<readonly RemoteTunnel[]> {
return Promise.all(Array.from(this._tunnels.values()).map(x => x.value));
const promises: Promise<RemoteTunnel>[] = [];
Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value)));
return Promise.all(promises);
}
dispose(): void {
for (const { value } of this._tunnels.values()) {
value.then(tunnel => tunnel.dispose());
for (const portMap of this._tunnels.values()) {
for (const { value } of portMap.values()) {
value.then(tunnel => tunnel.dispose());
}
portMap.clear();
}
this._tunnels.clear();
}
openTunnel(remotePort: number, localPort: number): Promise<RemoteTunnel> | undefined {
openTunnel(remoteHost: string | undefined, remotePort: number, localPort: number): Promise<RemoteTunnel> | undefined {
const remoteAuthority = this.environmentService.configuration.remoteAuthority;
if (!remoteAuthority) {
return undefined;
}
const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remotePort, localPort);
if (!remoteHost || (remoteHost === '127.0.0.1')) {
remoteHost = 'localhost';
}
const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remoteHost, remotePort, localPort);
if (!resolvedTunnel) {
return resolvedTunnel;
}
@@ -165,48 +174,62 @@ export class TunnelService implements ITunnelService {
tunnelLocalPort: tunnel.tunnelLocalPort,
localAddress: tunnel.localAddress,
dispose: () => {
const existing = this._tunnels.get(tunnel.tunnelRemotePort);
if (existing) {
existing.refcount--;
this.tryDisposeTunnel(tunnel.tunnelRemotePort, existing);
const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost);
if (existingHost) {
const existing = existingHost.get(tunnel.tunnelRemotePort);
if (existing) {
existing.refcount--;
this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing);
}
}
}
};
}
private async tryDisposeTunnel(remotePort: number, tunnel: { refcount: number, readonly value: Promise<RemoteTunnel> }): Promise<void> {
private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise<RemoteTunnel> }): Promise<void> {
if (tunnel.refcount <= 0) {
const disposePromise: Promise<void> = tunnel.value.then(tunnel => {
tunnel.dispose();
this._onTunnelClosed.fire(tunnel.tunnelRemotePort);
this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
});
this._tunnels.delete(remotePort);
if (this._tunnels.has(remoteHost)) {
this._tunnels.get(remoteHost)!.delete(remotePort);
}
return disposePromise;
}
}
async closeTunnel(remotePort: number): Promise<void> {
if (this._tunnels.has(remotePort)) {
const value = this._tunnels.get(remotePort)!;
async closeTunnel(remoteHost: string, remotePort: number): Promise<void> {
const portMap = this._tunnels.get(remoteHost);
if (portMap && portMap.has(remotePort)) {
const value = portMap.get(remotePort)!;
value.refcount = 0;
await this.tryDisposeTunnel(remotePort, value);
await this.tryDisposeTunnel(remoteHost, remotePort, value);
}
}
private retainOrCreateTunnel(remoteAuthority: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
const existing = this._tunnels.get(remotePort);
private addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise<RemoteTunnel>) {
if (!this._tunnels.has(remoteHost)) {
this._tunnels.set(remoteHost, new Map());
}
this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel });
}
private retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
const portMap = this._tunnels.get(remoteHost);
const existing = portMap ? portMap.get(remotePort) : undefined;
if (existing) {
++existing.refcount;
return existing.value;
}
if (this._tunnelProvider) {
const tunnel = this._tunnelProvider.forwardPort({ remote: { host: 'localhost', port: remotePort } });
const tunnel = this._tunnelProvider.forwardPort({ remote: { host: remoteHost, port: remotePort } });
if (tunnel) {
this._tunnels.set(remotePort, { refcount: 1, value: tunnel });
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}
return tunnel;
} else {
} else if (remoteHost === 'localhost') {
const options: IConnectionOptions = {
commit: product.commit,
socketFactory: nodeSocketFactory,
@@ -221,9 +244,10 @@ export class TunnelService implements ITunnelService {
};
const tunnel = createRemoteTunnel(options, remotePort, localPort);
this._tunnels.set(remotePort, { refcount: 1, value: tunnel });
this.addTunnelToMap(remoteHost, remotePort, tunnel);
return tunnel;
}
return undefined;
}
}
@@ -40,8 +40,8 @@ class ServiceAccessor {
class BeforeShutdownEventImpl implements BeforeShutdownEvent {
public value: boolean | Promise<boolean> | undefined;
public reason = ShutdownReason.CLOSE;
value: boolean | Promise<boolean> | undefined;
reason = ShutdownReason.CLOSE;
veto(value: boolean | Promise<boolean>): void {
this.value = value;
@@ -256,17 +256,17 @@ suite('Workbench base editor', () => {
}
class TestEditorInput extends EditorInput {
constructor(private resource: URI, private id = 'testEditorInput') {
constructor(private resource: URI, private id = 'testEditorInputForMementoTest') {
super();
}
public getTypeId() { return 'testEditorInput'; }
public resolve(): Promise<IEditorModel> { return Promise.resolve(null!); }
getTypeId() { return 'testEditorInputForMementoTest'; }
resolve(): Promise<IEditorModel> { return Promise.resolve(null!); }
public matches(other: TestEditorInput): boolean {
matches(other: TestEditorInput): boolean {
return other && this.id === other.id && other instanceof TestEditorInput;
}
public getResource(): URI {
getResource(): URI {
return this.resource;
}
}
@@ -11,7 +11,8 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenAction, QuickOpenHandler } from 'vs/workbench/browser/quickopen';
export class TestQuickOpenService implements IQuickOpenService {
public _serviceBrand: undefined;
_serviceBrand: undefined;
private callback?: (prefix?: string) => void;
@@ -44,8 +45,8 @@ export class TestQuickOpenService implements IQuickOpenService {
return null!;
}
public dispose() { }
public navigate(): void { }
dispose() { }
navigate(): void { }
}
suite('QuickOpen', () => {
@@ -16,7 +16,7 @@ suite('Viewlets', () => {
super('id', null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
}
public layout(dimension: any): void {
layout(dimension: any): void {
throw new Error('Method not implemented.');
}
}
@@ -21,7 +21,7 @@ import { TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTe
class MyEditorModel extends EditorModel { }
class MyTextEditorModel extends BaseTextEditorModel {
public createTextEditorModel(value: ITextBufferFactory, resource?: URI, preferredMode?: string) {
createTextEditorModel(value: ITextBufferFactory, resource?: URI, preferredMode?: string) {
return super.createTextEditorModel(value, resource, preferredMode);
}
+30 -29
View File
@@ -245,26 +245,25 @@ export class TestTextFileService extends NativeTextFileService {
this.resolveTextContentError = error;
}
readStream(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileStreamContent> {
async readStream(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileStreamContent> {
if (this.resolveTextContentError) {
const error = this.resolveTextContentError;
this.resolveTextContentError = null;
return Promise.reject(error);
throw error;
}
return this.fileService.readFileStream(resource, options).then(async (content): Promise<ITextFileStreamContent> => {
return {
resource: content.resource,
name: content.name,
mtime: content.mtime,
ctime: content.ctime,
etag: content.etag,
encoding: 'utf8',
value: await createTextBufferFactoryFromStream(content.value),
size: 10
};
});
const content = await this.fileService.readFileStream(resource, options);
return {
resource: content.resource,
name: content.name,
mtime: content.mtime,
ctime: content.ctime,
etag: content.etag,
encoding: 'utf8',
value: await createTextBufferFactoryFromStream(content.value),
size: 10
};
}
promptForPath(_resource: URI, _defaultPath: URI): Promise<URI> {
@@ -1021,12 +1020,14 @@ export class TestFileService implements IFileService {
});
}
resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise<IResolveFileResult[]> {
return Promise.all(toResolve.map(resourceAndOption => this.resolve(resourceAndOption.resource, resourceAndOption.options))).then(stats => stats.map(stat => ({ stat, success: true })));
async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise<IResolveFileResult[]> {
const stats = await Promise.all(toResolve.map(resourceAndOption => this.resolve(resourceAndOption.resource, resourceAndOption.options)));
return stats.map(stat => ({ stat, success: true }));
}
exists(_resource: URI): Promise<boolean> {
return Promise.resolve(true);
async exists(_resource: URI): Promise<boolean> {
return true;
}
readFile(resource: URI, options?: IReadFileOptions | undefined): Promise<IFileContent> {
@@ -1071,11 +1072,12 @@ export class TestFileService implements IFileService {
});
}
writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
return timeout(0).then(() => ({
async writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
await timeout(0);
return ({
resource,
etag: 'index.txt',
encoding: 'utf8',
mtime: Date.now(),
ctime: Date.now(),
size: 42,
@@ -1083,7 +1085,7 @@ export class TestFileService implements IFileService {
isDirectory: false,
isSymbolicLink: false,
name: resources.basename(resource)
}));
});
}
move(_source: URI, _target: URI, _overwrite?: boolean): Promise<IFileStatWithMetadata> {
@@ -1153,14 +1155,13 @@ export class TestBackupFileService implements IBackupFileService {
return false;
}
loadBackupResource(resource: URI): Promise<URI | undefined> {
return this.hasBackup(resource).then(hasBackup => {
if (hasBackup) {
return this.toBackupResource(resource);
}
async loadBackupResource(resource: URI): Promise<URI | undefined> {
const hasBackup = await this.hasBackup(resource);
if (hasBackup) {
return this.toBackupResource(resource);
}
return undefined;
});
return undefined;
}
registerResourceForBackup(_resource: URI): Promise<void> {