diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml index 62962bc3e98..cdd65c77202 100644 --- a/.github/workflows/feature-request.yml +++ b/.github/workflows/feature-request.yml @@ -35,7 +35,7 @@ jobs: upvotesRequired: 20 numCommentsOverride: 20 initComment: "This feature request is now a candidate for our backlog. The community has 60 days to [upvote](https://github.com/microsoft/vscode/wiki/Issues-Triaging#up-voting-a-feature-request) the issue. If it receives 20 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - warnComment: "This feature request has not yet received the 20 community [upvotes](https://github.com/microsoft/vscode/wiki/Issues-Triaging#up-voting-a-feature-request) it takes to make to our backlog. 10 days to go. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding" + warnComment: "This feature request has not yet received the 20 community [upvotes](https://github.com/microsoft/vscode/wiki/Issues-Triaging#up-voting-a-feature-request) it takes to make to our backlog. 10 days to go. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" acceptComment: ":slightly_smiling_face: This feature request received a sufficient number of community upvotes and we moved it to our backlog. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" rejectComment: ":slightly_frowning_face: In the last 60 days, this feature request has received less than 20 community upvotes and we closed it. Still a big Thank You to you for taking the time to create this issue! To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" warnDays: 10 diff --git a/build/package.json b/build/package.json index 3f779c55379..22206229ffb 100644 --- a/build/package.json +++ b/build/package.json @@ -44,7 +44,7 @@ "minimist": "^1.2.3", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "^4.0.0-dev.20200715", + "typescript": "^4.0.0-dev.20200722", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.6.0", "xml2js": "^0.4.17" diff --git a/build/yarn.lock b/build/yarn.lock index b0e68408127..79909133c5b 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2530,10 +2530,10 @@ typescript@^3.0.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== -typescript@^4.0.0-dev.20200715: - version "4.0.0-dev.20200715" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200715.tgz#d65961a5a6f13fde95a6f4db5f5946f15e4c59bc" - integrity sha512-gmPXoWktfXeutmWTM6el9U4vIn5kqOHGI1OESSOhPtLWrxodKqLfFuMygQtOUTtGjKLFQRFAJhHEwUhHZNOURA== +typescript@^4.0.0-dev.20200722: + version "4.0.0-dev.20200722" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200722.tgz#b59dd5a3cd84a98d5aae0e4f3a3c58f0c81a3b9b" + integrity sha512-MmJ1YyPNK3JYeKLiTg5sQXdeZaMgt99Fg4BMRZhJmhoq1/x2V1cpXMYvE1rtIYl9K7NvmTDdU3WDW7ZOD6ybaw== typical@^4.0.0: version "4.0.0" diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 477d530a0ed..3cadb80fd33 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -54,7 +54,7 @@ "url": "vscode://schemas/keybindings" }, { - "fileMatch": "vscode://defaultsettings/*/*.json", + "fileMatch": "vscode://defaultsettings/defaultSettings.json", "url": "vscode://schemas/settings/default" }, { diff --git a/extensions/git/package.json b/extensions/git/package.json index e96ef36abab..d9df45a10b6 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1801,7 +1801,7 @@ "Ignore", "ignore" ], - "filenames": [ + "extensions": [ ".gitignore_global", ".gitignore" ], diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index c1c78e629bf..cabcce04413 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -74,7 +74,7 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann new GitTimelineProvider(model) ); - await checkGitVersion(info); + checkGitVersion(info); return model; } diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 85ef490df23..a77b48647cd 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -40,13 +40,13 @@ "languages": [ { "id": "ignore", - "filenames": [ + "extensions": [ ".npmignore" ] }, { "id": "properties", - "filenames": [ + "extensions": [ ".npmrc" ] } diff --git a/extensions/package.json b/extensions/package.json index 7c668c9744a..69f5ea275b4 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.9.6" + "typescript": "3.9.7" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/typescript-language-features/extension-browser.webpack.config.js b/extensions/typescript-language-features/extension-browser.webpack.config.js index 0662fd46d5e..560a4bb021e 100644 --- a/extensions/typescript-language-features/extension-browser.webpack.config.js +++ b/extensions/typescript-language-features/extension-browser.webpack.config.js @@ -7,6 +7,8 @@ 'use strict'; const CopyPlugin = require('copy-webpack-plugin'); +const { lchmod } = require('graceful-fs'); +const Terser = require('terser'); const withBrowserDefaults = require('../shared.webpack.config').browser; @@ -19,7 +21,22 @@ module.exports = withBrowserDefaults({ // @ts-ignore new CopyPlugin({ patterns: [ - { from: 'node_modules/typescript-web-server', to: 'typescript-web' } + { + from: 'node_modules/typescript-web-server', + to: 'typescript-web', + transform: (content, absoluteFrom) => { + if (absoluteFrom.endsWith('tsserver.js')) { + return Terser.minify(content.toString()).code; + } + return content; + }, + transformPath: (targetPath) => { + if (targetPath.endsWith('tsserver.js')) { + return targetPath.replace('tsserver.js', 'tsserver.web.js'); + } + return targetPath; + } + } ], }), ], diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index f7266e34480..8c5e9a4165e 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -28,6 +28,7 @@ "@types/rimraf": "2.0.2", "@types/semver": "^5.5.0", "copy-webpack-plugin": "^6.0.3", + "terser": "^4.8.0", "typescript-web-server": "git://github.com/mjbvz/ts-server-web-build", "vscode": "^1.1.36" }, @@ -685,6 +686,22 @@ "description": "%typescript.preferences.importModuleSpecifierEnding%", "scope": "resource" }, + "typescript.preferences.includePackageJsonAutoImports": { + "type": "string", + "enum": [ + "all", + "exclude-dev", + "none" + ], + "enumDescriptions": [ + "%typescript.preferences.includePackageJsonAutoImports.all%", + "%typescript.preferences.includePackageJsonAutoImports.excludeDev%", + "%typescript.preferences.includePackageJsonAutoImports.none%" + ], + "default": "exclude-dev", + "markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%", + "scope": "window" + }, "javascript.preferences.renameShorthandProperties": { "type": "boolean", "default": true, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 6f4241434ef..c59d90a3267 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -74,8 +74,12 @@ "typescript.preferences.importModuleSpecifierEnding": "Preferred path ending for auto imports.", "typescript.preferences.importModuleSpecifierEnding.auto": "Use project settings to select a default.", "typescript.preferences.importModuleSpecifierEnding.minimal": "Shorten `./component/index.js` to `./component`.", - "typescript.preferences.importModuleSpecifierEnding.index": "Shorten `./component/index.js` to `./component/index`", + "typescript.preferences.importModuleSpecifierEnding.index": "Shorten `./component/index.js` to `./component/index`.", "typescript.preferences.importModuleSpecifierEnding.js": "Do not shorten path endings; include the `.js` extension.", + "typescript.preferences.includePackageJsonAutoImports": "Enable/disable processing `package.json` dependencies for available auto imports.", + "typescript.preferences.includePackageJsonAutoImports.all": "Include all listed dependencies.", + "typescript.preferences.includePackageJsonAutoImports.excludeDev": "Exclude devDependencies.", + "typescript.preferences.includePackageJsonAutoImports.none": "Disable package.json dependency processing.", "typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code. Requires using TypeScript 2.9 or newer in the workspace.", "typescript.updateImportsOnFileMove.enabled.prompt": "Prompt on each rename.", "typescript.updateImportsOnFileMove.enabled.always": "Always update paths automatically.", diff --git a/extensions/typescript-language-features/src/utils/commandManager.ts b/extensions/typescript-language-features/src/commands/commandManager.ts similarity index 100% rename from extensions/typescript-language-features/src/utils/commandManager.ts rename to extensions/typescript-language-features/src/commands/commandManager.ts diff --git a/extensions/typescript-language-features/src/commands/configurePlugin.ts b/extensions/typescript-language-features/src/commands/configurePlugin.ts index 8af85d8b94e..f781c4a50fa 100644 --- a/extensions/typescript-language-features/src/commands/configurePlugin.ts +++ b/extensions/typescript-language-features/src/commands/configurePlugin.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command } from '../utils/commandManager'; import { PluginManager } from '../utils/plugins'; +import { Command } from './commandManager'; export class ConfigurePluginCommand implements Command { public readonly id = '_typescript.configurePlugin'; diff --git a/extensions/typescript-language-features/src/commands/goToProjectConfiguration.ts b/extensions/typescript-language-features/src/commands/goToProjectConfiguration.ts index a4fb9c33b4e..11adec35251 100644 --- a/extensions/typescript-language-features/src/commands/goToProjectConfiguration.ts +++ b/extensions/typescript-language-features/src/commands/goToProjectConfiguration.ts @@ -5,9 +5,9 @@ import * as vscode from 'vscode'; import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { Command } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; import { openProjectConfigForFile, ProjectType } from '../utils/tsconfig'; +import { Command } from './commandManager'; export class TypeScriptGoToProjectConfigCommand implements Command { public readonly id = 'typescript.goToProjectConfig'; diff --git a/extensions/typescript-language-features/src/commands/index.ts b/extensions/typescript-language-features/src/commands/index.ts index 5d617603e13..7ab7ae09d20 100644 --- a/extensions/typescript-language-features/src/commands/index.ts +++ b/extensions/typescript-language-features/src/commands/index.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { CommandManager } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; import { PluginManager } from '../utils/plugins'; +import { CommandManager } from './commandManager'; import { ConfigurePluginCommand } from './configurePlugin'; import { JavaScriptGoToProjectConfigCommand, TypeScriptGoToProjectConfigCommand } from './goToProjectConfiguration'; import { LearnMoreAboutRefactoringsCommand } from './learnMoreAboutRefactorings'; diff --git a/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts b/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts index 21366d6c607..b166397e38b 100644 --- a/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts +++ b/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Command } from '../utils/commandManager'; import { isTypeScriptDocument } from '../utils/languageModeIds'; +import { Command } from './commandManager'; export class LearnMoreAboutRefactoringsCommand implements Command { public static readonly id = '_typescript.learnMoreAboutRefactorings'; diff --git a/extensions/typescript-language-features/src/commands/openTsServerLog.ts b/extensions/typescript-language-features/src/commands/openTsServerLog.ts index be47a985cd7..cd41445fef2 100644 --- a/extensions/typescript-language-features/src/commands/openTsServerLog.ts +++ b/extensions/typescript-language-features/src/commands/openTsServerLog.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { Command } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; +import { Command } from './commandManager'; export class OpenTsServerLogCommand implements Command { public readonly id = 'typescript.openTsServerLog'; diff --git a/extensions/typescript-language-features/src/commands/reloadProject.ts b/extensions/typescript-language-features/src/commands/reloadProject.ts index fbba68eb94e..4da59685f67 100644 --- a/extensions/typescript-language-features/src/commands/reloadProject.ts +++ b/extensions/typescript-language-features/src/commands/reloadProject.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { Command } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; +import { Command } from './commandManager'; export class ReloadTypeScriptProjectsCommand implements Command { public readonly id = 'typescript.reloadProjects'; diff --git a/extensions/typescript-language-features/src/commands/restartTsServer.ts b/extensions/typescript-language-features/src/commands/restartTsServer.ts index a357224d352..77dcae870ee 100644 --- a/extensions/typescript-language-features/src/commands/restartTsServer.ts +++ b/extensions/typescript-language-features/src/commands/restartTsServer.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { Command } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; +import { Command } from './commandManager'; export class RestartTsServerCommand implements Command { public readonly id = 'typescript.restartTsServer'; diff --git a/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts b/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts index f375b55e938..d70f59472ff 100644 --- a/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts +++ b/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { Command } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; +import { Command } from './commandManager'; export class SelectTypeScriptVersionCommand implements Command { public readonly id = 'typescript.selectTypeScriptVersion'; diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index d9c22cdb1a7..f48f2b5071d 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -11,9 +11,9 @@ import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost'; import { noopRequestCancellerFactory } from './tsServer/cancellation'; import { noopLogDirectoryProvider } from './tsServer/logDirectoryProvider'; import { ITypeScriptVersionProvider, TypeScriptVersion, TypeScriptVersionSource } from './tsServer/versionProvider'; -import { WorkerServerProcess } from './tsServer/workerServerProcess'; +import { WorkerServerProcess } from './tsServer/serverProcess.browser'; import API from './utils/api'; -import { CommandManager } from './utils/commandManager'; +import { CommandManager } from './commands/commandManager'; import { TypeScriptServiceConfiguration } from './utils/configuration'; import { PluginManager } from './utils/plugins'; @@ -52,7 +52,7 @@ export function activate( const versionProvider = new StaticVersionProvider( new TypeScriptVersion( TypeScriptVersionSource.Bundled, - '/builtin-extension/typescript-language-features/dist/browser/typescript-web/tsserver.js', + '/builtin-extension/typescript-language-features/dist/browser/typescript-web/tsserver.web.js', API.v400)); const lazyClientHost = createLazyClientHost(context, false, { diff --git a/extensions/typescript-language-features/src/extension.ts b/extensions/typescript-language-features/src/extension.ts index 99e304b6758..1d6ccd2914d 100644 --- a/extensions/typescript-language-features/src/extension.ts +++ b/extensions/typescript-language-features/src/extension.ts @@ -11,12 +11,12 @@ import { LanguageConfigurationManager } from './languageFeatures/languageConfigu import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost'; import { nodeRequestCancellerFactory } from './tsServer/cancellation.electron'; import { NodeLogDirectoryProvider } from './tsServer/logDirectoryProvider.electron'; -import { ChildServerProcess } from './tsServer/serverProcess'; +import { ChildServerProcess } from './tsServer/serverProcess.electron'; import { DiskTypeScriptVersionProvider } from './tsServer/versionProvider.electron'; -import { CommandManager } from './utils/commandManager'; -import * as electron from './utils/electron'; -import { onCaseInsenitiveFileSystem } from './utils/fileSystem'; +import { CommandManager } from './commands/commandManager'; +import { onCaseInsenitiveFileSystem } from './utils/fileSystem.electron'; import { PluginManager } from './utils/plugins'; +import * as temp from './utils/temp.electron'; export function activate( context: vscode.ExtensionContext @@ -62,5 +62,5 @@ export function activate( } export function deactivate() { - rimraf.sync(electron.getInstanceDir()); + rimraf.sync(temp.getInstanceTempDir()); } diff --git a/extensions/typescript-language-features/src/languageFeatures/completions.ts b/extensions/typescript-language-features/src/languageFeatures/completions.ts index 229192d9e08..9cac559ef20 100644 --- a/extensions/typescript-language-features/src/languageFeatures/completions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/completions.ts @@ -5,14 +5,14 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { Command, CommandManager } from '../commands/commandManager'; import type * as Proto from '../protocol'; import * as PConst from '../protocol.const'; import { ClientCapability, ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; import { applyCodeAction } from '../utils/codeAction'; -import { Command, CommandManager } from '../utils/commandManager'; -import { conditionalRegistration, requireSomeCapability, requireConfiguration } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireConfiguration, requireSomeCapability } from '../utils/dependentRegistration'; import { DocumentSelector } from '../utils/documentSelector'; import { parseKindModifier } from '../utils/modifiers'; import * as Previewer from '../utils/previewer'; @@ -330,10 +330,25 @@ class CompletionAcceptedCommand implements Command { public constructor( private readonly onCompletionAccepted: (item: vscode.CompletionItem) => void, + private readonly telemetryReporter: TelemetryReporter, ) { } public execute(item: vscode.CompletionItem) { this.onCompletionAccepted(item); + if (item instanceof MyCompletionItem) { + /* __GDPR__ + "completions.accept" : { + "isPackageJsonImport" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, + "${include}": [ + "${TypeScriptCommonProperties}" + ] + } + */ + this.telemetryReporter.logTelemetry('completions.accept', { + // @ts-expect-error - remove after TS 4.0 protocol update + isPackageJsonImport: item.tsEntry.isPackageJsonImport ? 'true' : undefined, + }); + } } } @@ -422,7 +437,7 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< ) { commandManager.register(new ApplyCompletionCodeActionCommand(this.client)); commandManager.register(new CompositeCommand()); - commandManager.register(new CompletionAcceptedCommand(onCompletionAccepted)); + commandManager.register(new CompletionAcceptedCommand(onCompletionAccepted, this.telemetryReporter)); } public async provideCompletionItems( @@ -471,34 +486,18 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< let dotAccessorContext: DotAccessorContext | undefined; let entries: ReadonlyArray; let metadata: any | undefined; + let response: ServerResponse.Response | undefined; + let duration: number | undefined; if (this.client.apiVersion.gte(API.v300)) { const startTime = Date.now(); - let response: ServerResponse.Response | undefined; try { response = await this.client.interruptGetErr(() => this.client.execute('completionInfo', args, token)); } finally { - const duration: number = Date.now() - startTime; - - /* __GDPR__ - "completions.execute" : { - "duration" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "type" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "count" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "updateGraphDurationMs" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "${include}": [ - "${TypeScriptCommonProperties}" - ] - } - */ - this.telemetryReporter.logTelemetry('completions.execute', { - duration: duration, - type: response?.type ?? 'unknown', - count: response?.type === 'response' && response.body ? response.body.entries.length : 0, - updateGraphDurationMs: response?.type === 'response' ? response.performanceData?.updateGraphDurationMs : undefined, - }); + duration = Date.now() - startTime; } if (response.type !== 'response' || !response.body) { + this.logCompletionsTelemetry(duration, response); return null; } isNewIdentifierLocation = response.body.isNewIdentifierLocation; @@ -536,15 +535,47 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< useFuzzyWordRangeLogic: this.client.apiVersion.lt(API.v390), }; + let includesPackageJsonImport = false; const items: MyCompletionItem[] = []; for (let entry of entries) { if (!shouldExcludeCompletionEntry(entry, completionConfiguration)) { items.push(new MyCompletionItem(position, document, entry, completionContext, metadata)); + // @ts-expect-error - remove after TS 4.0 protocol update + includesPackageJsonImport = !!entry.isPackageJsonImport; } } + if (duration !== undefined) { + this.logCompletionsTelemetry(duration, response, includesPackageJsonImport); + } return new vscode.CompletionList(items, isIncomplete); } + private logCompletionsTelemetry( + duration: number, + response: ServerResponse.Response | undefined, + includesPackageJsonImport?: boolean + ) { + /* __GDPR__ + "completions.execute" : { + "duration" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, + "type" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, + "count" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, + "updateGraphDurationMs" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, + "includesPackageJsonImport" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, + "${include}": [ + "${TypeScriptCommonProperties}" + ] + } + */ + this.telemetryReporter.logTelemetry('completions.execute', { + duration: duration, + type: response?.type ?? 'unknown', + count: response?.type === 'response' && response.body ? response.body.entries.length : 0, + updateGraphDurationMs: response?.type === 'response' ? response.performanceData?.updateGraphDurationMs : undefined, + includesPackageJsonImport: includesPackageJsonImport ? 'true' : undefined, + }); + } + private getTsTriggerCharacter(context: vscode.CompletionContext): Proto.CompletionsTriggerCharacter | undefined { switch (context.triggerCharacter) { case '@': // Workaround for https://github.com/Microsoft/TypeScript/issues/27321 diff --git a/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts index f8821262b04..218cda4103f 100644 --- a/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts +++ b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts @@ -9,7 +9,7 @@ import type * as Proto from '../protocol'; import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; -import { Command, CommandManager } from '../utils/commandManager'; +import { Command, CommandManager } from '../commands/commandManager'; import { conditionalRegistration, requireMinVersion, requireSomeCapability } from '../utils/dependentRegistration'; import { DocumentSelector } from '../utils/documentSelector'; import { TelemetryReporter } from '../utils/telemetry'; diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index b2c2e031df2..0965a574721 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -5,21 +5,21 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { Command, CommandManager } from '../commands/commandManager'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient, ClientCapability } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; import { applyCodeActionCommands, getEditForCodeAction } from '../utils/codeAction'; -import { Command, CommandManager } from '../utils/commandManager'; +import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as fixNames from '../utils/fixNames'; import { memoize } from '../utils/memoize'; +import { equals } from '../utils/objects'; import { TelemetryReporter } from '../utils/telemetry'; import * as typeConverters from '../utils/typeConverters'; import { DiagnosticsManager } from './diagnostics'; import FileConfigurationManager from './fileConfigurationManager'; -import { equals } from '../utils/objects'; -import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; -import { DocumentSelector } from '../utils/documentSelector'; const localize = nls.loadMessageBundle(); diff --git a/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts index 4b5612e41e7..fe1d3eeee3c 100644 --- a/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -5,13 +5,13 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { Command, CommandManager } from '../commands/commandManager'; import { LearnMoreAboutRefactoringsCommand } from '../commands/learnMoreAboutRefactorings'; import type * as Proto from '../protocol'; import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; -import { Command, CommandManager } from '../utils/commandManager'; -import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireMinVersion, requireSomeCapability } from '../utils/dependentRegistration'; import { DocumentSelector } from '../utils/documentSelector'; import * as fileSchemes from '../utils/fileSchemes'; import { TelemetryReporter } from '../utils/telemetry'; diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 7f15fad8e8f..92ab84be308 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -9,7 +9,7 @@ import { DiagnosticKind } from './languageFeatures/diagnostics'; import FileConfigurationManager from './languageFeatures/fileConfigurationManager'; import { CachedResponse } from './tsServer/cachedResponse'; import TypeScriptServiceClient from './typescriptServiceClient'; -import { CommandManager } from './utils/commandManager'; +import { CommandManager } from './commands/commandManager'; import { Disposable } from './utils/dispose'; import { DocumentSelector } from './utils/documentSelector'; import * as fileSchemes from './utils/fileSchemes'; diff --git a/extensions/typescript-language-features/src/lazyClientHost.ts b/extensions/typescript-language-features/src/lazyClientHost.ts index 4fb37e643e1..7f136285215 100644 --- a/extensions/typescript-language-features/src/lazyClientHost.ts +++ b/extensions/typescript-language-features/src/lazyClientHost.ts @@ -4,15 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { CommandManager } from './commands/commandManager'; import { OngoingRequestCancellerFactory } from './tsServer/cancellation'; import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider'; import { TsServerProcessFactory } from './tsServer/server'; import { ITypeScriptVersionProvider } from './tsServer/versionProvider'; import TypeScriptServiceClientHost from './typeScriptServiceClientHost'; import { flatten } from './utils/arrays'; -import { CommandManager } from './utils/commandManager'; import { standardLanguageDescriptions } from './utils/languageDescription'; -import * as ProjectStatus from './utils/largeProjectStatus'; import { lazy, Lazy } from './utils/lazy'; import ManagedFileContextManager from './utils/managedFileContext'; import { PluginManager } from './utils/plugins'; @@ -40,13 +39,6 @@ export function createLazyClientHost( context.subscriptions.push(clientHost); - clientHost.serviceClient.onReady(() => { - context.subscriptions.push( - ProjectStatus.create( - clientHost.serviceClient, - clientHost.serviceClient.telemetryReporter)); - }); - return clientHost; }); } diff --git a/extensions/typescript-language-features/src/languageFeatures/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts similarity index 100% rename from extensions/typescript-language-features/src/languageFeatures/bufferSyncSupport.ts rename to extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts diff --git a/extensions/typescript-language-features/src/tsServer/cancellation.electron.ts b/extensions/typescript-language-features/src/tsServer/cancellation.electron.ts index e098889b450..853ca0c1594 100644 --- a/extensions/typescript-language-features/src/tsServer/cancellation.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/cancellation.electron.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import { OngoingRequestCanceller, OngoingRequestCancellerFactory } from './cancellation'; -import { getTempFile } from '../utils/electron'; +import { getTempFile } from '../utils/temp.electron'; import Tracer from '../utils/tracer'; +import { OngoingRequestCanceller, OngoingRequestCancellerFactory } from './cancellation'; export class NodeRequestCanceller implements OngoingRequestCanceller { public readonly cancellationPipeName: string; diff --git a/extensions/typescript-language-features/src/tsServer/workerServerProcess.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts similarity index 100% rename from extensions/typescript-language-features/src/tsServer/workerServerProcess.ts rename to extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts similarity index 86% rename from extensions/typescript-language-features/src/tsServer/serverProcess.ts rename to extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index cdb3e1c8932..f9d70d858e0 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ChildProcess } from 'child_process'; +import * as child_process from 'child_process'; import * as fs from 'fs'; +import * as path from 'path'; import type { Readable } from 'stream'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; -import { TsServerProcess, TsServerProcessKind } from './server'; import { TypeScriptServiceConfiguration } from '../utils/configuration'; -import { fork } from '../utils/electron'; -import { TypeScriptVersionManager } from './versionManager'; import { Disposable } from '../utils/dispose'; +import { TsServerProcess, TsServerProcessKind } from './server'; +import { TypeScriptVersionManager } from './versionManager'; const localize = nls.loadMessageBundle(); @@ -149,20 +149,36 @@ export class ChildServerProcess extends Disposable implements TsServerProcess { versionManager.reset(); tsServerPath = versionManager.currentVersion.tsServerPath; } - const childProcess = fork(tsServerPath, args, this.getForkOptions(kind, configuration)); + + const childProcess = child_process.fork(tsServerPath, args, { + silent: true, + cwd: undefined, + env: this.generatePatchedEnv(process.env, tsServerPath), + execArgv: this.getExecArgv(kind, configuration), + }); + return new ChildServerProcess(childProcess); } - private static getForkOptions(kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration) { + private static generatePatchedEnv(env: any, modulePath: string): any { + const newEnv = Object.assign({}, env); + + newEnv['ELECTRON_RUN_AS_NODE'] = '1'; + newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..'); + + // Ensure we always have a PATH set + newEnv['PATH'] = newEnv['PATH'] || process.env.PATH; + + return newEnv; + } + + private static getExecArgv(kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration): string[] { const debugPort = this.getDebugPort(kind); const inspectFlag = process.env['TSS_DEBUG_BRK'] ? '--inspect-brk' : '--inspect'; - const tsServerForkOptions: any = { - execArgv: [ - ...(debugPort ? [`${inspectFlag}=${debugPort}`] : []), - ...(configuration.maxTsServerMemory ? [`--max-old-space-size=${configuration.maxTsServerMemory}`] : []) - ] - }; - return tsServerForkOptions; + return [ + ...(debugPort ? [`${inspectFlag}=${debugPort}`] : []), + ...(configuration.maxTsServerMemory ? [`--max-old-space-size=${configuration.maxTsServerMemory}`] : []) + ]; } private static getDebugPort(kind: TsServerProcessKind): number | undefined { @@ -181,7 +197,7 @@ export class ChildServerProcess extends Disposable implements TsServerProcess { } private constructor( - private readonly _process: ChildProcess, + private readonly _process: child_process.ChildProcess, ) { super(); this._reader = this._register(new Reader(this._process.stdout!)); diff --git a/extensions/typescript-language-features/src/tsServer/versionStatus.ts b/extensions/typescript-language-features/src/tsServer/versionStatus.ts index a459a967a8e..20b9debc27b 100644 --- a/extensions/typescript-language-features/src/tsServer/versionStatus.ts +++ b/extensions/typescript-language-features/src/tsServer/versionStatus.ts @@ -5,9 +5,9 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { Command, CommandManager } from '../commands/commandManager'; import { ITypeScriptServiceClient } from '../typescriptService'; import { coalesce } from '../utils/arrays'; -import { Command, CommandManager } from '../utils/commandManager'; import { Disposable } from '../utils/dispose'; import { isTypeScriptDocument } from '../utils/languageModeIds'; import { isImplicitProjectConfigFile, openOrCreateConfig, openProjectConfigForFile, openProjectConfigOrPromptToCreate, ProjectType } from '../utils/tsconfig'; diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index 4827250bda8..908735a7f2d 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -21,13 +21,14 @@ import { ITypeScriptVersionProvider } from './tsServer/versionProvider'; import VersionStatus from './tsServer/versionStatus'; import TypeScriptServiceClient from './typescriptServiceClient'; import { coalesce, flatten } from './utils/arrays'; -import { CommandManager } from './utils/commandManager'; +import { CommandManager } from './commands/commandManager'; import { Disposable } from './utils/dispose'; import * as errorCodes from './utils/errorCodes'; import { DiagnosticLanguage, LanguageDescription } from './utils/languageDescription'; import { PluginManager } from './utils/plugins'; import * as typeConverters from './utils/typeConverters'; import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus'; +import * as ProjectStatus from './utils/largeProjectStatus'; namespace Experimental { export interface Diagnostic extends Proto.Diagnostic { @@ -95,6 +96,8 @@ export default class TypeScriptServiceClientHost extends Disposable { this._register(new VersionStatus(this.client, services.commandManager)); this._register(new AtaProgressReporter(this.client)); this.typingsStatus = this._register(new TypingsStatus(this.client)); + this._register(ProjectStatus.create(this.client)); + this.fileConfigurationManager = this._register(new FileConfigurationManager(this.client, onCaseInsenitiveFileSystem)); for (const description of descriptions) { diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index f4b7ab17903..de32927d816 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import BufferSyncSupport from './languageFeatures/bufferSyncSupport'; import * as Proto from './protocol'; +import BufferSyncSupport from './tsServer/bufferSyncSupport'; import { ExectuionTarget } from './tsServer/server'; import { TypeScriptVersion } from './tsServer/versionProvider'; import API from './utils/api'; import { TypeScriptServiceConfiguration } from './utils/configuration'; import { PluginManager } from './utils/plugins'; +import { TelemetryReporter } from './utils/telemetry'; export namespace ServerResponse { @@ -159,9 +160,11 @@ export interface ITypeScriptServiceClient { showVersionPicker(): void; readonly apiVersion: API; + readonly pluginManager: PluginManager; readonly configuration: TypeScriptServiceConfiguration; readonly bufferSyncSupport: BufferSyncSupport; + readonly telemetryReporter: TelemetryReporter; execute( command: K, diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index d0465fed605..3f086292310 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -6,10 +6,10 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import BufferSyncSupport from './languageFeatures/bufferSyncSupport'; import { DiagnosticKind, DiagnosticsManager } from './languageFeatures/diagnostics'; import * as Proto from './protocol'; import { EventName } from './protocol.const'; +import BufferSyncSupport from './tsServer/bufferSyncSupport'; import { OngoingRequestCancellerFactory } from './tsServer/cancellation'; import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider'; import { ITypeScriptServer, TsServerProcessFactory } from './tsServer/server'; @@ -115,11 +115,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType private readonly loadingIndicator = new ServerInitializingIndicator(); public readonly telemetryReporter: TelemetryReporter; - public readonly bufferSyncSupport: BufferSyncSupport; public readonly diagnosticsManager: DiagnosticsManager; - public readonly pluginManager: PluginManager; + private readonly logDirectoryProvider: ILogDirectoryProvider; private readonly cancellerFactory: OngoingRequestCancellerFactory; private readonly versionProvider: ITypeScriptVersionProvider; @@ -527,6 +526,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType preferences: { providePrefixAndSuffixTextForRename: true, allowRenameOfImportPath: true, + // @ts-expect-error, remove after 4.0 protocol update + includePackageJsonAutoImports: this._configuration.includePackageJsonAutoImports, }, watchOptions }; diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index adeccbb8efc..cde1e14b9a0 100644 --- a/extensions/typescript-language-features/src/utils/configuration.ts +++ b/extensions/typescript-language-features/src/utils/configuration.ts @@ -66,6 +66,7 @@ export class TypeScriptServiceConfiguration { public readonly maxTsServerMemory: number; public readonly enablePromptUseWorkspaceTsdk: boolean; public readonly watchOptions: protocol.WatchOptions | undefined; + public readonly includePackageJsonAutoImports: string | undefined; public static loadFromWorkspace(): TypeScriptServiceConfiguration { return new TypeScriptServiceConfiguration(); @@ -88,6 +89,7 @@ export class TypeScriptServiceConfiguration { this.maxTsServerMemory = TypeScriptServiceConfiguration.readMaxTsServerMemory(configuration); this.enablePromptUseWorkspaceTsdk = TypeScriptServiceConfiguration.readEnablePromptUseWorkspaceTsdk(configuration); this.watchOptions = TypeScriptServiceConfiguration.readWatchOptions(configuration); + this.includePackageJsonAutoImports = TypeScriptServiceConfiguration.readIncludePackageJsonAutoImports(configuration); } public isEqualTo(other: TypeScriptServiceConfiguration): boolean { @@ -104,7 +106,8 @@ export class TypeScriptServiceConfiguration { && this.enableProjectDiagnostics === other.enableProjectDiagnostics && this.maxTsServerMemory === other.maxTsServerMemory && objects.equals(this.watchOptions, other.watchOptions) - && this.enablePromptUseWorkspaceTsdk === other.enablePromptUseWorkspaceTsdk; + && this.enablePromptUseWorkspaceTsdk === other.enablePromptUseWorkspaceTsdk + && this.includePackageJsonAutoImports === other.includePackageJsonAutoImports; } private static fixPathPrefixes(inspectValue: string): string { @@ -178,6 +181,10 @@ export class TypeScriptServiceConfiguration { return configuration.get('typescript.tsserver.watchOptions'); } + private static readIncludePackageJsonAutoImports(configuration: vscode.WorkspaceConfiguration): string | undefined { + return configuration.get('typescript.preferences.includePackageJsonAutoImports'); + } + private static readMaxTsServerMemory(configuration: vscode.WorkspaceConfiguration): number { const defaultMaxMemory = 3072; const minimumMaxMemory = 128; diff --git a/extensions/typescript-language-features/src/utils/electron.ts b/extensions/typescript-language-features/src/utils/electron.ts deleted file mode 100644 index 49204331101..00000000000 --- a/extensions/typescript-language-features/src/utils/electron.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as temp from './temp'; -import path = require('path'); -import fs = require('fs'); -import cp = require('child_process'); -import process = require('process'); - - -const getRootTempDir = (() => { - let dir: string | undefined; - return () => { - if (!dir) { - dir = temp.getTempFile(`vscode-typescript${process.platform !== 'win32' && process.getuid ? process.getuid() : ''}`); - } - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } - return dir; - }; -})(); - -export const getInstanceDir = (() => { - let dir: string | undefined; - return () => { - if (!dir) { - dir = path.join(getRootTempDir(), temp.makeRandomHexString(20)); - } - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } - return dir; - }; -})(); - -export function getTempFile(prefix: string): string { - return path.join(getInstanceDir(), `${prefix}-${temp.makeRandomHexString(20)}.tmp`); -} - -function generatePatchedEnv(env: any, modulePath: string): any { - const newEnv = Object.assign({}, env); - - newEnv['ELECTRON_RUN_AS_NODE'] = '1'; - newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..'); - - // Ensure we always have a PATH set - newEnv['PATH'] = newEnv['PATH'] || process.env.PATH; - - return newEnv; -} - -export interface ForkOptions { - readonly cwd?: string; - readonly execArgv?: string[]; -} - -export function fork( - modulePath: string, - args: readonly string[], - options: ForkOptions, -): cp.ChildProcess { - const newEnv = generatePatchedEnv(process.env, modulePath); - return cp.fork(modulePath, args, { - silent: true, - cwd: options.cwd, - env: newEnv, - execArgv: options.execArgv - }); -} diff --git a/extensions/typescript-language-features/src/utils/fileSystem.ts b/extensions/typescript-language-features/src/utils/fileSystem.electron.ts similarity index 94% rename from extensions/typescript-language-features/src/utils/fileSystem.ts rename to extensions/typescript-language-features/src/utils/fileSystem.electron.ts index a2531bc3703..3a6711224e2 100644 --- a/extensions/typescript-language-features/src/utils/fileSystem.ts +++ b/extensions/typescript-language-features/src/utils/fileSystem.electron.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import { getTempFile } from '../utils/temp'; +import { getTempFile } from './temp.electron'; export const onCaseInsenitiveFileSystem = (() => { let value: boolean | undefined; diff --git a/extensions/typescript-language-features/src/utils/largeProjectStatus.ts b/extensions/typescript-language-features/src/utils/largeProjectStatus.ts index f820101f1b1..223d7cb4716 100644 --- a/extensions/typescript-language-features/src/utils/largeProjectStatus.ts +++ b/extensions/typescript-language-features/src/utils/largeProjectStatus.ts @@ -111,16 +111,15 @@ function onConfigureExcludesSelected( export function create( client: ITypeScriptServiceClient, - telemetryReporter: TelemetryReporter -) { +): vscode.Disposable { const toDispose: vscode.Disposable[] = []; - const item = new ExcludeHintItem(telemetryReporter); + const item = new ExcludeHintItem(client.telemetryReporter); toDispose.push(vscode.commands.registerCommand('js.projectStatus.command', () => { if (item.configFileName) { onConfigureExcludesSelected(client, item.configFileName); } - let { message } = item.getCurrentHint(); + const { message } = item.getCurrentHint(); return vscode.window.showInformationMessage(message); })); @@ -128,4 +127,3 @@ export function create( return vscode.Disposable.from(...toDispose); } - diff --git a/extensions/typescript-language-features/src/utils/platform.ts b/extensions/typescript-language-features/src/utils/platform.ts index 81847ece464..2d754bf4054 100644 --- a/extensions/typescript-language-features/src/utils/platform.ts +++ b/extensions/typescript-language-features/src/utils/platform.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; + export function isWeb(): boolean { - return typeof process === 'undefined'; + // @ts-expect-error + return typeof navigator !== 'undefined' && vscode.env.uiKind === vscode.UIKind.Web; } diff --git a/extensions/typescript-language-features/src/utils/temp.electron.ts b/extensions/typescript-language-features/src/utils/temp.electron.ts new file mode 100644 index 00000000000..cd79b36415d --- /dev/null +++ b/extensions/typescript-language-features/src/utils/temp.electron.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; + +function makeRandomHexString(length: number): string { + const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + let result = ''; + for (let i = 0; i < length; i++) { + const idx = Math.floor(chars.length * Math.random()); + result += chars[idx]; + } + return result; +} + +const getRootTempDir = (() => { + let dir: string | undefined; + return () => { + if (!dir) { + const filename = `vscode-typescript${process.platform !== 'win32' && process.getuid ? process.getuid() : ''}`; + dir = path.join(os.tmpdir(), filename); + } + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + return dir; + }; +})(); + +export const getInstanceTempDir = (() => { + let dir: string | undefined; + return () => { + if (!dir) { + dir = path.join(getRootTempDir(), makeRandomHexString(20)); + } + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + return dir; + }; +})(); + +export function getTempFile(prefix: string): string { + return path.join(getInstanceTempDir(), `${prefix}-${makeRandomHexString(20)}.tmp`); +} diff --git a/extensions/typescript-language-features/src/utils/temp.ts b/extensions/typescript-language-features/src/utils/temp.ts deleted file mode 100644 index 2af5f1732b0..00000000000 --- a/extensions/typescript-language-features/src/utils/temp.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import path = require('path'); -import os = require('os'); - -export function makeRandomHexString(length: number): string { - const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; - let result = ''; - for (let i = 0; i < length; i++) { - const idx = Math.floor(chars.length * Math.random()); - result += chars[idx]; - } - return result; -} - -export function getTempFile(name: string): string { - return path.join(os.tmpdir(), name); -} \ No newline at end of file diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index ba0a9a2b12b..147be55e1c8 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -250,6 +250,11 @@ commander@2.15.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -1051,6 +1056,14 @@ source-map-support@^0.5.0: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-support@~0.5.12: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map@^0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -1097,6 +1110,15 @@ tar@^6.0.2: mkdirp "^1.0.3" yallist "^4.0.0" +terser@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -1131,7 +1153,7 @@ typescript-vscode-sh-plugin@^0.6.14: "typescript-web-server@git://github.com/mjbvz/ts-server-web-build": version "0.0.0" - resolved "git://github.com/mjbvz/ts-server-web-build#6018bbd0049444cccee29c57ddabf394564fbf35" + resolved "git://github.com/mjbvz/ts-server-web-build#1d85be25043f9b5e36a531941ea345dd5a2ca007" unique-filename@^1.1.1: version "1.1.1" diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index 788585e3d4d..f5c785ca8b2 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -6,7 +6,7 @@ import 'mocha'; import * as assert from 'assert'; import * as vscode from 'vscode'; -import { join } from 'path'; +import { createRandomFile } from './utils'; export function timeoutAsync(n: number): Promise { return new Promise(resolve => { @@ -56,6 +56,42 @@ async function splitEditor() { await once; } +async function saveFileAndCloseAll(resource: vscode.Uri) { + const documentClosed = new Promise((resolve, _reject) => { + const d = vscode.notebook.onDidCloseNotebookDocument(e => { + if (e.uri.toString() === resource.toString()) { + d.dispose(); + resolve(); + } + }); + }); + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await documentClosed; +} + +async function saveAllFilesAndCloseAll(resource: vscode.Uri) { + const documentClosed = new Promise((resolve, _reject) => { + const d = vscode.notebook.onDidCloseNotebookDocument(e => { + if (e.uri.toString() === resource.toString()) { + d.dispose(); + resolve(); + } + }); + }); + await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await documentClosed; +} + +function assertInitalState() { + // no-op unless we figure out why some documents are opened after the editor is closed + + // assert.equal(vscode.notebook.activeNotebookEditor, undefined); + // assert.equal(vscode.notebook.notebookDocuments.length, 0); + // assert.equal(vscode.notebook.visibleNotebookEditors.length, 0); +} + suite('Notebook API tests', () => { // test.only('crash', async function () { // for (let i = 0; i < 200; i++) { @@ -83,7 +119,9 @@ suite('Notebook API tests', () => { // }); test('document open/close event', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); const firstDocumentOpen = getEventOncePromise(vscode.notebook.onDidOpenNotebookDocument); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await firstDocumentOpen; @@ -94,7 +132,9 @@ suite('Notebook API tests', () => { }); test('shared document in notebook editors', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); let counter = 0; const disposables: vscode.Disposable[] = []; disposables.push(vscode.notebook.onDidOpenNotebookDocument(() => { @@ -115,7 +155,9 @@ suite('Notebook API tests', () => { }); test('editor open/close event', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); const firstEditorOpen = getEventOncePromise(vscode.notebook.onDidChangeVisibleNotebookEditors); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await firstEditorOpen; @@ -126,7 +168,9 @@ suite('Notebook API tests', () => { }); test('editor open/close event 2', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); let count = 0; const disposables: vscode.Disposable[] = []; disposables.push(vscode.notebook.onDidChangeVisibleNotebookEditors(() => { @@ -144,7 +188,9 @@ suite('Notebook API tests', () => { }); test('editor editing event 2', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); @@ -216,7 +262,8 @@ suite('Notebook API tests', () => { }); test('editor move cell event', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); @@ -257,7 +304,8 @@ suite('Notebook API tests', () => { }); test('notebook editor active/visible', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const firstEditor = vscode.notebook.activeNotebookEditor; assert.equal(firstEditor?.active, true); @@ -292,7 +340,8 @@ suite('Notebook API tests', () => { }); test('notebook active editor change', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); const firstEditorOpen = getEventOncePromise(vscode.notebook.onDidChangeActiveNotebookEditor); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await firstEditorOpen; @@ -301,12 +350,12 @@ suite('Notebook API tests', () => { await vscode.commands.executeCommand('workbench.action.splitEditor'); await firstEditorDeactivate; - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); test('edit API', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); @@ -321,12 +370,12 @@ suite('Notebook API tests', () => { assert.deepEqual(cellChangeEventRet.changes[0].deletedCount, 0); assert.equal(cellChangeEventRet.changes[0].items[0], vscode.notebook.activeNotebookEditor!.document.cells[1]); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); test('initialzation should not emit cell change events.', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); let count = 0; const disposables: vscode.Disposable[] = []; @@ -338,14 +387,15 @@ suite('Notebook API tests', () => { assert.equal(count, 0); disposables.forEach(d => d.dispose()); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + + await saveFileAndCloseAll(resource); }); }); suite('notebook workflow', () => { test('notebook open', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -366,7 +416,8 @@ suite('notebook workflow', () => { }); test('notebook cell actions', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -439,7 +490,8 @@ suite('notebook workflow', () => { }); test('notebook join cells', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -447,7 +499,9 @@ suite('notebook workflow', () => { await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.joinAbove'); @@ -460,7 +514,8 @@ suite('notebook workflow', () => { }); test('move cells will not recreate cells in ExtHost', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); @@ -473,15 +528,14 @@ suite('notebook workflow', () => { const newActiveCell = vscode.notebook.activeNotebookEditor!.selection; assert.deepEqual(activeCell, newActiveCell); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); // TODO@rebornix, there are still some events order issue. // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(newActiveCell!), 2); }); // test.only('document metadata is respected', async function () { - // const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); @@ -505,7 +559,8 @@ suite('notebook workflow', () => { // }); test('cell runnable metadata is respected', async () => { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.notebook.activeNotebookEditor!; @@ -526,7 +581,8 @@ suite('notebook workflow', () => { }); test('document runnable metadata is respected', async () => { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.notebook.activeNotebookEditor!; @@ -548,7 +604,8 @@ suite('notebook workflow', () => { suite('notebook dirty state', () => { test('notebook open', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -564,25 +621,22 @@ suite('notebook dirty state', () => { assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 3); assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1); - - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); - await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); - - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + const edit = new vscode.WorkspaceEdit(); + edit.insert(activeCell!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[1], vscode.notebook.activeNotebookEditor?.selection); assert.equal(vscode.notebook.activeNotebookEditor?.selection?.document.getText(), 'var abc = 0;'); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await saveFileAndCloseAll(resource); }); }); suite('notebook undo redo', () => { test('notebook open', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -600,7 +654,9 @@ suite('notebook undo redo', () => { // modify the second cell, delete it - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); await vscode.commands.executeCommand('notebook.cell.delete'); assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 2); assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(vscode.notebook.activeNotebookEditor!.selection!), 1); @@ -618,12 +674,12 @@ suite('notebook undo redo', () => { // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(vscode.notebook.activeNotebookEditor!.selection!), 1); // assert.equal(vscode.notebook.activeNotebookEditor?.selection?.document.getText(), 'test'); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await saveFileAndCloseAll(resource); }); test.skip('execute and then undo redo', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); @@ -681,15 +737,14 @@ suite('notebook undo redo', () => { }); assert.equal(cellOutputsAddedRet.cells[0].outputs.length, 0); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); }); suite('notebook working copy', () => { // test('notebook revert on close', async function () { - // const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); // assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); @@ -710,7 +765,7 @@ suite('notebook working copy', () => { // }); // test('notebook revert', async function () { - // const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); // assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); @@ -730,15 +785,18 @@ suite('notebook working copy', () => { // }); test('multiple tabs: dirty + clean', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); - const secondResource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './second.vsctestnb')); + const secondResource = await createRandomFile('', undefined, 'second', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); @@ -749,20 +807,22 @@ suite('notebook working copy', () => { assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells.length, 3); assert.equal(vscode.notebook.activeNotebookEditor?.selection?.document.getText(), 'var abc = 0;'); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await saveFileAndCloseAll(resource); }); test('multiple tabs: two dirty tabs and switching', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); - const secondResource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './second.vsctestnb')); + const secondResource = await createRandomFile('', undefined, 'second', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); @@ -783,13 +843,15 @@ suite('notebook working copy', () => { assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells.length, 2); assert.equal(vscode.notebook.activeNotebookEditor?.selection?.document.getText(), ''); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveAllFilesAndCloseAll(secondResource); + // await vscode.commands.executeCommand('workbench.action.files.saveAll'); + // await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); test('multiple tabs: different editors with same document', async function () { + assertInitalState(); - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const firstNotebookEditor = vscode.notebook.activeNotebookEditor; assert.equal(firstNotebookEditor !== undefined, true, 'notebook first'); @@ -806,28 +868,31 @@ suite('notebook working copy', () => { assert.equal(firstNotebookEditor?.document, secondNotebookEditor?.document, 'split notebook editors share the same document'); assert.notEqual(firstNotebookEditor?.asWebviewUri(vscode.Uri.file('./hello.png')), secondNotebookEditor?.asWebviewUri(vscode.Uri.file('./hello.png'))); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveAllFilesAndCloseAll(resource); + + // await vscode.commands.executeCommand('workbench.action.files.saveAll'); + // await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); }); suite('metadata', () => { test('custom metadata should be supported', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.document.metadata.custom!['testMetadata'] as boolean, false); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.metadata.custom!['testCellMetadata'] as number, 123); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); // TODO@rebornix skip as it crashes the process all the time - test.skip('custom metadata should be supported', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + test.skip('custom metadata should be supported 2', async function () { + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.document.metadata.custom!['testMetadata'] as boolean, false); @@ -840,27 +905,29 @@ suite('metadata', () => { // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1); // assert.equal(activeCell?.metadata.custom!['testCellMetadata'] as number, 123); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); }); suite('regression', () => { test('microsoft/vscode-github-issue-notebooks#26. Insert template cell in the new empty document', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'empty', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); test('#97830, #97764. Support switch to other editor types', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'empty', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'var abc = 0;'); @@ -869,33 +936,37 @@ suite('regression', () => { await vscode.commands.executeCommand('vscode.openWith', resource, 'default'); assert.equal(vscode.window.activeTextEditor?.document.uri.path, resource.path); - await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); // open text editor, pin, and then open a notebook test('#96105 - dirty editors', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'empty', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'default'); - await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(resource, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); // now it's dirty, open the resource with notebook editor should open a new one await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.notEqual(vscode.notebook.activeNotebookEditor, undefined, 'notebook first'); assert.notEqual(vscode.window.activeTextEditor, undefined); - await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); test('#102411 - untitled notebook creation failed', async function () { + assertInitalState(); await vscode.commands.executeCommand('workbench.action.files.newUntitledFile', { viewType: 'notebookCoreTest' }); assert.notEqual(vscode.notebook.activeNotebookEditor, undefined, 'untitled notebook editor is not undefined'); + + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); test('#102423 - copy/paste shares the same text buffer', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); let activeCell = vscode.notebook.activeNotebookEditor!.selection; @@ -907,10 +978,14 @@ suite('regression', () => { assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1); assert.equal(activeCell?.document.getText(), 'test'); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 2); assert.notEqual(vscode.notebook.activeNotebookEditor!.document.cells[0].document.getText(), vscode.notebook.activeNotebookEditor!.document.cells[1].document.getText()); + + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); }); @@ -921,11 +996,11 @@ suite('webview', () => { // return; // } - // const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); // const uri = vscode.notebook.activeNotebookEditor!.asWebviewUri(vscode.Uri.file('./hello.png')); - // assert.equal(uri.scheme, 'vscode-resource'); + // assert.equal(uri.scheme, 'vscode-webview-resource'); // await vscode.commands.executeCommand('workbench.action.closeAllEditors'); // }); diff --git a/extensions/vscode-notebook-tests/src/notebookTestMain.ts b/extensions/vscode-notebook-tests/src/notebookTestMain.ts index 3a80303a502..d77e0725f5a 100644 --- a/extensions/vscode-notebook-tests/src/notebookTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookTestMain.ts @@ -15,7 +15,7 @@ export function activate(context: vscode.ExtensionContext): any { context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { onDidChangeNotebook: _onDidChangeNotebook.event, openNotebook: async (_resource: vscode.Uri) => { - if (_resource.path.endsWith('empty.vsctestnb')) { + if (/.*empty\-.*\.vsctestnb$/.test(_resource.path)) { return { languages: ['typescript'], metadata: {}, diff --git a/extensions/vscode-notebook-tests/src/utils.ts b/extensions/vscode-notebook-tests/src/utils.ts new file mode 100644 index 00000000000..95dae41d5dc --- /dev/null +++ b/extensions/vscode-notebook-tests/src/utils.ts @@ -0,0 +1,261 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import * as path from 'path'; +import * as vscode from 'vscode'; + +class File implements vscode.FileStat { + + type: vscode.FileType; + ctime: number; + mtime: number; + size: number; + + name: string; + data?: Uint8Array; + + constructor(name: string) { + this.type = vscode.FileType.File; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.size = 0; + this.name = name; + } +} + +class Directory implements vscode.FileStat { + + type: vscode.FileType; + ctime: number; + mtime: number; + size: number; + + name: string; + entries: Map; + + constructor(name: string) { + this.type = vscode.FileType.Directory; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.size = 0; + this.name = name; + this.entries = new Map(); + } +} + +export type Entry = File | Directory; + +export class TestFS implements vscode.FileSystemProvider { + + constructor( + readonly scheme: string, + readonly isCaseSensitive: boolean + ) { } + + readonly root = new Directory(''); + + // --- manage file metadata + + stat(uri: vscode.Uri): vscode.FileStat { + return this._lookup(uri, false); + } + + readDirectory(uri: vscode.Uri): [string, vscode.FileType][] { + const entry = this._lookupAsDirectory(uri, false); + let result: [string, vscode.FileType][] = []; + for (const [name, child] of entry.entries) { + result.push([name, child.type]); + } + return result; + } + + // --- manage file contents + + readFile(uri: vscode.Uri): Uint8Array { + const data = this._lookupAsFile(uri, false).data; + if (data) { + return data; + } + throw vscode.FileSystemError.FileNotFound(); + } + + writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void { + let basename = path.posix.basename(uri.path); + let parent = this._lookupParentDirectory(uri); + let entry = parent.entries.get(basename); + if (entry instanceof Directory) { + throw vscode.FileSystemError.FileIsADirectory(uri); + } + if (!entry && !options.create) { + throw vscode.FileSystemError.FileNotFound(uri); + } + if (entry && options.create && !options.overwrite) { + throw vscode.FileSystemError.FileExists(uri); + } + if (!entry) { + entry = new File(basename); + parent.entries.set(basename, entry); + this._fireSoon({ type: vscode.FileChangeType.Created, uri }); + } + entry.mtime = Date.now(); + entry.size = content.byteLength; + entry.data = content; + + this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); + } + + // --- manage files/folders + + rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): void { + + if (!options.overwrite && this._lookup(newUri, true)) { + throw vscode.FileSystemError.FileExists(newUri); + } + + let entry = this._lookup(oldUri, false); + let oldParent = this._lookupParentDirectory(oldUri); + + let newParent = this._lookupParentDirectory(newUri); + let newName = path.posix.basename(newUri.path); + + oldParent.entries.delete(entry.name); + entry.name = newName; + newParent.entries.set(newName, entry); + + this._fireSoon( + { type: vscode.FileChangeType.Deleted, uri: oldUri }, + { type: vscode.FileChangeType.Created, uri: newUri } + ); + } + + delete(uri: vscode.Uri): void { + let dirname = uri.with({ path: path.posix.dirname(uri.path) }); + let basename = path.posix.basename(uri.path); + let parent = this._lookupAsDirectory(dirname, false); + if (!parent.entries.has(basename)) { + throw vscode.FileSystemError.FileNotFound(uri); + } + parent.entries.delete(basename); + parent.mtime = Date.now(); + parent.size -= 1; + this._fireSoon({ type: vscode.FileChangeType.Changed, uri: dirname }, { uri, type: vscode.FileChangeType.Deleted }); + } + + createDirectory(uri: vscode.Uri): void { + let basename = path.posix.basename(uri.path); + let dirname = uri.with({ path: path.posix.dirname(uri.path) }); + let parent = this._lookupAsDirectory(dirname, false); + + let entry = new Directory(basename); + parent.entries.set(entry.name, entry); + parent.mtime = Date.now(); + parent.size += 1; + this._fireSoon({ type: vscode.FileChangeType.Changed, uri: dirname }, { type: vscode.FileChangeType.Created, uri }); + } + + // --- lookup + + private _lookup(uri: vscode.Uri, silent: false): Entry; + private _lookup(uri: vscode.Uri, silent: boolean): Entry | undefined; + private _lookup(uri: vscode.Uri, silent: boolean): Entry | undefined { + let parts = uri.path.split('/'); + let entry: Entry = this.root; + for (const part of parts) { + const partLow = part.toLowerCase(); + if (!part) { + continue; + } + let child: Entry | undefined; + if (entry instanceof Directory) { + if (this.isCaseSensitive) { + child = entry.entries.get(part); + } else { + for (let [key, value] of entry.entries) { + if (key.toLowerCase() === partLow) { + child = value; + break; + } + } + } + } + if (!child) { + if (!silent) { + throw vscode.FileSystemError.FileNotFound(uri); + } else { + return undefined; + } + } + entry = child; + } + return entry; + } + + private _lookupAsDirectory(uri: vscode.Uri, silent: boolean): Directory { + let entry = this._lookup(uri, silent); + if (entry instanceof Directory) { + return entry; + } + throw vscode.FileSystemError.FileNotADirectory(uri); + } + + private _lookupAsFile(uri: vscode.Uri, silent: boolean): File { + let entry = this._lookup(uri, silent); + if (entry instanceof File) { + return entry; + } + throw vscode.FileSystemError.FileIsADirectory(uri); + } + + private _lookupParentDirectory(uri: vscode.Uri): Directory { + const dirname = uri.with({ path: path.posix.dirname(uri.path) }); + return this._lookupAsDirectory(dirname, false); + } + + // --- manage file events + + private _emitter = new vscode.EventEmitter(); + private _bufferedEvents: vscode.FileChangeEvent[] = []; + private _fireSoonHandle?: NodeJS.Timer; + + readonly onDidChangeFile: vscode.Event = this._emitter.event; + + watch(_resource: vscode.Uri): vscode.Disposable { + // ignore, fires for all changes... + return new vscode.Disposable(() => { }); + } + + private _fireSoon(...events: vscode.FileChangeEvent[]): void { + this._bufferedEvents.push(...events); + + if (this._fireSoonHandle) { + clearTimeout(this._fireSoonHandle); + } + + this._fireSoonHandle = setTimeout(() => { + this._emitter.fire(this._bufferedEvents); + this._bufferedEvents.length = 0; + }, 5); + } +} + +export function rndName() { + return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10); +} + +export const testFs = new TestFS('fake-fs', true); +vscode.workspace.registerFileSystemProvider(testFs.scheme, testFs, { isCaseSensitive: testFs.isCaseSensitive }); + +export async function createRandomFile(contents = '', dir: vscode.Uri | undefined = undefined, prefix = '', ext = ''): Promise { + let fakeFile: vscode.Uri; + if (dir) { + fakeFile = dir.with({ path: dir.path + '/' + rndName() + ext }); + } else { + fakeFile = vscode.Uri.parse(`${testFs.scheme}:/${prefix}-${rndName() + ext}`); + } + + await testFs.writeFile(fakeFile, Buffer.from(contents), { create: true, overwrite: true }); + return fakeFile; +} diff --git a/extensions/yarn.lock b/extensions/yarn.lock index d41a4ab48b5..86223e77212 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -76,10 +76,10 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -typescript@3.9.6: - version "3.9.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" - integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== +typescript@3.9.7: + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== wrappy@1: version "1.0.2" diff --git a/package.json b/package.json index af86041877d..63036471803 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,7 @@ "source-map": "^0.4.4", "style-loader": "^1.0.0", "ts-loader": "^4.4.2", - "typescript": "^4.0.0-dev.20200715", + "typescript": "^4.0.0-dev.20200722", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 9c04f50698b..4450d838c8c 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -44,9 +44,6 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: Tests in the extension host -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-notebook-tests\test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-notebook-tests --extensionTestsPath=%~dp0\..\extensions\vscode-notebook-tests\out --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% -REM if %errorlevel% neq 0 exit /b %errorlevel% - call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\singlefolder-tests --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% @@ -65,6 +62,9 @@ if %errorlevel% neq 0 exit /b %errorlevel% call "%INTEGRATION_TEST_ELECTRON_PATH%" $%~dp0\..\extensions\emmet\out\test\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% . if %errorlevel% neq 0 exit /b %errorlevel% +call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-notebook-tests\test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-notebook-tests --extensionTestsPath=%~dp0\..\extensions\vscode-notebook-tests\out --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% +if %errorlevel% neq 0 exit /b %errorlevel% + for /f "delims=" %%i in ('node -p "require('fs').realpathSync.native(require('os').tmpdir())"') do set TEMPDIR=%%i set GITWORKSPACE=%TEMPDIR%\git-%RANDOM% mkdir %GITWORKSPACE% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index f07f4dde622..3288a8c0516 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -56,7 +56,7 @@ fi #"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/emmet/out/test/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-notebook-tests/test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-notebook-tests --extensionTestsPath=$ROOT/extensions/vscode-notebook-tests/out/ --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-notebook-tests/test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-notebook-tests --extensionTestsPath=$ROOT/extensions/vscode-notebook-tests/out/ --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR # Tests in commonJS cd $ROOT/extensions/css-language-features/server && $ROOT/scripts/node-electron.sh test/index.js diff --git a/src/vs/base/browser/contextmenu.ts b/src/vs/base/browser/contextmenu.ts index d13cae295c0..2119700f859 100644 --- a/src/vs/base/browser/contextmenu.ts +++ b/src/vs/base/browser/contextmenu.ts @@ -26,7 +26,7 @@ export interface IContextMenuDelegate { actionRunner?: IActionRunner; autoSelectFirstItem?: boolean; anchorAlignment?: AnchorAlignment; - anchorAsContainer?: boolean; + domForShadowRoot?: HTMLElement; } export interface IContextMenuProvider { diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 9b304e81a80..79c2a927201 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -5,7 +5,6 @@ .monaco-action-bar { text-align: right; - overflow: hidden; white-space: nowrap; } diff --git a/src/vs/base/browser/ui/codicons/codiconStyles.ts b/src/vs/base/browser/ui/codicons/codiconStyles.ts index 899af845e8f..b3dc12fb3f3 100644 --- a/src/vs/base/browser/ui/codicons/codiconStyles.ts +++ b/src/vs/base/browser/ui/codicons/codiconStyles.ts @@ -28,7 +28,7 @@ function initialize() { delayer.schedule(); } -function formatRule(c: Codicon) { +export function formatRule(c: Codicon) { let def = c.definition; while (def instanceof Codicon) { def = def.definition; diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index 93ecd03dfec..0863191afd2 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -10,6 +10,12 @@ import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/ import { Range } from 'vs/base/common/range'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; +export const enum ContextViewDOMPosition { + ABSOLUTE = 1, + FIXED, + FIXED_SHADOW +} + export interface IAnchor { x: number; y: number; @@ -105,32 +111,62 @@ export class ContextView extends Disposable { private container: HTMLElement | null = null; private view: HTMLElement; private useFixedPosition: boolean; + private useShadowDOM: boolean; private delegate: IDelegate | null = null; private toDisposeOnClean: IDisposable = Disposable.None; private toDisposeOnSetContainer: IDisposable = Disposable.None; + private shadowRoot: ShadowRoot | null = null; + private shadowRootHostElement: HTMLElement | null = null; - constructor(container: HTMLElement, useFixedPosition: boolean) { + constructor(container: HTMLElement, domPosition: ContextViewDOMPosition) { super(); this.view = DOM.$('.context-view'); this.useFixedPosition = false; + this.useShadowDOM = false; DOM.hide(this.view); - this.setContainer(container, useFixedPosition); + this.setContainer(container, domPosition); - this._register(toDisposable(() => this.setContainer(null, false))); + this._register(toDisposable(() => this.setContainer(null, ContextViewDOMPosition.ABSOLUTE))); } - setContainer(container: HTMLElement | null, useFixedPosition: boolean): void { + setContainer(container: HTMLElement | null, domPosition: ContextViewDOMPosition): void { if (this.container) { this.toDisposeOnSetContainer.dispose(); - this.container.removeChild(this.view); + + if (this.shadowRoot) { + this.shadowRoot.removeChild(this.view); + this.shadowRoot = null; + DOM.removeNode(this.shadowRootHostElement!); + this.shadowRootHostElement = null; + } else { + this.container.removeChild(this.view); + } + this.container = null; } if (container) { this.container = container; - this.container.appendChild(this.view); + + this.useFixedPosition = domPosition !== ContextViewDOMPosition.ABSOLUTE; + this.useShadowDOM = domPosition === ContextViewDOMPosition.FIXED_SHADOW; + + if (this.useShadowDOM) { + this.shadowRootHostElement = DOM.$('.shadow-root-host'); + this.container.appendChild(this.shadowRootHostElement); + this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'closed' }); + this.shadowRoot.innerHTML = ` + + `; + this.shadowRoot.appendChild(this.view); + this.shadowRoot.appendChild(DOM.$('slot')); + } else { + this.container.appendChild(this.view); + } const toDisposeOnSetContainer = new DisposableStore(); @@ -148,8 +184,6 @@ export class ContextView extends Disposable { this.toDisposeOnSetContainer = toDisposeOnSetContainer; } - - this.useFixedPosition = useFixedPosition; } show(delegate: IDelegate): void { @@ -162,6 +196,7 @@ export class ContextView extends Disposable { this.view.className = 'context-view'; this.view.style.top = '0px'; this.view.style.left = '0px'; + this.view.style.zIndex = '2500'; this.view.style.position = this.useFixedPosition ? 'fixed' : 'absolute'; DOM.show(this.view); @@ -300,3 +335,45 @@ export class ContextView extends Disposable { super.dispose(); } } + +let SHADOW_ROOT_CSS = /* css */ ` + :host { + all: initial; /* 1st rule so subsequent properties are reset. */ + } + + @font-face { + font-family: "codicon"; + src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype"); + } + + .codicon[class*='codicon-'] { + font: normal normal normal 16px/1 codicon; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + } + + :host-context(.mac) { font-family: -apple-system, BlinkMacSystemFont, sans-serif; } + :host-context(.mac:lang(zh-Hans)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; } + :host-context(.mac:lang(zh-Hant)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; } + :host-context(.mac:lang(ja)) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; } + :host-context(.mac:lang(ko)) { font-family: -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; } + + :host-context(.windows) { font-family: "Segoe WPC", "Segoe UI", sans-serif; } + :host-context(.windows:lang(zh-Hans)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; } + :host-context(.windows:lang(zh-Hant)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; } + :host-context(.windows:lang(ja)) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; } + :host-context(.windows:lang(ko)) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; } + + :host-context(.mac).linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; } + :host-context(.mac).linux:lang(zh-Hans)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } + :host-context(.mac).linux:lang(zh-Hant)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; } + :host-context(.mac).linux:lang(ja)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } + :host-context(.mac).linux:lang(ko)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } +`; diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 93efe9eba1f..b74cab78bb6 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -265,7 +265,7 @@ export class DropdownMenu extends BaseDropdown { onHide: () => this.onHide(), actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined, anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT, - anchorAsContainer: this.menuAsChild + domForShadowRoot: this.menuAsChild ? this.element : undefined }); } diff --git a/src/vs/base/browser/ui/menu/menu.css b/src/vs/base/browser/ui/menu/menu.css deleted file mode 100644 index d86ee055d51..00000000000 --- a/src/vs/base/browser/ui/menu/menu.css +++ /dev/null @@ -1,225 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-menu .monaco-action-bar.vertical { - margin-left: 0; - overflow: visible; -} - -.monaco-menu .monaco-action-bar.vertical .actions-container { - display: block; -} - -.monaco-menu .monaco-action-bar.vertical .action-item { - padding: 0; - transform: none; - display: flex; -} - -.monaco-menu .monaco-action-bar.vertical .action-item.active { - transform: none; -} - -.monaco-menu .monaco-action-bar.vertical .action-menu-item { - flex: 1 1 auto; - display: flex; - height: 2em; - align-items: center; - position: relative; -} - -.monaco-menu .monaco-action-bar.vertical .action-label { - flex: 1 1 auto; - text-decoration: none; - padding: 0 1em; - background: none; - font-size: 12px; - line-height: 1; -} - -.monaco-menu .monaco-action-bar.vertical .keybinding, -.monaco-menu .monaco-action-bar.vertical .submenu-indicator { - display: inline-block; - flex: 2 1 auto; - padding: 0 1em; - text-align: right; - font-size: 12px; - line-height: 1; -} - -.monaco-menu .monaco-action-bar.vertical .submenu-indicator { - height: 100%; -} - -.monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon { - font-size: 16px !important; - display: flex; - align-items: center; -} - -.monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon::before { - margin-left: auto; - margin-right: -20px; -} - -.monaco-menu .monaco-action-bar.vertical .action-item.disabled .keybinding, -.monaco-menu .monaco-action-bar.vertical .action-item.disabled .submenu-indicator { - opacity: 0.4; -} - -.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator) { - display: inline-block; - box-sizing: border-box; - margin: 0; -} - -.monaco-menu .monaco-action-bar.vertical .action-item { - position: static; - overflow: visible; -} - -.monaco-menu .monaco-action-bar.vertical .action-item .monaco-submenu { - position: absolute; -} - -.monaco-menu .monaco-action-bar.vertical .action-label.separator { - padding: 0.5em 0 0 0; - margin-bottom: 0.5em; - width: 100%; - height: 0px !important; - margin-left: .8em !important; - margin-right: .8em !important; -} - -.monaco-menu .monaco-action-bar.vertical .action-label.separator.text { - padding: 0.7em 1em 0.1em 1em; - font-weight: bold; - opacity: 1; -} - -.monaco-menu .monaco-action-bar.vertical .action-label:hover { - color: inherit; -} - -.monaco-menu .monaco-action-bar.vertical .menu-item-check { - position: absolute; - visibility: hidden; - width: 1em; - height: 100%; -} - -.monaco-menu .monaco-action-bar.vertical .action-menu-item.checked .menu-item-check { - visibility: visible; - display: flex; - align-items: center; - justify-content: center; -} - -/* Context Menu */ - -.context-view.monaco-menu-container { - outline: 0; - border: none; - animation: fadeIn 0.083s linear; -} - -.context-view.monaco-menu-container :focus, -.context-view.monaco-menu-container .monaco-action-bar.vertical:focus, -.context-view.monaco-menu-container .monaco-action-bar.vertical :focus { - outline: 0; -} - -.monaco-menu .monaco-action-bar.vertical .action-item { - border: thin solid transparent; /* prevents jumping behaviour on hover or focus */ -} - - -/* High Contrast Theming */ -.hc-black .context-view.monaco-menu-container { - box-shadow: none; -} - -.hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused { - background: none; -} - -/* Menubar styles */ - -.menubar { - display: flex; - flex-shrink: 1; - box-sizing: border-box; - height: 30px; - overflow: hidden; - flex-wrap: wrap; -} - -.fullscreen .menubar:not(.compact) { - margin: 0px; - padding: 0px 5px; -} - -.menubar > .menubar-menu-button { - align-items: center; - box-sizing: border-box; - padding: 0px 8px; - cursor: default; - -webkit-app-region: no-drag; - zoom: 1; - white-space: nowrap; - outline: 0; -} - -.menubar.compact { - flex-shrink: 0; - overflow: visible; /* to avoid the compact menu to be repositioned when clicking */ -} - -.menubar.compact > .menubar-menu-button { - width: 100%; - height: 100%; - padding: 0px; -} - -.menubar .menubar-menu-items-holder { - position: absolute; - left: 0px; - opacity: 1; - z-index: 2000; -} - -.menubar .menubar-menu-items-holder.monaco-menu-container { - outline: 0; - border: none; -} - -.menubar .menubar-menu-items-holder.monaco-menu-container :focus { - outline: 0; -} - -.menubar .toolbar-toggle-more { - width: 20px; - height: 100%; -} - -.menubar.compact .toolbar-toggle-more { - position: relative; - left: 0px; - top: 0px; - cursor: pointer; - width: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.menubar .toolbar-toggle-more { - padding: 0; - vertical-align: sub; -} - -.menubar.compact .toolbar-toggle-more::before { - content: "\eb94" !important; -} diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 61fd40002d2..6c65ff2d364 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./menu'; import * as nls from 'vs/nls'; import * as strings from 'vs/base/common/strings'; import { IActionRunner, IAction, SubmenuAction, Separator, IActionViewItemProvider } from 'vs/base/common/actions'; import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; -import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses, clearNode } from 'vs/base/browser/dom'; +import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses, clearNode, createStyleSheet, isInShadowDOM } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { RunOnceScheduler } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -21,6 +20,7 @@ import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { Codicon, registerIcon, stripCodicons } from 'vs/base/common/codicons'; import { BaseActionViewItem, ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { formatRule } from 'vs/base/browser/ui/codicons/codiconStyles'; export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/; export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g; @@ -67,6 +67,8 @@ export class Menu extends ActionBar { private readonly menuDisposables: DisposableStore; private scrollableElement: DomScrollableElement; private menuElement: HTMLElement; + static globalStyleSheet: HTMLStyleElement; + protected styleSheet: HTMLStyleElement | undefined; constructor(container: HTMLElement, actions: ReadonlyArray, options: IMenuOptions = {}) { addClass(container, 'monaco-menu-container'); @@ -92,6 +94,8 @@ export class Menu extends ActionBar { this.menuDisposables = this._register(new DisposableStore()); + this.initializeStyleSheet(container); + addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => { const event = new StandardKeyboardEvent(e); @@ -220,6 +224,20 @@ export class Menu extends ActionBar { }); } + private initializeStyleSheet(container: HTMLElement): void { + if (isInShadowDOM(container)) { + this.styleSheet = createStyleSheet(container); + this.styleSheet.innerHTML = MENU_WIDGET_CSS; + } else { + if (!Menu.globalStyleSheet) { + Menu.globalStyleSheet = createStyleSheet(); + Menu.globalStyleSheet.innerHTML = MENU_WIDGET_CSS; + } + + this.styleSheet = Menu.globalStyleSheet; + } + } + style(style: IMenuStyles): void { const container = this.getContainer(); @@ -883,3 +901,298 @@ export function cleanMnemonic(label: string): string { return label.replace(regex, mnemonicInText ? '$2$3' : '').trim(); } + +let MENU_WIDGET_CSS: string = /* css */` +.monaco-menu { + font-size: 13px; + +} + +${formatRule(menuSelectionIcon)} +${formatRule(menuSubmenuIcon)} + +.monaco-action-bar { + text-align: right; + overflow: hidden; + white-space: nowrap; +} + +.monaco-action-bar .actions-container { + display: flex; + margin: 0 auto; + padding: 0; + width: 100%; + justify-content: flex-end; +} + +.monaco-action-bar.vertical .actions-container { + display: inline-block; +} + +.monaco-action-bar.reverse .actions-container { + flex-direction: row-reverse; +} + +.monaco-action-bar .action-item { + cursor: pointer; + display: inline-block; + transition: transform 50ms ease; + position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */ +} + +.monaco-action-bar .action-item.disabled { + cursor: default; +} + +.monaco-action-bar.animated .action-item.active { + transform: scale(1.272019649, 1.272019649); /* 1.272019649 = √φ */ +} + +.monaco-action-bar .action-item .icon, +.monaco-action-bar .action-item .codicon { + display: inline-block; +} + +.monaco-action-bar .action-item .codicon { + display: flex; + align-items: center; +} + +.monaco-action-bar .action-label { + font-size: 11px; + margin-right: 4px; +} + +.monaco-action-bar .action-item.disabled .action-label, +.monaco-action-bar .action-item.disabled .action-label:hover { + opacity: 0.4; +} + +/* Vertical actions */ + +.monaco-action-bar.vertical { + text-align: left; +} + +.monaco-action-bar.vertical .action-item { + display: block; +} + +.monaco-action-bar.vertical .action-label.separator { + display: block; + border-bottom: 1px solid #bbb; + padding-top: 1px; + margin-left: .8em; + margin-right: .8em; +} + +.monaco-action-bar.animated.vertical .action-item.active { + transform: translate(5px, 0); +} + +.secondary-actions .monaco-action-bar .action-label { + margin-left: 6px; +} + +/* Action Items */ +.monaco-action-bar .action-item.select-container { + overflow: hidden; /* somehow the dropdown overflows its container, we prevent it here to not push */ + flex: 1; + max-width: 170px; + min-width: 60px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; +} + +.monaco-menu .monaco-action-bar.vertical { + margin-left: 0; + overflow: visible; +} + +.monaco-menu .monaco-action-bar.vertical .actions-container { + display: block; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + padding: 0; + transform: none; + display: flex; +} + +.monaco-menu .monaco-action-bar.vertical .action-item.active { + transform: none; +} + +.monaco-menu .monaco-action-bar.vertical .action-menu-item { + flex: 1 1 auto; + display: flex; + height: 2em; + align-items: center; + position: relative; +} + +.monaco-menu .monaco-action-bar.vertical .action-label { + flex: 1 1 auto; + text-decoration: none; + padding: 0 1em; + background: none; + font-size: 12px; + line-height: 1; +} + +.monaco-menu .monaco-action-bar.vertical .keybinding, +.monaco-menu .monaco-action-bar.vertical .submenu-indicator { + display: inline-block; + flex: 2 1 auto; + padding: 0 1em; + text-align: right; + font-size: 12px; + line-height: 1; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator { + height: 100%; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon { + font-size: 16px !important; + display: flex; + align-items: center; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon::before { + margin-left: auto; + margin-right: -20px; +} + +.monaco-menu .monaco-action-bar.vertical .action-item.disabled .keybinding, +.monaco-menu .monaco-action-bar.vertical .action-item.disabled .submenu-indicator { + opacity: 0.4; +} + +.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator) { + display: inline-block; + box-sizing: border-box; + margin: 0; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + position: static; + overflow: visible; +} + +.monaco-menu .monaco-action-bar.vertical .action-item .monaco-submenu { + position: absolute; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator { + padding: 0.5em 0 0 0; + margin-bottom: 0.5em; + width: 100%; + height: 0px !important; + margin-left: .8em !important; + margin-right: .8em !important; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator.text { + padding: 0.7em 1em 0.1em 1em; + font-weight: bold; + opacity: 1; +} + +.monaco-menu .monaco-action-bar.vertical .action-label:hover { + color: inherit; +} + +.monaco-menu .monaco-action-bar.vertical .menu-item-check { + position: absolute; + visibility: hidden; + width: 1em; + height: 100%; +} + +.monaco-menu .monaco-action-bar.vertical .action-menu-item.checked .menu-item-check { + visibility: visible; + display: flex; + align-items: center; + justify-content: center; +} + +/* Context Menu */ + +.context-view.monaco-menu-container { + outline: 0; + border: none; + animation: fadeIn 0.083s linear; +} + +.context-view.monaco-menu-container :focus, +.context-view.monaco-menu-container .monaco-action-bar.vertical:focus, +.context-view.monaco-menu-container .monaco-action-bar.vertical :focus { + outline: 0; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + border: thin solid transparent; /* prevents jumping behaviour on hover or focus */ +} + + +/* High Contrast Theming */ +.hc-black .context-view.monaco-menu-container { + box-shadow: none; +} + +.hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused { + background: none; +} + +/* Vertical Action Bar Styles */ + +.monaco-menu .monaco-action-bar.vertical { + padding: .5em 0; +} + +.monaco-menu .monaco-action-bar.vertical .action-menu-item { + height: 1.8em; +} + +.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator), +.monaco-menu .monaco-action-bar.vertical .keybinding { + font-size: inherit; + padding: 0 2em; +} + +.monaco-menu .monaco-action-bar.vertical .menu-item-check { + font-size: inherit; + width: 2em; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator { + font-size: inherit; + padding: 0.2em 0 0 0; + margin-bottom: 0.2em; +} + +linux .monaco-menu .monaco-action-bar.vertical .action-label.separator { + margin-left: 0; + margin-right: 0; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator { + font-size: 60%; + padding: 0 1.8em; +} + +:host-context(.linux) .monaco-menu .monaco-action-bar.vertical .submenu-indicator { + height: 100%; + mask-size: 10px 10px; + -webkit-mask-size: 10px 10px; +} + +.monaco-menu .action-item { + cursor: default; +} + +`; diff --git a/src/vs/base/browser/ui/menu/menubar.css b/src/vs/base/browser/ui/menu/menubar.css new file mode 100644 index 00000000000..d815cfeddb9 --- /dev/null +++ b/src/vs/base/browser/ui/menu/menubar.css @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Menubar styles */ + +.menubar { + display: flex; + flex-shrink: 1; + box-sizing: border-box; + height: 30px; + overflow: hidden; + flex-wrap: wrap; +} + +.fullscreen .menubar:not(.compact) { + margin: 0px; + padding: 0px 5px; +} + +.menubar > .menubar-menu-button { + align-items: center; + box-sizing: border-box; + padding: 0px 8px; + cursor: default; + -webkit-app-region: no-drag; + zoom: 1; + white-space: nowrap; + outline: 0; +} + +.menubar.compact { + flex-shrink: 0; + overflow: visible; /* to avoid the compact menu to be repositioned when clicking */ +} + +.menubar.compact > .menubar-menu-button { + width: 100%; + height: 100%; + padding: 0px; +} + +.menubar .menubar-menu-items-holder { + position: absolute; + left: 0px; + opacity: 1; + z-index: 2000; +} + +.menubar .menubar-menu-items-holder.monaco-menu-container { + outline: 0; + border: none; +} + +.menubar .menubar-menu-items-holder.monaco-menu-container :focus { + outline: 0; +} + +.menubar .toolbar-toggle-more { + width: 20px; + height: 100%; +} + +.menubar.compact .toolbar-toggle-more { + position: relative; + left: 0px; + top: 0px; + cursor: pointer; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.menubar .toolbar-toggle-more { + padding: 0; + vertical-align: sub; +} + +.menubar.compact .toolbar-toggle-more::before { + content: "\eb94" !important; +} diff --git a/src/vs/base/browser/ui/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts index a0ce779ec88..65249946405 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./menubar'; import * as browser from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import * as strings from 'vs/base/common/strings'; diff --git a/src/vs/base/common/marshalling.ts b/src/vs/base/common/marshalling.ts index 335ee5691e0..e76ba91738f 100644 --- a/src/vs/base/common/marshalling.ts +++ b/src/vs/base/common/marshalling.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; +import { VSBuffer } from 'vs/base/common/buffer'; import { regExpFlags } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; export function stringify(obj: any): string { return JSON.stringify(obj, replacer); @@ -44,10 +45,23 @@ export function revive(obj: any, depth = 0): any { case 2: return new RegExp(obj.source, obj.flags); } - // walk object (or array) - for (let key in obj) { - if (Object.hasOwnProperty.call(obj, key)) { - obj[key] = revive(obj[key], depth + 1); + if ( + obj instanceof VSBuffer + || obj instanceof Uint8Array + ) { + return obj; + } + + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; ++i) { + obj[i] = revive(obj[i], depth + 1); + } + } else { + // walk object + for (const key in obj) { + if (Object.hasOwnProperty.call(obj, key)) { + obj[key] = revive(obj[key], depth + 1); + } } } } diff --git a/src/vs/base/parts/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts index 132654b320f..05af5d7ceb1 100644 --- a/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -16,6 +16,7 @@ export interface ISocket extends IDisposable { onEnd(listener: () => void): IDisposable; write(buffer: VSBuffer): void; end(): void; + drain(): Promise; } let emptyBuffer: VSBuffer | null = null; @@ -277,6 +278,11 @@ class ProtocolWriter { this._isDisposed = true; } + public drain(): Promise { + this.flush(); + return this._socket.drain(); + } + public flush(): void { // flush this._writeNow(); @@ -372,6 +378,10 @@ export class Protocol extends Disposable implements IMessagePassingProtocol { this._register(this._socket.onClose(() => this._onClose.fire())); } + drain(): Promise { + return this._socketWriter.drain(); + } + getSocket(): ISocket { return this._socket; } @@ -619,6 +629,10 @@ export class PersistentProtocol implements IMessagePassingProtocol { this._socketDisposables = dispose(this._socketDisposables); } + drain(): Promise { + return this._socketWriter.drain(); + } + sendDisconnect(): void { const msg = new ProtocolMessage(ProtocolMessageType.Disconnect, 0, 0, getEmptyBuffer()); this._socketWriter.write(msg); diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 411bfe46cca..1a583e185da 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -70,6 +70,10 @@ interface IHandler { export interface IMessagePassingProtocol { send(buffer: VSBuffer): void; onMessage: Event; + /** + * Wait for the write buffer (if applicable) to become empty. + */ + drain?(): Promise; } enum State { @@ -482,10 +486,7 @@ export class ChannelClient implements IChannelClient, IDisposable { return e(errors.canceled()); } - let uninitializedPromise: CancelablePromise | null = createCancelablePromise(_ => this.whenInitialized()); - uninitializedPromise.then(() => { - uninitializedPromise = null; - + const doRequest = () => { const handler: IHandler = response => { switch (response.type) { case ResponseType.PromiseSuccess: @@ -510,7 +511,18 @@ export class ChannelClient implements IChannelClient, IDisposable { this.handlers.set(id, handler); this.sendRequest(request); - }); + }; + + let uninitializedPromise: CancelablePromise | null = null; + if (this.state === State.Idle) { + doRequest(); + } else { + uninitializedPromise = createCancelablePromise(_ => this.whenInitialized()); + uninitializedPromise.then(() => { + uninitializedPromise = null; + doRequest(); + }); + } const cancel = () => { if (uninitializedPromise) { diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index ec80ba3f1c3..2b6c70afa70 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -12,6 +12,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; import { ISocket, Protocol, Client, ChunkStream } from 'vs/base/parts/ipc/common/ipc.net'; +import { onUnexpectedError } from 'vs/base/common/errors'; export class NodeSocket implements ISocket { public readonly socket: Socket; @@ -57,12 +58,47 @@ export class NodeSocket implements ISocket { // > https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback // > However, the false return value is only advisory and the writable stream will unconditionally // > accept and buffer chunk even if it has not been allowed to drain. - this.socket.write(buffer.buffer); + try { + this.socket.write(buffer.buffer); + } catch (err) { + if (err.code === 'EPIPE') { + // An EPIPE exception at the wrong time can lead to a renderer process crash + // so ignore the error since the socket will fire the close event soon anyways: + // > https://nodejs.org/api/errors.html#errors_common_system_errors + // > EPIPE (Broken pipe): A write on a pipe, socket, or FIFO for which there is no + // > process to read the data. Commonly encountered at the net and http layers, + // > indicative that the remote side of the stream being written to has been closed. + return; + } + onUnexpectedError(err); + } } public end(): void { this.socket.end(); } + + public drain(): Promise { + return new Promise((resolve, reject) => { + if (this.socket.bufferSize === 0) { + resolve(); + return; + } + const finished = () => { + this.socket.off('close', finished); + this.socket.off('end', finished); + this.socket.off('error', finished); + this.socket.off('timeout', finished); + this.socket.off('drain', finished); + resolve(); + }; + this.socket.on('close', finished); + this.socket.on('end', finished); + this.socket.on('error', finished); + this.socket.on('timeout', finished); + this.socket.on('drain', finished); + }); + } } const enum Constants { @@ -229,6 +265,10 @@ export class WebSocketNodeSocket extends Disposable implements ISocket { } } } + + public drain(): Promise { + return this.socket.drain(); + } } function unmask(buffer: VSBuffer, mask: number): void { diff --git a/src/vs/editor/contrib/codeAction/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/codeActionMenu.ts index 930f85529a2..0c9ef9ec71a 100644 --- a/src/vs/editor/contrib/codeAction/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/codeActionMenu.ts @@ -89,6 +89,7 @@ export class CodeActionMenu extends Disposable { const resolver = this._keybindingResolver.getResolver(); this._contextMenuService.showContextMenu({ + domForShadowRoot: this._editor.getDomNode()!, getAnchor: () => anchor, getActions: () => menuActions, onHide: () => { diff --git a/src/vs/editor/contrib/contextmenu/contextmenu.ts b/src/vs/editor/contrib/contextmenu/contextmenu.ts index cf88fbf9059..04d3c4c4080 100644 --- a/src/vs/editor/contrib/contextmenu/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/contextmenu.ts @@ -204,6 +204,8 @@ export class ContextMenuController implements IEditorContribution { // Show menu this._contextMenuIsBeingShownCount++; this._contextMenuService.showContextMenu({ + domForShadowRoot: this._editor.getDomNode(), + getAnchor: () => anchor!, getActions: () => actions, diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index 599e804eb05..b4bdd4336fd 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -54,6 +54,7 @@ export class DragAndDropController extends Disposable implements IEditorContribu this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this.onEditorKeyDown(e))); this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this.onEditorKeyUp(e))); this._register(this._editor.onDidBlurEditorWidget(() => this.onEditorBlur())); + this._register(this._editor.onDidBlurEditorText(() => this.onEditorBlur())); this._dndDecorationIds = []; this._mouseDown = false; this._modifierPressed = false; diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index dfe22ad235a..303f435450a 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -6,7 +6,7 @@ /* Find widget */ .monaco-editor .find-widget { position: absolute; - z-index: 10; + z-index: 20; height: 33px; overflow: hidden; line-height: 19px; diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index 3daf186140d..5f037f5c219 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -50,7 +50,7 @@ export class ContextMenuHandler { let menu: Menu | undefined; - const anchor = delegate.getAnchor(); + let shadowRootElement = isHTMLElement(delegate.domForShadowRoot) ? delegate.domForShadowRoot : undefined; this.contextViewService.showContextView({ getAnchor: () => delegate.getAnchor(), canRelayout: false, @@ -133,7 +133,7 @@ export class ContextMenuHandler { this.focusToReturn.focus(); } } - }, !!delegate.anchorAsContainer && isHTMLElement(anchor) ? anchor : undefined); + }, shadowRootElement, !!shadowRootElement); } private onActionRun(e: IRunEvent): void { diff --git a/src/vs/platform/contextview/browser/contextView.ts b/src/vs/platform/contextview/browser/contextView.ts index fde55c58e76..855fe03a334 100644 --- a/src/vs/platform/contextview/browser/contextView.ts +++ b/src/vs/platform/contextview/browser/contextView.ts @@ -15,7 +15,7 @@ export interface IContextViewService extends IContextViewProvider { readonly _serviceBrand: undefined; - showContextView(delegate: IContextViewDelegate, container?: HTMLElement): IDisposable; + showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable; hideContextView(data?: any): void; layout(): void; anchorAlignment?: AnchorAlignment; diff --git a/src/vs/platform/contextview/browser/contextViewService.ts b/src/vs/platform/contextview/browser/contextViewService.ts index 685b7e21cff..0d5d61b3d9f 100644 --- a/src/vs/platform/contextview/browser/contextViewService.ts +++ b/src/vs/platform/contextview/browser/contextViewService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IContextViewService, IContextViewDelegate } from './contextView'; -import { ContextView } from 'vs/base/browser/ui/contextview/contextview'; +import { ContextView, ContextViewDOMPosition } from 'vs/base/browser/ui/contextview/contextview'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; @@ -21,7 +21,7 @@ export class ContextViewService extends Disposable implements IContextViewServic super(); this.container = layoutService.container; - this.contextView = this._register(new ContextView(this.container, false)); + this.contextView = this._register(new ContextView(this.container, ContextViewDOMPosition.ABSOLUTE)); this.layout(); this._register(layoutService.onLayout(() => this.layout())); @@ -29,20 +29,20 @@ export class ContextViewService extends Disposable implements IContextViewServic // ContextView - setContainer(container: HTMLElement, useFixedPosition?: boolean): void { - this.contextView.setContainer(container, !!useFixedPosition); + setContainer(container: HTMLElement, domPosition?: ContextViewDOMPosition): void { + this.contextView.setContainer(container, domPosition || ContextViewDOMPosition.ABSOLUTE); } - showContextView(delegate: IContextViewDelegate, container?: HTMLElement): IDisposable { + showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable { if (container) { if (container !== this.container) { this.container = container; - this.setContainer(container, true); + this.setContainer(container, shadowRoot ? ContextViewDOMPosition.FIXED_SHADOW : ContextViewDOMPosition.FIXED); } } else { if (this.container !== this.layoutService.container) { this.container = this.layoutService.container; - this.setContainer(this.container, false); + this.setContainer(this.container, ContextViewDOMPosition.ABSOLUTE); } } diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 17673328b27..7ae1bd39fbf 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -142,8 +142,25 @@ export interface IExtensionIdentifier { uuid?: string; } -export const EXTENSION_CATEGORIES = ['Programming Languages', 'Snippets', 'Linters', 'Themes', 'Debuggers', 'Other', 'Keymaps', 'Formatters', 'Extension Packs', - 'SCM Providers', 'Azure', 'Language Packs', 'Data Science', 'Machine Learning', 'Visualization', 'Testing', 'Notebooks']; +export const EXTENSION_CATEGORIES = [ + 'Azure', + 'Data Science', + 'Debuggers', + 'Extension Packs', + 'Formatters', + 'Keymaps', + 'Language Packs', + 'Linters', + 'Machine Learning', + 'Notebooks', + 'Programming Languages', + 'SCM Providers', + 'Snippets', + 'Themes', + 'Testing', + 'Visualization', + 'Other', +]; export interface IExtensionManifest { readonly name: string; diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index d0f6e6b18a6..3715cbb8e6e 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -194,6 +194,9 @@ class BrowserSocket implements ISocket { this.socket.close(); } + public drain(): Promise { + return Promise.resolve(); + } } diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index afcc3102022..673f3fb3d93 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -390,7 +390,7 @@ export const menuSeparatorBackground = registerColor('menu.separatorBackground', export const snippetTabstopHighlightBackground = registerColor('editor.snippetTabstopHighlightBackground', { dark: new Color(new RGBA(124, 124, 124, 0.3)), light: new Color(new RGBA(10, 50, 100, 0.2)), hc: new Color(new RGBA(124, 124, 124, 0.3)) }, nls.localize('snippetTabstopHighlightBackground', "Highlight background color of a snippet tabstop.")); export const snippetTabstopHighlightBorder = registerColor('editor.snippetTabstopHighlightBorder', { dark: null, light: null, hc: null }, nls.localize('snippetTabstopHighlightBorder', "Highlight border color of a snippet tabstop.")); export const snippetFinalTabstopHighlightBackground = registerColor('editor.snippetFinalTabstopHighlightBackground', { dark: null, light: null, hc: null }, nls.localize('snippetFinalTabstopHighlightBackground', "Highlight background color of the final tabstop of a snippet.")); -export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', { dark: '#525252', light: new Color(new RGBA(10, 50, 100, 0.5)), hc: '#525252' }, nls.localize('snippetFinalTabstopHighlightBorder', "Highlight border color of the final stabstop of a snippet.")); +export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', { dark: '#525252', light: new Color(new RGBA(10, 50, 100, 0.5)), hc: '#525252' }, nls.localize('snippetFinalTabstopHighlightBorder', "Highlight border color of the final tabstop of a snippet.")); /** * Breadcrumb colors diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 99b06d9ed73..a42d889cc8b 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -82,10 +82,19 @@ class RemovedResources { let messages: string[] = []; if (externalRemoval.length > 0) { - messages.push(nls.localize('externalRemoval', "The following files have been closed and modified on disk: {0}.", externalRemoval.join(', '))); + messages.push( + nls.localize( + { key: 'externalRemoval', comment: ['{0} is a list of filenames'] }, + "The following files have been closed and modified on disk: {0}.", externalRemoval.join(', ') + ) + ); } if (noParallelUniverses.length > 0) { - messages.push(nls.localize('noParallelUniverses', "The following files have been modified in an incompatible way: {0}.", noParallelUniverses.join(', '))); + messages.push( + nls.localize( + { key: 'noParallelUniverses', comment: ['{0} is a list of filenames'] }, + "The following files have been modified in an incompatible way: {0}.", noParallelUniverses.join(', ') + )); } return messages.join('\n'); } @@ -771,10 +780,26 @@ export class UndoRedoService implements IUndoRedoService { private _checkWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, checkInvalidatedResources: boolean): WorkspaceVerificationError | null { if (element.removedResources) { - return this._tryToSplitAndUndo(strResource, element, element.removedResources, nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage())); + return this._tryToSplitAndUndo( + strResource, + element, + element.removedResources, + nls.localize( + { key: 'cannotWorkspaceUndo', comment: ['{0} is a label for an operation. {1} is another message.'] }, + "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage() + ) + ); } if (checkInvalidatedResources && element.invalidatedResources) { - return this._tryToSplitAndUndo(strResource, element, element.invalidatedResources, nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage())); + return this._tryToSplitAndUndo( + strResource, + element, + element.invalidatedResources, + nls.localize( + { key: 'cannotWorkspaceUndo', comment: ['{0} is a label for an operation. {1} is another message.'] }, + "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage() + ) + ); } // this must be the last past element in all the impacted resources! @@ -785,7 +810,15 @@ export class UndoRedoService implements IUndoRedoService { } } if (cannotUndoDueToResources.length > 0) { - return this._tryToSplitAndUndo(strResource, element, null, nls.localize('cannotWorkspaceUndoDueToChanges', "Could not undo '{0}' across all files because changes were made to {1}", element.label, cannotUndoDueToResources.join(', '))); + return this._tryToSplitAndUndo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceUndoDueToChanges', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not undo '{0}' across all files because changes were made to {1}", element.label, cannotUndoDueToResources.join(', ') + ) + ); } const cannotLockDueToResources: string[] = []; @@ -795,12 +828,28 @@ export class UndoRedoService implements IUndoRedoService { } } if (cannotLockDueToResources.length > 0) { - return this._tryToSplitAndUndo(strResource, element, null, nls.localize('cannotWorkspaceUndoDueToInProgressUndoRedo', "Could not undo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', '))); + return this._tryToSplitAndUndo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceUndoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not undo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ') + ) + ); } // check if new stack elements were added in the meantime... if (!editStackSnapshot.isValid()) { - return this._tryToSplitAndUndo(strResource, element, null, nls.localize('cannotWorkspaceUndoDueToInMeantimeUndoRedo', "Could not undo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label)); + return this._tryToSplitAndUndo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceUndoDueToInMeantimeUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not undo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label + ) + ); } return null; @@ -881,7 +930,10 @@ export class UndoRedoService implements IUndoRedoService { return; } if (editStack.locked) { - const message = nls.localize('cannotResourceUndoDueToInProgressUndoRedo', "Could not undo '{0}' because there is already an undo or redo operation running.", element.label); + const message = nls.localize( + { key: 'cannotResourceUndoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation.'] }, + "Could not undo '{0}' because there is already an undo or redo operation running.", element.label + ); this._notificationService.info(message); return; } @@ -942,10 +994,26 @@ export class UndoRedoService implements IUndoRedoService { private _checkWorkspaceRedo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, checkInvalidatedResources: boolean): WorkspaceVerificationError | null { if (element.removedResources) { - return this._tryToSplitAndRedo(strResource, element, element.removedResources, nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage())); + return this._tryToSplitAndRedo( + strResource, + element, + element.removedResources, + nls.localize( + { key: 'cannotWorkspaceRedo', comment: ['{0} is a label for an operation. {1} is another message.'] }, + "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage() + ) + ); } if (checkInvalidatedResources && element.invalidatedResources) { - return this._tryToSplitAndRedo(strResource, element, element.invalidatedResources, nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage())); + return this._tryToSplitAndRedo( + strResource, + element, + element.invalidatedResources, + nls.localize( + { key: 'cannotWorkspaceRedo', comment: ['{0} is a label for an operation. {1} is another message.'] }, + "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage() + ) + ); } // this must be the last future element in all the impacted resources! @@ -956,7 +1024,15 @@ export class UndoRedoService implements IUndoRedoService { } } if (cannotRedoDueToResources.length > 0) { - return this._tryToSplitAndRedo(strResource, element, null, nls.localize('cannotWorkspaceRedoDueToChanges', "Could not redo '{0}' across all files because changes were made to {1}", element.label, cannotRedoDueToResources.join(', '))); + return this._tryToSplitAndRedo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceRedoDueToChanges', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not redo '{0}' across all files because changes were made to {1}", element.label, cannotRedoDueToResources.join(', ') + ) + ); } const cannotLockDueToResources: string[] = []; @@ -966,12 +1042,28 @@ export class UndoRedoService implements IUndoRedoService { } } if (cannotLockDueToResources.length > 0) { - return this._tryToSplitAndRedo(strResource, element, null, nls.localize('cannotWorkspaceRedoDueToInProgressUndoRedo', "Could not redo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', '))); + return this._tryToSplitAndRedo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceRedoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not redo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ') + ) + ); } // check if new stack elements were added in the meantime... if (!editStackSnapshot.isValid()) { - return this._tryToSplitAndRedo(strResource, element, null, nls.localize('cannotWorkspaceRedoDueToInMeantimeUndoRedo', "Could not redo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label)); + return this._tryToSplitAndRedo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceRedoDueToInMeantimeUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not redo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label + ) + ); } return null; @@ -1015,7 +1107,10 @@ export class UndoRedoService implements IUndoRedoService { return; } if (editStack.locked) { - const message = nls.localize('cannotResourceRedoDueToInProgressUndoRedo', "Could not redo '{0}' because there is already an undo or redo operation running.", element.label); + const message = nls.localize( + { key: 'cannotResourceRedoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation.'] }, + "Could not redo '{0}' because there is already an undo or redo operation running.", element.label + ); this._notificationService.info(message); return; } diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index c6dc7a03736..ab42605bd79 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -32,6 +32,7 @@ type AutoSyncErrorClassification = { const enablementKey = 'sync.enable'; const disableMachineEventuallyKey = 'sync.disableMachineEventually'; const sessionIdKey = 'sync.sessionId'; +const storeUrlKey = 'sync.storeUrl'; export class UserDataAutoSyncEnablementService extends Disposable { @@ -97,15 +98,20 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i this.syncTriggerDelayer = this._register(new Delayer(0)); if (userDataSyncStoreService.userDataSyncStore) { + + storageService.store(storeUrlKey, userDataSyncStoreService.userDataSyncStore.url.toString(), StorageScope.GLOBAL); + if (this.isEnabled()) { this.logService.info('Auto Sync is enabled.'); } else { this.logService.info('Auto Sync is disabled.'); } this.updateAutoSync(); + if (this.hasToDisableMachineEventually()) { this.disableMachineEventually(); } + this._register(userDataSyncAccountService.onDidChangeAccount(() => this.updateAutoSync())); this._register(userDataSyncStoreService.onDidChangeDonotMakeRequestsUntil(() => this.updateAutoSync())); this._register(Event.debounce(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false))); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index edab37c635f..f7a368b8927 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -868,22 +868,6 @@ declare module 'vscode' { debugAdapterExecutable?(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult; } - export interface DebugSession { - - /** - * Terminates the session. - */ - terminate(): Thenable; - } - - export interface DebugSession { - - /** - * Terminates the session. - */ - terminate(): Thenable; - } - export namespace debug { /** diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 368c7ee34a9..978e6416a5d 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -262,14 +262,6 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return Promise.reject(new Error('debug session not found')); } - public $terminateDebugSession(sessionId: DebugSessionUUID): Promise { - const session = this.debugService.getModel().getSession(sessionId, true); - if (session) { - return session.terminate(); - } - return Promise.reject(new Error('debug session not found')); - } - public $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise { if (sessionId) { const session = this.debugService.getModel().getSession(sessionId, true); diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 212aecd829e..f805b7f9cbe 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -269,6 +269,14 @@ export class MainThreadTextEditor { } })); + const isValidCodeEditor = () => { + // Due to event timings, it is possible that there is a model change event not yet delivered to us. + // > e.g. a model change event is emitted to a listener which then decides to update editor options + // > In this case the editor configuration change event reaches us first. + // So simply check that the model is still attached to this code editor + return (this._codeEditor && this._codeEditor.getModel() === this._model); + }; + const updateProperties = (selectionChangeSource: string | null) => { // Some editor events get delivered faster than model content changes. This is // problematic, as this leads to editor properties reaching the extension host @@ -287,18 +295,30 @@ export class MainThreadTextEditor { this._codeEditorListeners.add(this._codeEditor.onDidChangeCursorSelection((e) => { // selection + if (!isValidCodeEditor()) { + return; + } updateProperties(e.source); })); - this._codeEditorListeners.add(this._codeEditor.onDidChangeConfiguration(() => { + this._codeEditorListeners.add(this._codeEditor.onDidChangeConfiguration((e) => { // options + if (!isValidCodeEditor()) { + return; + } updateProperties(null); })); this._codeEditorListeners.add(this._codeEditor.onDidLayoutChange(() => { // visibleRanges + if (!isValidCodeEditor()) { + return; + } updateProperties(null); })); this._codeEditorListeners.add(this._codeEditor.onDidScrollChange(() => { // visibleRanges + if (!isValidCodeEditor()) { + return; + } updateProperties(null); })); this._updatePropertiesNow(null); diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index ced7611d1e4..e0b3986200d 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -128,7 +128,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha } } - $onExtensionHostExit(code: number): void { + async $onExtensionHostExit(code: number): Promise { this._extensionService._onExtensionHostExit(code); } } diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index e4166f6df8f..536fd02dbd3 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -634,7 +634,7 @@ export class MainThreadTask implements MainThreadTaskShape { return URI.parse(`${info.scheme}://${info.authority}${path}`); }, context: this._extHostContext, - resolveVariables: (workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise => { + resolveVariables: (workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise => { const vars: string[] = []; toResolve.variables.forEach(item => vars.push(item)); return Promise.resolve(this._proxy.$resolveVariables(workspaceFolder.uri, { process: toResolve.process, variables: vars })).then(values => { @@ -642,8 +642,12 @@ export class MainThreadTask implements MainThreadTaskShape { forEach(values.variables, (entry) => { partiallyResolvedVars.push(entry.value); }); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this._configurationResolverService.resolveWithInteraction(workspaceFolder, partiallyResolvedVars, 'tasks', undefined, target).then(resolvedVars => { + if (!resolvedVars) { + resolve(undefined); + } + const result: ResolvedVariables = { process: undefined, variables: new Map() @@ -677,4 +681,9 @@ export class MainThreadTask implements MainThreadTaskShape { } }); } + + async $registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): Promise { + return this._taskService.registerSupportedExecutions(custom, shell, process); + } + } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e278c674cb5..d155c7ac978 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -787,6 +787,7 @@ export interface MainThreadTaskShape extends IDisposable { $terminateTask(id: string): Promise; $registerTaskSystem(scheme: string, info: tasks.TaskSystemInfoDTO): void; $customExecutionComplete(id: string, result?: number): Promise; + $registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): Promise; } export interface MainThreadExtensionServiceShape extends IDisposable { @@ -795,7 +796,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void; $onExtensionActivationError(extensionId: ExtensionIdentifier, error: ExtensionActivationError): Promise; $onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void; - $onExtensionHostExit(code: number): void; + $onExtensionHostExit(code: number): Promise; } export interface SCMProviderFeatures { @@ -878,7 +879,6 @@ export interface MainThreadDebugServiceShape extends IDisposable { $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise; $setDebugSessionName(id: DebugSessionUUID, name: string): void; $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Promise; - $terminateDebugSession(id: DebugSessionUUID): Promise; $appendDebugConsole(value: string): void; $startBreakpointEvents(): void; $registerBreakpoints(breakpoints: Array): Promise; diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index af74467148f..f9dcbecccef 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -957,10 +957,6 @@ export class ExtHostDebugSession implements vscode.DebugSession { public customRequest(command: string, args: any): Promise { return this._debugServiceProxy.$customDebugAdapterRequest(this._id, command, args); } - - public terminate(): Promise { - return this._debugServiceProxy.$terminateDebugSession(this._id); - } } export class ExtHostDebugConsole implements vscode.DebugConsole { diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index b9b1d92ca20..eaf57da1a75 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -557,7 +557,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } // after tests have run, we shutdown the host - this._gracefulExit(error || (typeof failures === 'number' && failures > 0) ? 1 /* ERROR */ : 0 /* OK */); + this._testRunnerExit(error || (typeof failures === 'number' && failures > 0) ? 1 /* ERROR */ : 0 /* OK */); }; const runResult = testRunner!.run(extensionTestsPath, oldTestRunnerCallback); @@ -567,11 +567,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme runResult .then(() => { c(); - this._gracefulExit(0); + this._testRunnerExit(0); }) .catch((err: Error) => { e(err.toString()); - this._gracefulExit(1); + this._testRunnerExit(1); }); } }); @@ -579,24 +579,20 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme // Otherwise make sure to shutdown anyway even in case of an error else { - this._gracefulExit(1 /* ERROR */); + this._testRunnerExit(1 /* ERROR */); } return Promise.reject(new Error(requireError ? requireError.toString() : nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", extensionTestsPath))); } - private _gracefulExit(code: number): void { - // to give the PH process a chance to flush any outstanding console - // messages to the main process, we delay the exit() by some time - setTimeout(() => { - // If extension tests are running, give the exit code to the renderer - if (this._initData.remote.isRemote && !!this._initData.environment.extensionTestsLocationURI) { - this._mainThreadExtensionsProxy.$onExtensionHostExit(code); - return; - } - + private _testRunnerExit(code: number): void { + // wait at most 5000ms for the renderer to confirm our exit request and for the renderer socket to drain + // (this is to ensure all outstanding messages reach the renderer) + const exitPromise = this._mainThreadExtensionsProxy.$onExtensionHostExit(code); + const drainPromise = this._extHostContext.drain(); + Promise.race([Promise.all([exitPromise, drainPromise]), timeout(5000)]).then(() => { this._hostUtils.exit(code); - }, 500); + }); } private _startExtensionHost(): Promise { diff --git a/src/vs/workbench/api/common/extHostRpcService.ts b/src/vs/workbench/api/common/extHostRpcService.ts index 58237cf24bc..6582ef5fb3f 100644 --- a/src/vs/workbench/api/common/extHostRpcService.ts +++ b/src/vs/workbench/api/common/extHostRpcService.ts @@ -18,12 +18,12 @@ export class ExtHostRpcService implements IExtHostRpcService { readonly getProxy: (identifier: ProxyIdentifier) => T; readonly set: (identifier: ProxyIdentifier, instance: R) => R; readonly assertRegistered: (identifiers: ProxyIdentifier[]) => void; + readonly drain: () => Promise; constructor(rpcProtocol: IRPCProtocol) { this.getProxy = rpcProtocol.getProxy.bind(rpcProtocol); this.set = rpcProtocol.set.bind(rpcProtocol); this.assertRegistered = rpcProtocol.assertRegistered.bind(rpcProtocol); - + this.drain = rpcProtocol.drain.bind(rpcProtocol); } - } diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 785685a2679..ee6423408fd 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -419,6 +419,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask this._activeCustomExecutions2 = new Map(); this._logService = logService; this._deprecationService = deprecationService; + this._proxy.$registerSupportedExecutions(true); } public registerTaskProvider(extension: IExtensionDescription, type: string, provider: vscode.TaskProvider): vscode.Disposable { diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index ad10c7cb61e..0c59bbd7a6f 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -47,6 +47,7 @@ export class ExtHostTask extends ExtHostTaskBase { platform: process.platform }); } + this._proxy.$registerSupportedExecutions(true, true, true); } public async executeTask(extension: IExtensionDescription, task: vscode.Task): Promise { diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index dd8f4e1fe7c..020f78b7b7f 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -51,7 +51,10 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // fetch JS sources as text and create a new function around it const source = await response.text(); - const initFn = new Function('module', 'exports', 'require', `${source}\n//# sourceURL=${module.toString(true)}`); + // Here we append #vscode-extension to serve as a marker, such that source maps + // can be adjusted for the extra wrapping function. + const sourceURL = `${module.toString(true)}#vscode-extension`; + const initFn = new Function('module', 'exports', 'require', `${source}\n//# sourceURL=${sourceURL}`); // define commonjs globals: `module`, `exports`, and `require` const _exports = {}; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index a81dc59e0cf..bbf6fb7b245 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -32,6 +32,7 @@ import { IAuthenticationService } from 'vs/workbench/services/authentication/bro import { AuthenticationSession } from 'vs/editor/common/modes'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; export class ViewContainerActivityAction extends ActivityAction { @@ -97,6 +98,8 @@ export class ViewContainerActivityAction extends ActivityAction { } } +export const ACCOUNTS_VISIBILITY_PREFERENCE_KEY = 'workbench.activity.showAccounts'; + export class AccountsActionViewItem extends ActivityActionViewItem { constructor( action: ActivityAction, @@ -106,7 +109,8 @@ export class AccountsActionViewItem extends ActivityActionViewItem { @IMenuService protected menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IStorageService private readonly storageService: IStorageService ) { super(action, { draggable: false, colors, icon: true }, themeService); } @@ -189,6 +193,15 @@ export class AccountsActionViewItem extends ActivityActionViewItem { } }); + if (menus.length) { + menus.push(new Separator()); + } + + menus.push(new Action('hide', nls.localize('hide', "Hide"), undefined, true, _ => { + this.storageService.store(ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL); + return Promise.resolve(); + })); + return menus; } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 87e4aabcf4b..f7b675a55b1 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -5,10 +5,10 @@ import 'vs/css!./media/activitybarpart'; import * as nls from 'vs/nls'; -import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionsOrientation, ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { GLOBAL_ACTIVITY_ID, IActivity, ACCOUNTS_ACTIIVTY_ID } from 'vs/workbench/common/activity'; import { Part } from 'vs/workbench/browser/part'; -import { GlobalActivityActionViewItem, ViewContainerActivityAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewContainerActivityAction, AccountsActionViewItem, HomeAction, HomeActionViewItem } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; +import { GlobalActivityActionViewItem, ViewContainerActivityAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewContainerActivityAction, AccountsActionViewItem, HomeAction, HomeActionViewItem, ACCOUNTS_VISIBILITY_PREFERENCE_KEY } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -75,7 +75,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { static readonly PINNED_VIEW_CONTAINERS = 'workbench.activity.pinnedViewlets2'; private static readonly PLACEHOLDER_VIEW_CONTAINERS = 'workbench.activity.placeholderViewlets'; private static readonly HOME_BAR_VISIBILITY_PREFERENCE = 'workbench.activity.showHomeIndicator'; - + private static readonly ACCOUNTS_ACTION_INDEX = 0; //#region IView readonly minimumWidth: number = 48; @@ -164,6 +164,18 @@ export class ActivitybarPart extends Part implements IActivityBarService { actions.push(this.instantiationService.createInstance(ToggleMenuBarAction, ToggleMenuBarAction.ID, menuBarVisibility === 'compact' ? nls.localize('hideMenu', "Hide Menu") : nls.localize('showMenu', "Show Menu"))); } + const toggleAccountsVisibilityAction = new Action( + 'toggleAccountsVisibility', + nls.localize('accounts', "Accounts"), + undefined, + true, + async () => { this.accountsVisibilityPreference = !this.accountsVisibilityPreference; } + ); + + toggleAccountsVisibilityAction.checked = !!this.accountsActivityAction; + actions.push(toggleAccountsVisibilityAction); + actions.push(new Separator()); + actions.push(new Action( ToggleActivityBarVisibilityAction.ID, nls.localize('hideActivitBar', "Hide Activity Bar"), @@ -587,17 +599,35 @@ export class ActivitybarPart extends Part implements IActivityBarService { cssClass: Codicon.settingsGear.classNames }); - this.accountsActivityAction = new ActivityAction({ - id: 'workbench.actions.accounts', - name: nls.localize('accounts', "Accounts"), - cssClass: Codicon.account.classNames - }); + if (this.accountsVisibilityPreference) { + this.accountsActivityAction = new ActivityAction({ + id: 'workbench.actions.accounts', + name: nls.localize('accounts', "Accounts"), + cssClass: Codicon.account.classNames + }); - this.globalActivityActionBar.push(this.accountsActivityAction); + this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX }); + } this.globalActivityActionBar.push(this.globalActivityAction); } + private toggleAccountsActivity() { + if (this.globalActivityActionBar) { + if (this.accountsActivityAction) { + this.globalActivityActionBar.pull(ActivitybarPart.ACCOUNTS_ACTION_INDEX); + this.accountsActivityAction = undefined; + } else { + this.accountsActivityAction = new ActivityAction({ + id: 'workbench.actions.accounts', + name: nls.localize('accounts', "Accounts"), + cssClass: Codicon.account.classNames + }); + this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX }); + } + } + } + private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction } { let compositeActions = this.compositeActions.get(compositeId); if (!compositeActions) { @@ -827,6 +857,10 @@ export class ActivitybarPart extends Part implements IActivityBarService { if (e.key === ActivitybarPart.HOME_BAR_VISIBILITY_PREFERENCE && e.scope === StorageScope.GLOBAL) { this.onDidChangeHomeBarVisibility(); } + + if (e.key === ACCOUNTS_VISIBILITY_PREFERENCE_KEY && e.scope === StorageScope.GLOBAL) { + this.toggleAccountsActivity(); + } } private saveCachedViewContainers(): void { @@ -964,6 +998,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.storageService.store(ActivitybarPart.HOME_BAR_VISIBILITY_PREFERENCE, value, StorageScope.GLOBAL); } + private get accountsVisibilityPreference(): boolean { + return this.storageService.getBoolean(ACCOUNTS_VISIBILITY_PREFERENCE_KEY, StorageScope.GLOBAL, true); + } + + private set accountsVisibilityPreference(value: boolean) { + this.storageService.store(ACCOUNTS_VISIBILITY_PREFERENCE_KEY, value, StorageScope.GLOBAL); + } + private migrateFromOldCachedViewContainersValue(): void { const value = this.storageService.get('workbench.activity.pinnedViewlets', StorageScope.GLOBAL); if (value !== undefined) { diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index b63345e4653..5cdb3864c88 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -66,18 +66,18 @@ export abstract class Viewlet extends PaneComposite implements IViewlet { } getSecondaryActions(): IAction[] { - const viewSecondaryActions = this.viewPaneContainer.getViewsVisibilityActions(); + const viewVisibilityActions = this.viewPaneContainer.getViewsVisibilityActions(); const secondaryActions = this.viewPaneContainer.getSecondaryActions(); - if (viewSecondaryActions.length <= 1) { + if (viewVisibilityActions.length <= 1 || viewVisibilityActions.every(({ enabled }) => !enabled)) { return secondaryActions; } if (secondaryActions.length === 0) { - return viewSecondaryActions; + return viewVisibilityActions; } return [ - new SubmenuAction('workbench.views', nls.localize('views', "Views"), viewSecondaryActions), + new SubmenuAction('workbench.views', nls.localize('views', "Views"), viewVisibilityActions), new Separator(), ...secondaryActions ]; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index c0d9595c1c0..1a8a5879ca6 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -689,7 +689,7 @@ export class Breakpoint extends BaseBreakpoint implements IBreakpoint { toJSON(): any { const result = super.toJSON(); - result.uri = this.uri; + result.uri = this._uri; result.lineNumber = this._lineNumber; result.column = this._column; result.adapterData = this.adapterData; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 48764849cb4..9e535b75489 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1763,6 +1763,29 @@ export class ShowPopularExtensionsAction extends Action { } } +export class RecentlyPublishedExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.recentlyPublishedExtensions'; + static readonly LABEL = localize('recentlyPublishedExtensions', "Recently Published Extensions"); + + constructor( + id: string, + label: string, + @IViewletService private readonly viewletService: IViewletService + ) { + super(id, label, undefined, true); + } + + run(): Promise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) + .then(viewlet => { + viewlet.search('@sort:publishedDate '); + viewlet.focus(); + }); + } +} + export class ShowRecommendedExtensionsAction extends Action { static readonly ID = 'workbench.extensions.action.showRecommendedExtensions'; @@ -2038,6 +2061,27 @@ export class ShowAzureExtensionsAction extends Action { } } +export class SearchCategoryAction extends Action { + + constructor( + id: string, + label: string, + private readonly category: string, + @IViewletService private readonly viewletService: IViewletService + ) { + super(id, label, undefined, true); + } + + run(): Promise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) + .then(viewlet => { + viewlet.search(`@category:"${this.category.toLowerCase()}"`); + viewlet.focus(); + }); + } +} + export class ChangeSortAction extends Action { private query: Query; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 3da213cf1f8..4b0a9f1bd38 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -10,23 +10,23 @@ import { isPromiseCanceledError } from 'vs/base/common/errors'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Event as EventOf, Emitter } from 'vs/base/common/event'; -import { IAction, Action, Separator } from 'vs/base/common/actions'; +import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { append, $, addClass, toggleClass, Dimension, hide, show } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, ShowRecommendationsOnlyOnDemandKey, CloseExtensionDetailsOnViewChangeKey } from '../common/extensions'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, CloseExtensionDetailsOnViewChangeKey } from '../common/extensions'; import { - ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowDisabledExtensionsAction, - ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, - EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction + ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, + ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, + EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, SearchCategoryAction, RecentlyPublishedExtensionsAction, ShowInstalledExtensionsAction, ShowOutdatedExtensionsAction, ShowDisabledExtensionsAction, ShowEnabledExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; -import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView, BuiltInThemesExtensionsView, BuiltInBasicsExtensionsView, ServerExtensionsView, DefaultRecommendedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; +import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerExtensionsView, DefaultRecommendedExtensionsView, OutdatedExtensionsView, InstalledExtensionsView, SearchBuiltInExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import Severity from 'vs/base/common/severity'; @@ -49,9 +49,8 @@ import { SuggestEnabledInput, attachSuggestEnabledInputBoxStyler } from 'vs/work import { alert } from 'vs/base/browser/ui/aria/aria'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { RemoteNameContext } from 'vs/workbench/browser/contextkeys'; import { ILabelService } from 'vs/platform/label/common/label'; import { MementoObject } from 'vs/workbench/common/memento'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -68,24 +67,9 @@ const SearchOutdatedExtensionsContext = new RawContextKey('searchOutdat const SearchEnabledExtensionsContext = new RawContextKey('searchEnabledExtensions', false); const SearchDisabledExtensionsContext = new RawContextKey('searchDisabledExtensions', false); const HasInstalledExtensionsContext = new RawContextKey('hasInstalledExtensions', true); +const BuiltInExtensionsContext = new RawContextKey('builtInExtensions', false); const SearchBuiltInExtensionsContext = new RawContextKey('searchBuiltInExtensions', false); const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); -const DefaultRecommendedExtensionsContext = new RawContextKey('defaultRecommendedExtensions', false); -const viewIdNameMappings: { [id: string]: string } = { - 'extensions.listView': localize('marketPlace', "Marketplace"), - 'extensions.enabledExtensionList': localize('enabledExtensions', "Enabled"), - 'extensions.enabledExtensionList2': localize('enabledExtensions', "Enabled"), - 'extensions.disabledExtensionList': localize('disabledExtensions', "Disabled"), - 'extensions.disabledExtensionList2': localize('disabledExtensions', "Disabled"), - 'extensions.popularExtensionsList': localize('popularExtensions', "Popular"), - 'extensions.recommendedList': localize('recommendedExtensions', "Recommended"), - 'extensions.otherrecommendedList': localize('otherRecommendedExtensions', "Other Recommendations"), - 'extensions.workspaceRecommendedList': localize('workspaceRecommendedExtensions', "Workspace Recommendations"), - 'extensions.builtInExtensionsList': localize('builtInExtensions', "Features"), - 'extensions.builtInThemesExtensionsList': localize('builtInThemesExtensions', "Themes"), - 'extensions.builtInBasicsExtensionsList': localize('builtInBasicsExtensions', "Programming Languages"), - 'extensions.syncedExtensionsList': localize('syncedExtensions', "My Account"), -}; export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { @@ -101,220 +85,233 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio } private registerViews(): void { - let viewDescriptors: IViewDescriptor[] = []; - viewDescriptors.push(this.createMarketPlaceExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDefaultEnabledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDefaultDisabledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDefaultPopularExtensionsListViewDescriptor()); - viewDescriptors.push(this.createEnabledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDisabledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createBuiltInExtensionsListViewDescriptor()); - viewDescriptors.push(this.createBuiltInBasicsExtensionsListViewDescriptor()); - viewDescriptors.push(this.createBuiltInThemesExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDefaultRecommendedExtensionsListViewDescriptor()); - viewDescriptors.push(this.createOtherRecommendedExtensionsListViewDescriptor()); - viewDescriptors.push(this.createWorkspaceRecommendedExtensionsListViewDescriptor()); + const viewDescriptors: IViewDescriptor[] = []; - if (this.extensionManagementServerService.localExtensionManagementServer) { - viewDescriptors.push(...this.createExtensionsViewDescriptorsForServer(this.extensionManagementServerService.localExtensionManagementServer)); - } - if (this.extensionManagementServerService.remoteExtensionManagementServer) { - viewDescriptors.push(...this.createExtensionsViewDescriptorsForServer(this.extensionManagementServerService.remoteExtensionManagementServer)); - } + /* Default views */ + viewDescriptors.push(...this.createDefaultExtensionsViewDescriptors()); + + /* Search views */ + viewDescriptors.push(...this.createSearchExtensionsViewDescriptors()); + + /* Recommendations views */ + viewDescriptors.push(...this.createRecommendedExtensionsViewDescriptors()); + + /* Built-in extensions views */ + viewDescriptors.push(...this.createBuiltinExtensionsViewDescriptors()); Registry.as(Extensions.ViewsRegistry).registerViews(viewDescriptors, this.container); } - // View used for any kind of searching - private createMarketPlaceExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.listView'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(ExtensionsListView), - when: ContextKeyExpr.and(ContextKeyExpr.has('searchMarketplaceExtensions')), - weight: 100 - }; - } + private createDefaultExtensionsViewDescriptors(): IViewDescriptor[] { + const viewDescriptors: IViewDescriptor[] = []; - // Separate view for enabled extensions required as we need to show enabled, disabled and recommended sections - // in the default view when there is no search text, but user has installed extensions. - private createDefaultEnabledExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.enabledExtensionList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(EnabledExtensionsView), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteNameContext.isEqualTo('')), - weight: 40, - canToggleVisibility: true, - order: 1 - }; - } - - // Separate view for disabled extensions required as we need to show enabled, disabled and recommended sections - // in the default view when there is no search text, but user has installed extensions. - private createDefaultDisabledExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.disabledExtensionList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(DisabledExtensionsView), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteNameContext.isEqualTo('')), - weight: 10, - canToggleVisibility: true, - order: 3, - collapsed: true - }; - } - - // Separate view for popular extensions required as we need to show popular and recommended sections - // in the default view when there is no search text, and user has no installed extensions. - private createDefaultPopularExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.popularExtensionsList'; - return { - id, - name: viewIdNameMappings[id], + /* + * Default popular extensions view + * Separate view for popular extensions required as we need to show popular and recommended sections + * in the default view when there is no search text, and user has no installed extensions. + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.popular', + name: localize('popularExtensions', "Popular"), ctorDescriptor: new SyncDescriptor(ExtensionsListView), when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')), weight: 60, - order: 1 - }; - } + order: 1, + }); - private createExtensionsViewDescriptorsForServer(server: IExtensionManagementServer): IViewDescriptor[] { + /* + * Default installed extensions views - Shows all user installed extensions. + */ + const servers: IExtensionManagementServer[] = []; + if (this.extensionManagementServerService.localExtensionManagementServer) { + servers.push(this.extensionManagementServerService.localExtensionManagementServer); + } + if (this.extensionManagementServerService.remoteExtensionManagementServer) { + servers.push(this.extensionManagementServerService.remoteExtensionManagementServer); + } const getViewName = (viewTitle: string, server: IExtensionManagementServer): string => { - const serverLabel = server.label; - if (viewTitle && this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { - return `${serverLabel} - ${viewTitle}`; - } - return viewTitle ? viewTitle : serverLabel; + return servers.length > 1 ? `${server.label} - ${viewTitle}` : viewTitle; }; - const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server); - const getOutdatedViewName = (): string => getViewName(localize('outdated', "Outdated"), server); - const onDidChangeServerLabel: EventOf = EventOf.map(this.labelService.onDidChangeFormatters, () => undefined); - return [{ - id: `extensions.${server.id}.installed`, - get name() { return getInstalledViewName(); }, - ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())]), - when: ContextKeyExpr.and(ContextKeyExpr.has('searchInstalledExtensions')), - weight: 100 - }, { - id: `extensions.${server.id}.outdated`, - get name() { return getOutdatedViewName(); }, - ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getOutdatedViewName())]), - when: ContextKeyExpr.and(ContextKeyExpr.has('searchOutdatedExtensions')), - weight: 100 - }, { - id: `extensions.${server.id}.default`, - get name() { return getInstalledViewName(); }, - ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteNameContext.notEqualsTo('')), - weight: 40, - order: 1 - }]; - } + for (const server of servers) { + const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server); + const onDidChangeServerLabel: EventOf = EventOf.map(this.labelService.onDidChangeFormatters, () => undefined); + viewDescriptors.push({ + id: servers.length > 1 ? `workbench.views.extensions.${server.id}.installed` : `workbench.views.extensions.installed`, + get name() { return getInstalledViewName(); }, + ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())]), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + weight: 100, + order: 2, + /* Installed extensions views shall not be hidden when there are more than one server */ + canToggleVisibility: servers.length === 1 + }); + } - // Separate view for recommended extensions required as we need to show it along with other views when there is no search text. - // When user has installed extensions, this is shown along with the views for enabled & disabled extensions - // When user has no installed extensions, this is shown along with the view for popular extensions - private createDefaultRecommendedExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.recommendedList'; - return { - id, - name: viewIdNameMappings[id], + /* + * Default recommended extensions view + * When user has installed extensions, this is shown along with the views for enabled & disabled extensions + * When user has no installed extensions, this is shown along with the view for popular extensions + */ + viewDescriptors.push({ + id: 'extensions.recommendedList', + name: localize('recommendedExtensions', "Recommended"), ctorDescriptor: new SyncDescriptor(DefaultRecommendedExtensionsView), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('defaultRecommendedExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')), weight: 40, - order: 2, + order: 3, canToggleVisibility: true - }; + }); + + /* Installed views shall be default in multi server window */ + if (servers.length === 1) { + /* + * Default enabled extensions view - Shows all user installed enabled extensions. + * Hidden by default + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.enabled', + name: localize('enabledExtensions', "Enabled"), + ctorDescriptor: new SyncDescriptor(EnabledExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + hideByDefault: true, + weight: 40, + order: 4, + canToggleVisibility: true + }); + + /* + * Default disabled extensions view - Shows all disabled extensions. + * Hidden by default + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.disabled', + name: localize('disabledExtensions', "Disabled"), + ctorDescriptor: new SyncDescriptor(DisabledExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + hideByDefault: true, + weight: 10, + order: 5, + canToggleVisibility: true + }); + + } + + return viewDescriptors; } - // Separate view for recommedations that are not workspace recommendations. - // Shown along with view for workspace recommendations, when using the command that shows recommendations - private createOtherRecommendedExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.otherrecommendedList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(RecommendedExtensionsView), - when: ContextKeyExpr.has('recommendedExtensions'), - weight: 50, - order: 2 - }; - } + private createSearchExtensionsViewDescriptors(): IViewDescriptor[] { + const viewDescriptors: IViewDescriptor[] = []; - // Separate view for workspace recommendations. - // Shown along with view for other recommendations, when using the command that shows recommendations - private createWorkspaceRecommendedExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.workspaceRecommendedList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView), - when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), ContextKeyExpr.has('nonEmptyWorkspace')), - weight: 50, - order: 1 - }; - } + /* + * View used for searching Marketplace + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.marketplace', + name: localize('marketPlace', "Marketplace"), + ctorDescriptor: new SyncDescriptor(ExtensionsListView), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchMarketplaceExtensions')), + }); - private createEnabledExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.enabledExtensionList2'; - return { - id, - name: viewIdNameMappings[id], + /* + * View used for searching all installed extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchInstalled', + name: localize('installed', "Installed"), + ctorDescriptor: new SyncDescriptor(InstalledExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchInstalledExtensions')), + }); + + /* + * View used for searching enabled extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchEnabled', + name: localize('enabled', "Enabled"), ctorDescriptor: new SyncDescriptor(EnabledExtensionsView), when: ContextKeyExpr.and(ContextKeyExpr.has('searchEnabledExtensions')), - weight: 40, - order: 1 - }; - } + }); - private createDisabledExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.disabledExtensionList2'; - return { - id, - name: viewIdNameMappings[id], + /* + * View used for searching disabled extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchDisabled', + name: localize('disabled', "Disabled"), ctorDescriptor: new SyncDescriptor(DisabledExtensionsView), when: ContextKeyExpr.and(ContextKeyExpr.has('searchDisabledExtensions')), - weight: 10, - order: 3, - collapsed: true - }; + }); + + /* + * View used for searching outdated extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchOutdated', + name: localize('outdated', "Outdated"), + ctorDescriptor: new SyncDescriptor(OutdatedExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchOutdatedExtensions')), + }); + + /* + * View used for searching builtin extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchBuiltin', + name: localize('builtin', "Builtin"), + ctorDescriptor: new SyncDescriptor(SearchBuiltInExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchBuiltInExtensions')), + }); + + return viewDescriptors; } - private createBuiltInExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.builtInExtensionsList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(BuiltInExtensionsView), - when: ContextKeyExpr.has('searchBuiltInExtensions'), - weight: 100 - }; + private createRecommendedExtensionsViewDescriptors(): IViewDescriptor[] { + const viewDescriptors: IViewDescriptor[] = []; + + viewDescriptors.push({ + id: 'workbench.views.extensions.workspaceRecommendations', + name: localize('workspaceRecommendedExtensions', "Workspace Recommendations"), + ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), ContextKeyExpr.has('nonEmptyWorkspace')), + order: 1 + }); + + viewDescriptors.push({ + id: 'workbench.views.extensions.otherRecommendations', + name: localize('otherRecommendedExtensions', "Other Recommendations"), + ctorDescriptor: new SyncDescriptor(RecommendedExtensionsView), + when: ContextKeyExpr.has('recommendedExtensions'), + order: 2 + }); + + return viewDescriptors; } - private createBuiltInThemesExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.builtInThemesExtensionsList'; - return { - id, - name: viewIdNameMappings[id], + private createBuiltinExtensionsViewDescriptors(): IViewDescriptor[] { + const viewDescriptors: IViewDescriptor[] = []; + + viewDescriptors.push({ + id: 'workbench.views.extensions.builtinFeatureExtensions', + name: localize('builtinFeatureExtensions', "Features"), + ctorDescriptor: new SyncDescriptor(BuiltInFeatureExtensionsView), + when: ContextKeyExpr.has('builtInExtensions'), + }); + + viewDescriptors.push({ + id: 'workbench.views.extensions.builtinThemeExtensions', + name: localize('builtInThemesExtensions', "Themes"), ctorDescriptor: new SyncDescriptor(BuiltInThemesExtensionsView), - when: ContextKeyExpr.has('searchBuiltInExtensions'), - weight: 100 - }; - } + when: ContextKeyExpr.has('builtInExtensions'), + }); - private createBuiltInBasicsExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.builtInBasicsExtensionsList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(BuiltInBasicsExtensionsView), - when: ContextKeyExpr.has('searchBuiltInExtensions'), - weight: 100 - }; + viewDescriptors.push({ + id: 'workbench.views.extensions.builtinProgrammingLanguageExtensions', + name: localize('builtinProgrammingLanguageExtensions', "Programming Languages"), + ctorDescriptor: new SyncDescriptor(BuiltInProgrammingLanguageExtensionsView), + when: ContextKeyExpr.has('builtInExtensions'), + }); + + return viewDescriptors; } } @@ -331,15 +328,13 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private searchEnabledExtensionsContextKey: IContextKey; private searchDisabledExtensionsContextKey: IContextKey; private hasInstalledExtensionsContextKey: IContextKey; + private builtInExtensionsContextKey: IContextKey; private searchBuiltInExtensionsContextKey: IContextKey; private recommendedExtensionsContextKey: IContextKey; - private defaultRecommendedExtensionsContextKey: IContextKey; private searchDelayer: Delayer; private root: HTMLElement | undefined; private searchBox: SuggestEnabledInput | undefined; - private primaryActions: IAction[] | undefined; - private secondaryActions: IAction[] | null = null; private readonly searchViewletState: MementoObject; constructor( @@ -349,6 +344,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IInstantiationService instantiationService: IInstantiationService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @INotificationService private readonly notificationService: INotificationService, @IViewletService private readonly viewletService: IViewletService, @IThemeService themeService: IThemeService, @@ -359,7 +355,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IPreferencesService private readonly preferencesService: IPreferencesService + @IPreferencesService private readonly preferencesService: IPreferencesService, ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); @@ -372,10 +368,9 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchEnabledExtensionsContextKey = SearchEnabledExtensionsContext.bindTo(contextKeyService); this.searchDisabledExtensionsContextKey = SearchDisabledExtensionsContext.bindTo(contextKeyService); this.hasInstalledExtensionsContextKey = HasInstalledExtensionsContext.bindTo(contextKeyService); + this.builtInExtensionsContextKey = BuiltInExtensionsContext.bindTo(contextKeyService); this.searchBuiltInExtensionsContextKey = SearchBuiltInExtensionsContext.bindTo(contextKeyService); this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService); - this.defaultRecommendedExtensionsContextKey = DefaultRecommendedExtensionsContext.bindTo(contextKeyService); - this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); this._register(this.viewletService.onDidViewletOpen(this.onViewletOpen, this)); this.searchViewletState = this.getMemento(StorageScope.WORKSPACE); @@ -385,12 +380,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { - this.secondaryActions = null; this.updateTitleArea(); } - if (e.affectedKeys.indexOf(ShowRecommendationsOnlyOnDemandKey) > -1) { - this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); - } }, this)); } @@ -504,39 +495,60 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } getActions(): IAction[] { - if (!this.primaryActions) { - this.primaryActions = [ - this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, this.searchBox ? this.searchBox.getValue() : '') - ]; + const filterActions: IAction[] = []; + + // Local extensions filters + filterActions.push(...[ + this.instantiationService.createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, localize('builtin filter', "Built-in")), + this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, localize('installed filter', "Installed")), + this.instantiationService.createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, localize('enabled filter', "Enabled")), + this.instantiationService.createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, localize('disabled filter', "Disabled")), + this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, localize('outdated filter', "Outdated")), + ]); + + if (this.extensionGalleryService.isEnabled()) { + filterActions.splice(0, 0, ...[ + this.instantiationService.createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, localize('most popular filter', "Most Popular")), + this.instantiationService.createInstance(RecentlyPublishedExtensionsAction, RecentlyPublishedExtensionsAction.ID, localize('recently published filter', "Recently Published")), + this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('recomended filter', "Recommended")), + new SubmenuAction('workbench.extensions.action.filterExtensionsByCategory', localize('filter by category', "Category"), EXTENSION_CATEGORIES.map(category => this.instantiationService.createInstance(SearchCategoryAction, `extensions.actions.searchByCategory.${category}`, category, category))), + new Separator(), + ]); + filterActions.push(...[ + new Separator(), + new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), [ + this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Install Count"), this.onSearchChange, 'installs'), + this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Rating"), this.onSearchChange, 'rating'), + this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.name', localize('sort by name', "Name"), this.onSearchChange, 'name'), + this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.publishedDate', localize('sort by date', "Published Date"), this.onSearchChange, 'publishedDate'), + ]), + ]); } - return this.primaryActions; + + return [ + new SubmenuAction('workbench.extensions.action.filterExtensions', localize('filterExtensions', "Filter Extensions..."), filterActions, 'codicon-filter'), + this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, this.searchBox ? this.searchBox.getValue() : ''), + ]; } getSecondaryActions(): IAction[] { - if (!this.secondaryActions) { - this.secondaryActions = [ - this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, ShowOutdatedExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, ShowEnabledExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, ShowDisabledExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, ShowBuiltInExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL), - new Separator(), - this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Sort By: Install Count"), this.onSearchChange, 'installs'), - this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Sort By: Rating"), this.onSearchChange, 'rating'), - this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.name', localize('sort by name', "Sort By: Name"), this.onSearchChange, 'name'), - new Separator(), - this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL), - ...(this.configurationService.getValue(AutoUpdateConfigurationKey) ? [this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)] : [this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)]), - this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL), - new Separator(), - this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL), - this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL) - ]; + const actions: IAction[] = []; + + actions.push(this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL)); + if (this.configurationService.getValue(AutoUpdateConfigurationKey)) { + actions.push(this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)); + } else { + actions.push(this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)); } - return this.secondaryActions; + actions.push(new Separator()); + actions.push(this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL)); + actions.push(this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL)); + + actions.push(new Separator()); + actions.push(this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL)); + + return actions; } search(value: string, refresh: boolean = false): void { @@ -574,7 +586,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchOutdatedExtensionsContextKey.set(ExtensionsListView.isOutdatedExtensionsQuery(value)); this.searchEnabledExtensionsContextKey.set(ExtensionsListView.isEnabledExtensionsQuery(value)); this.searchDisabledExtensionsContextKey.set(ExtensionsListView.isDisabledExtensionsQuery(value)); - this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); + this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isSearchBuiltInExtensionsQuery(value)); + this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery); this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY); @@ -596,19 +609,20 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } private alertSearchResult(count: number, viewId: string): void { + const view = this.viewContainerModel.visibleViewDescriptors.find(view => view.id === viewId); switch (count) { case 0: break; case 1: - if (viewIdNameMappings[viewId]) { - alert(localize('extensionFoundInSection', "1 extension found in the {0} section.", viewIdNameMappings[viewId])); + if (view) { + alert(localize('extensionFoundInSection', "1 extension found in the {0} section.", view.name)); } else { alert(localize('extensionFound', "1 extension found.")); } break; default: - if (viewIdNameMappings[viewId]) { - alert(localize('extensionsFoundInSection', "{0} extensions found in the {1} section.", count, viewIdNameMappings[viewId])); + if (view) { + alert(localize('extensionsFoundInSection', "{0} extensions found in the {1} section.", count, view.name)); } else { alert(localize('extensionsFound', "{0} extensions found.", count)); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index fa630ae0d90..9e9a43d2fbb 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -196,6 +196,7 @@ export class ExtensionsListView extends ViewPane { case 'installs': options = assign(options, { sortBy: SortBy.InstallCount }); break; case 'rating': options = assign(options, { sortBy: SortBy.WeightedRating }); break; case 'name': options = assign(options, { sortBy: SortBy.Title }); break; + case 'publishedDate': options = assign(options, { sortBy: SortBy.PublishedDate }); break; } const successCallback = (model: IPagedModel) => { @@ -807,16 +808,21 @@ export class ExtensionsListView extends ViewPane { this.list = null; } - static isBuiltInExtensionsQuery(query: string): boolean { - return /^\s*@builtin\s*$/i.test(query); - } - static isLocalExtensionsQuery(query: string): boolean { return this.isInstalledExtensionsQuery(query) || this.isOutdatedExtensionsQuery(query) || this.isEnabledExtensionsQuery(query) || this.isDisabledExtensionsQuery(query) - || this.isBuiltInExtensionsQuery(query); + || this.isBuiltInExtensionsQuery(query) + || this.isSearchBuiltInExtensionsQuery(query); + } + + static isSearchBuiltInExtensionsQuery(query: string): boolean { + return /@builtin\s.+/i.test(query); + } + + static isBuiltInExtensionsQuery(query: string): boolean { + return /@builtin$/i.test(query.trim()); } static isInstalledExtensionsQuery(query: string): boolean { @@ -897,7 +903,7 @@ export class ServerExtensionsView extends ExtensionsListView { async show(query: string): Promise> { query = query ? query : '@installed'; - if (!ExtensionsListView.isLocalExtensionsQuery(query) && !ExtensionsListView.isBuiltInExtensionsQuery(query)) { + if (!ExtensionsListView.isLocalExtensionsQuery(query)) { query = query += ' @installed'; } return super.show(query.trim()); @@ -929,7 +935,29 @@ export class DisabledExtensionsView extends ExtensionsListView { } } -export class BuiltInExtensionsView extends ExtensionsListView { +export class OutdatedExtensionsView extends ExtensionsListView { + + async show(query: string): Promise> { + query = query || '@outdated'; + return ExtensionsListView.isOutdatedExtensionsQuery(query) ? super.show(query) : this.showEmptyModel(); + } +} + +export class InstalledExtensionsView extends ExtensionsListView { + + async show(query: string): Promise> { + query = query || '@installed'; + return ExtensionsListView.isInstalledExtensionsQuery(query) ? super.show(query) : this.showEmptyModel(); + } +} + +export class SearchBuiltInExtensionsView extends ExtensionsListView { + async show(query: string): Promise> { + return ExtensionsListView.isSearchBuiltInExtensionsQuery(query) ? super.show(query) : this.showEmptyModel(); + } +} + +export class BuiltInFeatureExtensionsView extends ExtensionsListView { async show(query: string): Promise> { return (query && query.trim() !== '@builtin') ? this.showEmptyModel() : super.show('@builtin:features'); } @@ -941,7 +969,7 @@ export class BuiltInThemesExtensionsView extends ExtensionsListView { } } -export class BuiltInBasicsExtensionsView extends ExtensionsListView { +export class BuiltInProgrammingLanguageExtensionsView extends ExtensionsListView { async show(query: string): Promise> { return (query && query.trim() !== '@builtin') ? this.showEmptyModel() : super.show('@builtin:basics'); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 7f11c1ca54f..fbe745b3696 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -39,6 +39,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { asDomUri } from 'vs/base/browser/dom'; import { getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge'; import { isWeb } from 'vs/base/common/platform'; +import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil'; interface IExtensionStateProvider { (extension: Extension): T; @@ -665,14 +666,79 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extensions.length === 1) { return extensions[0]; } + const enabledExtensions = extensions.filter(e => e.local && this.extensionEnablementService.isEnabled(e.local)); - if (enabledExtensions.length === 0) { - return extensions[0]; - } if (enabledExtensions.length === 1) { return enabledExtensions[0]; } - return enabledExtensions.find(e => e.server === this.extensionManagementServerService.remoteExtensionManagementServer) || enabledExtensions[0]; + + const extensionsToChoose = enabledExtensions.length ? enabledExtensions : extensions; + + let extension = extensionsToChoose.find(extension => { + for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) { + switch (extensionKind) { + case 'ui': + /* UI extension is chosen only if it is installed locally */ + if (extension.server === this.extensionManagementServerService.localExtensionManagementServer) { + return true; + } + return false; + case 'workspace': + /* Choose remote workspace extension if exists */ + if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) { + return true; + } + return false; + case 'web': + /* Choose web extension if exists */ + if (extension.server === this.extensionManagementServerService.webExtensionManagementServer) { + return true; + } + return false; + } + } + return false; + }); + + if (!extension && this.extensionManagementServerService.localExtensionManagementServer) { + extension = extensionsToChoose.find(extension => { + for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) { + switch (extensionKind) { + case 'workspace': + /* Choose local workspace extension if exists */ + if (extension.server === this.extensionManagementServerService.localExtensionManagementServer) { + return true; + } + return false; + case 'web': + /* Choose local web extension if exists */ + if (extension.server === this.extensionManagementServerService.localExtensionManagementServer) { + return true; + } + return false; + } + } + return false; + }); + } + + if (!extension && this.extensionManagementServerService.remoteExtensionManagementServer) { + extension = extensionsToChoose.find(extension => { + for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) { + switch (extensionKind) { + case 'web': + /* Choose remote web extension if exists */ + if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) { + return true; + } + return false; + } + } + return false; + }); + } + + return extension || extensions[0]; } private fromGallery(gallery: IGalleryExtension, maliciousExtensionSet: Set): IExtension { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index c55a176ec3a..e0542111088 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -164,16 +164,17 @@ export class RuntimeExtensionsEditor extends BaseEditor { this._register(this._extensionService.onDidChangeExtensionsStatus(() => this._updateSoon.schedule())); } - private _updateExtensions(): void { - this._elements = this._resolveExtensions(); + private async _updateExtensions(): Promise { + this._elements = await this._resolveExtensions(); if (this._list) { this._list.splice(0, this._list.length, this._elements); } } - private _resolveExtensions(): IRuntimeExtension[] { + private async _resolveExtensions(): Promise { let marketplaceMap: { [id: string]: IExtension; } = Object.create(null); - for (let extension of this._extensionsWorkbenchService.local) { + const marketPlaceExtensions = await this._extensionsWorkbenchService.queryLocal(); + for (let extension of marketPlaceExtensions) { marketplaceMap[ExtensionIdentifier.toKey(extension.identifier.id)] = extension; } @@ -328,7 +329,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { } else { data.icon.style.visibility = 'inherit'; } - data.name.textContent = element.marketplaceInfo ? element.marketplaceInfo.displayName : element.description.displayName || ''; + data.name.textContent = element.marketplaceInfo.displayName; data.version.textContent = element.description.version; const activationTimes = element.status.activationTimes!; @@ -462,11 +463,10 @@ export class RuntimeExtensionsEditor extends BaseEditor { actions.push(new ReportExtensionIssueAction(e.element, this._openerService, this._clipboardService, this._productService)); actions.push(new Separator()); - if (e.element.marketplaceInfo) { - actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledWorkspace))); - actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledGlobally))); - actions.push(new Separator()); - } + actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledWorkspace))); + actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledGlobally))); + actions.push(new Separator()); + const state = this._extensionHostProfileService.state; if (state === ProfileSessionState.Running) { actions.push(this._instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL)); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 82da373c436..b7f98de6930 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -14,7 +14,7 @@ import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, InstallOperation, IExtensionTipsService, IGalleryMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; @@ -34,7 +34,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { NativeURLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtension, ExtensionKind } from 'vs/platform/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; @@ -46,6 +46,8 @@ import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestSer import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/extensionTipsService'; +import { Schemas } from 'vs/base/common/network'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -981,6 +983,384 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.equal(actual[0].enablementState, EnablementState.DisabledWorkspace); }); + test('test user extension is preferred when the same extension exists as system and user extension', async () => { + testObject = await aWorkbenchService(); + const userExtension = aLocalExtension('pub.a'); + const systemExtension = aLocalExtension('pub.a', {}, { type: ExtensionType.System }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [systemExtension, userExtension]); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, userExtension); + }); + + test('test user extension is disabled when the same extension exists as system and user extension and system extension is disabled', async () => { + testObject = await aWorkbenchService(); + const systemExtension = aLocalExtension('pub.a', {}, { type: ExtensionType.System }); + await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([systemExtension], EnablementState.DisabledGlobally); + const userExtension = aLocalExtension('pub.a'); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [systemExtension, userExtension]); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, userExtension); + assert.equal(actual[0].enablementState, EnablementState.DisabledGlobally); + }); + + test('Test local ui extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local workspace extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local web extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['web']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local ui,workspace extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local workspace,ui extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace', 'ui']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local ui,workspace,web extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'workspace', 'web']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local ui,web,workspace extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'web', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local web,ui,workspace extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['web', 'ui', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local web,workspace,ui extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['web', 'workspace', 'ui']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local workspace,web,ui extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace', 'web', 'ui']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local workspace,ui,web extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace', 'ui', 'web']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local UI extension is chosen if it exists in both servers', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local ui,workspace extension is chosen if it exists in both servers', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test remote workspace extension is chosen if it exists in remote server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace']; + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, remoteExtension); + }); + + test('Test remote workspace extension is chosen if it exists in both servers', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, remoteExtension); + }); + + test('Test remote workspace extension is chosen if it exists in both servers and local is disabled', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localExtension], EnablementState.DisabledGlobally); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, remoteExtension); + assert.equal(actual[0].enablementState, EnablementState.DisabledGlobally); + }); + + test('Test remote workspace extension is chosen if it exists in both servers and remote is disabled in workspace', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([remoteExtension], EnablementState.DisabledWorkspace); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, remoteExtension); + assert.equal(actual[0].enablementState, EnablementState.DisabledWorkspace); + }); + + test('Test local ui, workspace extension is chosen if it exists in both servers and local is disabled', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localExtension], EnablementState.DisabledGlobally); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + assert.equal(actual[0].enablementState, EnablementState.DisabledGlobally); + }); + + test('Test local ui, workspace extension is chosen if it exists in both servers and local is disabled in workspace', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localExtension], EnablementState.DisabledWorkspace); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + assert.equal(actual[0].enablementState, EnablementState.DisabledWorkspace); + }); + + test('Test local web extension is chosen if it exists in both servers', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['web']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test remote web extension is chosen if it exists only in remote', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['web']; + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, remoteExtension); + }); + async function aWorkbenchService(): Promise { const workbenchService: ExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); await workbenchService.queryLocal(); @@ -1031,4 +1411,49 @@ suite('ExtensionsWorkbenchServiceTest', () => { }); }); } + + function aMultiExtensionManagementServerService(instantiationService: TestInstantiationService, localExtensionManagementService?: IExtensionManagementService, remoteExtensionManagementService?: IExtensionManagementService): IExtensionManagementServerService { + const localExtensionManagementServer: IExtensionManagementServer = { + id: 'vscode-local', + label: 'local', + extensionManagementService: localExtensionManagementService || createExtensionManagementService() + }; + const remoteExtensionManagementServer: IExtensionManagementServer = { + id: 'vscode-remote', + label: 'remote', + extensionManagementService: remoteExtensionManagementService || createExtensionManagementService() + }; + return { + _serviceBrand: undefined, + localExtensionManagementServer, + remoteExtensionManagementServer, + webExtensionManagementServer: null, + getExtensionManagementServer: (extension: IExtension) => { + if (extension.location.scheme === Schemas.file) { + return localExtensionManagementServer; + } + if (extension.location.scheme === REMOTE_HOST_SCHEME) { + return remoteExtensionManagementServer; + } + throw new Error(''); + } + }; + } + + function createExtensionManagementService(installed: ILocalExtension[] = []): IExtensionManagementService { + return { + onInstallExtension: Event.None, + onDidInstallExtension: Event.None, + onUninstallExtension: Event.None, + onDidUninstallExtension: Event.None, + getInstalled: () => Promise.resolve(installed), + installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), + updateMetadata: async (local: ILocalExtension, metadata: IGalleryMetadata) => { + local.identifier.uuid = metadata.id; + local.publisherDisplayName = metadata.publisherDisplayName; + local.publisherId = metadata.publisherId; + return local; + } + }; + } }); diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index cdc24904254..ab5511a5a35 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -13,7 +13,8 @@ export const CELL_RUN_GUTTER = 28; export const CODE_CELL_LEFT_MARGIN = 32; export const EDITOR_TOOLBAR_HEIGHT = 0; -export const BOTTOM_CELL_TOOLBAR_HEIGHT = 28; +export const BOTTOM_CELL_TOOLBAR_HEIGHT = 18; +export const BOTTOM_CELL_TOOLBAR_OFFSET = 12; export const CELL_STATUSBAR_HEIGHT = 22; // Margin above editor diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 6a235155596..499fc927e36 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -74,7 +74,8 @@ const FOCUS_OUT_OUTPUT_COMMAND_ID = 'notebook.cell.focusOutOutput'; export const NOTEBOOK_ACTIONS_CATEGORY = { value: localize('notebookActions.category', "Notebook"), original: 'Notebook' }; -export const CELL_TITLE_GROUP_ID = 'inline'; +export const CELL_TITLE_CELL_GROUP_ID = 'inline/cell'; +export const CELL_TITLE_OUTPUT_GROUP_ID = 'inline/output'; const EDITOR_WIDGET_ACTION_WEIGHT = KeybindingWeight.EditorContrib; // smaller than Suggest Widget, etc @@ -403,6 +404,11 @@ registerAction2(class extends NotebookCellAction { weight: KeybindingWeight.WorkbenchContrib }, precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR), + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), + group: '2_edit', + } }); } @@ -421,6 +427,11 @@ registerAction2(class extends NotebookCellAction { primary: KeyCode.KEY_M, weight: KeybindingWeight.WorkbenchContrib }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), + group: '2_edit', + } }); } @@ -597,7 +608,7 @@ registerAction2(class extends NotebookCellAction { NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.toNegated(), NOTEBOOK_CELL_EDITABLE), order: CellToolbarOrder.EditCell, - group: CELL_TITLE_GROUP_ID + group: CELL_TITLE_CELL_GROUP_ID }, icon: { id: 'codicon/pencil' } }); @@ -621,7 +632,7 @@ registerAction2(class extends NotebookCellAction { NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_EDITABLE), order: CellToolbarOrder.SaveCell, - group: CELL_TITLE_GROUP_ID + group: CELL_TITLE_CELL_GROUP_ID }, icon: { id: 'codicon/check' }, keybinding: { @@ -655,7 +666,7 @@ registerAction2(class extends NotebookCellAction { id: MenuId.NotebookCellTitle, order: CellToolbarOrder.DeleteCell, when: NOTEBOOK_EDITOR_EDITABLE, - group: CELL_TITLE_GROUP_ID + group: CELL_TITLE_CELL_GROUP_ID }, keybinding: { primary: KeyCode.Delete, @@ -754,6 +765,11 @@ registerAction2(class extends NotebookCellAction { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, weight: EDITOR_WIDGET_ACTION_WEIGHT }, + menu: { + id: MenuId.NotebookCellTitle, + when: NOTEBOOK_EDITOR_FOCUSED, + group: '1_copy', + } }); } @@ -776,6 +792,11 @@ registerAction2(class extends NotebookCellAction { primary: KeyMod.CtrlCmd | KeyCode.KEY_X, weight: EDITOR_WIDGET_ACTION_WEIGHT }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), + group: '1_copy', + } }); } @@ -860,6 +881,11 @@ registerAction2(class extends NotebookAction { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, weight: EDITOR_WIDGET_ACTION_WEIGHT }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE), + group: '1_copy', + } }); } @@ -1172,7 +1198,7 @@ registerAction2(class extends NotebookCellAction { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), NOTEBOOK_EDITOR_RUNNABLE), order: CellToolbarOrder.ClearCellOutput, - group: CELL_TITLE_GROUP_ID + group: CELL_TITLE_OUTPUT_GROUP_ID }, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey), NOTEBOOK_CELL_HAS_OUTPUTS), @@ -1335,7 +1361,11 @@ registerAction2(class extends NotebookCellAction { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, InputFocusedContext), order: CellToolbarOrder.SplitCell, - group: CELL_TITLE_GROUP_ID + group: CELL_TITLE_CELL_GROUP_ID, + // alt: { + // id: JOIN_CELL_BELOW_COMMAND_ID, + // title: localize('notebookActions.joinCellBelow', "Join with Next Cell") + // } }, icon: { id: 'codicon/split-vertical' }, keybinding: { @@ -1388,6 +1418,11 @@ registerAction2(class extends NotebookCellAction { when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.KEY_J, weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), + group: '2_edit', } }); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index 552e93395bf..99fc865265a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -166,8 +166,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { const activeEditor = getActiveNotebookEditor(this._editorService); - if (activeEditor && activeEditor.multipleKernelsAvailable) { - this.showKernelStatus(activeEditor.activeKernel); + if (activeEditor) { this._editorDisposable.add(activeEditor.onDidChangeKernel(() => { if (activeEditor.multipleKernelsAvailable) { this.showKernelStatus(activeEditor.activeKernel); @@ -175,6 +174,18 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { this.kernelInfoElement.clear(); } })); + + this._editorDisposable.add(activeEditor.onDidChangeAvailableKernels(() => { + if (activeEditor.multipleKernelsAvailable) { + this.showKernelStatus(activeEditor.activeKernel); + } else { + this.kernelInfoElement.clear(); + } + })); + } + + if (activeEditor && activeEditor.multipleKernelsAvailable) { + this.showKernelStatus(activeEditor.activeKernel); } else { this.kernelInfoElement.clear(); } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 4d3d640acc7..61103a1c71d 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -488,6 +488,9 @@ .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { position: absolute; display: flex; + justify-content: center; + z-index: 30; /* over the focus outline on the editor */ + width: 100%; opacity: 0; transition: opacity 0.2s ease-in-out; cursor: auto; @@ -531,19 +534,8 @@ align-items: center; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .separator { - height: 1px; - flex-grow: 1; - align-self: center; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item:first-child::after { - content: ' '; - display: block; - height: 1px; - width: 16px; - align-self: center; - margin: 0px 8px; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item:first-child { + margin-right: 16px; } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { @@ -699,7 +691,7 @@ .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator .codicon { visibility: visible; - padding: 8px 0 0 10px; + padding: 10px 0 0 10px; } /** Theming */ @@ -739,3 +731,11 @@ .monaco-workbench.vs-dark .monaco-workbench .notebookOverlay .cell.markdown table > tbody > tr > td { border-color: rgba(255, 255, 255, 0.18); } */ + +.monaco-action-bar .action-item.verticalSeparator { + width: 1px !important; + background-color: #bbb; + height: 16px !important; + margin: 5px 4px !important; + cursor: none; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index f65bbe5beeb..0ddd8114cda 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -180,6 +180,7 @@ export interface INotebookEditor extends IEditor { isNotebookEditor: boolean; activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined; multipleKernelsAvailable: boolean; + readonly onDidChangeAvailableKernels: Event; readonly onDidChangeKernel: Event; isDisposed: boolean; @@ -240,10 +241,16 @@ export interface INotebookEditor extends IEditor { moveCellDown(cell: ICellViewModel): Promise; /** + * @deprecated Note that this method doesn't support batch operations, use #moveCellToIdx instead. * Move a cell above or below another cell */ moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise; + /** + * Move a cell to a specific position + */ + moveCellToIdx(cell: ICellViewModel, index: number): Promise; + /** * Focus the container of a cell (the monaco editor inside is not focused). */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 1918231eeb4..1ffd922e3b0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -75,6 +75,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _overlayContainer!: HTMLElement; private _body!: HTMLElement; private _webview: BackLayerWebView | null = null; + private _webviewResolved: boolean = false; + private _webviewResolvePromise: Promise | null = null; private _webviewTransparentCover: HTMLElement | null = null; private _list: INotebookCellList | undefined; private _dndController: CellDragAndDropController | null = null; @@ -135,6 +137,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined = undefined; private readonly _onDidChangeKernel = this._register(new Emitter()); readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; + private readonly _onDidChangeAvailableKernels = this._register(new Emitter()); + readonly onDidChangeAvailableKernels: Event = this._onDidChangeAvailableKernels.event; get activeKernel() { return this._activeKernel; @@ -150,7 +154,16 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } private _currentKernelTokenSource: CancellationTokenSource | undefined = undefined; - multipleKernelsAvailable: boolean = false; + private _multipleKernelsAvailable: boolean = false; + + get multipleKernelsAvailable() { + return this._multipleKernelsAvailable; + } + + set multipleKernelsAvailable(state: boolean) { + this._multipleKernelsAvailable = state; + this._onDidChangeAvailableKernels.fire(); + } private readonly _onDidChangeActiveEditor = this._register(new Emitter()); readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; @@ -571,7 +584,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // @deprecated if (provider && provider.kernel) { // it has a builtin kernel, don't automatically choose a kernel - this._loadKernelPreloads(provider.providerExtensionLocation, provider.kernel); + await this._loadKernelPreloads(provider.providerExtensionLocation, provider.kernel); tokenSource.dispose(); return; } @@ -590,7 +603,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // the provider doesn't have a builtin kernel, choose a kernel this.activeKernel = availableKernels[0]; if (this.activeKernel) { - this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); } tokenSource.dispose(); @@ -610,7 +623,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } if (this.activeKernel) { - this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); await this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); } @@ -623,7 +636,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (kernelsFromSameExtension.length) { const preferedKernel = kernelsFromSameExtension.find(kernel => kernel.isPreferred) || kernelsFromSameExtension[0]; this.activeKernel = preferedKernel; - this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); await preferedKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); tokenSource.dispose(); return; @@ -632,15 +645,16 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // the provider doesn't have a builtin kernel, choose a kernel this.activeKernel = kernels[0]; if (this.activeKernel) { - this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); await this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); } tokenSource.dispose(); } - private _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfoDto) { - if (kernel.preloads) { + private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfoDto) { + if (kernel.preloads && kernel.preloads.length) { + await this._resolveWebview(); this._webview?.updateKernelPreloads([extensionLocation], kernel.preloads.map(preload => URI.revive(preload))); } } @@ -655,34 +669,63 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._notebookExecuting?.set(notebookMetadata.runState === NotebookRunState.Running); } + private async _resolveWebview(): Promise { + if (!this.textModel) { + return null; + } + + if (this._webviewResolvePromise) { + return this._webviewResolvePromise; + } + + if (!this._webview) { + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, this.getId(), this.textModel!.uri); + // attach the webview container to the DOM tree first + this._list?.rowsContainer.insertAdjacentElement('afterbegin', this._webview.element); + } + + this._webviewResolvePromise = new Promise(async resolve => { + await this._webview!.createWebview(); + this._webview!.webview!.onDidBlur(() => { + this._outputFocus?.set(false); + this.updateEditorFocus(); + + if (this._overlayContainer.contains(document.activeElement)) { + this._webiewFocused = false; + } + }); + this._webview!.webview!.onDidFocus(() => { + this._outputFocus?.set(true); + this.updateEditorFocus(); + this._onDidFocusEmitter.fire(); + + if (this._overlayContainer.contains(document.activeElement)) { + this._webiewFocused = true; + } + }); + + this._localStore.add(this._webview!.onMessage(({ message, forRenderer }) => { + if (this.viewModel) { + this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.getId(), forRenderer, message); + } + })); + + if (this.viewModel && this.viewModel!.renderers.size) { + this._webview?.updateRendererPreloads(this.viewModel!.renderers); + } + + this._webviewResolved = true; + + resolve(this._webview!); + }); + + return this._webviewResolvePromise; + } + private async _createWebview(id: string, resource: URI): Promise { this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource); // attach the webview container to the DOM tree first this._list?.rowsContainer.insertAdjacentElement('afterbegin', this._webview.element); - await this._webview.createWebview(); - this._webview.webview.onDidBlur(() => { - this._outputFocus?.set(false); - this.updateEditorFocus(); - - if (this._overlayContainer.contains(document.activeElement)) { - this._webiewFocused = false; - } - }); - this._webview.webview.onDidFocus(() => { - this._outputFocus?.set(true); - this.updateEditorFocus(); - this._onDidFocusEmitter.fire(); - - if (this._overlayContainer.contains(document.activeElement)) { - this._webiewFocused = true; - } - }); - - this._localStore.add(this._webview.onMessage(({ message, forRenderer }) => { - if (this.viewModel) { - this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.getId(), forRenderer, message); - } - })); } private async _attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined) { @@ -716,10 +759,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - this._webview?.updateRendererPreloads(this.viewModel.renderers); + if (this.viewModel.renderers.size) { + await this._resolveWebview(); + this._webview?.updateRendererPreloads(this.viewModel.renderers); + } this._localStore.add(this._list!.onWillScroll(e => { - this._webview!.updateViewScrollTop(-e.scrollTop, []); + if (!this._webviewResolved) { + return; + } + + this._webview?.updateViewScrollTop(-e.scrollTop, []); this._webviewTransparentCover!.style.top = `${e.scrollTop}px`; })); @@ -731,6 +781,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const scrollTop = this._list?.scrollTop || 0; const scrollHeight = this._list?.scrollHeight || 0; + + if (!this._webviewResolved) { + return; + } + this._webview!.element.style.height = `${scrollHeight}px`; if (this._webview?.insetMapping) { @@ -1127,6 +1182,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._moveCellToIndex(originalIdx, newIdx); } + async moveCellToIdx(cell: ICellViewModel, index: number): Promise { + if (!this._notebookViewModel!.metadata.editable) { + return null; + } + + const originalIdx = this._notebookViewModel!.getCellIndex(cell); + return this._moveCellToIndex(originalIdx, index); + } + private async _moveCellToIndex(index: number, newIdx: number): Promise { if (index === newIdx) { return null; @@ -1355,6 +1419,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } + await this._resolveWebview(); + let preloads = this._notebookViewModel!.renderers; if (!this._webview!.insetMapping.has(output)) { @@ -1369,7 +1435,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } removeInset(output: IProcessedOutput) { - if (!this._webview) { + if (!this._webview || !this._webviewResolved) { return; } @@ -1377,7 +1443,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } hideInset(output: IProcessedOutput) { - if (!this._webview) { + if (!this._webview || !this._webviewResolved) { return; } @@ -1389,10 +1455,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } postMessage(forRendererId: string | undefined, message: any) { + if (!this._webview || !this._webviewResolved) { + return; + } + if (forRendererId === undefined) { - this._webview?.webview.postMessage(message); + this._webview.webview?.postMessage(message); } else { - this._webview?.postRendererMessage(forRendererId, message); + this._webview.postRendererMessage(forRendererId, message); } } @@ -1599,13 +1669,13 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { background-color: ${editorBackgroundColor}; }`); collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { background-color: ${editorBackgroundColor} }`); } const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); if (cellToolbarSeperator) { - collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .separator { background-color: ${cellToolbarSeperator} }`); - collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item:first-child::after { background-color: ${cellToolbarSeperator} }`); collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { border: solid 1px ${cellToolbarSeperator}; }`); + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { border: solid 1px ${cellToolbarSeperator} }`); } const focusedCellBackgroundColor = theme.getColor(focusedCellBackground); @@ -1642,14 +1712,10 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .monaco-list-row.cell-editor-focus .cell-editor-part:before { outline: solid 1px ${focusedEditorBorderColorColor}; }`); } - const editorBorderColor = theme.getColor(notebookCellBorder); - if (editorBorderColor) { - collector.addRule(`.notebookOverlay .monaco-list-row .cell-editor-part:before { outline: solid 1px ${editorBorderColor}; }`); - } - - const headingBorderColor = theme.getColor(notebookCellBorder); - if (headingBorderColor) { - collector.addRule(`.notebookOverlay .cell.markdown h1 { border-color: ${headingBorderColor}; }`); + const cellBorderColor = theme.getColor(notebookCellBorder); + if (cellBorderColor) { + collector.addRule(`.notebookOverlay .cell.markdown h1 { border-color: ${cellBorderColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .cell-editor-part:before { outline: solid 1px ${cellBorderColor}; }`); } const cellStatusSuccessIcon = theme.getColor(cellStatusIconSuccess); @@ -1700,10 +1766,8 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN}px; }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { padding-top: ${EDITOR_TOP_MARGIN}px; }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row { padding-bottom: ${CELL_BOTTOM_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row .cell-bottom-toolbar-container { margin-top: ${CELL_BOTTOM_MARGIN}px; }`); collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_MARGIN}px 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .output { width: calc(100% - ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER + (CELL_MARGIN * 2)}px); }`); - collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container { width: calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px); margin: 0px ${CELL_MARGIN * 2}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .cell .run-button-container { width: ${CELL_RUN_GUTTER}px; }`); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 41761f5ce9e..35a3ebf3654 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -219,7 +219,7 @@ export interface INotebookWebviewMessage { let version = 0; export class BackLayerWebView extends Disposable { element: HTMLElement; - webview!: WebviewElement; + webview: WebviewElement | undefined = undefined; insetMapping: Map = new Map(); hiddenInsetMapping: Set = new Set(); reversedInsetMapping: Map = new Map(); @@ -714,7 +714,7 @@ ${loaderJs} return; } - this.webview.focus(); + this.webview?.focus(); } focusOutput(cellId: string) { @@ -722,7 +722,7 @@ ${loaderJs} return; } - this.webview.focus(); + this.webview?.focus(); setTimeout(() => { // Need this, or focus decoration is not shown. No clue. this._sendMessageToWebview({ type: 'focus-output', @@ -814,6 +814,10 @@ ${loaderJs} } private _updatePreloads(resources: IPreloadResource[], source: 'renderer' | 'kernel') { + if (!this.webview) { + return; + } + const mixedResourceRoots = [...(this.localResourceRootsCache || []), ...this.rendererRootsCache, ...this.kernelRootsCache]; this.webview.localResourcesRoot = mixedResourceRoots; @@ -830,7 +834,7 @@ ${loaderJs} return; } - this.webview.postMessage(message); + this.webview?.postMessage(message); } clearPreloadsCache() { @@ -839,7 +843,7 @@ ${loaderJs} dispose() { this._disposed = true; - this.webview.dispose(); + this.webview?.dispose(); super.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts new file mode 100644 index 00000000000..fd0749969eb --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Action, IAction } from 'vs/base/common/actions'; +import { BaseActionViewItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; + +export class VerticalSeparator extends Action { + static readonly ID = 'vs.actions.verticalSeparator'; + + constructor( + label?: string + ) { + super(VerticalSeparator.ID, label, label ? 'verticalSeparator text' : 'verticalSeparator'); + this.checked = false; + this.enabled = false; + } +} + +export class VerticalSeparatorViewItem extends BaseActionViewItem { + render(container: HTMLElement) { + DOM.addClass(container, 'verticalSeparator'); + // const iconContainer = DOM.append(container, $('.verticalSeparator')); + // DOM.addClasses(iconContainer, 'codicon', 'codicon-chrome-minimize'); + } +} + +export function createAndFillInActionBarActionsWithVerticalSeparators(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable { + const groups = menu.getActions(options); + // Action bars handle alternative actions on their own so the alternative actions should be ignored + fillInActions(groups, target, false, isPrimaryGroup); + return asDisposable(groups); +} + +function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { + for (let tuple of groups) { + let [group, actions] = tuple; + if (useAlternativeActions) { + actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a); + } + + if (isPrimaryGroup(group)) { + const to = Array.isArray(target) ? target : target.primary; + + if (to.length > 0) { + to.push(new VerticalSeparator()); + } + + to.push(...actions); + } else { + const to = Array.isArray(target) ? target : target.secondary; + + if (to.length > 0) { + to.push(new Separator()); + } + + to.push(...actions); + } + } +} + +function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray]>): IDisposable { + const disposables = new DisposableStore(); + for (const [, actions] of groups) { + for (const action of actions) { + disposables.add(action); + } + } + return disposables; +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts index 0857d782123..8c3f6e513b5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts @@ -3,16 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction } from 'vs/base/common/actions'; -import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; export class CellMenus { constructor( @IMenuService private readonly menuService: IMenuService, - @IContextMenuService private readonly contextMenuService: IContextMenuService ) { } getCellTitleMenu(contextKeyService: IContextKeyService): IMenu { @@ -26,11 +22,6 @@ export class CellMenus { private getMenu(menuId: MenuId, contextKeyService: IContextKeyService): IMenu { const menu = this.menuService.createMenu(menuId, contextKeyService); - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary }; - - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); return menu; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index b3c976ac38b..bd91c758f6f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -36,17 +36,18 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CancelCellAction, ChangeCellLanguageAction, ExecuteCellAction, INotebookCellActionContext, CELL_TITLE_GROUP_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, ICellViewModel, INotebookCellList, INotebookEditor, MarkdownCellRenderTemplate, isCodeCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CancelCellAction, ChangeCellLanguageAction, ExecuteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, ICellViewModel, INotebookCellList, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; -import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; +import { StatefulMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, NotebookCellRunState, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; +import { CellKind, NotebookCellMetadata, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { VerticalSeparator, createAndFillInActionBarActionsWithVerticalSeparators, VerticalSeparatorViewItem } from './cellActionView'; const $ = DOM.$; @@ -204,9 +205,6 @@ abstract class AbstractCellRenderer { } }); - toolbar.getContainer().style.height = `${BOTTOM_CELL_TOOLBAR_HEIGHT}px`; - container.style.height = `${BOTTOM_CELL_TOOLBAR_HEIGHT}px`; - const cellMenu = this.instantiationService.createInstance(CellMenus); const menu = disposables.add(cellMenu.getCellInsertionMenu(contextKeyService)); @@ -220,19 +218,18 @@ abstract class AbstractCellRenderer { templateData.betweenCellToolbar.context = context; const container = templateData.bottomCellContainer; - if (element instanceof CodeCellViewModel) { + const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; + container.style.top = `${bottomToolbarOffset}px`; + + templateData.elementDisposables.add(element.onDidChangeLayout(() => { const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; container.style.top = `${bottomToolbarOffset}px`; - - templateData.elementDisposables.add(element.onDidChangeLayout(() => { - const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; - container.style.top = `${bottomToolbarOffset}px`; - })); - } + })); } protected createToolbar(container: HTMLElement): ToolBar { const toolbar = new ToolBar(container, this.contextMenuService, { + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), actionViewItemProvider: action => { if (action instanceof MenuItemAction) { return this.instantiationService.createInstance(MenuEntryActionViewItem, action); @@ -240,6 +237,10 @@ abstract class AbstractCellRenderer { return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } + if (action.id === VerticalSeparator.ID) { + return new VerticalSeparatorViewItem(undefined, action); + } + return undefined; } }); @@ -250,16 +251,11 @@ abstract class AbstractCellRenderer { private getCellToolbarActions(menu: IMenu): { primary: IAction[], secondary: IAction[] } { const primary: IAction[] = []; const secondary: IAction[] = []; - const actions = menu.getActions({ shouldForwardArgs: true }); - for (let [id, menuActions] of actions) { - if (id === CELL_TITLE_GROUP_ID) { - primary.push(...menuActions); - } else { - secondary.push(...menuActions); - } - } + const result = { primary, secondary }; - return { primary, secondary }; + createAndFillInActionBarActionsWithVerticalSeparators(menu, { shouldForwardArgs: true }, result, g => /^inline/.test(g)); + + return result; } protected setupCellToolbarActions(templateData: BaseCellRenderTemplate, disposables: DisposableStore): void { @@ -352,9 +348,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const foldingIndicator = DOM.append(focusIndicator, DOM.$('.notebook-folding-indicator')); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); - DOM.append(bottomCellContainer, $('.separator')); const betweenCellToolbar = disposables.add(this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService)); - DOM.append(bottomCellContainer, $('.separator')); const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart); const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService)); @@ -440,7 +434,8 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR this.setBetweenCellToolbarContext(templateData, element, toolbarContext); - const markdownCell = this.instantiationService.createInstance(StatefullMarkdownCell, this.notebookEditor, element, templateData, this.editorOptions.value, this.renderedEditors); + const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService])); + const markdownCell = scopedInstaService.createInstance(StatefulMarkdownCell, this.notebookEditor, element, templateData, this.editorOptions.value, this.renderedEditors); elementDisposables.add(this.editorOptions.onDidChange(newValue => markdownCell.updateEditorOptions(newValue))); elementDisposables.add(markdownCell); @@ -612,6 +607,18 @@ export class CellDragAndDropController extends Disposable { private onCellDrop(event: CellDragEvent): void { const draggedCell = this.currentDraggedCell!; + let draggedCells: ICellViewModel[] = [draggedCell]; + + if (draggedCell.cellKind === CellKind.Markdown) { + const currCellIndex = this.notebookEditor.viewModel!.getCellIndex(draggedCell); + const nextVisibleCellIndex = this.notebookEditor.viewModel!.getNextVisibleCellIndex(currCellIndex); + + if (nextVisibleCellIndex > currCellIndex + 1) { + // folding ;) + draggedCells = this.notebookEditor.viewModel!.viewCells.slice(currCellIndex, nextVisibleCellIndex); + } + } + this.dragCleanup(); const isCopy = (event.browserEvent.ctrlKey && !platform.isMacintosh) || (event.browserEvent.altKey && platform.isMacintosh); @@ -626,9 +633,9 @@ export class CellDragAndDropController extends Disposable { } if (isCopy) { - this.copyCell(draggedCell, event.draggedOverCell, dropDirection); + this.copyCells(draggedCells, event.draggedOverCell, dropDirection); } else { - this.moveCell(draggedCell, event.draggedOverCell, dropDirection); + this.moveCells(draggedCells, event.draggedOverCell, dropDirection); } } @@ -674,16 +681,37 @@ export class CellDragAndDropController extends Disposable { })); } - private async moveCell(draggedCell: ICellViewModel, ontoCell: ICellViewModel, direction: 'above' | 'below') { - await this.notebookEditor.moveCell(draggedCell, ontoCell, direction); + private async moveCells(draggedCells: ICellViewModel[], ontoCell: ICellViewModel, direction: 'above' | 'below') { + const relativeToIndex = this.notebookEditor!.viewModel!.getCellIndex(ontoCell); + const newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1; + + this.notebookEditor.textModel!.pushStackElement('Move Cells'); + for (let i = draggedCells.length - 1; i >= 0; i--) { + await this.notebookEditor.moveCellToIdx(draggedCells[i], newIdx); + } + + this.notebookEditor.textModel!.pushStackElement('Move Cells'); } - private copyCell(draggedCell: ICellViewModel, ontoCell: ICellViewModel, direction: 'above' | 'below') { - const editState = draggedCell.editState; - const newCell = this.notebookEditor.insertNotebookCell(ontoCell, draggedCell.cellKind, direction, draggedCell.getText()); - if (newCell) { - this.notebookEditor.focusNotebookCell(newCell, editState === CellEditState.Editing ? 'editor' : 'container'); + private copyCells(draggedCells: ICellViewModel[], ontoCell: ICellViewModel, direction: 'above' | 'below') { + this.notebookEditor.textModel!.pushStackElement('Copy Cells'); + let firstNewCell: ICellViewModel | undefined = undefined; + let firstNewCellState: CellEditState = CellEditState.Preview; + for (let i = 0; i < draggedCells.length; i++) { + const draggedCell = draggedCells[i]; + const newCell = this.notebookEditor.insertNotebookCell(ontoCell, draggedCell.cellKind, direction, draggedCell.getText()); + + if (newCell && !firstNewCell) { + firstNewCell = newCell; + firstNewCellState = draggedCell.editState; + } } + + if (firstNewCell) { + this.notebookEditor.focusNotebookCell(firstNewCell, firstNewCellState === CellEditState.Editing ? 'editor' : 'container'); + } + + this.notebookEditor.textModel!.pushStackElement('Copy Cells'); } } @@ -913,11 +941,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const focusSinkElement = DOM.append(container, $('.cell-editor-focus-sink')); focusSinkElement.setAttribute('tabindex', '0'); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); - DOM.append(bottomCellContainer, $('.separator')); - const betweenCellToolbar = this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService); - DOM.append(bottomCellContainer, $('.separator')); - const focusIndicatorBottom = DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom')); + const betweenCellToolbar = this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService); const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService)); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 71f4a39cf0c..f1ea22af3f5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -21,7 +21,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; -export class StatefullMarkdownCell extends Disposable { +export class StatefulMarkdownCell extends Disposable { private editor: CodeEditorWidget | null = null; private markdownContainer: HTMLElement; @@ -95,7 +95,7 @@ export class StatefullMarkdownCell extends Disposable { this._register(viewCell.onDidChangeLayout((e) => { const layoutInfo = this.editor?.getLayoutInfo(); - if (e.outerWidth && layoutInfo && layoutInfo.width !== viewCell.layoutInfo.editorWidth) { + if (e.outerWidth && this.viewCell.editState === CellEditState.Editing && layoutInfo && layoutInfo.width !== viewCell.layoutInfo.editorWidth) { this.onCellEditorWidthChange(); } else if (e.totalHeight || e.outerWidth) { this.relayoutCell(); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index e509ca91b1b..d5b0de57ae2 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -8,7 +8,7 @@ import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_MARGIN, CELL_RUN_GUTTER, CELL_STATUSBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; +import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_MARGIN, CELL_RUN_GUTTER, CELL_STATUSBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, BOTTOM_CELL_TOOLBAR_OFFSET } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellViewModel, NotebookLayoutInfo, CodeCellLayoutState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, NotebookCellOutputsSplice, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -122,7 +122,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod const indicatorHeight = editorHeight + CELL_STATUSBAR_HEIGHT + outputTotalHeight; const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN + editorHeight + CELL_STATUSBAR_HEIGHT; - const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT; + const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET; const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; this._layoutInfo = { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index db92613d437..ae2cc967583 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_MARGIN, CELL_STATUSBAR_HEIGHT, EDITOR_TOP_MARGIN, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; +import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_MARGIN, CELL_STATUSBAR_HEIGHT, EDITOR_TOP_MARGIN, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, BOTTOM_CELL_TOOLBAR_OFFSET } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellFindMatch, ICellViewModel, MarkdownCellLayoutChangeEvent, MarkdownCellLayoutInfo, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; @@ -93,13 +93,14 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie layoutChange(state: MarkdownCellLayoutChangeEvent) { // recompute const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo.editorWidth; + const totalHeight = state.totalHeight === undefined ? this._layoutInfo.totalHeight : state.totalHeight; this._layoutInfo = { fontInfo: state.font || this._layoutInfo.fontInfo, editorWidth, editorHeight: this._editorHeight, - bottomToolbarOffset: BOTTOM_CELL_TOOLBAR_HEIGHT, - totalHeight: state.totalHeight === undefined ? this._layoutInfo.totalHeight : state.totalHeight + bottomToolbarOffset: totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET, + totalHeight }; this._onDidChangeLayout.fire(state); @@ -115,6 +116,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie totalHeight: totalHeight, editorHeight: this._editorHeight }; + this.layoutChange({}); } } diff --git a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts index 2bf2293f03c..0d609102629 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -31,7 +31,7 @@ export class InsertCellEdit implements IResourceUndoRedoElement { ) { } - undo(): void | Promise { + undo(): void { if (!this.editingDelegate.deleteCell) { throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); } @@ -41,7 +41,7 @@ export class InsertCellEdit implements IResourceUndoRedoElement { this.editingDelegate.emitSelections(this.beforedSelections); } } - redo(): void | Promise { + redo(): void { if (!this.editingDelegate.insertCell) { throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); } @@ -70,7 +70,7 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { // this._rawCell.source = [cell.getText()]; } - undo(): void | Promise { + undo(): void { if (!this.editingDelegate.insertCell) { throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); } @@ -81,7 +81,7 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { } } - redo(): void | Promise { + redo(): void { if (!this.editingDelegate.deleteCell) { throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); } @@ -95,7 +95,7 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { export class MoveCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - label: string = 'Delete Cell'; + label: string = 'Move Cell'; constructor( public resource: URI, @@ -107,7 +107,7 @@ export class MoveCellEdit implements IResourceUndoRedoElement { ) { } - undo(): void | Promise { + undo(): void { if (!this.editingDelegate.moveCell) { throw new Error('Notebook Move Cell not implemented for Undo/Redo'); } @@ -118,7 +118,7 @@ export class MoveCellEdit implements IResourceUndoRedoElement { } } - redo(): void | Promise { + redo(): void { if (!this.editingDelegate.moveCell) { throw new Error('Notebook Move Cell not implemented for Undo/Redo'); } @@ -142,7 +142,7 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { ) { } - undo(): void | Promise { + undo(): void { if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) { throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo'); } @@ -162,7 +162,7 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { } } - redo(): void | Promise { + redo(): void { if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) { throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo'); } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 4bcb41cab07..702d445cc32 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2, IMainCellDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextSnapshot } from 'vs/editor/common/model'; -import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -66,6 +66,53 @@ export class NotebookTextModelSnapshot implements ITextSnapshot { } +class StackOperation implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource; + + private _operations: IUndoRedoElement[] = []; + + constructor(readonly resource: URI, readonly label: string) { + this.type = UndoRedoElementType.Resource; + } + + pushEditOperation(element: IUndoRedoElement) { + this._operations.push(element); + } + + undo(): void { + this._operations.reverse().forEach(o => o.undo()); + } + redo(): void | Promise { + this._operations.forEach(o => o.redo()); + } +} + +export class NotebookOperationManager { + private _pendingStackOperation: StackOperation | null = null; + constructor(private _undoService: IUndoRedoService, private _resource: URI) { + + } + + pushStackElement(label: string) { + if (this._pendingStackOperation) { + this._undoService.pushElement(this._pendingStackOperation); + this._pendingStackOperation = null; + return; + } + + this._pendingStackOperation = new StackOperation(this._resource, label); + } + + pushEditOperation(element: IUndoRedoElement) { + if (this._pendingStackOperation) { + this._pendingStackOperation.pushEditOperation(element); + return; + } + + this._undoService.pushElement(element); + } +} + export class NotebookTextModel extends Disposable implements INotebookTextModel { private _cellhandlePool: number = 0; @@ -112,6 +159,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel protected readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; + private _operationManager: NotebookOperationManager; + constructor( public handle: number, public viewType: string, @@ -122,6 +171,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel ) { super(); this.cells = []; + + this._operationManager = new NotebookOperationManager(this._undoService, uri); } get isDirty() { @@ -173,6 +224,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._increaseVersionId(); } + pushStackElement(label: string) { + this._operationManager.pushStackElement(label); + } + $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean): boolean { if (modelVersionId !== this._versionId) { return false; @@ -255,7 +310,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return [diff[0], deletedCells, diff[2]] as [number, NotebookCellTextModel[], NotebookCellTextModel[]]; }); - this._undoService.pushElement(new SpliceCellsEdit(this.uri, undoDiff, { + this._operationManager.pushEditOperation(new SpliceCellsEdit(this.uri, undoDiff, { insertCell: this._insertCellDelegate.bind(this), deleteCell: this._deleteCellDelegate.bind(this), emitSelections: this._emitSelectionsDelegate.bind(this) @@ -266,7 +321,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } $handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { - this._undoService.pushElement({ + this._operationManager.pushEditOperation({ type: UndoRedoElementType.Resource, resource: this.uri, label: label ?? nls.localize('defaultEditLabel', "Edit"), @@ -502,7 +557,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const cell = this.createCellTextModel(source, language, type, [], metadata); if (pushUndoStop) { - this._undoService.pushElement(new InsertCellEdit(this.uri, index, cell, { + this._operationManager.pushEditOperation(new InsertCellEdit(this.uri, index, cell, { insertCell: this._insertCellDelegate.bind(this), deleteCell: this._deleteCellDelegate.bind(this), emitSelections: this._emitSelectionsDelegate.bind(this) @@ -522,7 +577,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel insertCell2(index: number, cell: NotebookCellTextModel, synchronous: boolean, pushUndoStop: boolean): void { if (pushUndoStop) { - this._undoService.pushElement(new InsertCellEdit(this.uri, index, cell, { + this._operationManager.pushEditOperation(new InsertCellEdit(this.uri, index, cell, { insertCell: this._insertCellDelegate.bind(this), deleteCell: this._deleteCellDelegate.bind(this), emitSelections: this._emitSelectionsDelegate.bind(this) @@ -536,7 +591,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel deleteCell2(index: number, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { const cell = this.cells[index]; if (pushUndoStop) { - this._undoService.pushElement(new DeleteCellEdit(this.uri, index, cell, { + this._operationManager.pushEditOperation(new DeleteCellEdit(this.uri, index, cell, { insertCell: this._insertCellDelegate.bind(this), deleteCell: this._deleteCellDelegate.bind(this), emitSelections: this._emitSelectionsDelegate.bind(this) @@ -553,7 +608,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel moveCellToIdx2(index: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined): boolean { const cell = this.cells[index]; if (pushedToUndoStack) { - this._undoService.pushElement(new MoveCellEdit(this.uri, index, newIdx, { + this._operationManager.pushEditOperation(new MoveCellEdit(this.uri, index, newIdx, { moveCell: (fromIndex: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined) => { this.moveCellToIdx2(fromIndex, toIndex, true, false, beforeSelections, endSelections); }, diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 63020e5d178..5a5affa30d9 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -63,6 +63,8 @@ export class TestNotebookEditor implements INotebookEditor { ) { } multipleKernelsAvailable: boolean = false; + onDidChangeAvailableKernels: Event = new Emitter().event; + uri?: URI | undefined; textModel?: NotebookTextModel | undefined; @@ -158,6 +160,10 @@ export class TestNotebookEditor implements INotebookEditor { throw new Error('Method not implemented.'); } + moveCellToIdx(cell: ICellViewModel, index: number): Promise { + throw new Error('Method not implemented.'); + } + moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 7732517c737..e757824eb43 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -80,6 +80,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { isWorkspaceFolder, TaskQuickPickEntry, QUICKOPEN_DETAIL_CONFIG, TaskQuickPick, QUICKOPEN_SKIP_CONFIG } from 'vs/workbench/contrib/tasks/browser/taskQuickPick'; import { ILogService } from 'vs/platform/log/common/log'; +import { once } from 'vs/base/common/functional'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const PROBLEM_MATCHER_NEVER_CONFIG = 'task.problemMatchers.neverPrompt'; @@ -223,6 +224,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected _outputChannel: IOutputChannel; protected readonly _onDidStateChange: Emitter; + private _waitForSupportedExecutions: Promise; + private _onDidRegisterSupportedExecutions: Emitter = new Emitter(); constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @@ -331,16 +334,26 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } return task._label; }); - this.setExecutionContexts(); + + this._waitForSupportedExecutions = new Promise(resolve => { + once(this._onDidRegisterSupportedExecutions.event)(() => resolve()); + }); } - protected setExecutionContexts(custom: boolean = true, shell: boolean = true, process: boolean = true): void { - const customContext = CustomExecutionSupportedContext.bindTo(this.contextKeyService); - customContext.set(custom); - const shellContext = ShellExecutionSupportedContext.bindTo(this.contextKeyService); - shellContext.set(shell); - const processContext = ProcessExecutionSupportedContext.bindTo(this.contextKeyService); - processContext.set(process); + public registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean) { + if (custom !== undefined) { + const customContext = CustomExecutionSupportedContext.bindTo(this.contextKeyService); + customContext.set(custom); + } + if (shell !== undefined) { + const shellContext = ShellExecutionSupportedContext.bindTo(this.contextKeyService); + shellContext.set(shell); + } + if (process !== undefined) { + const processContext = ProcessExecutionSupportedContext.bindTo(this.contextKeyService); + processContext.set(process); + } + this._onDidRegisterSupportedExecutions.fire(); } public get onDidStateChange(): Event { @@ -530,9 +543,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } public registerTaskSystem(key: string, info: TaskSystemInfo): void { - if (info.platform === Platform.Platform.Web) { - this.setExecutionContexts(true, false, false); - } this._taskSystemInfos.set(key, info); } @@ -834,7 +844,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?LinkId=733558')); } - public build(): Promise { + public async build(): Promise { return this.getGroupedTasks().then((tasks) => { let runnable = this.createRunnableTask(tasks, TaskGroup.Build); if (!runnable || !runnable.task) { @@ -844,7 +854,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer throw new TaskError(Severity.Info, nls.localize('TaskService.noBuildTask2', 'No build task defined. Mark a task with as a \'build\' group in the tasks.json file.'), TaskErrors.NoBuildTask); } } - return this.executeTask(runnable.task, runnable.resolver); + return this.executeTask(runnable.task, runnable.resolver, TaskRunSource.User); }).then(value => value, (error) => { this.handleError(error); return Promise.reject(error); @@ -861,7 +871,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer throw new TaskError(Severity.Info, nls.localize('TaskService.noTestTask2', 'No test task defined. Mark a task with as a \'test\' group in the tasks.json file.'), TaskErrors.NoTestTask); } } - return this.executeTask(runnable.task, runnable.resolver); + return this.executeTask(runnable.task, runnable.resolver, TaskRunSource.User); }).then(value => value, (error) => { this.handleError(error); return Promise.reject(error); @@ -878,12 +888,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (options && options.attachProblemMatcher && this.shouldAttachProblemMatcher(task) && !InMemoryTask.is(task)) { const toExecute = await this.attachProblemMatcher(task); if (toExecute) { - resolve(this.executeTask(toExecute, resolver)); + resolve(this.executeTask(toExecute, resolver, runSource)); } else { resolve(undefined); } } else { - resolve(this.executeTask(task, resolver)); + resolve(this.executeTask(task, resolver, runSource)); } }).then((value) => { if (runSource === TaskRunSource.User) { @@ -1452,7 +1462,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }; } - private executeTask(task: Task, resolver: ITaskResolver): Promise { + private executeTask(task: Task, resolver: ITaskResolver, runSource: TaskRunSource): Promise { enum SaveBeforeRunConfigOptions { Always = 'always', Never = 'never', @@ -1464,7 +1474,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const execTask = async (task: Task, resolver: ITaskResolver): Promise => { return ProblemMatcherRegistry.onReady().then(() => { let executeResult = this.getTaskSystem().run(task, resolver); - return this.handleExecuteResult(executeResult); + return this.handleExecuteResult(executeResult, runSource); }); }; @@ -1501,7 +1511,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - private async handleExecuteResult(executeResult: ITaskExecuteResult): Promise { + private async handleExecuteResult(executeResult: ITaskExecuteResult, runSource?: TaskRunSource): Promise { if (executeResult.task.taskLoadMessages && executeResult.task.taskLoadMessages.length > 0) { executeResult.task.taskLoadMessages.forEach(loadMessage => { this._outputChannel.append(loadMessage + '\n'); @@ -1509,7 +1519,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.showOutput(); } - await this.setRecentlyUsedTask(executeResult.task); + if (runSource === TaskRunSource.User) { + await this.setRecentlyUsedTask(executeResult.task); + } if (executeResult.kind === TaskExecuteKind.Active) { let active = executeResult.active; if (active && active.same) { @@ -1829,7 +1841,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return result; } - public getWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise> { + public async getWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise> { + await this._waitForSupportedExecutions; if (this._workspaceTasksPromise) { return this._workspaceTasksPromise; } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index e0a6e05bc8e..b0528ffb1b8 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -528,7 +528,7 @@ export class TerminalTaskSystem implements ITaskSystem { } } - private resolveVariablesFromSet(taskSystemInfo: TaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, variables: Set, alreadyResolved: Map): Promise { + private resolveVariablesFromSet(taskSystemInfo: TaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, variables: Set, alreadyResolved: Map): Promise { let isProcess = task.command && task.command.runtime === RuntimeType.Process; let options = task.command && task.command.options ? task.command.options : undefined; let cwd = options ? options.cwd : undefined; @@ -544,7 +544,7 @@ export class TerminalTaskSystem implements ITaskSystem { } } const unresolved = this.findUnresolvedVariables(variables, alreadyResolved); - let resolvedVariables: Promise; + let resolvedVariables: Promise; if (taskSystemInfo && workspaceFolder) { let resolveSet: ResolveSet = { variables: unresolved @@ -560,6 +560,10 @@ export class TerminalTaskSystem implements ITaskSystem { } } resolvedVariables = taskSystemInfo.resolveVariables(workspaceFolder, resolveSet, TaskSourceKind.toConfigurationTarget(task._source.kind)).then(async (resolved) => { + if (!resolved) { + return undefined; + } + this.mergeMaps(alreadyResolved, resolved.variables); resolved.variables = new Map(alreadyResolved); if (isProcess) { @@ -569,14 +573,14 @@ export class TerminalTaskSystem implements ITaskSystem { } resolved.variables.set(TerminalTaskSystem.ProcessVarName, process); } - return Promise.resolve(resolved); + return resolved; }); return resolvedVariables; } else { let variablesArray = new Array(); unresolved.forEach(variable => variablesArray.push(variable)); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.configurationResolverService.resolveWithInteraction(workspaceFolder, variablesArray, 'tasks', undefined, TaskSourceKind.toConfigurationTarget(task._source.kind)).then(async (resolvedVariablesMap: Map | undefined) => { if (resolvedVariablesMap) { this.mergeMaps(alreadyResolved, resolvedVariablesMap); @@ -657,6 +661,9 @@ export class TerminalTaskSystem implements ITaskSystem { if (!hasAllVariables) { return this.resolveVariablesFromSet(lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().workspaceFolder, task, variables, alreadyResolved).then((resolvedVariables) => { + if (!resolvedVariables) { + return { exitCode: 0 }; + } this.currentTask.resolvedVariables = resolvedVariables; return this.executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this.configurationResolverService), workspaceFolder!); }, reason => { diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index e456f8e7b53..7f395bdf770 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -95,6 +95,7 @@ export interface ITaskService { registerTaskProvider(taskProvider: ITaskProvider, type: string): IDisposable; registerTaskSystem(scheme: string, taskSystemInfo: TaskSystemInfo): void; + registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): void; setJsonTasksSupported(areSuppored: Promise): void; extensionCallbackTaskComplete(task: Task, result: number | undefined): Promise; diff --git a/src/vs/workbench/contrib/tasks/common/taskSystem.ts b/src/vs/workbench/contrib/tasks/common/taskSystem.ts index 56bc04b325c..7f5b1758e18 100644 --- a/src/vs/workbench/contrib/tasks/common/taskSystem.ts +++ b/src/vs/workbench/contrib/tasks/common/taskSystem.ts @@ -119,7 +119,7 @@ export interface TaskSystemInfo { platform: Platform; context: any; uriProvider: (this: void, path: string) => URI; - resolveVariables(workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise; + resolveVariables(workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise; getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }>; findExecutable(command: string, cwd?: string, paths?: string[]): Promise; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 4b74eba639f..2bda3f9a028 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -840,6 +840,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { setTimeout(() => this.layout(this._timeoutDimension!), 0); } } + if (!visible) { + this._widgetManager.hideHovers(); + } } public scrollDownLine(): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 98eca949588..55d0d5e1865 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -109,6 +109,8 @@ export class TerminalViewPane extends ViewPane { } else { this.layoutBody(this._bodyDimensions.height, this._bodyDimensions.width); } + } else { + this._terminalService.getActiveTab()?.setVisible(false); } })); diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts b/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts index 032610dbea7..b5bf843ffc7 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts @@ -5,11 +5,15 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { ITerminalWidget } from 'vs/workbench/contrib/terminal/browser/widgets/widgets'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; export class TerminalWidgetManager implements IDisposable { private _container: HTMLElement | undefined; private _attached: Map = new Map(); + constructor(@IHoverService private readonly _hoverService: IHoverService) { + } + attachToElement(terminalWrapper: HTMLElement) { if (!this._container) { this._container = document.createElement('div'); @@ -25,6 +29,10 @@ export class TerminalWidgetManager implements IDisposable { } } + hideHovers(): void { + this._hoverService.hideHover(); + } + attachWidget(widget: ITerminalWidget): IDisposable | undefined { if (!this._container) { return; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataManualSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts similarity index 97% rename from src/vs/workbench/contrib/userDataSync/browser/userDataManualSyncView.ts rename to src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts index e1ed5d7309c..92156df07fa 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataManualSyncView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts @@ -16,7 +16,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, dispose } from 'vs/base/common/lifecycle'; import { Codicon } from 'vs/base/common/codicons'; -import { IUserDataSyncWorkbenchService, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, MANUAL_SYNC_VIEW_ID } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { IUserDataSyncWorkbenchService, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, SYNC_MERGES_VIEW_ID } from 'vs/workbench/services/userDataSync/common/userDataSync'; import { isEqual, basename } from 'vs/base/common/resources'; import { IDecorationsProvider, IDecorationData, IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; import { IProgressService } from 'vs/platform/progress/common/progress'; @@ -39,7 +39,7 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { Severity } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -export class UserDataManualSyncViewPane extends TreeViewPane { +export class UserDataSyncMergesViewPane extends TreeViewPane { private userDataSyncPreview: IUserDataSyncPreview; @@ -83,7 +83,7 @@ export class UserDataManualSyncViewPane extends TreeViewPane { this.createButtons(container); const that = this; - this.treeView.message = localize('explanation', "Please go through each entry and accept the change to enable sync."); + this.treeView.message = localize('explanation', "Please go through each entry and merge to enable sync."); this.treeView.dataProvider = { getChildren() { return that.getTreeItems(); } }; } @@ -164,7 +164,7 @@ export class UserDataManualSyncViewPane extends TreeViewPane { icon: Codicon.cloudDownload, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', MANUAL_SYNC_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), group: 'inline', order: 1, }, @@ -184,7 +184,7 @@ export class UserDataManualSyncViewPane extends TreeViewPane { icon: Codicon.cloudUpload, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', MANUAL_SYNC_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), group: 'inline', order: 2, }, @@ -204,7 +204,7 @@ export class UserDataManualSyncViewPane extends TreeViewPane { icon: Codicon.merge, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', MANUAL_SYNC_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), group: 'inline', order: 3, }, @@ -224,7 +224,7 @@ export class UserDataManualSyncViewPane extends TreeViewPane { icon: Codicon.discard, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', MANUAL_SYNC_VIEW_ID), ContextKeyExpr.or(ContextKeyExpr.equals('viewItem', 'sync-resource-accepted'), ContextKeyExpr.equals('viewItem', 'sync-resource-conflict'))), + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.or(ContextKeyExpr.equals('viewItem', 'sync-resource-accepted'), ContextKeyExpr.equals('viewItem', 'sync-resource-conflict'))), group: 'inline', order: 3, }, @@ -373,7 +373,7 @@ export class UserDataManualSyncViewPane extends TreeViewPane { } private withProgress(task: () => Promise): Promise { - return this.progressService.withProgress({ location: MANUAL_SYNC_VIEW_ID, delay: 500 }, task); + return this.progressService.withProgress({ location: SYNC_MERGES_VIEW_ID, delay: 500 }, task); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 8fcd161c6eb..bda1953893c 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -30,13 +30,13 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IAction, Action } from 'vs/base/common/actions'; -import { IUserDataSyncWorkbenchService, CONTEXT_SYNC_STATE, getSyncAreaLabel, CONTEXT_ACCOUNT_STATE, AccountStatus, CONTEXT_ENABLE_ACTIVITY_VIEWS, SHOW_SYNC_LOG_COMMAND_ID, CONFIGURE_SYNC_COMMAND_ID, MANUAL_SYNC_VIEW_ID, CONTEXT_ENABLE_MANUAL_SYNC_VIEW } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { IUserDataSyncWorkbenchService, CONTEXT_SYNC_STATE, getSyncAreaLabel, CONTEXT_ACCOUNT_STATE, AccountStatus, CONTEXT_ENABLE_ACTIVITY_VIEWS, SHOW_SYNC_LOG_COMMAND_ID, CONFIGURE_SYNC_COMMAND_ID, SYNC_MERGES_VIEW_ID, CONTEXT_ENABLE_SYNC_MERGES_VIEW } from 'vs/workbench/services/userDataSync/common/userDataSync'; import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { TreeView } from 'vs/workbench/contrib/views/browser/treeView'; import { flatten } from 'vs/base/common/arrays'; -import { UserDataManualSyncViewPane } from 'vs/workbench/contrib/userDataSync/browser/userDataManualSyncView'; +import { UserDataSyncMergesViewPane } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView'; export class UserDataSyncViewPaneContainer extends ViewPaneContainer { @@ -86,7 +86,7 @@ export class UserDataSyncDataViews extends Disposable { } private registerViews(container: ViewContainer): void { - this.registerManualSyncView(container); + this.registerMergesView(container); this.registerActivityView(container, true); this.registerMachinesView(container); @@ -94,17 +94,17 @@ export class UserDataSyncDataViews extends Disposable { this.registerActivityView(container, false); } - private registerManualSyncView(container: ViewContainer): void { + private registerMergesView(container: ViewContainer): void { const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - const viewName = localize('manual sync', "Manual Sync"); + const viewName = localize('merges', "Merges"); viewsRegistry.registerViews([{ - id: MANUAL_SYNC_VIEW_ID, + id: SYNC_MERGES_VIEW_ID, name: viewName, - ctorDescriptor: new SyncDescriptor(UserDataManualSyncViewPane), - when: CONTEXT_ENABLE_MANUAL_SYNC_VIEW, + ctorDescriptor: new SyncDescriptor(UserDataSyncMergesViewPane), + when: CONTEXT_ENABLE_SYNC_MERGES_VIEW, canToggleVisibility: false, canMoveView: false, - treeView: this.instantiationService.createInstance(TreeView, MANUAL_SYNC_VIEW_ID, viewName), + treeView: this.instantiationService.createInstance(TreeView, SYNC_MERGES_VIEW_ID, viewName), collapsed: false, order: 100, }], container); diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index d862050cfc3..a83f6ce3abf 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -527,12 +527,13 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private registerConfigurationSchemas(): void { if (this.workspace) { const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); + const defaultSettingsSchema: IJSONSchema = { additionalProperties: true, allowTrailingCommas: true, allowComments: true }; const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; const userSettingsSchema: IJSONSchema = this.remoteUserConfiguration ? { properties: { ...applicationSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true } : allSettingsSchema; const machineSettingsSchema: IJSONSchema = { properties: { ...machineSettings.properties, ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; const workspaceSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - jsonRegistry.registerSchema(defaultSettingsSchemaId, allSettingsSchema); + jsonRegistry.registerSchema(defaultSettingsSchemaId, defaultSettingsSchema); jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema); jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); diff --git a/src/vs/workbench/services/editor/common/editorOpenWith.ts b/src/vs/workbench/services/editor/common/editorOpenWith.ts index af8462e5f42..a824ac2e408 100644 --- a/src/vs/workbench/services/editor/common/editorOpenWith.ts +++ b/src/vs/workbench/services/editor/common/editorOpenWith.ts @@ -49,7 +49,12 @@ export async function openEditorWith( return; } - const overrideToUse = typeof id === 'string' && allEditorOverrides.find(([_, entry]) => entry.id === id); + let overrideToUse; + if (typeof id === 'string') { + overrideToUse = allEditorOverrides.find(([_, entry]) => entry.id === id); + } else if (allEditorOverrides.length === 1) { + overrideToUse = allEditorOverrides[0]; + } if (overrideToUse) { return overrideToUse[0].open(input, overrideOptions, group, OpenEditorContext.NEW_EDITOR)?.override; } diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 9d05e123c8c..0e26dd0f668 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -184,6 +184,7 @@ export class ExtensionHostManager extends Disposable { getProxy: (identifier: ProxyIdentifier): T => this._rpcProtocol!.getProxy(identifier), set: (identifier: ProxyIdentifier, instance: R): R => this._rpcProtocol!.set(identifier, instance), assertRegistered: (identifiers: ProxyIdentifier[]): void => this._rpcProtocol!.assertRegistered(identifiers), + drain: (): Promise => this._rpcProtocol!.drain(), }; // Named customers diff --git a/src/vs/workbench/services/extensions/common/proxyIdentifier.ts b/src/vs/workbench/services/extensions/common/proxyIdentifier.ts index e0e9999a62f..edadcae9eb2 100644 --- a/src/vs/workbench/services/extensions/common/proxyIdentifier.ts +++ b/src/vs/workbench/services/extensions/common/proxyIdentifier.ts @@ -18,6 +18,11 @@ export interface IRPCProtocol { * Assert these identifiers are already registered via `.set`. */ assertRegistered(identifiers: ProxyIdentifier[]): void; + + /** + * Wait for the write buffer (if applicable) to become empty. + */ + drain(): Promise; } export class ProxyIdentifier { diff --git a/src/vs/workbench/services/extensions/common/rpcProtocol.ts b/src/vs/workbench/services/extensions/common/rpcProtocol.ts index 022fd1f4c41..199ea6e15ef 100644 --- a/src/vs/workbench/services/extensions/common/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/common/rpcProtocol.ts @@ -115,6 +115,13 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { }); } + public drain(): Promise { + if (typeof this._protocol.drain === 'function') { + return this._protocol.drain(); + } + return Promise.resolve(); + } + private _onWillSendRequest(req: number): void { if (this._unacknowledgedCount === 0) { // Since this is the first request we are sending in a while, diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts index 05b5fd012d2..49542eda74c 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -96,10 +96,10 @@ let onTerminate = function () { nativeExit(); }; -function _createExtHostProtocol(): Promise { +function _createExtHostProtocol(): Promise { if (process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { let protocol: PersistentProtocol | null = null; @@ -163,7 +163,7 @@ function _createExtHostProtocol(): Promise { const pipeName = process.env.VSCODE_IPC_HOOK_EXTHOST!; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const socket = net.createConnection(pipeName, () => { socket.removeListener('error', reject); @@ -203,6 +203,10 @@ async function createExtHostProtocol(): Promise { protocol.send(msg); } } + + drain(): Promise { + return protocol.drain(); + } }; } diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 6c446ffc535..c5d3872e797 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -6,7 +6,7 @@ import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask } from 'vs/platform/userDataSync/common/userDataSync'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, CONTEXT_ENABLE_MANUAL_SYNC_VIEW, MANUAL_SYNC_VIEW_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, CONTEXT_ENABLE_SYNC_MERGES_VIEW, SYNC_MERGES_VIEW_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID } from 'vs/workbench/services/userDataSync/common/userDataSync'; import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; @@ -79,7 +79,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat private readonly syncEnablementContext: IContextKey; private readonly syncStatusContext: IContextKey; private readonly accountStatusContext: IContextKey; - private readonly manualSyncViewEnablementContext: IContextKey; + private readonly mergesViewEnablementContext: IContextKey; private readonly activityViewsEnablementContext: IContextKey; readonly userDataSyncPreview: UserDataSyncPreview = this._register(new UserDataSyncPreview(this.userDataSyncService)); @@ -110,7 +110,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService); this.activityViewsEnablementContext = CONTEXT_ENABLE_ACTIVITY_VIEWS.bindTo(contextKeyService); - this.manualSyncViewEnablementContext = CONTEXT_ENABLE_MANUAL_SYNC_VIEW.bindTo(contextKeyService); + this.mergesViewEnablementContext = CONTEXT_ENABLE_SYNC_MERGES_VIEW.bindTo(contextKeyService); if (this.authenticationProviders.length) { @@ -302,16 +302,16 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat const result = await this.dialogService.show( Severity.Info, - localize('preferences sync', "Preferences Sync"), + localize('merge or replace', "Merge or Replace"), [ localize('merge', "Merge"), localize('replace local', "Replace Local"), - localize('sync manually', "Sync Manually..."), + localize('merge Manually', "Merge Manually..."), localize('cancel', "Cancel"), ], { cancelId: 3, - detail: localize('first time sync detail', "It looks like you last synced from another machine.\nWould you like to replace or merge with your data in the cloud or sync manually?"), + detail: localize('first time sync detail', "It looks like you last synced from another machine.\nWould you like to merge or replace with your data in the cloud?"), } ); switch (result.choice) { @@ -333,18 +333,18 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat const visibleViewContainer = this.viewsService.getVisibleViewContainer(ViewContainerLocation.Sidebar); this.userDataSyncPreview.setManualSyncPreview(task, preview); - this.manualSyncViewEnablementContext.set(true); + this.mergesViewEnablementContext.set(true); await this.waitForActiveSyncViews(); - await this.viewsService.openView(MANUAL_SYNC_VIEW_ID); + await this.viewsService.openView(SYNC_MERGES_VIEW_ID); const error = await Event.toPromise(this.userDataSyncPreview.onDidCompleteManualSync); this.userDataSyncPreview.unsetManualSyncPreview(); - this.manualSyncViewEnablementContext.set(false); + this.mergesViewEnablementContext.set(false); if (visibleViewContainer) { this.viewsService.openViewContainer(visibleViewContainer.id); } else { - const viewContainer = this.viewDescriptorService.getViewContainerByViewId(MANUAL_SYNC_VIEW_ID); + const viewContainer = this.viewDescriptorService.getViewContainerByViewId(SYNC_MERGES_VIEW_ID); this.viewsService.closeViewContainer(viewContainer!.id); } diff --git a/src/vs/workbench/services/userDataSync/common/userDataSync.ts b/src/vs/workbench/services/userDataSync/common/userDataSync.ts index f5d0e9b2d49..963831be05f 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSync.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSync.ts @@ -82,7 +82,7 @@ export const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncSt export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey('syncEnabled', false); export const CONTEXT_ACCOUNT_STATE = new RawContextKey('userDataSyncAccountStatus', AccountStatus.Uninitialized); export const CONTEXT_ENABLE_ACTIVITY_VIEWS = new RawContextKey(`enableSyncActivityViews`, false); -export const CONTEXT_ENABLE_MANUAL_SYNC_VIEW = new RawContextKey(`enableManualSyncView`, false); +export const CONTEXT_ENABLE_SYNC_MERGES_VIEW = new RawContextKey(`enableSyncMergesView`, false); // Commands export const CONFIGURE_SYNC_COMMAND_ID = 'workbench.userDataSync.actions.configure'; @@ -90,4 +90,4 @@ export const SHOW_SYNC_LOG_COMMAND_ID = 'workbench.userDataSync.actions.showLog' // VIEWS export const SYNC_VIEW_CONTAINER_ID = 'workbench.view.sync'; -export const MANUAL_SYNC_VIEW_ID = 'workbench.views.manualSyncView'; +export const SYNC_MERGES_VIEW_ID = 'workbench.views.sync.merges'; diff --git a/src/vs/workbench/test/browser/api/extHostDiagnostics.test.ts b/src/vs/workbench/test/browser/api/extHostDiagnostics.test.ts index 1bb7bafe7ca..db7351cfcb7 100644 --- a/src/vs/workbench/test/browser/api/extHostDiagnostics.test.ts +++ b/src/vs/workbench/test/browser/api/extHostDiagnostics.test.ts @@ -389,6 +389,9 @@ suite('ExtHostDiagnostics', () => { assertRegistered(): void { } + drain() { + return undefined!; + } }, new NullLogService()); let collection1 = diags.createDiagnosticCollection(nullExtensionDescription.identifier, 'foo'); @@ -438,6 +441,9 @@ suite('ExtHostDiagnostics', () => { assertRegistered(): void { } + drain() { + return undefined!; + } }, new NullLogService()); diff --git a/src/vs/workbench/test/browser/api/extHostFileSystemEventService.test.ts b/src/vs/workbench/test/browser/api/extHostFileSystemEventService.test.ts index c20cee41ce1..0d26033abf7 100644 --- a/src/vs/workbench/test/browser/api/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/test/browser/api/extHostFileSystemEventService.test.ts @@ -15,7 +15,8 @@ suite('ExtHostFileSystemEventService', () => { const protocol: IMainContext = { getProxy: () => { return undefined!; }, set: undefined!, - assertRegistered: undefined! + assertRegistered: undefined!, + drain: undefined! }; const watcher1 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher('**/somethingInteresting', false, false, false); diff --git a/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts b/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts index 6cbc2c7a930..7e9732b500a 100644 --- a/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts @@ -298,7 +298,8 @@ suite('ExtHostWorkspace', function () { const protocol: IMainContext = { getProxy: () => { return undefined!; }, set: () => { return undefined!; }, - assertRegistered: () => { } + assertRegistered: () => { }, + drain: () => { return undefined!; }, }; const ws = createExtHostWorkspace(protocol, { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); diff --git a/src/vs/workbench/test/browser/api/mainThreadDiagnostics.test.ts b/src/vs/workbench/test/browser/api/mainThreadDiagnostics.test.ts index 410665cd79b..3f10f0cffd6 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDiagnostics.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDiagnostics.test.ts @@ -32,6 +32,7 @@ suite('MainThreadDiagnostics', function () { $acceptMarkersChange() { } }; } + drain(): any { return null; } }, markerService, new class extends mock() { diff --git a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts index 936a51b094f..e88fcd90b6d 100644 --- a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts @@ -66,6 +66,7 @@ suite('MainThreadHostTreeView', function () { getProxy(): any { return extHostTreeViewsShape; } + drain(): any { return null; } }, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService()); mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false }); await testExtensionService.whenInstalledExtensionsRegistered(); diff --git a/src/vs/workbench/test/browser/api/testRPCProtocol.ts b/src/vs/workbench/test/browser/api/testRPCProtocol.ts index 16673942bd6..d2d2b1c504f 100644 --- a/src/vs/workbench/test/browser/api/testRPCProtocol.ts +++ b/src/vs/workbench/test/browser/api/testRPCProtocol.ts @@ -19,7 +19,8 @@ export function SingleProxyRPCProtocol(thing: any): IExtHostContext & IExtHostRp set(identifier: ProxyIdentifier, value: R): R { return value; }, - assertRegistered: undefined! + assertRegistered: undefined!, + drain: undefined! }; } @@ -40,6 +41,10 @@ export class TestRPCProtocol implements IExtHostContext, IExtHostRpcService { this._proxies = Object.create(null); } + drain(): Promise { + return Promise.resolve(); + } + private get _callCount(): number { return this._callCountValue; } diff --git a/yarn.lock b/yarn.lock index 543779f3d52..4e23e2cd735 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9348,10 +9348,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.0.0-dev.20200715: - version "4.0.0-dev.20200715" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200715.tgz#d65961a5a6f13fde95a6f4db5f5946f15e4c59bc" - integrity sha512-gmPXoWktfXeutmWTM6el9U4vIn5kqOHGI1OESSOhPtLWrxodKqLfFuMygQtOUTtGjKLFQRFAJhHEwUhHZNOURA== +typescript@^4.0.0-dev.20200722: + version "4.0.0-dev.20200722" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200722.tgz#b59dd5a3cd84a98d5aae0e4f3a3c58f0c81a3b9b" + integrity sha512-MmJ1YyPNK3JYeKLiTg5sQXdeZaMgt99Fg4BMRZhJmhoq1/x2V1cpXMYvE1rtIYl9K7NvmTDdU3WDW7ZOD6ybaw== uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3"