diff --git a/resources/serverless/code-web.js b/resources/serverless/code-web.js index 4345136b625..10fc947fa22 100644 --- a/resources/serverless/code-web.js +++ b/resources/serverless/code-web.js @@ -25,7 +25,6 @@ const WEB_MAIN = path.join(APP_ROOT, 'src', 'vs', 'code', 'browser', 'workbench' const args = minimist(process.argv, { boolean: [ - 'watch', 'no-launch', 'help' ], @@ -33,19 +32,20 @@ const args = minimist(process.argv, { 'scheme', 'host', 'port', - 'local_port' + 'local_port', + 'extension' ], }); if (args.help) { console.log( 'yarn web [options]\n' + - ' --watch Watch extensions that require browser specific builds\n' + - ' --no-launch Do not open VSCode web in the browser\n' + - ' --scheme Protocol (https or http)\n' + - ' --host Remote host\n' + - ' --port Remote/Local port\n' + - ' --local_port Local port override\n' + + ' --no-launch Do not open VSCode web in the browser\n' + + ' --scheme Protocol (https or http)\n' + + ' --host Remote host\n' + + ' --port Remote/Local port\n' + + ' --local_port Local port override\n' + + ' --extension Path of an extension to include\n' + ' --help\n' + '[Example]\n' + ' yarn web --scheme https --host example.com --port 8080 --local_port 30000' @@ -61,76 +61,114 @@ const AUTHORITY = process.env.VSCODE_AUTHORITY || `${HOST}:${PORT}`; const exists = (path) => util.promisify(fs.exists)(path); const readFile = (path) => util.promisify(fs.readFile)(path); +const readdir = (path) => util.promisify(fs.readdir)(path); +const readdirWithFileTypes = (path) => util.promisify(fs.readdir)(path, { withFileTypes: true }); -let unbuiltExensions = []; - -async function initialize() { +async function getBuiltInExtensionInfos(extensionsRoot) { const builtinExtensions = []; - - const children = await util.promisify(fs.readdir)(EXTENSIONS_ROOT, { withFileTypes: true }); - const folders = children.filter(c => !c.isFile()); - await Promise.all(folders.map(async folder => { - const folderName = folder.name; - const extensionPath = path.join(EXTENSIONS_ROOT, folderName); - - let children = []; - try { - children = await util.promisify(fs.readdir)(extensionPath); - } catch (error) { - console.log(error); - return; - } - - const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; - const readmePath = readme ? path.join(extensionPath, readme) : undefined; - const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; - const changelogPath = changelog ? path.join(extensionPath, changelog) : undefined; - - const packageJSONPath = path.join(EXTENSIONS_ROOT, folderName, 'package.json'); - if (await exists(packageJSONPath)) { - try { - let packageJSON = JSON.parse((await readFile(packageJSONPath)).toString()); - if (packageJSON.main && !packageJSON.browser) { - return; // unsupported - } - - if (packageJSON.browser) { - packageJSON.main = packageJSON.browser; - - let mainFilePath = path.join(EXTENSIONS_ROOT, folderName, packageJSON.browser); - if (path.extname(mainFilePath) !== '.js') { - mainFilePath += '.js'; - } - if (!await exists(mainFilePath)) { - unbuiltExensions.push(path.relative(EXTENSIONS_ROOT, mainFilePath)); - } - } - packageJSON.extensionKind = ['web']; // enable for Web - - const packageNLSPath = path.join(folderName, 'package.nls.json'); - const packageNLSExists = await exists(path.join(EXTENSIONS_ROOT, packageNLSPath)); - if (packageNLSExists) { - packageJSON = extensions.translatePackageJSON(packageJSON, path.join(EXTENSIONS_ROOT, packageNLSPath)); // temporary, until fixed in core - } - builtinExtensions.push({ - extensionPath: folderName, - packageJSON, - packageNLSPath: packageNLSExists ? packageNLSPath : undefined, - readmePath, - changelogPath - }); - } catch (e) { - console.log(e); + const children = await readdirWithFileTypes(extensionsRoot); + await Promise.all(children.map(async child => { + if (child.isDirectory()) { + const info = await getBuiltInExtensionInfo(path.join(extensionsRoot, child.name)); + if (info) { + builtinExtensions.push(info); } } })); - if (unbuiltExensions.length) { - fancyLog(`${ansiColors.yellow('Warning')}: Make sure to run ${ansiColors.cyan('yarn gulp watch-web')}\nCould not find the following browser main files: \n${unbuiltExensions.join('\n')}`); - } return builtinExtensions; } -const builtinExtensionsPromise = initialize(); +async function getBuiltInExtensionInfo(extensionPath) { + const packageJSON = await getExtensionPackageJSON(extensionPath); + if (!packageJSON) { + return undefined; + } + const builtInExtensionPath = path.basename(extensionPath); + + let children = []; + try { + children = await readdir(extensionPath); + } catch (error) { + console.log(`Can not read extension folder ${extensionPath}: ${error}`); + return; + } + const readme = children.find(child => /^readme(\.txt|\.md|)$/i.test(child)); + const changelog = children.find(child => /^changelog(\.txt|\.md|)$/i.test(child)); + const packageJSONNLS = children.find(child => /^package.nls.json$/i.test(child)); + return { + extensionPath: builtInExtensionPath, + packageJSON, + packageNLSPath: packageJSONNLS ? `${builtInExtensionPath}/${packageJSONNLS}` : undefined, + readmePath: readme ? `${builtInExtensionPath}/${readme}` : undefined, + changelogPath: changelog ? `${builtInExtensionPath}/${changelog}` : undefined + }; +} + +async function getDefaultExtensionInfos() { + const extensions = []; + const locations = {}; + + let extensionArg = args['extension']; + if (!extensionArg) { + return { extensions, locations } + } + + const extensionPaths = Array.isArray(extensionArg) ? extensionArg : [extensionArg]; + await Promise.all(extensionPaths.map(async extensionPath => { + extensionPath = path.resolve(process.cwd(), extensionPath); + const packageJSON = await getExtensionPackageJSON(extensionPath); + if (packageJSON) { + const extensionId = `${packageJSON.publisher}.${packageJSON.name}`; + extensions.push({ + packageJSON, + extensionLocation: { scheme: SCHEME, authority: AUTHORITY, path: `/extension/${extensionId}` } + }); + locations[extensionId] = extensionPath; + } + })); + return { extensions, locations }; +} + +async function getExtensionPackageJSON(extensionPath) { + + const packageJSONPath = path.join(extensionPath, 'package.json'); + if (await exists(packageJSONPath)) { + try { + let packageJSON = JSON.parse((await readFile(packageJSONPath)).toString()); + if (packageJSON.main && !packageJSON.browser) { + return; // unsupported + } + + if (packageJSON.browser) { + packageJSON.main = packageJSON.browser; + + let mainFilePath = path.join(extensionPath, packageJSON.browser); + if (path.extname(mainFilePath) !== '.js') { + mainFilePath += '.js'; + } + if (!await exists(mainFilePath)) { + fancyLog(`${ansiColors.yellow('Warning')}: Could not find ${mainFilePath}. Use ${ansiColors.cyan('yarn gulp watch-web')} to build the built-in extensions.`); + } + } + packageJSON.extensionKind = ['web']; // enable for Web + + const packageNLSPath = path.join(extensionPath, 'package.nls.json'); + const packageNLSExists = await exists(packageNLSPath); + if (packageNLSExists) { + packageJSON = extensions.translatePackageJSON(packageJSON, packageNLSPath); // temporary, until fixed in core + } + + return packageJSON; + } catch (e) { + console.log(e); + } + } + return undefined; +} + + +const builtinExtensionsPromise = getBuiltInExtensionInfos(EXTENSIONS_ROOT); +const defaultExtensionsPromise = getDefaultExtensionInfos(); const mapCallbackUriToRequestId = new Map(); @@ -158,9 +196,13 @@ const server = http.createServer((req, res) => { // static requests return handleStatic(req, res, parsedUrl); } - if (/^\/static-extension\//.test(pathname)) { - // static extension requests - return handleStaticExtension(req, res, parsedUrl); + if (/^\/extension\//.test(pathname)) { + // default extension requests + return handleExtension(req, res, parsedUrl); + } + if (/^\/builtin-extension\//.test(pathname)) { + // builtin extension requests + return handleBuiltinExtension(req, res, parsedUrl); } if (pathname === '/') { // main web @@ -211,13 +253,34 @@ function handleStatic(req, res, parsedUrl) { * @param {import('http').ServerResponse} res * @param {import('url').UrlWithParsedQuery} parsedUrl */ -function handleStaticExtension(req, res, parsedUrl) { +async function handleExtension(req, res, parsedUrl) { + // Strip `/extension/` from the path + const relativePath = decodeURIComponent(parsedUrl.pathname.substr('/extension/'.length)); + const firstSlash = relativePath.indexOf('/'); + if (firstSlash === -1) { + return serveError(req, res, 400, `Bad request.`); + } + const extensionId = relativePath.substr(0, firstSlash); + const { locations } = await defaultExtensionsPromise; - // Strip `/static-extension/` from the path - const relativeFilePath = path.normalize(decodeURIComponent(parsedUrl.pathname.substr('/static-extension/'.length))); + const extensionPath = locations[extensionId]; + if (!extensionPath) { + return serveError(req, res, 400, `Bad request.`); + } - const filePath = path.join(EXTENSIONS_ROOT, relativeFilePath); + const filePath = path.join(extensionPath, relativePath.substr(firstSlash + 1)); + return serveFile(req, res, filePath); +} +/** + * @param {import('http').IncomingMessage} req + * @param {import('http').ServerResponse} res + * @param {import('url').UrlWithParsedQuery} parsedUrl + */ +async function handleBuiltinExtension(req, res, parsedUrl) { + // Strip `/builtin-extension/` from the path + const relativePath = decodeURIComponent(parsedUrl.pathname.substr('/builtin-extension/'.length)); + const filePath = path.join(EXTENSIONS_ROOT, relativePath); return serveFile(req, res, filePath); } @@ -254,13 +317,15 @@ async function handleRoot(req, res) { } const builtinExtensions = await builtinExtensionsPromise; + const { extensions } = await defaultExtensionsPromise; const webConfigJSON = escapeAttribute(JSON.stringify({ folderUri: folderUri, - builtinExtensionsServiceUrl: `${SCHEME}://${AUTHORITY}/static-extension` + staticExtensions: extensions, + builtinExtensionsServiceUrl: `${SCHEME}://${AUTHORITY}/builtin-extension` })); - const data = (await util.promisify(fs.readFile)(WEB_MAIN)).toString() + const data = (await readFile(WEB_MAIN)).toString() .replace('{{WORKBENCH_WEB_CONFIGURATION}}', () => webConfigJSON) // use a replace function to avoid that regexp replace patterns ($&, $0, ...) are applied .replace('{{WORKBENCH_BUILTIN_EXTENSIONS}}', () => escapeAttribute(JSON.stringify(builtinExtensions))) .replace('{{WEBVIEW_ENDPOINT}}', '') @@ -422,10 +487,6 @@ async function serveFile(req, res, filePath, responseHeaders = Object.create(nul // Sanity checks filePath = path.normalize(filePath); // ensure no "." and ".." - if (filePath.indexOf(`${APP_ROOT}${path.sep}`) !== 0) { - // invalid location outside of APP_ROOT - return serveError(req, res, 400, `Bad request.`); - } const stat = await util.promisify(fs.stat)(filePath); diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index c00cef45a27..39e05d7fe73 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -24,6 +24,7 @@ import Severity from 'vs/base/common/severity'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { mixin } from 'vs/base/common/objects'; export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; @@ -650,9 +651,9 @@ export interface ITreeItem { } export class ResolvableTreeItem implements ITreeItem { - handle: string; + handle!: string; parentHandle?: string; - collapsibleState: TreeItemCollapsibleState; + collapsibleState!: TreeItemCollapsibleState; label?: ITreeItemLabel; description?: string | boolean; icon?: UriComponents; @@ -668,20 +669,7 @@ export class ResolvableTreeItem implements ITreeItem { private resolved: boolean = false; private _hasResolve: boolean = false; constructor(treeItem: ITreeItem, resolve?: (() => Promise)) { - this.handle = treeItem.handle; - this.parentHandle = treeItem.parentHandle; - this.collapsibleState = treeItem.collapsibleState; - this.label = treeItem.label; - this.description = treeItem.description; - this.icon = treeItem.icon; - this.iconDark = treeItem.iconDark; - this.themeIcon = treeItem.themeIcon; - this.resourceUri = treeItem.resourceUri; - this.tooltip = treeItem.tooltip; - this.contextValue = treeItem.contextValue; - this.command = treeItem.command; - this.children = treeItem.children; - this.accessibilityInformation = treeItem.accessibilityInformation; + mixin(this, treeItem); this._hasResolve = !!resolve; this.resolve = async () => { if (resolve && !this.resolved) { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts index a5d0ae74f46..de8cb63d107 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts @@ -94,13 +94,16 @@ export function convertBufferRangeToViewport(bufferRange: IBufferRange, viewport } export function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string { - let line = ''; + let content = ''; for (let i = lineStart; i <= lineEnd; i++) { // Make sure only 0 to cols are considered as resizing when windows mode is enabled will // retain buffer data outside of the terminal width as reflow is disabled. - line += buffer.getLine(i)!.translateToString(true, 0, cols); + const line = buffer.getLine(i); + if (line) { + content += line.translateToString(true, 0, cols); + } } - return line; + return content; } export function positionIsInRange(position: IBufferCellPosition, range: IBufferRange): boolean { diff --git a/src/vs/workbench/contrib/update/browser/update.contribution.ts b/src/vs/workbench/contrib/update/browser/update.contribution.ts index 38b453f6f5f..f842ef50965 100644 --- a/src/vs/workbench/contrib/update/browser/update.contribution.ts +++ b/src/vs/workbench/contrib/update/browser/update.contribution.ts @@ -9,7 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { ShowCurrentReleaseNotesAction, ProductContribution, UpdateContribution, CheckForVSCodeUpdateAction, CONTEXT_UPDATE_STATE } from 'vs/workbench/contrib/update/browser/update'; +import { ShowCurrentReleaseNotesAction, ProductContribution, UpdateContribution, CheckForVSCodeUpdateAction, CONTEXT_UPDATE_STATE, SwitchProductQualityContribution } from 'vs/workbench/contrib/update/browser/update'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import product from 'vs/platform/product/common/product'; import { StateType } from 'vs/platform/update/common/update'; @@ -18,6 +18,7 @@ const workbench = Registry.as(WorkbenchExtensio workbench.registerWorkbenchContribution(ProductContribution, LifecyclePhase.Restored); workbench.registerWorkbenchContribution(UpdateContribution, LifecyclePhase.Restored); +workbench.registerWorkbenchContribution(SwitchProductQualityContribution, LifecyclePhase.Restored); const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index c328064ed42..568cbec7112 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -28,6 +28,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; export const CONTEXT_UPDATE_STATE = new RawContextKey('updateState', StateType.Idle); @@ -181,7 +182,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu @IActivityService private readonly activityService: IActivityService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IProductService private readonly productService: IProductService, - @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { super(); @@ -221,7 +222,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu case StateType.Idle: if (state.error) { this.onError(state.error); - } else if (this.state.type === StateType.CheckingForUpdates && this.state.context === this.workbenchEnvironmentService.configuration.sessionId) { + } else if (this.state.type === StateType.CheckingForUpdates && this.state.context === this.environmentService.configuration.sessionId) { this.onUpdateNotAvailable(); } break; @@ -402,7 +403,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu } private registerGlobalActivityActions(): void { - CommandsRegistry.registerCommand('update.check', () => this.updateService.checkForUpdates(this.workbenchEnvironmentService.configuration.sessionId)); + CommandsRegistry.registerCommand('update.check', () => this.updateService.checkForUpdates(this.environmentService.configuration.sessionId)); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '6_update', command: { @@ -477,6 +478,48 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu } } +export class SwitchProductQualityContribution extends Disposable implements IWorkbenchContribution { + + constructor( + @IProductService private readonly productService: IProductService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + ) { + super(); + + this.registerGlobalActivityActions(); + } + + private registerGlobalActivityActions(): void { + const quality = this.productService.quality; + const productQualityChangeHandler = this.environmentService.options?.productQualityChangeHandler; + if (productQualityChangeHandler && (quality === 'stable' || quality === 'insider')) { + const newQuality = quality === 'stable' ? 'insider' : 'stable'; + const commandId = `update.switchQuality.${newQuality}`; + CommandsRegistry.registerCommand(commandId, async accessor => { + const dialogService = accessor.get(IDialogService); + + const res = await dialogService.confirm({ + type: 'info', + message: nls.localize('relaunchMessage', "Changing the version requires a reload to take effect"), + detail: newQuality === 'insider' ? nls.localize('relaunchDetailInsiders', "Press the reload button to switch to the insiders version.") : nls.localize('relaunchDetailStable', "Press the reload button to switch to the stable version."), + primaryButton: nls.localize('reload', "&&Reload") + }); + + if (res.confirmed) { + productQualityChangeHandler(newQuality); + } + }); + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '6_update', + command: { + id: commandId, + title: newQuality === 'insider' ? nls.localize('switchToInsiders', "Switch to Insiders Version...") : nls.localize('switchToStable', "Switch to Stable Version...") + } + }); + } + } +} + export class CheckForVSCodeUpdateAction extends Action { static readonly ID = CheckForVSCodeUpdateActionId; diff --git a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts new file mode 100644 index 00000000000..936a51b094f --- /dev/null +++ b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ExtHostTreeViewsShape, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; +import { mock } from 'vs/base/test/common/mock'; +import { ITreeItem, IViewsRegistry, Extensions, ViewContainerLocation, IViewContainersRegistry, ITreeViewDescriptor, ITreeView, ViewContainer, IViewDescriptorService, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { MainThreadTreeViews } from 'vs/workbench/api/browser/mainThreadTreeViews'; +import { TestViewsService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestExtensionService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { CustomTreeView } from 'vs/workbench/contrib/views/browser/treeView'; +import { ViewDescriptorService } from 'vs/workbench/services/views/browser/viewDescriptorService'; + +suite('MainThreadHostTreeView', function () { + const testTreeViewId = 'testTreeView'; + const customValue = 'customValue'; + const ViewsRegistry = Registry.as(Extensions.ViewsRegistry); + + interface CustomTreeItem extends ITreeItem { + customProp: string; + } + + class MockExtHostTreeViewsShape extends mock() { + async $getChildren(treeViewId: string, treeItemHandle?: string): Promise { + return [{ handle: 'testItem1', collapsibleState: TreeItemCollapsibleState.Expanded, customProp: customValue }]; + } + + async $hasResolve(): Promise { + return false; + } + + $setVisible(): void { } + } + + let container: ViewContainer; + let mainThreadTreeViews: MainThreadTreeViews; + let extHostTreeViewsShape: MockExtHostTreeViewsShape; + + setup(async () => { + const instantiationService: TestInstantiationService = workbenchInstantiationService(); + const viewDescriptorService = instantiationService.createInstance(ViewDescriptorService); + instantiationService.stub(IViewDescriptorService, viewDescriptorService); + container = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer({ id: 'testContainer', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const viewDescriptor: ITreeViewDescriptor = { + id: testTreeViewId, + ctorDescriptor: null!, + name: 'Test View 1', + treeView: instantiationService.createInstance(CustomTreeView, 'testTree', 'Test Title'), + }; + ViewsRegistry.registerViews([viewDescriptor], container); + + const testExtensionService = new TestExtensionService(); + extHostTreeViewsShape = new MockExtHostTreeViewsShape(); + mainThreadTreeViews = new MainThreadTreeViews( + new class implements IExtHostContext { + remoteAuthority = ''; + assertRegistered() { } + set(v: any): any { return null; } + getProxy(): any { + return extHostTreeViewsShape; + } + }, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService()); + mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false }); + await testExtensionService.whenInstalledExtensionsRegistered(); + }); + + teardown(() => { + ViewsRegistry.deregisterViews(ViewsRegistry.getViews(container), container); + }); + + test('getChildren keeps custom properties', async () => { + const treeView: ITreeView = (ViewsRegistry.getView(testTreeViewId)).treeView; + const children = await treeView.dataProvider?.getChildren({ handle: 'root', collapsibleState: TreeItemCollapsibleState.Expanded }); + assert(children!.length === 1, 'Exactly one child should be returned'); + assert((children![0]).customProp === customValue, 'Tree Items should keep custom properties'); + }); + + +}); diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 240d8f0ef84..17a4f725d27 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -38,10 +38,12 @@ interface IExternalUriResolver { } interface ITunnelProvider { + /** * Support for creating tunnels. */ tunnelFactory?: ITunnelFactory; + /** * Support for filtering candidate ports */ @@ -169,14 +171,23 @@ interface IDefaultEditor { } interface IDefaultLayout { - /** @deprecated Use views instead */ + /** @deprecated Use views instead (TODO@eamodio remove eventually) */ readonly sidebar?: IDefaultSideBarLayout; - /** @deprecated Use views instead */ + /** @deprecated Use views instead (TODO@eamodio remove eventually) */ readonly panel?: IDefaultPanelLayout; readonly views?: IDefaultView[]; readonly editors?: IDefaultEditor[]; } +interface IProductQualityChangeHandler { + + /** + * Handler is being called when the user wants to switch between + * `insider` or `stable` product qualities. + */ + (newQuality: 'insider' | 'stable'): void; +} + interface IWorkbenchConstructionOptions { //#region Connection related configuration @@ -265,11 +276,6 @@ interface IWorkbenchConstructionOptions { */ readonly urlCallbackProvider?: IURLCallbackProvider; - /** - * Support for update reporting. - */ - readonly updateProvider?: IUpdateProvider; - /** * Support adding additional properties to telemetry. */ @@ -291,6 +297,21 @@ interface IWorkbenchConstructionOptions { //#endregion + //#region Update/Quality related + + /** + * Support for update reporting + */ + readonly updateProvider?: IUpdateProvider; + + /** + * Support for product quality switching + */ + readonly productQualityChangeHandler?: IProductQualityChangeHandler; + + //#endregion + + //#region Branding /** @@ -426,9 +447,10 @@ export { // LogLevel LogLevel, - // Updates + // Updates/Quality IUpdateProvider, IUpdate, + IProductQualityChangeHandler, // Telemetry ICommonTelemetryPropertiesResolver,