diff --git a/.vscode/settings.json b/.vscode/settings.json index e0fd4e2342..d56f75c41d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,6 +22,8 @@ "i18n/**": true, "extensions/**/out/**": true, "test/smoke/out/**": true, + "test/automation/out/**": true, + "test/integration/browser/out/**": true, "src/vs/base/test/node/uri.test.data.txt": true }, "lcov.path": [ diff --git a/build/azure-pipelines/darwin/entitlements.plist b/build/azure-pipelines/darwin/entitlements.plist index 46f6756611..be8b7163da 100644 --- a/build/azure-pipelines/darwin/entitlements.plist +++ b/build/azure-pipelines/darwin/entitlements.plist @@ -8,5 +8,7 @@ com.apple.security.cs.disable-library-validation + com.apple.security.cs.allow-dyld-environment-variables + diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index bb75d8570d..6be05972f7 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -120,17 +120,19 @@ steps: - script: | set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ - ./resources/server/test/test-web-integration.sh --browser webkit - displayName: Run integration tests (Browser) + APP_ROOT=$(agent.builddirectory)/VSCode-darwin + APP_NAME="`ls $APP_ROOT | head -n 1`" + INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin" \ + ./resources/server/test/test-remote-integration.sh + displayName: Run remote integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ - yarn smoketest --web --headless --browser webkit - continueOnError: true - displayName: Run smoke tests (Browser) + ./resources/server/test/test-web-integration.sh --browser webkit + displayName: Run integration tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | diff --git a/build/azure-pipelines/darwin/publish.sh b/build/azure-pipelines/darwin/publish.sh index 58f110c5df..f0375d2a3c 100755 --- a/build/azure-pipelines/darwin/publish.sh +++ b/build/azure-pipelines/darwin/publish.sh @@ -19,7 +19,9 @@ node build/azure-pipelines/common/createAsset.js \ ../vscode-server-darwin.zip # publish hockeyapp symbols -node build/azure-pipelines/common/symbols.js "$VSCODE_MIXIN_PASSWORD" "$VSCODE_HOCKEYAPP_TOKEN" x64 "$VSCODE_HOCKEYAPP_ID_MACOS" +# node build/azure-pipelines/common/symbols.js "$VSCODE_MIXIN_PASSWORD" "$VSCODE_HOCKEYAPP_TOKEN" x64 "$VSCODE_HOCKEYAPP_ID_MACOS" +# Skip hockey app because build failure. +# https://github.com/microsoft/vscode/issues/90491 # upload configuration yarn gulp upload-vscode-configuration diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 7cd8e73039..cbe3bf051e 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -123,20 +123,21 @@ steps: displayName: Run integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) -# Fails due to weird error: Protocol error (Target.getBrowserContexts): Target closed. -# - script: | -# set -e -# VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-x64" \ -# DISPLAY=:10 ./resources/server/test/test-web-integration.sh --browser chromium -# displayName: Run integration tests (Browser) -# condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-linux-x64 + APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") + INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-x64" \ + DISPLAY=:10 ./resources/server/test/test-remote-integration.sh + displayName: Run remote integration tests (Electron) + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-x64" \ - yarn smoketest --web --headless --browser firefox - continueOnError: true - displayName: Run smoke tests (Firefox) + DISPLAY=:10 ./resources/server/test/test-web-integration.sh --browser chromium + displayName: Run integration tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | diff --git a/build/azure-pipelines/linux/publish.sh b/build/azure-pipelines/linux/publish.sh index 3da6ea3eed..b168ab9cf7 100755 --- a/build/azure-pipelines/linux/publish.sh +++ b/build/azure-pipelines/linux/publish.sh @@ -28,7 +28,9 @@ rm -rf $ROOT/vscode-server-*.tar.* node build/azure-pipelines/common/createAsset.js "server-$PLATFORM_LINUX" archive-unsigned "$SERVER_TARBALL_FILENAME" "$SERVER_TARBALL_PATH" # Publish hockeyapp symbols -node build/azure-pipelines/common/symbols.js "$VSCODE_MIXIN_PASSWORD" "$VSCODE_HOCKEYAPP_TOKEN" "x64" "$VSCODE_HOCKEYAPP_ID_LINUX64" +# node build/azure-pipelines/common/symbols.js "$VSCODE_MIXIN_PASSWORD" "$VSCODE_HOCKEYAPP_TOKEN" "x64" "$VSCODE_HOCKEYAPP_ID_LINUX64" +# Skip hockey app because build failure. +# https://github.com/microsoft/vscode/issues/90491 # Publish DEB PLATFORM_DEB="linux-deb-x64" diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 796259ab98..7a8a12aa28 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -135,16 +135,18 @@ steps: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-web-integration.bat --browser chromium } - displayName: Run integration tests (Browser) + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json + $AppNameShort = $AppProductJson.nameShort + exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-remote-integration.bat } + displayName: Run remote integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; yarn smoketest --web --headless --browser chromium } - continueOnError: true - displayName: Run smoke tests (Browser) + exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-web-integration.bat --browser firefox } + displayName: Run integration tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 diff --git a/build/azure-pipelines/win32/publish.ps1 b/build/azure-pipelines/win32/publish.ps1 index 5a22d4749c..c9ff9d09ec 100644 --- a/build/azure-pipelines/win32/publish.ps1 +++ b/build/azure-pipelines/win32/publish.ps1 @@ -11,13 +11,12 @@ $SystemExe = "$Repo\.build\win32-$Arch\system-setup\VSCodeSetup.exe" $UserExe = "$Repo\.build\win32-$Arch\user-setup\VSCodeSetup.exe" $Zip = "$Repo\.build\win32-$Arch\archive\VSCode-win32-$Arch.zip" $LegacyServer = "$Root\vscode-reh-win32-$Arch" -$ServerName = "vscode-server-win32-$Arch" -$Server = "$Root\$ServerName" +$Server = "$Root\vscode-server-win32-$Arch" $ServerZip = "$Repo\.build\vscode-server-win32-$Arch.zip" $Build = "$Root\VSCode-win32-$Arch" # Create server archive -exec { Rename-Item -Path $LegacyServer -NewName $ServerName } +exec { xcopy $LegacyServer $Server /H /E /I } exec { .\node_modules\7zip\7zip-lite\7z.exe a -tzip $ServerZip $Server -r } # get version @@ -31,6 +30,8 @@ exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform" setup " exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform-user" setup "VSCodeUserSetup-$Arch-$Version.exe" $UserExe } exec { node build/azure-pipelines/common/createAsset.js "server-$AssetPlatform" archive "vscode-server-win32-$Arch.zip" $ServerZip } +# Skip hockey app because build failure. +# https://github.com/microsoft/vscode/issues/90491 # publish hockeyapp symbols -$hockeyAppId = if ("$Arch" -eq "ia32") { "$env:VSCODE_HOCKEYAPP_ID_WIN32" } else { "$env:VSCODE_HOCKEYAPP_ID_WIN64" } -exec { node build/azure-pipelines/common/symbols.js "$env:VSCODE_MIXIN_PASSWORD" "$env:VSCODE_HOCKEYAPP_TOKEN" "$Arch" $hockeyAppId } +# $hockeyAppId = if ("$Arch" -eq "ia32") { "$env:VSCODE_HOCKEYAPP_ID_WIN32" } else { "$env:VSCODE_HOCKEYAPP_ID_WIN64" } +# exec { node build/azure-pipelines/common/symbols.js "$env:VSCODE_MIXIN_PASSWORD" "$env:VSCODE_HOCKEYAPP_TOKEN" "$Arch" $hockeyAppId } diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 31446bfe4d..fe693b695b 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -110,6 +110,10 @@ "name": "vs/workbench/contrib/output", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/openInDesktop", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/performance", "project": "vscode-workbench" diff --git a/extensions/azurecore/src/apiWrapper.ts b/extensions/azurecore/src/apiWrapper.ts index 750a43f7f8..303a3e5611 100644 --- a/extensions/azurecore/src/apiWrapper.ts +++ b/extensions/azurecore/src/apiWrapper.ts @@ -93,7 +93,7 @@ export class ApiWrapper { return vscode.workspace.asRelativePath(uri); } - public getWorkspaceFolders(): vscode.WorkspaceFolder[] { + public getWorkspaceFolders(): readonly vscode.WorkspaceFolder[] { return vscode.workspace.workspaceFolders; } @@ -178,7 +178,7 @@ export class ApiWrapper { return vscode.window.createStatusBarItem(alignment, priority); } - public get workspaceFolders(): vscode.WorkspaceFolder[] { + public get workspaceFolders(): readonly vscode.WorkspaceFolder[] { return vscode.workspace.workspaceFolders; } diff --git a/extensions/cms/src/apiWrapper.ts b/extensions/cms/src/apiWrapper.ts index 9294dd12d8..87a07139bd 100644 --- a/extensions/cms/src/apiWrapper.ts +++ b/extensions/cms/src/apiWrapper.ts @@ -65,7 +65,7 @@ export class ApiWrapper { return vscode.workspace.asRelativePath(uri); } - public getWorkspaceFolders(): vscode.WorkspaceFolder[] { + public getWorkspaceFolders(): readonly vscode.WorkspaceFolder[] { return vscode.workspace.workspaceFolders; } @@ -144,7 +144,7 @@ export class ApiWrapper { return vscode.window.createStatusBarItem(alignment, priority); } - public get workspaceFolders(): vscode.WorkspaceFolder[] { + public get workspaceFolders(): readonly vscode.WorkspaceFolder[] { return vscode.workspace.workspaceFolders; } diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index 362799fe39..918b7b2e04 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -86,7 +86,7 @@ export class SettingsDocument { })); } else { // Value - return this.provideLanguageCompletionItems(location, range); + return this.provideLanguageCompletionItemsForLanguageOverrides(location, range); } } @@ -158,6 +158,11 @@ export class SettingsDocument { } private provideLanguageCompletionItems(_location: Location, range: vscode.Range, formatFunc: (string: string) => string = (l) => JSON.stringify(l)): Thenable { + return vscode.languages.getLanguages() + .then(languages => languages.map(l => this.newSimpleCompletionItem(formatFunc(l), range))); + } + + private provideLanguageCompletionItemsForLanguageOverrides(_location: Location, range: vscode.Range, formatFunc: (string: string) => string = (l) => JSON.stringify(l)): Thenable { return vscode.languages.getLanguages().then(languages => { const completionItems = []; const configuration = vscode.workspace.getConfiguration(); @@ -182,7 +187,7 @@ export class SettingsDocument { let text = this.document.getText(range); if (text && text.trim().startsWith('[')) { range = new vscode.Range(new vscode.Position(range.start.line, range.start.character + text.indexOf('[')), range.end); - return this.provideLanguageCompletionItems(location, range, language => `"[${language}]"`); + return this.provideLanguageCompletionItemsForLanguageOverrides(location, range, language => `"[${language}]"`); } range = this.document.getWordRangeAtPosition(position) || new vscode.Range(position, position); @@ -209,7 +214,7 @@ export class SettingsDocument { // Suggestion model word matching includes closed sqaure bracket and ending quote // Hence include them in the proposal to replace let range = this.document.getWordRangeAtPosition(position) || new vscode.Range(position, position); - return this.provideLanguageCompletionItems(location, range, language => `"[${language}]"`); + return this.provideLanguageCompletionItemsForLanguageOverrides(location, range, language => `"[${language}]"`); } return Promise.resolve([]); } diff --git a/extensions/mssql/src/apiWrapper.ts b/extensions/mssql/src/apiWrapper.ts index 08c2d090c0..5193cd1078 100644 --- a/extensions/mssql/src/apiWrapper.ts +++ b/extensions/mssql/src/apiWrapper.ts @@ -98,7 +98,7 @@ export class ApiWrapper { return vscode.window.showTextDocument(document, options); } - public get workspaceFolders(): vscode.WorkspaceFolder[] { + public get workspaceFolders(): readonly vscode.WorkspaceFolder[] { return vscode.workspace.workspaceFolders; } diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts index cde65112d2..1b5e3f21dd 100644 --- a/extensions/notebook/src/extension.ts +++ b/extensions/notebook/src/extension.ts @@ -104,7 +104,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi return undefined; } - let workspaceFolders = vscode.workspace.workspaceFolders || []; + let workspaceFolders = vscode.workspace.workspaceFolders?.slice() ?? []; const bookTreeViewProvider = new BookTreeViewProvider(workspaceFolders, extensionContext, false, BOOKS_VIEWID); await bookTreeViewProvider.initialized; const untitledBookTreeViewProvider = new BookTreeViewProvider([], extensionContext, true, READONLY_BOOKS_VIEWID); diff --git a/extensions/search-result/syntaxes/generateTMLanguage.js b/extensions/search-result/syntaxes/generateTMLanguage.js index ce2acc34ff..7a9eb9204f 100644 --- a/extensions/search-result/syntaxes/generateTMLanguage.js +++ b/extensions/search-result/syntaxes/generateTMLanguage.js @@ -1,6 +1,4 @@ // @ts-check -// todo@jackson -/* eslint code-no-unexternalized-strings: 0 */ const mappings = [ ['bat', 'source.batchfile'], @@ -40,6 +38,7 @@ const mappings = [ ['perl', 'source.perl'], ['php', 'source.php'], ['pl', 'source.perl'], + ['pm', 'source.perl'], ['ps1', 'source.powershell'], ['pug', 'text.pug'], ['py', 'source.python'], @@ -104,43 +103,43 @@ mappings.forEach(([ext, scope, regexp]) => repository[ext] = { name: scopes.resultBlock.meta, begin: `^(?!\\s)(.*?)([^\\\\\\/\\n]*${regexp || `\\.${ext}`})(:)$`, - end: "^(?!\\s)", + end: '^(?!\\s)', beginCaptures: { - "0": { name: scopes.resultBlock.path.meta }, - "1": { name: scopes.resultBlock.path.dirname }, - "2": { name: scopes.resultBlock.path.basename }, - "3": { name: scopes.resultBlock.path.colon }, + '0': { name: scopes.resultBlock.path.meta }, + '1': { name: scopes.resultBlock.path.dirname }, + '2': { name: scopes.resultBlock.path.basename }, + '3': { name: scopes.resultBlock.path.colon }, }, patterns: [ { name: [scopes.resultBlock.result.meta, scopes.resultBlock.result.metaMultiLine].join(' '), - begin: "^ ((\\d+) )", - while: "^ (?:((\\d+)(:))|((\\d+) ))", + begin: '^ ((\\d+) )', + while: '^ (?:((\\d+)(:))|((\\d+) ))', beginCaptures: { - "0": { name: scopes.resultBlock.result.prefix.meta }, - "1": { name: scopes.resultBlock.result.prefix.metaContext }, - "2": { name: scopes.resultBlock.result.prefix.lineNumber }, + '0': { name: scopes.resultBlock.result.prefix.meta }, + '1': { name: scopes.resultBlock.result.prefix.metaContext }, + '2': { name: scopes.resultBlock.result.prefix.lineNumber }, }, whileCaptures: { - "0": { name: scopes.resultBlock.result.prefix.meta }, - "1": { name: scopes.resultBlock.result.prefix.metaMatch }, - "2": { name: scopes.resultBlock.result.prefix.lineNumber }, - "3": { name: scopes.resultBlock.result.prefix.colon }, + '0': { name: scopes.resultBlock.result.prefix.meta }, + '1': { name: scopes.resultBlock.result.prefix.metaMatch }, + '2': { name: scopes.resultBlock.result.prefix.lineNumber }, + '3': { name: scopes.resultBlock.result.prefix.colon }, - "4": { name: scopes.resultBlock.result.prefix.metaContext }, - "5": { name: scopes.resultBlock.result.prefix.lineNumber }, + '4': { name: scopes.resultBlock.result.prefix.metaContext }, + '5': { name: scopes.resultBlock.result.prefix.lineNumber }, }, patterns: [{ include: scope }] }, { - begin: "^ ((\\d+)(:))", - while: "(?=not)possible", + begin: '^ ((\\d+)(:))', + while: '(?=not)possible', name: [scopes.resultBlock.result.meta, scopes.resultBlock.result.metaSingleLine].join(' '), beginCaptures: { - "0": { name: scopes.resultBlock.result.prefix.meta }, - "1": { name: scopes.resultBlock.result.prefix.metaMatch }, - "2": { name: scopes.resultBlock.result.prefix.lineNumber }, - "3": { name: scopes.resultBlock.result.prefix.colon }, + '0': { name: scopes.resultBlock.result.prefix.meta }, + '1': { name: scopes.resultBlock.result.prefix.metaMatch }, + '2': { name: scopes.resultBlock.result.prefix.lineNumber }, + '3': { name: scopes.resultBlock.result.prefix.colon }, }, patterns: [{ include: scope }] } @@ -149,10 +148,10 @@ mappings.forEach(([ext, scope, regexp]) => const header = [ { - begin: "^(# Query): ", - end: "\n", + begin: '^(# Query): ', + end: '\n', name: scopes.header.meta, - beginCaptures: { "1": { name: scopes.header.key }, }, + beginCaptures: { '1': { name: scopes.header.key }, }, patterns: [ { match: '(\\\\n)|(\\\\\\\\)', @@ -169,10 +168,10 @@ const header = [ ] }, { - begin: "^(# Flags): ", - end: "\n", + begin: '^(# Flags): ', + end: '\n', name: scopes.header.meta, - beginCaptures: { "1": { name: scopes.header.key }, }, + beginCaptures: { '1': { name: scopes.header.key }, }, patterns: [ { match: '(RegExp|CaseSensitive|IgnoreExcludeSettings|WordMatch)', @@ -182,10 +181,10 @@ const header = [ ] }, { - begin: "^(# ContextLines): ", - end: "\n", + begin: '^(# ContextLines): ', + end: '\n', name: scopes.header.meta, - beginCaptures: { "1": { name: scopes.header.key }, }, + beginCaptures: { '1': { name: scopes.header.key }, }, patterns: [ { match: '\\d', @@ -195,42 +194,42 @@ const header = [ ] }, { - match: "^(# (?:Including|Excluding)): (.*)$", + match: '^(# (?:Including|Excluding)): (.*)$', name: scopes.header.meta, captures: { - "1": { name: scopes.header.key }, - "2": { name: scopes.header.value } + '1': { name: scopes.header.key }, + '2': { name: scopes.header.value } } }, ]; const plainText = [ { - match: "^(?!\\s)(.*?)([^\\\\\\/\\n]*)(:)$", + match: '^(?!\\s)(.*?)([^\\\\\\/\\n]*)(:)$', name: [scopes.resultBlock.meta, scopes.resultBlock.path.meta].join(' '), captures: { - "1": { name: scopes.resultBlock.path.dirname }, - "2": { name: scopes.resultBlock.path.basename }, - "3": { name: scopes.resultBlock.path.colon } + '1': { name: scopes.resultBlock.path.dirname }, + '2': { name: scopes.resultBlock.path.basename }, + '3': { name: scopes.resultBlock.path.colon } } }, { - match: "^ (?:((\\d+)(:))|((\\d+)( ))(.*))", + match: '^ (?:((\\d+)(:))|((\\d+)( ))(.*))', name: [scopes.resultBlock.meta, scopes.resultBlock.result.meta].join(' '), captures: { - "1": { name: [scopes.resultBlock.result.prefix.meta, scopes.resultBlock.result.prefix.metaMatch].join(' ') }, - "2": { name: scopes.resultBlock.result.prefix.lineNumber }, - "3": { name: scopes.resultBlock.result.prefix.colon }, + '1': { name: [scopes.resultBlock.result.prefix.meta, scopes.resultBlock.result.prefix.metaMatch].join(' ') }, + '2': { name: scopes.resultBlock.result.prefix.lineNumber }, + '3': { name: scopes.resultBlock.result.prefix.colon }, - "4": { name: [scopes.resultBlock.result.prefix.meta, scopes.resultBlock.result.prefix.metaContext].join(' ') }, - "5": { name: scopes.resultBlock.result.prefix.lineNumber }, + '4': { name: [scopes.resultBlock.result.prefix.meta, scopes.resultBlock.result.prefix.metaContext].join(' ') }, + '5': { name: scopes.resultBlock.result.prefix.lineNumber }, } } ]; const tmLanguage = { - "information_for_contributors": "This file is generated from ./generateTMLanguage.js.", - name: "Search Results", + 'information_for_contributors': 'This file is generated from ./generateTMLanguage.js.', + name: 'Search Results', scopeName: scopes.root, patterns: [ ...header, diff --git a/extensions/search-result/syntaxes/searchResult.tmLanguage.json b/extensions/search-result/syntaxes/searchResult.tmLanguage.json index ea4e3efb15..8edb4a7f89 100644 --- a/extensions/search-result/syntaxes/searchResult.tmLanguage.json +++ b/extensions/search-result/syntaxes/searchResult.tmLanguage.json @@ -189,6 +189,9 @@ { "include": "#pl" }, + { + "include": "#pm" + }, { "include": "#ps1" }, @@ -3457,6 +3460,92 @@ } ] }, + "pm": { + "name": "meta.resultBlock.search", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.pm)(:)$", + "end": "^(?!\\s)", + "beginCaptures": { + "0": { + "name": "string meta.path.search" + }, + "1": { + "name": "meta.path.dirname.search" + }, + "2": { + "name": "meta.path.basename.search" + }, + "3": { + "name": "punctuation.separator" + } + }, + "patterns": [ + { + "name": "meta.resultLine.search meta.resultLine.multiLine.search", + "begin": "^ ((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", + "beginCaptures": { + "0": { + "name": "constant.numeric.integer meta.resultLinePrefix.search" + }, + "1": { + "name": "meta.resultLinePrefix.contextLinePrefix.search" + }, + "2": { + "name": "meta.resultLinePrefix.lineNumber.search" + } + }, + "whileCaptures": { + "0": { + "name": "constant.numeric.integer meta.resultLinePrefix.search" + }, + "1": { + "name": "meta.resultLinePrefix.matchLinePrefix.search" + }, + "2": { + "name": "meta.resultLinePrefix.lineNumber.search" + }, + "3": { + "name": "punctuation.separator" + }, + "4": { + "name": "meta.resultLinePrefix.contextLinePrefix.search" + }, + "5": { + "name": "meta.resultLinePrefix.lineNumber.search" + } + }, + "patterns": [ + { + "include": "source.perl" + } + ] + }, + { + "begin": "^ ((\\d+)(:))", + "while": "(?=not)possible", + "name": "meta.resultLine.search meta.resultLine.singleLine.search", + "beginCaptures": { + "0": { + "name": "constant.numeric.integer meta.resultLinePrefix.search" + }, + "1": { + "name": "meta.resultLinePrefix.matchLinePrefix.search" + }, + "2": { + "name": "meta.resultLinePrefix.lineNumber.search" + }, + "3": { + "name": "punctuation.separator" + } + }, + "patterns": [ + { + "include": "source.perl" + } + ] + } + ] + }, "ps1": { "name": "meta.resultBlock.search", "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.ps1)(:)$", diff --git a/package.json b/package.json index bc8919aec3..6671c45317 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "vscode-ripgrep": "^1.5.8", "vscode-sqlite3": "4.0.9", "vscode-textmate": "4.4.0", - "xterm": "4.4.0", + "xterm": "4.5.0-beta.4", "xterm-addon-search": "0.5.0", "xterm-addon-unicode11": "0.1.1", "xterm-addon-web-links": "0.2.1", diff --git a/remote/package.json b/remote/package.json index 14b25d4666..7d3a3755dd 100644 --- a/remote/package.json +++ b/remote/package.json @@ -20,7 +20,7 @@ "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.5.8", "vscode-textmate": "4.4.0", - "xterm": "4.4.0", + "xterm": "4.5.0-beta.4", "xterm-addon-search": "0.5.0", "xterm-addon-unicode11": "0.1.1", "xterm-addon-web-links": "0.2.1", diff --git a/remote/web/package.json b/remote/web/package.json index cdb1fd8db2..ffcd7add28 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "onigasm-umd": "2.2.5", "semver-umd": "^5.5.5", "vscode-textmate": "4.4.0", - "xterm": "4.4.0", + "xterm": "4.5.0-beta.4", "xterm-addon-search": "0.5.0", "xterm-addon-unicode11": "0.1.1", "xterm-addon-web-links": "0.2.1", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 0b1d4fb601..c9f5c9ebb9 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -51,7 +51,7 @@ xterm-addon-webgl@0.5.0: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0.tgz#c1031dc7599cce3509824643ab5f15361c928e3e" integrity sha512-hQrvabKCnwXFaEZ+YtoJM9Pm0CIBXL5KSwoU+RiGStU3KYTAcqYP2GsH3dWdvKX6kTWhWLS81dtDsGkfbOciuA== -xterm@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0.tgz#5915d3c4c8800fadbcf555a0a603c672ab9df589" - integrity sha512-JGIpigWM3EBWvnS3rtBuefkiToIILSK1HYMXy4BCsUpO+O4UeeV+/U1AdAXgCB6qJrnPNb7yLgBsVCQUNMteig== +xterm@4.5.0-beta.4: + version "4.5.0-beta.4" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.5.0-beta.4.tgz#701f05553b643236d3fcd8bb7f14045bd4537c92" + integrity sha512-Yv1Bf60LTLBMaig1rv033hPz8hQGXZN6VYW2oe/409t2NbJXPg5xZgf47qyaWFV7a5k1BFiwjayJCWaL2nYBew== diff --git a/remote/yarn.lock b/remote/yarn.lock index 3fa3b05348..d576786f98 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -433,10 +433,10 @@ xterm-addon-webgl@0.5.0: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0.tgz#c1031dc7599cce3509824643ab5f15361c928e3e" integrity sha512-hQrvabKCnwXFaEZ+YtoJM9Pm0CIBXL5KSwoU+RiGStU3KYTAcqYP2GsH3dWdvKX6kTWhWLS81dtDsGkfbOciuA== -xterm@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0.tgz#5915d3c4c8800fadbcf555a0a603c672ab9df589" - integrity sha512-JGIpigWM3EBWvnS3rtBuefkiToIILSK1HYMXy4BCsUpO+O4UeeV+/U1AdAXgCB6qJrnPNb7yLgBsVCQUNMteig== +xterm@4.5.0-beta.4: + version "4.5.0-beta.4" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.5.0-beta.4.tgz#701f05553b643236d3fcd8bb7f14045bd4537c92" + integrity sha512-Yv1Bf60LTLBMaig1rv033hPz8hQGXZN6VYW2oe/409t2NbJXPg5xZgf47qyaWFV7a5k1BFiwjayJCWaL2nYBew== yauzl@^2.9.2: version "2.10.0" diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 14c62def64..f40cc44a96 100755 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -67,10 +67,6 @@ if %errorlevel% neq 0 exit /b %errorlevel% call .\scripts\node-electron.bat .\node_modules\mocha\bin\_mocha .\extensions\*\server\out\test\**\*.test.js if %errorlevel% neq 0 exit /b %errorlevel% -if exist ".\resources\server\test\test-remote-integration.bat" ( - call .\resources\server\test\test-remote-integration.bat -) - rmdir /s /q %VSCODEUSERDATADIR% popd diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index b74c5189a3..a90a4870e4 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -51,11 +51,6 @@ fi "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/azurecore/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --skip-getting-started --user-data-dir=$VSCODEUSERDATADIR -# Remote Integration Tests -if [ -f ./resources/server/test/test-remote-integration.sh ]; then - ./resources/server/test/test-remote-integration.sh -fi - # Tests in commonJS cd $ROOT/extensions/css-language-features/server && $ROOT/scripts/node-electron.sh test/index.js cd $ROOT/extensions/html-language-features/server && $ROOT/scripts/node-electron.sh test/index.js diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index 1f45d47b46..7707680e06 100644 --- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -38,6 +38,7 @@ import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/commo import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; class MainThreadNotebookEditor extends Disposable { private _contentChangedEmitter = new Emitter(); @@ -458,12 +459,17 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements }; let isUntitled: boolean = uri.scheme === Schemas.untitled; - let fileInput; + let fileInput: UntitledTextEditorInput | FileEditorInput; if (isUntitled && path.isAbsolute(uri.fsPath)) { - fileInput = this._untitledEditorService.create({ associatedResource: uri, mode: 'notebook', initialValue: options.initialContent }); + const model = this._untitledEditorService.create({ associatedResource: uri, mode: 'notebook', initialValue: options.initialContent }); + fileInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); } else { - fileInput = isUntitled ? this._untitledEditorService.create({ untitledResource: uri, mode: 'notebook', initialValue: options.initialContent }) : - this._editorService.createInput({ resource: uri, mode: 'notebook' }); + if (isUntitled) { + const model = this._untitledEditorService.create({ untitledResource: uri, mode: 'notebook', initialValue: options.initialContent }); + fileInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); + } else { + fileInput = this._editorService.createInput({ forceFile: true, resource: uri, mode: 'notebook' }) as FileEditorInput; + } } let input: NotebookInput; if (isUntitled) { @@ -478,7 +484,7 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements await untitledModel.load(); input.untitledEditorModel = untitledModel; if (options.initialDirtyState === false) { - input.untitledEditorModel.setDirty(false); + (input.untitledEditorModel as UntitledTextEditorModel).setDirty(false); } } let editor = await this._editorService.openEditor(input, editorOptions, viewColumnToEditorGroup(this._editorGroupService, options.position)); diff --git a/src/sql/workbench/browser/modal/optionsDialog.ts b/src/sql/workbench/browser/modal/optionsDialog.ts index 52bb2db3af..1a42f4a865 100644 --- a/src/sql/workbench/browser/modal/optionsDialog.ts +++ b/src/sql/workbench/browser/modal/optionsDialog.ts @@ -36,6 +36,7 @@ import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/vie import { attachModalDialogStyler, attachPanelStyler } from 'sql/workbench/common/styler'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { ServiceOptionType } from 'sql/platform/connection/common/interfaces'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class CategoryView extends ViewPane { @@ -48,9 +49,11 @@ export class CategoryView extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IOpenerService protected openerService: IOpenerService, + @IThemeService protected themeService: IThemeService ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, opener, themeService); } // we want a fixed size, so when we render to will measure our content and set that to be our diff --git a/src/sql/workbench/browser/modelComponents/editor.component.ts b/src/sql/workbench/browser/modelComponents/editor.component.ts index 91f4025304..f6010527f9 100644 --- a/src/sql/workbench/browser/modelComponents/editor.component.ts +++ b/src/sql/workbench/browser/modelComponents/editor.component.ts @@ -24,6 +24,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { SimpleProgressIndicator } from 'sql/workbench/services/progress/browser/simpleProgressIndicator'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/platform/dashboard/browser/interfaces'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @Component({ template: '', @@ -47,7 +48,8 @@ export default class EditorComponent extends ComponentBase implements IComponent @Inject(IInstantiationService) private _instantiationService: IInstantiationService, @Inject(IModelService) private _modelService: IModelService, @Inject(IModeService) private _modeService: IModeService, - @Inject(ILogService) private _logService: ILogService + @Inject(ILogService) private _logService: ILogService, + @Inject(IEditorService) private readonly editorService: IEditorService ) { super(changeRef, el); } @@ -66,7 +68,7 @@ export default class EditorComponent extends ComponentBase implements IComponent this._editor.create(this._el.nativeElement); this._editor.setVisible(true); let uri = this.createUri(); - this._editorInput = this._instantiationService.createInstance(UntitledTextEditorInput, uri, false, 'plaintext', '', ''); + this._editorInput = this.editorService.createInput({ forceUntitled: true, resource: uri, mode: 'plaintext' }) as UntitledTextEditorInput; await this._editor.setInput(this._editorInput, undefined); const model = await this._editorInput.resolve(); this._editorModel = model.textEditorModel; diff --git a/src/sql/workbench/browser/parts/views/customView.ts b/src/sql/workbench/browser/parts/views/customView.ts index 8b00dd1c8e..94b3ccbb9f 100644 --- a/src/sql/workbench/browser/parts/views/customView.ts +++ b/src/sql/workbench/browser/parts/views/customView.ts @@ -25,7 +25,7 @@ import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { URI } from 'vs/base/common/uri'; import { dirname, basename } from 'vs/base/common/resources'; -import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService'; import { FileKind } from 'vs/platform/files/common/files'; import { WorkbenchAsyncDataTree, TreeResourceNavigator } from 'vs/platform/list/browser/listService'; import { localize } from 'vs/nls'; @@ -63,9 +63,11 @@ export class CustomTreeViewPanel extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IOpenerService protected openerService: IOpenerService, + @IThemeService protected themeService: IThemeService ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(options.id)); this.treeView = treeView as ITreeView; this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this)); diff --git a/src/sql/workbench/contrib/accounts/browser/accountDialog.ts b/src/sql/workbench/contrib/accounts/browser/accountDialog.ts index 7ebc6d9d3c..0105d0703d 100644 --- a/src/sql/workbench/contrib/accounts/browser/accountDialog.ts +++ b/src/sql/workbench/contrib/accounts/browser/accountDialog.ts @@ -38,6 +38,7 @@ import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { attachModalDialogStyler, attachPanelStyler } from 'sql/workbench/common/styler'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; class AccountPanel extends ViewPane { public index: number; @@ -48,12 +49,13 @@ class AccountPanel extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, - @IThemeService private themeService: IThemeService, + @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IOpenerService openerService: IOpenerService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); } protected renderBody(container: HTMLElement): void { @@ -130,7 +132,8 @@ export class AccountDialog extends Modal { @IClipboardService clipboardService: IClipboardService, @ILogService logService: ILogService, @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, - @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService + @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, + @IOpenerService protected readonly openerService: IOpenerService ) { super( localize('linkedAccounts', "Linked accounts"), @@ -301,7 +304,8 @@ export class AccountDialog extends Modal { this._themeService, this.contextKeyService, this._instantiationService, - this.viewDescriptorService + this.viewDescriptorService, + this.openerService ); attachPanelStyler(providerView, this._themeService); diff --git a/src/sql/workbench/contrib/accounts/test/browser/accountDialogController.test.ts b/src/sql/workbench/contrib/accounts/test/browser/accountDialogController.test.ts index b1cc1abc28..6e33dea445 100644 --- a/src/sql/workbench/contrib/accounts/test/browser/accountDialogController.test.ts +++ b/src/sql/workbench/contrib/accounts/test/browser/accountDialogController.test.ts @@ -86,7 +86,7 @@ function createInstantiationService(addAccountFailureEmitter?: Emitter): .returns(() => undefined); // Create a mock account dialog - let accountDialog = new AccountDialog(undefined!, undefined!, instantiationService.object, undefined!, undefined!, undefined!, undefined!, new MockContextKeyService(), undefined!, undefined!, undefined!, undefined!); + let accountDialog = new AccountDialog(undefined!, undefined!, instantiationService.object, undefined!, undefined!, undefined!, undefined!, new MockContextKeyService(), undefined!, undefined!, undefined!, undefined!, undefined!); let mockAccountDialog = TypeMoq.Mock.ofInstance(accountDialog); mockAccountDialog.setup(x => x.onAddAccountErrorEvent) .returns(() => { return addAccountFailureEmitter ? addAccountFailureEmitter.event : mockEvent.event; }); diff --git a/src/sql/workbench/contrib/charts/browser/actions.ts b/src/sql/workbench/contrib/charts/browser/actions.ts index 794e6201a3..c647c4d299 100644 --- a/src/sql/workbench/contrib/charts/browser/actions.ts +++ b/src/sql/workbench/contrib/charts/browser/actions.ts @@ -21,6 +21,8 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { assign } from 'vs/base/common/objects'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; export interface IChartActionContext { options: IInsightOptions; @@ -35,7 +37,8 @@ export class CreateInsightAction extends Action { constructor( @IEditorService private editorService: IEditorService, @INotificationService private notificationService: INotificationService, - @IUntitledTextEditorService private untitledEditorService: IUntitledTextEditorService + @IUntitledTextEditorService private untitledEditorService: IUntitledTextEditorService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(CreateInsightAction.ID, CreateInsightAction.LABEL, CreateInsightAction.ICON); } @@ -74,7 +77,7 @@ export class CreateInsightAction extends Action { let input = this.untitledEditorService.create({ mode: 'json', initialValue: JSON.stringify(widgetConfig) }); - return this.editorService.openEditor(input, { pinned: true }) + return this.editorService.openEditor(this.instantiationService.createInstance(UntitledTextEditorInput, input), { pinned: true }) .then( () => true, error => { diff --git a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts index 105477746c..ae805c819f 100644 --- a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts +++ b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts @@ -22,19 +22,18 @@ import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { TestEditorService, TestDialogService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorService, TestDialogService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; import { TestQueryModelService } from 'sql/workbench/services/query/test/common/testQueryModelService'; import { Event } from 'vs/base/common/event'; import { IQueryModelService } from 'sql/workbench/services/query/common/queryModel'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { LabelService } from 'vs/workbench/services/label/common/labelService'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; class TestParsedArgs implements ParsedArgs { [arg: string]: any; @@ -391,9 +390,11 @@ suite('commandLineService tests', () => { const querymodelService = TypeMoq.Mock.ofType(TestQueryModelService, TypeMoq.MockBehavior.Strict); querymodelService.setup(c => c.onRunQueryStart).returns(() => Event.None); querymodelService.setup(c => c.onRunQueryComplete).returns(() => Event.None); - const instantiationService = new TestInstantiationService(); let uri = URI.file(args._[0]); - const untitledEditorInput = new UntitledTextEditorInput(uri, false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); + const workbenchinstantiationService = workbenchInstantiationService(); + const accessor = workbenchinstantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + const untitledEditorInput = workbenchinstantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: uri })); const queryInput = new UntitledQueryEditorInput(undefined, untitledEditorInput, undefined, connectionManagementService.object, querymodelService.object, configurationService.object); queryInput.state.connected = true; const editorService: TypeMoq.Mock = TypeMoq.Mock.ofType(TestEditorService, TypeMoq.MockBehavior.Strict); @@ -567,3 +568,9 @@ suite('commandLineService tests', () => { }); }); + +class ServiceAccessor { + constructor( + @IUntitledTextEditorService public readonly untitledTextEditorService: IUntitledTextEditorService + ) { } +} diff --git a/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts b/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts index 4c120582e3..78af40bc1c 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts @@ -21,6 +21,8 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ITree } from 'vs/base/parts/tree/browser/tree'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; export class ConnectionViewletPanel extends ViewPane { @@ -41,8 +43,10 @@ export class ConnectionViewletPanel extends ViewPane { @IObjectExplorerService private readonly objectExplorerService: IObjectExplorerService, @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IOpenerService protected openerService: IOpenerService, + @IThemeService protected themeService: IThemeService ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, opener, themeService); this._addServerAction = this.instantiationService.createInstance(AddServerAction, AddServerAction.ID, AddServerAction.LABEL); diff --git a/src/sql/workbench/contrib/editData/browser/editDataInput.ts b/src/sql/workbench/contrib/editData/browser/editDataInput.ts index 12a30edb45..8577805ea9 100644 --- a/src/sql/workbench/contrib/editData/browser/editDataInput.ts +++ b/src/sql/workbench/contrib/editData/browser/editDataInput.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, EditorModel, EncodingMode, IEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput, EncodingMode, IEditorInput } from 'vs/workbench/common/editor'; import { IConnectionManagementService, IConnectableInput, INewConnectionParams } from 'sql/platform/connection/common/connectionManagement'; import { IQueryModelService } from 'sql/workbench/services/query/common/queryModel'; import { Event, Emitter } from 'vs/base/common/event'; @@ -15,6 +15,8 @@ import Severity from 'vs/base/common/severity'; import { EditDataResultsInput } from 'sql/workbench/contrib/editData/browser/editDataResultsInput'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; +import { IUntitledTextEditorModel, UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; /** * Input for the EditDataEditor. @@ -61,7 +63,7 @@ export class EditDataInput extends EditorInput implements IConnectableInput { // also set dirty status to false to prevent rerendering. if (this._sql) { this._register(this._sql.onDidChangeDirty(async () => { - const model = await this._sql.resolve(); + const model = await this._sql.resolve() as UntitledTextEditorModel; model.setDirty(false); this._onDidChangeDirty.fire(); })); @@ -222,11 +224,10 @@ export class EditDataInput extends EditorInput implements IConnectableInput { return this._connectionManagementService.getTabColorForUri(this.uri); } - public get onDidModelChangeEncoding(): Event { return this._sql.onDidModelChangeEncoding; } - public resolve(refresh?: boolean): Promise { return this._sql.resolve(); } + public resolve(refresh?: boolean): Promise { return this._sql.resolve(); } public getEncoding(): string { return this._sql.getEncoding(); } public getName(): string { return this._sql.getName(); } - public get hasAssociatedFilePath(): boolean { return this._sql.hasAssociatedFilePath; } + public get hasAssociatedFilePath(): boolean { return this._sql.model.hasAssociatedFilePath; } public setEncoding(encoding: string, mode: EncodingMode /* ignored, we only have Encode */): void { this._sql.setEncoding(encoding, mode); diff --git a/src/sql/workbench/contrib/editorReplacement/test/common/editorReplacerContribution.test.ts b/src/sql/workbench/contrib/editorReplacement/test/common/editorReplacerContribution.test.ts index 823a655ee4..1b9c8bc52e 100644 --- a/src/sql/workbench/contrib/editorReplacement/test/common/editorReplacerContribution.test.ts +++ b/src/sql/workbench/contrib/editorReplacement/test/common/editorReplacerContribution.test.ts @@ -28,6 +28,7 @@ import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/u import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; import { NotebookServiceStub } from 'sql/workbench/contrib/notebook/test/stubs'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; const languageAssociations = Registry.as(LanguageAssociationExtensions.LanguageAssociations); @@ -122,7 +123,10 @@ suite('Editor Replacer Contribution', () => { const instantiationService = workbenchInstantiationService(); instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); - const input = instantiationService.createInstance(UntitledTextEditorInput, URI.file('/test/file'), false, undefined, undefined, undefined); + const accessor = instantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); assert(response?.override); const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this @@ -137,7 +141,9 @@ suite('Editor Replacer Contribution', () => { const instantiationService = workbenchInstantiationService(); instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); - const untitled = instantiationService.createInstance(UntitledTextEditorInput, URI.file('/test/file'), false, undefined, undefined, undefined); + const accessor = instantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create()); const input = instantiationService.createInstance(UntitledQueryEditorInput, '', untitled, undefined); const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); assert(response === undefined); @@ -150,7 +156,9 @@ suite('Editor Replacer Contribution', () => { const instantiationService = workbenchInstantiationService(); instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); - const input = instantiationService.createInstance(UntitledTextEditorInput, URI.file('/test/file.unknown'), false, undefined, undefined, undefined); + const accessor = instantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); assert(response === undefined); @@ -254,3 +262,9 @@ class TestModeService implements IModeService { throw new Error('Method not implemented.'); } } + +class ServiceAccessor { + constructor( + @IUntitledTextEditorService public readonly untitledTextEditorService: IUntitledTextEditorService + ) { } +} diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts index 1bc8527fba..9cd45a590e 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts @@ -210,11 +210,12 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { let uri = this.cellModel.cellUri; let cellModelSource: string; cellModelSource = Array.isArray(this.cellModel.source) ? this.cellModel.source.join('') : this.cellModel.source; - this._editorInput = this._instantiationService.createInstance(UntitledTextEditorInput, uri, false, this.cellModel.language, cellModelSource, ''); + const model = this._instantiationService.createInstance(UntitledTextEditorModel, uri, false, cellModelSource, this.cellModel.language, undefined); + this._editorInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); await this._editor.setInput(this._editorInput, undefined); this.setFocusAndScroll(); - let untitledEditorModel: UntitledTextEditorModel = await this._editorInput.resolve(); + let untitledEditorModel = await this._editorInput.resolve() as UntitledTextEditorModel; this._editorModel = untitledEditorModel.textEditorModel; let isActive = this.cellModel.id === this._activeCellId; diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts index b6f9bdb16c..d40de963ee 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts @@ -16,7 +16,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { Schemas } from 'vs/base/common/network'; -import { ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileSaveOptions, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { LocalContentManager } from 'sql/workbench/services/notebook/common/localContentManager'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -26,7 +26,7 @@ import { Deferred } from 'sql/base/common/promise'; import { NotebookTextFileModel } from 'sql/workbench/contrib/notebook/browser/models/notebookTextFileModel'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; -import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { UntitledTextEditorModel, IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; @@ -42,7 +42,7 @@ export class NotebookEditorModel extends EditorModel { private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); private _lastEditFullReplacement: boolean; constructor(public readonly notebookUri: URI, - private textEditorModel: TextFileEditorModel | UntitledTextEditorModel | ResourceEditorModel, + private textEditorModel: ITextFileEditorModel | IUntitledTextEditorModel | ResourceEditorModel, @INotebookService private notebookService: INotebookService, @ITextResourcePropertiesService private textResourcePropertiesService: ITextResourcePropertiesService ) { @@ -198,7 +198,7 @@ export abstract class NotebookInput extends EditorInput { private _parentContainer: HTMLElement; private readonly _layoutChanged: Emitter = this._register(new Emitter()); private _model: NotebookEditorModel; - private _untitledEditorModel: UntitledTextEditorModel; + private _untitledEditorModel: IUntitledTextEditorModel; private _contentManager: IContentManager; private _providersLoaded: Promise; private _dirtyListener: IDisposable; @@ -328,11 +328,11 @@ export abstract class NotebookInput extends EditorInput { return this.resource; } - public get untitledEditorModel(): UntitledTextEditorModel { + public get untitledEditorModel(): IUntitledTextEditorModel { return this._untitledEditorModel; } - public set untitledEditorModel(value: UntitledTextEditorModel) { + public set untitledEditorModel(value: IUntitledTextEditorModel) { this._untitledEditorModel = value; } @@ -346,7 +346,7 @@ export abstract class NotebookInput extends EditorInput { if (this._model) { return Promise.resolve(this._model); } else { - let textOrUntitledEditorModel: TextFileEditorModel | UntitledTextEditorModel | ResourceEditorModel; + let textOrUntitledEditorModel: ITextFileEditorModel | IUntitledTextEditorModel | ResourceEditorModel; if (this.resource.scheme === Schemas.untitled) { if (this._untitledEditorModel) { this._untitledEditorModel.textEditorModel.onBeforeAttached(); diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookTextFileModel.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookTextFileModel.ts index 6b9b9c024e..a0cf75d853 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookTextFileModel.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookTextFileModel.ts @@ -7,8 +7,8 @@ import { Range, IRange } from 'vs/editor/common/core/range'; import { FindMatch } from 'vs/editor/common/model'; import { NotebookContentChange, INotebookModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces'; import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contracts'; -import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { repeat } from 'vs/base/common/strings'; +import { ITextEditorModel } from 'vs/workbench/common/editor'; export class NotebookTextFileModel { // save active cell's line/column in editor model for the beginning of the source property @@ -33,7 +33,7 @@ export class NotebookTextFileModel { } } - public transformAndApplyEditForSourceUpdate(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean { + public transformAndApplyEditForSourceUpdate(contentChange: NotebookContentChange, textEditorModel: ITextEditorModel): boolean { let cellGuidRange = this.getCellNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid); // convert the range to leverage offsets in the json @@ -109,7 +109,7 @@ export class NotebookTextFileModel { return false; } - public transformAndApplyEditForOutputUpdate(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean { + public transformAndApplyEditForOutputUpdate(contentChange: NotebookContentChange, textEditorModel: ITextEditorModel): boolean { if (Array.isArray(contentChange.cells[0].outputs) && contentChange.cells[0].outputs.length > 0) { let newOutput = JSON.stringify(contentChange.cells[0].outputs[contentChange.cells[0].outputs.length - 1], undefined, ' '); if (contentChange.cells[0].outputs.length > 1) { @@ -136,7 +136,7 @@ export class NotebookTextFileModel { return false; } - public transformAndApplyEditForCellUpdated(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean { + public transformAndApplyEditForCellUpdated(contentChange: NotebookContentChange, textEditorModel: ITextEditorModel): boolean { let executionCountMatch = this.getExecutionCountRange(textEditorModel, contentChange.cells[0].cellGuid); if (executionCountMatch && executionCountMatch.range) { // Execution count can be between 0 and n characters long @@ -161,7 +161,7 @@ export class NotebookTextFileModel { return true; } - public transformAndApplyEditForClearOutput(contentChange: NotebookContentChange, textEditorModel: BaseTextEditorModel): boolean { + public transformAndApplyEditForClearOutput(contentChange: NotebookContentChange, textEditorModel: ITextEditorModel): boolean { if (!textEditorModel || !contentChange || !contentChange.cells || !contentChange.cells[0] || !contentChange.cells[0].cellGuid) { return false; } @@ -178,7 +178,7 @@ export class NotebookTextFileModel { return false; } - public replaceEntireTextEditorModel(notebookModel: INotebookModel, type: NotebookChangeType, textEditorModel: BaseTextEditorModel) { + public replaceEntireTextEditorModel(notebookModel: INotebookModel, type: NotebookChangeType, textEditorModel: ITextEditorModel) { let content = JSON.stringify(notebookModel.toJSON(type), undefined, ' '); let model = textEditorModel.textEditorModel; let endLine = model.getLineCount(); @@ -190,7 +190,7 @@ export class NotebookTextFileModel { } // Find the beginning of a cell's source in the text editor model - private updateSourceBeginRange(textEditorModel: BaseTextEditorModel, cellGuid: string): void { + private updateSourceBeginRange(textEditorModel: ITextEditorModel, cellGuid: string): void { if (!cellGuid) { return; } @@ -210,7 +210,7 @@ export class NotebookTextFileModel { } // Find the beginning of a cell's outputs in the text editor model - private updateOutputBeginRange(textEditorModel: BaseTextEditorModel, cellGuid: string): void { + private updateOutputBeginRange(textEditorModel: ITextEditorModel, cellGuid: string): void { if (!cellGuid) { return undefined; } @@ -230,7 +230,7 @@ export class NotebookTextFileModel { // Find the end of a cell's outputs in the text editor model // This will be used as a starting point for any future outputs - private getEndOfOutputs(textEditorModel: BaseTextEditorModel, cellGuid: string) { + private getEndOfOutputs(textEditorModel: ITextEditorModel, cellGuid: string) { let outputsBegin; if (this._activeCellGuid === cellGuid) { outputsBegin = this._outputBeginRange; @@ -272,7 +272,7 @@ export class NotebookTextFileModel { } // Determine what text needs to be replaced when execution counts are updated - private getExecutionCountRange(textEditorModel: BaseTextEditorModel, cellGuid: string) { + private getExecutionCountRange(textEditorModel: ITextEditorModel, cellGuid: string) { let endOutputRange = this.getEndOfOutputs(textEditorModel, cellGuid); if (endOutputRange && endOutputRange.endLineNumber) { return textEditorModel.textEditorModel.findNextMatch('"execution_count": ', { lineNumber: endOutputRange.endLineNumber, column: endOutputRange.endColumn }, false, true, undefined, true); @@ -282,14 +282,14 @@ export class NotebookTextFileModel { // Find a cell's location, given its cellGuid // If it doesn't exist (e.g. it's not the active cell), attempt to find it - private getCellNodeByGuid(textEditorModel: BaseTextEditorModel, guid: string) { + private getCellNodeByGuid(textEditorModel: ITextEditorModel, guid: string) { if (this._activeCellGuid !== guid || !this._sourceBeginRange) { this.updateSourceBeginRange(textEditorModel, guid); } return this._sourceBeginRange; } - private getOutputNodeByGuid(textEditorModel: BaseTextEditorModel, guid: string) { + private getOutputNodeByGuid(textEditorModel: ITextEditorModel, guid: string) { if (this._activeCellGuid !== guid) { this.updateOutputBeginRange(textEditorModel, guid); } @@ -302,7 +302,7 @@ function areRangePropertiesPopulated(range: Range) { return range && range.startLineNumber !== 0 && range.startColumn !== 0 && range.endLineNumber !== 0 && range.endColumn !== 0; } -function findOrSetCellGuidMatch(textEditorModel: BaseTextEditorModel, cellGuid: string): FindMatch[] { +function findOrSetCellGuidMatch(textEditorModel: ITextEditorModel, cellGuid: string): FindMatch[] { if (!textEditorModel || !cellGuid) { return undefined; } diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts index 10516a86fe..61f993d194 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts @@ -6,7 +6,7 @@ import * as TypeMoq from 'typemoq'; import * as assert from 'assert'; import { nb } from 'azdata'; -import { workbenchInstantiationService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { UntitledNotebookInput } from 'sql/workbench/contrib/notebook/common/models/untitledNotebookInput'; @@ -19,7 +19,7 @@ import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/u import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { INotebookService, IProviderInfo } from 'sql/workbench/services/notebook/browser/notebookService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { LabelService } from 'vs/workbench/services/label/common/labelService'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; suite('Notebook Input', function (): void { const instantiationService = workbenchInstantiationService(); @@ -48,7 +48,9 @@ suite('Notebook Input', function (): void { let untitledNotebookInput: UntitledNotebookInput; setup(() => { - untitledTextInput = new UntitledTextEditorInput(untitledUri, false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); + const accessor = instantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + untitledTextInput = instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: untitledUri })); untitledNotebookInput = new UntitledNotebookInput( testTitle, untitledUri, untitledTextInput, undefined, instantiationService, mockNotebookService.object, mockExtensionService.object); @@ -169,9 +171,17 @@ suite('Notebook Input', function (): void { assert.ok(untitledNotebookInput.matches(untitledNotebookInput), 'Input should match itself.'); let otherTestUri = URI.from({ scheme: Schemas.untitled, path: 'OtherTestPath' }); - let otherTextInput = new UntitledTextEditorInput(otherTestUri, false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); + const accessor = instantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + let otherTextInput = instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: otherTestUri })); let otherInput = instantiationService.createInstance(UntitledNotebookInput, 'OtherTestInput', otherTestUri, otherTextInput); assert.strictEqual(untitledNotebookInput.matches(otherInput), false, 'Input should not match different input.'); }); }); + +class ServiceAccessor { + constructor( + @IUntitledTextEditorService public readonly untitledTextEditorService: IUntitledTextEditorService + ) { } +} diff --git a/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts index 9b3bb61254..9f252981c9 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts @@ -53,6 +53,7 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe import { find } from 'vs/base/common/arrays'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { attachTabbedPanelStyler } from 'sql/workbench/common/styler'; +import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; class BasicView implements IView { public get element(): HTMLElement { @@ -433,7 +434,8 @@ export class ProfilerEditor extends BaseEditor { editorContainer.className = 'profiler-editor'; this._editor.create(editorContainer); this._editor.setVisible(true); - this._editorInput = this._instantiationService.createInstance(UntitledTextEditorInput, URI.from({ scheme: Schemas.untitled }), false, 'sql', '', ''); + const model = this._instantiationService.createInstance(UntitledTextEditorModel, URI.from({ scheme: Schemas.untitled }), false, undefined, 'sql', undefined); + this._editorInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); this._editor.setInput(this._editorInput, undefined); this._editorInput.resolve().then(model => this._editorModel = model.textEditorModel); return editorContainer; diff --git a/src/sql/workbench/contrib/query/browser/gridPanel.ts b/src/sql/workbench/contrib/query/browser/gridPanel.ts index f1d5dfb127..d14f49af22 100644 --- a/src/sql/workbench/contrib/query/browser/gridPanel.ts +++ b/src/sql/workbench/contrib/query/browser/gridPanel.ts @@ -580,8 +580,7 @@ export abstract class GridTableBase extends Disposable implements IView { let content = value.displayValue; const input = this.untitledEditorService.create({ mode: column.isXml ? 'xml' : 'json', initialValue: content }); - const model = await input.resolve(); - await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, model.textEditorModel, FormattingMode.Explicit, CancellationToken.None); + await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, input.textEditorModel, FormattingMode.Explicit, CancellationToken.None); return this.editorService.openEditor(input); }); } diff --git a/src/sql/workbench/contrib/query/common/untitledQueryEditorInput.ts b/src/sql/workbench/contrib/query/common/untitledQueryEditorInput.ts index 8bf22aae15..31e3612bf1 100644 --- a/src/sql/workbench/contrib/query/common/untitledQueryEditorInput.ts +++ b/src/sql/workbench/contrib/query/common/untitledQueryEditorInput.ts @@ -12,14 +12,12 @@ import { IEncodingSupport, EncodingMode } from 'vs/workbench/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; export class UntitledQueryEditorInput extends QueryEditorInput implements IEncodingSupport { public static readonly ID = 'workbench.editorInput.untitledQueryInput'; - public readonly onDidModelChangeEncoding = this.text.onDidModelChangeEncoding; - constructor( description: string, text: UntitledTextEditorInput, @@ -31,7 +29,7 @@ export class UntitledQueryEditorInput extends QueryEditorInput implements IEncod super(description, text, results, connectionManagementService, queryModelService, configurationService); } - public resolve(): Promise { + public resolve(): Promise { return this.text.resolve(); } @@ -40,7 +38,7 @@ export class UntitledQueryEditorInput extends QueryEditorInput implements IEncod } public get hasAssociatedFilePath(): boolean { - return this.text.hasAssociatedFilePath; + return this.text.model.hasAssociatedFilePath; } public setMode(mode: string): void { diff --git a/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts b/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts index 11746b3dd7..a6e2ded637 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts @@ -23,17 +23,16 @@ import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import * as TypeMoq from 'typemoq'; import * as assert from 'assert'; -import { TestStorageService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestStorageService, TestFileService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { TestQueryModelService } from 'sql/workbench/services/query/test/common/testQueryModelService'; import { URI } from 'vs/base/common/uri'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { LabelService } from 'vs/workbench/services/label/common/labelService'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; suite('SQL QueryAction Tests', () => { @@ -69,8 +68,10 @@ suite('SQL QueryAction Tests', () => { queryModelService.setup(q => q.onRunQueryComplete).returns(() => Event.None); connectionManagementService = TypeMoq.Mock.ofType(TestConnectionManagementService); connectionManagementService.setup(q => q.onDisconnect).returns(() => Event.None); - const instantiationService = new TestInstantiationService(); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); + const workbenchinstantiationService = workbenchInstantiationService(); + const accessor = workbenchinstantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + let fileInput = workbenchinstantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: URI.parse('file://testUri') })); // Setup a reusable mock QueryInput testQueryInput = TypeMoq.Mock.ofType(UntitledQueryEditorInput, TypeMoq.MockBehavior.Strict, undefined, fileInput, undefined, connectionManagementService.object, queryModelService.object, configurationService.object); testQueryInput.setup(x => x.uri).returns(() => testUri); @@ -174,8 +175,10 @@ suite('SQL QueryAction Tests', () => { let queryModelService = TypeMoq.Mock.ofType(QueryModelService, TypeMoq.MockBehavior.Loose); queryModelService.setup(x => x.onRunQueryStart).returns(() => Event.None); queryModelService.setup(x => x.onRunQueryComplete).returns(() => Event.None); - const instantiationService = new TestInstantiationService(); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); + const workbenchinstantiationService = workbenchInstantiationService(); + const accessor = workbenchinstantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + let fileInput = workbenchinstantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: URI.parse('file://testUri') })); // ... Mock "isSelectionEmpty" in QueryEditor let queryInput = TypeMoq.Mock.ofType(UntitledQueryEditorInput, TypeMoq.MockBehavior.Strict, undefined, fileInput, undefined, connectionManagementService.object, queryModelService.object, configurationService.object); @@ -223,8 +226,10 @@ suite('SQL QueryAction Tests', () => { let predefinedSelection: ISelectionData = { startLine: 1, startColumn: 2, endLine: 3, endColumn: 4 }; // ... Mock "getSelection" in QueryEditor - const instantiationService = new TestInstantiationService(); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); + const workbenchinstantiationService = workbenchInstantiationService(); + const accessor = workbenchinstantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + let fileInput = workbenchinstantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: URI.parse('file://testUri') })); let queryInput = TypeMoq.Mock.ofType(UntitledQueryEditorInput, TypeMoq.MockBehavior.Loose, undefined, fileInput, undefined, connectionManagementService.object, queryModelService.object, configurationService.object); queryInput.setup(x => x.uri).returns(() => testUri); @@ -556,3 +561,9 @@ suite('SQL QueryAction Tests', () => { assert.equal(listItem.currentDatabaseName, eventParams.connectionProfile.databaseName); }); }); + +class ServiceAccessor { + constructor( + @IUntitledTextEditorService public readonly untitledTextEditorService: IUntitledTextEditorService + ) { } +} diff --git a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts index 5370a7d7c8..1ec51ba3d1 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts @@ -17,15 +17,15 @@ import * as TypeMoq from 'typemoq'; import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { TestFileService, TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestStorageService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; import { TestQueryModelService } from 'sql/workbench/services/query/test/common/testQueryModelService'; import { Event } from 'vs/base/common/event'; -import { LabelService } from 'vs/workbench/services/label/common/labelService'; -import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; import { IStorageService } from 'vs/platform/storage/common/storage'; suite('SQL QueryEditor Tests', () => { @@ -302,7 +302,10 @@ suite('SQL QueryEditor Tests', () => { return new RunQueryAction(undefined, undefined, undefined); }); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, undefined, undefined, undefined, instantiationService.object, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); + const workbenchinstantiationService = workbenchInstantiationService(); + const accessor = workbenchinstantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + let fileInput = workbenchinstantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: URI.parse('file://testUri') })); queryModelService = TypeMoq.Mock.ofType(TestQueryModelService, TypeMoq.MockBehavior.Strict); queryModelService.setup(x => x.disposeQuery(TypeMoq.It.isAny())); queryModelService.setup(x => x.onRunQueryComplete).returns(() => Event.None); @@ -343,3 +346,9 @@ suite('SQL QueryEditor Tests', () => { }); }); }); + +class ServiceAccessor { + constructor( + @IUntitledTextEditorService public readonly untitledTextEditorService: IUntitledTextEditorService + ) { } +} diff --git a/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts b/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts index 0b10557aa2..b5645ae52e 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts @@ -20,6 +20,7 @@ import { IConnectionManagementService, IConnectionCompletionOptions, IConnection import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; suite('Query Input Factory', () => { @@ -63,13 +64,22 @@ suite('Query Input Factory', () => { }); +class ServiceAccessor { + constructor( + @IUntitledTextEditorService public readonly untitledTextEditorService: IUntitledTextEditorService + ) { } +} + class MockEditorService extends TestEditorService { public readonly activeEditor: IEditorInput | undefined = undefined; constructor(instantiationService?: IInstantiationService) { super(); if (instantiationService) { - const untitledInput = instantiationService.createInstance(UntitledTextEditorInput, URI.file('/test/file'), false, undefined, undefined, undefined); + const workbenchinstantiationService = workbenchInstantiationService(); + const accessor = workbenchinstantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + const untitledInput = instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: URI.file('/test/file') })); this.activeEditor = instantiationService.createInstance(UntitledQueryEditorInput, '', untitledInput, undefined); } } diff --git a/src/sql/workbench/services/insights/browser/insightsDialogView.ts b/src/sql/workbench/services/insights/browser/insightsDialogView.ts index 255cac86f1..d055417808 100644 --- a/src/sql/workbench/services/insights/browser/insightsDialogView.ts +++ b/src/sql/workbench/services/insights/browser/insightsDialogView.ts @@ -45,6 +45,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { attachPanelStyler, attachModalDialogStyler } from 'sql/workbench/common/styler'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const labelDisplay = nls.localize("insights.item", "Item"); const valueDisplay = nls.localize("insights.value", "Value"); @@ -66,9 +67,11 @@ class InsightTableView extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, opener, themeService); } protected renderBody(container: HTMLElement): void { diff --git a/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts b/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts index 0dd139e36b..d71d5e7110 100644 --- a/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts +++ b/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts @@ -21,6 +21,8 @@ import { replaceConnection } from 'sql/workbench/browser/taskUtilities'; import { EditDataResultsInput } from 'sql/workbench/contrib/editData/browser/editDataResultsInput'; import { ILogService } from 'vs/platform/log/common/log'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; /** * Service wrapper for opening and creating SQL documents as sql editor inputs @@ -52,8 +54,8 @@ export class QueryEditorService implements IQueryEditorService { let docUri: URI = URI.from({ scheme: Schemas.untitled, path: filePath }); // Create a sql document pane with accoutrements - const fileInput = this._untitledEditorService.create({ associatedResource: docUri, mode: 'sql' }); - let untitledEditorModel = await fileInput.resolve(); + const fileInput = this._editorService.createInput({ forceUntitled: true, resource: docUri, mode: 'sql' }) as UntitledTextEditorInput; + let untitledEditorModel = await fileInput.resolve() as UntitledTextEditorModel; if (sqlContent) { untitledEditorModel.textEditorModel.setValue(sqlContent); if (isDirty === false || (isDirty === undefined && !this._configurationService.getValue('sql.promptToSaveGeneratedFiles'))) { @@ -87,8 +89,8 @@ export class QueryEditorService implements IQueryEditorService { let docUri: URI = URI.from({ scheme: Schemas.untitled, path: filePath }); // Create a sql document pane with accoutrements - const fileInput = this._untitledEditorService.create({ associatedResource: docUri, mode: 'sql' }); - const m = await fileInput.resolve(); + const fileInput = this._editorService.createInput({ forceUntitled: true, resource: docUri, mode: 'sql' }) as UntitledTextEditorInput; + const m = await fileInput.resolve() as UntitledTextEditorModel; //when associatedResource editor is created it is dirty, this must be set to false to be able to detect changes to the editor. m.setDirty(false); // Create an EditDataInput for editing diff --git a/src/vs/base/browser/linkedText.ts b/src/vs/base/browser/linkedText.ts new file mode 100644 index 0000000000..764b078eca --- /dev/null +++ b/src/vs/base/browser/linkedText.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface ILink { + readonly label: string; + readonly href: string; + readonly title?: string; +} + +export type LinkedTextNode = string | ILink; +export type LinkedText = LinkedTextNode[]; + +const LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: "([^"]+)")?\)/gi; + +export function parseLinkedText(text: string): LinkedText { + const result: LinkedTextNode[] = []; + + let index = 0; + let match: RegExpExecArray | null; + + while (match = LINK_REGEX.exec(text)) { + if (match.index - index > 0) { + result.push(text.substring(index, match.index)); + } + + const [, label, href, title] = match; + + if (title) { + result.push({ label, href, title }); + } else { + result.push({ label, href }); + } + + index = match.index + match[0].length; + } + + if (index < text.length) { + result.push(text.substring(index)); + } + + return result; +} diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index a443546c8d..05696da41b 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -14,7 +14,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Gesture, EventType } from 'vs/base/browser/touch'; export interface IButtonOptions extends IButtonStyles { - title?: boolean; + title?: boolean | string; } export interface IButtonStyles { @@ -151,10 +151,10 @@ export class Button extends Disposable { DOM.addClass(this._element, 'monaco-text-button'); } this._element.textContent = value; - //{{SQL CARBON EDIT}} - this._element.setAttribute('aria-label', value); - //{{END}} - if (this.options.title) { + this._element.setAttribute('aria-label', value); // {{SQL CARBON EDIT}} + if (typeof this.options.title === 'string') { + this._element.title = this.options.title; + } else if (this.options.title) { this._element.title = value; } } diff --git a/src/vs/base/browser/ui/centered/centeredViewLayout.ts b/src/vs/base/browser/ui/centered/centeredViewLayout.ts index 64eabc1900..22e24a823b 100644 --- a/src/vs/base/browser/ui/centered/centeredViewLayout.ts +++ b/src/vs/base/browser/ui/centered/centeredViewLayout.ts @@ -9,6 +9,7 @@ import { Event } from 'vs/base/common/event'; import { IView, IViewSize } from 'vs/base/browser/ui/grid/grid'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; +import { IBoundarySashes } from 'vs/base/browser/ui/grid/gridview'; export interface CenteredViewState { leftMarginRatio: number; @@ -72,6 +73,19 @@ export class CenteredViewLayout implements IDisposable { get maximumHeight(): number { return this.view.maximumHeight; } get onDidChange(): Event { return this.view.onDidChange; } + private _boundarySashes: IBoundarySashes = {}; + get boundarySashes(): IBoundarySashes { return this._boundarySashes; } + set boundarySashes(boundarySashes: IBoundarySashes) { + this._boundarySashes = boundarySashes; + + if (!this.splitView) { + return; + } + + this.splitView.orthogonalStartSash = boundarySashes.top; + this.splitView.orthogonalEndSash = boundarySashes.bottom; + } + layout(width: number, height: number): void { this.width = width; this.height = height; @@ -119,6 +133,8 @@ export class CenteredViewLayout implements IDisposable { orientation: Orientation.HORIZONTAL, styles: this.style }); + this.splitView.orthogonalStartSash = this.boundarySashes.top; + this.splitView.orthogonalEndSash = this.boundarySashes.bottom; this.splitViewDisposables.add(this.splitView.onDidSashChange(() => { if (this.splitView) { diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index 986a7808c0..03fa06167d 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -5,7 +5,7 @@ @font-face { font-family: "codicon"; - src: url("./codicon.ttf?be537a78617db0869caa4b4cc683a24a") format("truetype"); + src: url("./codicon.ttf?6caeeccc06315e827f3bff83885456fb") format("truetype"); } .codicon[class*='codicon-'] { @@ -413,4 +413,5 @@ .codicon-feedback:before { content: "\eb96" } .codicon-group-by-ref-type:before { content: "\eb97" } .codicon-ungroup-by-ref-type:before { content: "\eb98" } -.codicon-debug-alt:before { content: "\f101" } +.codicon-debug-alt-2:before { content: "\f101" } +.codicon-debug-alt:before { content: "\f102" } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index 90ace76ff7..a62bc6bd74 100644 Binary files a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf and b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index 3dd57bf12a..178c26093a 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -7,7 +7,7 @@ import 'vs/css!./gridview'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { Disposable } from 'vs/base/common/lifecycle'; import { tail2 as tail, equals } from 'vs/base/common/arrays'; -import { orthogonal, IView as IGridViewView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize, IGridViewOptions } from './gridview'; +import { orthogonal, IView as IGridViewView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize, IGridViewOptions, IBoundarySashes } from './gridview'; import { Event } from 'vs/base/common/event'; export { Orientation, Sizing as GridViewSizing, IViewSize, orthogonal, LayoutPriority } from './gridview'; @@ -212,6 +212,9 @@ export class Grid extends Disposable { get maximumHeight(): number { return this.gridview.maximumHeight; } get onDidChange(): Event<{ width: number; height: number; } | undefined> { return this.gridview.onDidChange; } + get boundarySashes(): IBoundarySashes { return this.gridview.boundarySashes; } + set boundarySashes(boundarySashes: IBoundarySashes) { this.gridview.boundarySashes = boundarySashes; } + get element(): HTMLElement { return this.gridview.element; } private didLayout = false; diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 83bce75206..6a22c4fdf4 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -21,6 +21,20 @@ export interface IViewSize { readonly height: number; } +interface IRelativeBoundarySashes { + readonly start?: Sash; + readonly end?: Sash; + readonly orthogonalStart?: Sash; + readonly orthogonalEnd?: Sash; +} + +export interface IBoundarySashes { + readonly top?: Sash; + readonly right?: Sash; + readonly bottom?: Sash; + readonly left?: Sash; +} + export interface IView { readonly element: HTMLElement; readonly minimumWidth: number; @@ -32,6 +46,7 @@ export interface IView { readonly snap?: boolean; layout(width: number, height: number, top: number, left: number): void; setVisible?(visible: boolean): void; + setBoundarySashes?(sashes: IBoundarySashes): void; } export interface ISerializableView extends IView { @@ -125,6 +140,22 @@ interface ILayoutContext { readonly absoluteOrthogonalSize: number; } +function toAbsoluteBoundarySashes(sashes: IRelativeBoundarySashes, orientation: Orientation): IBoundarySashes { + if (orientation === Orientation.HORIZONTAL) { + return { left: sashes.start, right: sashes.end, top: sashes.orthogonalStart, bottom: sashes.orthogonalEnd }; + } else { + return { top: sashes.start, bottom: sashes.end, left: sashes.orthogonalStart, right: sashes.orthogonalEnd }; + } +} + +function fromAbsoluteBoundarySashes(sashes: IBoundarySashes, orientation: Orientation): IRelativeBoundarySashes { + if (orientation === Orientation.HORIZONTAL) { + return { start: sashes.left, end: sashes.right, orthogonalStart: sashes.top, orthogonalEnd: sashes.bottom }; + } else { + return { start: sashes.top, end: sashes.bottom, orthogonalStart: sashes.left, orthogonalEnd: sashes.right }; + } +} + class BranchNode implements ISplitView, IDisposable { readonly element: HTMLElement; @@ -217,10 +248,27 @@ class BranchNode implements ISplitView, IDisposable { private splitviewSashResetDisposable: IDisposable = Disposable.None; private childrenSashResetDisposable: IDisposable = Disposable.None; - get orthogonalStartSash(): Sash | undefined { return this.splitview.orthogonalStartSash; } - set orthogonalStartSash(sash: Sash | undefined) { this.splitview.orthogonalStartSash = sash; } - get orthogonalEndSash(): Sash | undefined { return this.splitview.orthogonalEndSash; } - set orthogonalEndSash(sash: Sash | undefined) { this.splitview.orthogonalEndSash = sash; } + private _boundarySashes: IRelativeBoundarySashes = {}; + get boundarySashes(): IRelativeBoundarySashes { return this._boundarySashes; } + set boundarySashes(boundarySashes: IRelativeBoundarySashes) { + this._boundarySashes = boundarySashes; + + this.splitview.orthogonalStartSash = boundarySashes.orthogonalStart; + this.splitview.orthogonalEndSash = boundarySashes.orthogonalEnd; + + for (let index = 0; index < this.children.length; index++) { + const child = this.children[index]; + const first = index === 0; + const last = index === this.children.length - 1; + + child.boundarySashes = { + start: boundarySashes.orthogonalStart, + end: boundarySashes.orthogonalEnd, + orthogonalStart: first ? boundarySashes.start : child.boundarySashes.orthogonalStart, + orthogonalEnd: last ? boundarySashes.end : child.boundarySashes.orthogonalEnd, + }; + } + } constructor( readonly orientation: Orientation, @@ -260,9 +308,15 @@ class BranchNode implements ISplitView, IDisposable { this.splitview = new SplitView(this.element, { ...options, descriptor }); this.children.forEach((node, index) => { - // Set up orthogonal sashes for children - node.orthogonalStartSash = this.splitview.sashes[index - 1]; - node.orthogonalEndSash = this.splitview.sashes[index]; + const first = index === 0; + const last = index === this.children.length; + + node.boundarySashes = { + start: this.boundarySashes.orthogonalStart, + end: this.boundarySashes.orthogonalEnd, + orthogonalStart: first ? this.boundarySashes.start : this.splitview.sashes[index - 1], + orthogonalEnd: last ? this.boundarySashes.end : this.splitview.sashes[index], + }; }); } @@ -335,15 +389,26 @@ class BranchNode implements ISplitView, IDisposable { const first = index === 0; const last = index === this.children.length; this.children.splice(index, 0, node); - node.orthogonalStartSash = this.splitview.sashes[index - 1]; - node.orthogonalEndSash = this.splitview.sashes[index]; + + node.boundarySashes = { + start: this.boundarySashes.orthogonalStart, + end: this.boundarySashes.orthogonalEnd, + orthogonalStart: first ? this.boundarySashes.start : this.splitview.sashes[index - 1], + orthogonalEnd: last ? this.boundarySashes.end : this.splitview.sashes[index], + }; if (!first) { - this.children[index - 1].orthogonalEndSash = this.splitview.sashes[index - 1]; + this.children[index - 1].boundarySashes = { + ...this.children[index - 1].boundarySashes, + orthogonalEnd: this.splitview.sashes[index - 1] + }; } if (!last) { - this.children[index + 1].orthogonalStartSash = this.splitview.sashes[index]; + this.children[index + 1].boundarySashes = { + ...this.children[index + 1].boundarySashes, + orthogonalStart: this.splitview.sashes[index] + }; } } @@ -363,11 +428,17 @@ class BranchNode implements ISplitView, IDisposable { const [child] = this.children.splice(index, 1); if (!first) { - this.children[index - 1].orthogonalEndSash = this.splitview.sashes[index - 1]; + this.children[index - 1].boundarySashes = { + ...this.children[index - 1].boundarySashes, + orthogonalEnd: this.splitview.sashes[index - 1] + }; } if (!last) { // [0,1,2,3] (2) => [0,1,3] - this.children[index].orthogonalStartSash = this.splitview.sashes[Math.max(index - 1, 0)]; + this.children[index].boundarySashes = { + ...this.children[index].boundarySashes, + orthogonalStart: this.splitview.sashes[Math.max(index - 1, 0)] + }; } return child; @@ -408,7 +479,12 @@ class BranchNode implements ISplitView, IDisposable { to = clamp(to, 0, this.children.length); this.splitview.swapViews(from, to); - [this.children[from].orthogonalStartSash, this.children[from].orthogonalEndSash, this.children[to].orthogonalStartSash, this.children[to].orthogonalEndSash] = [this.children[to].orthogonalStartSash, this.children[to].orthogonalEndSash, this.children[from].orthogonalStartSash, this.children[from].orthogonalEndSash]; + + // swap boundary sashes + [this.children[from].boundarySashes, this.children[to].boundarySashes] + = [this.children[from].boundarySashes, this.children[to].boundarySashes]; + + // swap children [this.children[from], this.children[to]] = [this.children[to], this.children[from]]; this.onDidChildrenChange(); @@ -655,12 +731,14 @@ class LeafNode implements ISplitView, IDisposable { return this.orientation === Orientation.HORIZONTAL ? this.maximumWidth : this.maximumHeight; } - set orthogonalStartSash(sash: Sash) { - // noop - } + private _boundarySashes: IRelativeBoundarySashes = {}; + get boundarySashes(): IRelativeBoundarySashes { return this._boundarySashes; } + set boundarySashes(boundarySashes: IRelativeBoundarySashes) { + this._boundarySashes = boundarySashes; - set orthogonalEndSash(sash: Sash) { - // noop + if (this.view.setBoundarySashes) { + this.view.setBoundarySashes(toAbsoluteBoundarySashes(boundarySashes, this.orientation)); + } } layout(size: number, offset: number, ctx: ILayoutContext | undefined): void { @@ -764,6 +842,7 @@ export class GridView implements IDisposable { const { size, orthogonalSize } = this._root; this.root = flipNode(this._root, orthogonalSize, size); this.root.layout(size, 0, { orthogonalSize, absoluteOffset: 0, absoluteOrthogonalOffset: 0, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize }); + this.boundarySashes = this.boundarySashes; } get width(): number { return this.root.width; } @@ -777,6 +856,13 @@ export class GridView implements IDisposable { private _onDidChange = new Relay(); readonly onDidChange = this._onDidChange.event; + private _boundarySashes: IBoundarySashes = {}; + get boundarySashes(): IBoundarySashes { return this._boundarySashes; } + set boundarySashes(boundarySashes: IBoundarySashes) { + this._boundarySashes = boundarySashes; + this.root.boundarySashes = fromAbsoluteBoundarySashes(boundarySashes, this.orientation); + } + /** * The first layout controller makes sure layout only propagates * to the views after the very first call to gridview.layout() @@ -898,6 +984,7 @@ export class GridView implements IDisposable { // we must promote sibling to be the new root parent.removeChild(0); this.root = sibling; + this.boundarySashes = this.boundarySashes; return node.view; } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index c2670776ea..daa30e2c20 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -128,14 +128,14 @@ class AsyncDataTreeRenderer implements IT } } -function asTreeEvent(e: ITreeEvent>): ITreeEvent { +function asTreeEvent(e: ITreeEvent | null>): ITreeEvent { return { browserEvent: e.browserEvent, - elements: e.elements.map(e => e.element as T) + elements: e.elements.map(e => e!.element as T) }; } -function asTreeMouseEvent(e: ITreeMouseEvent>): ITreeMouseEvent { +function asTreeMouseEvent(e: ITreeMouseEvent | null>): ITreeMouseEvent { return { browserEvent: e.browserEvent, element: e.element && e.element.element as T, @@ -143,7 +143,7 @@ function asTreeMouseEvent(e: ITreeMouseEvent(e: ITreeContextMenuEvent>): ITreeContextMenuEvent { +function asTreeContextMenuEvent(e: ITreeContextMenuEvent | null>): ITreeContextMenuEvent { return { browserEvent: e.browserEvent, element: e.element && e.element.element as T, @@ -793,7 +793,11 @@ export class AsyncDataTree implements IDisposable return result.finally(() => this.refreshPromises.delete(node)); } - private _onDidChangeCollapseState({ node, deep }: ICollapseStateChangeEvent, any>): void { + private _onDidChangeCollapseState({ node, deep }: ICollapseStateChangeEvent | null, any>): void { + if (node.element === null) { + return; + } + if (!node.collapsed && node.element.stale) { if (deep) { this.collapse(node.element.element as T); diff --git a/src/vs/base/browser/ui/tree/dataTree.ts b/src/vs/base/browser/ui/tree/dataTree.ts index 766628925b..1eaf31141c 100644 --- a/src/vs/base/browser/ui/tree/dataTree.ts +++ b/src/vs/base/browser/ui/tree/dataTree.ts @@ -37,7 +37,7 @@ export class DataTree extends AbstractTree, options: IDataTreeOptions = {} ) { - super(user, container, delegate, renderers, options); + super(user, container, delegate, renderers, options as IDataTreeOptions); this.identityProvider = options.identityProvider; } @@ -182,7 +182,7 @@ export class DataTree extends AbstractTree this.identityProvider!.getId(element).toString(); + const getId = (element: T | null) => this.identityProvider!.getId(element!).toString(); const focus = this.getFocus().map(getId); const selection = this.getSelection().map(getId); diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index a21f9b8be4..be20a0181a 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -30,7 +30,7 @@ export class ObjectTree, TFilterData = void> extends renderers: ITreeRenderer[], options: IObjectTreeOptions = {} ) { - super(user, container, delegate, renderers, options); + super(user, container, delegate, renderers, options as IObjectTreeOptions); } setChildren(element: T | null, children?: ISequence>): void { @@ -181,7 +181,7 @@ export class CompressibleObjectTree, TFilterData = vo ) { const compressedTreeNodeProvider = () => this; const compressibleRenderers = renderers.map(r => new CompressibleRenderer(compressedTreeNodeProvider, r)); - super(user, container, delegate, compressibleRenderers, asObjectTreeOptions(compressedTreeNodeProvider, options)); + super(user, container, delegate, compressibleRenderers, asObjectTreeOptions(compressedTreeNodeProvider, options)); } setChildren(element: T | null, children?: ISequence>): void { diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index f0e0abdd47..d319ccc219 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -79,35 +79,47 @@ export class ObjectTreeModel, TFilterData extends Non const insertedElements = new Set(); const insertedElementIds = new Set(); - const _onDidCreateNode = (node: ITreeNode) => { - insertedElements.add(node.element); - this.nodes.set(node.element, node); + const _onDidCreateNode = (node: ITreeNode) => { + if (node.element === null) { + return; + } + + const tnode = node as ITreeNode; + + insertedElements.add(tnode.element); + this.nodes.set(tnode.element, tnode); if (this.identityProvider) { - const id = this.identityProvider.getId(node.element).toString(); + const id = this.identityProvider.getId(tnode.element).toString(); insertedElementIds.add(id); - this.nodesByIdentity.set(id, node); + this.nodesByIdentity.set(id, tnode); } if (onDidCreateNode) { - onDidCreateNode(node); + onDidCreateNode(tnode); } }; - const _onDidDeleteNode = (node: ITreeNode) => { - if (!insertedElements.has(node.element)) { - this.nodes.delete(node.element); + const _onDidDeleteNode = (node: ITreeNode) => { + if (node.element === null) { + return; + } + + const tnode = node as ITreeNode; + + if (!insertedElements.has(tnode.element)) { + this.nodes.delete(tnode.element); } if (this.identityProvider) { - const id = this.identityProvider.getId(node.element).toString(); + const id = this.identityProvider.getId(tnode.element).toString(); if (!insertedElementIds.has(id)) { this.nodesByIdentity.delete(id); } } if (onDidDeleteNode) { - onDidDeleteNode(node); + onDidDeleteNode(tnode); } }; diff --git a/src/vs/base/common/cancellation.ts b/src/vs/base/common/cancellation.ts index 5a38066025..7570c124ee 100644 --- a/src/vs/base/common/cancellation.ts +++ b/src/vs/base/common/cancellation.ts @@ -7,18 +7,27 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; export interface CancellationToken { - readonly isCancellationRequested: boolean; + /** - * An event emitted when cancellation is requested + * A flag signalling is cancellation has been requested. + */ + readonly isCancellationRequested: boolean; + + /** + * An event which fires when cancellation is requested. This event + * only ever fires `once` as cancellation can only happen once. Listeners + * that are registered after cancellation will be called (next event loop run), + * but also only once. + * * @event */ - readonly onCancellationRequested: Event; + readonly onCancellationRequested: (listener: (e: any) => any, thisArgs?: any, disposables?: IDisposable[]) => IDisposable; } -const shortcutEvent = Object.freeze(function (callback, context?): IDisposable { +const shortcutEvent: Event = Object.freeze(function (callback, context?): IDisposable { const handle = setTimeout(callback.bind(context), 0); return { dispose() { clearTimeout(handle); } }; -} as Event); +}); export namespace CancellationToken { diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 0c16242740..32992e5523 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -85,6 +85,8 @@ export namespace Event { * Given a collection of events, returns a single event which emits * whenever any of the provided events emit. */ + export function any(...events: Event[]): Event; + export function any(...events: Event[]): Event; export function any(...events: Event[]): Event { return (listener, thisArgs = null, disposables?) => combinedDisposable(...events.map(event => event(e => listener.call(thisArgs, e), null, disposables))); } @@ -271,6 +273,7 @@ export namespace Event { map(fn: (i: T) => O): IChainableEvent; forEach(fn: (i: T) => void): IChainableEvent; filter(fn: (e: T) => boolean): IChainableEvent; + filter(fn: (e: T | R) => e is R): IChainableEvent; reduce(merge: (last: R | undefined, event: T) => R, initial?: R): IChainableEvent; latch(): IChainableEvent; debounce(merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent; @@ -291,6 +294,8 @@ export namespace Event { return new ChainableEvent(forEach(this.event, fn)); } + filter(fn: (e: T) => boolean): IChainableEvent; + filter(fn: (e: T | R) => e is R): IChainableEvent; filter(fn: (e: T) => boolean): IChainableEvent { return new ChainableEvent(filter(this.event, fn)); } diff --git a/src/vs/base/node/config.ts b/src/vs/base/node/config.ts deleted file mode 100644 index 41dac28ee2..0000000000 --- a/src/vs/base/node/config.ts +++ /dev/null @@ -1,189 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as fs from 'fs'; -import { dirname } from 'vs/base/common/path'; -import * as objects from 'vs/base/common/objects'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; -import * as json from 'vs/base/common/json'; -import { statLink } from 'vs/base/node/pfs'; -import { realpath } from 'vs/base/node/extpath'; -import { watchFolder, watchFile } from 'vs/base/node/watcher'; - -export interface IConfigurationChangeEvent { - config: T; -} - -export interface IConfigWatcher { - path: string; - hasParseErrors: boolean; - - reload(callback: (config: T) => void): void; - getConfig(): T; -} - -export interface IConfigOptions { - onError: (error: Error | string) => void; - defaultConfig: T; - changeBufferDelay?: number; - parse?: (content: string, errors: any[]) => T; - initCallback?: (config: T) => void; -} - -/** - * A simple helper to watch a configured file for changes and process its contents as JSON object. - * Supports: - * - comments in JSON files and errors - * - symlinks for the config file itself - * - delayed processing of changes to accomodate for lots of changes - * - configurable defaults - */ -export class ConfigWatcher extends Disposable implements IConfigWatcher { - private cache: T | undefined; - private parseErrors: json.ParseError[] | undefined; - private disposed: boolean | undefined; - private loaded: boolean | undefined; - private timeoutHandle: NodeJS.Timer | null | undefined; - private readonly _onDidUpdateConfiguration: Emitter>; - - constructor(private _path: string, private options: IConfigOptions = { defaultConfig: Object.create(null), onError: error => console.error(error) }) { - super(); - this._onDidUpdateConfiguration = this._register(new Emitter>()); - - this.registerWatcher(); - this.initAsync(); - } - - get path(): string { - return this._path; - } - - get hasParseErrors(): boolean { - return !!this.parseErrors && this.parseErrors.length > 0; - } - - get onDidUpdateConfiguration(): Event> { - return this._onDidUpdateConfiguration.event; - } - - private initAsync(): void { - this.loadAsync(config => { - if (!this.loaded) { - this.updateCache(config); // prevent race condition if config was loaded sync already - } - if (this.options.initCallback) { - this.options.initCallback(this.getConfig()); - } - }); - } - - private updateCache(value: T): void { - this.cache = value; - this.loaded = true; - } - - private loadSync(): T { - try { - return this.parse(fs.readFileSync(this._path).toString()); - } catch (error) { - return this.options.defaultConfig; - } - } - - private loadAsync(callback: (config: T) => void): void { - fs.readFile(this._path, (error, raw) => { - if (error) { - return callback(this.options.defaultConfig); - } - - return callback(this.parse(raw.toString())); - }); - } - - private parse(raw: string): T { - let res: T; - try { - this.parseErrors = []; - res = this.options.parse ? this.options.parse(raw, this.parseErrors) : json.parse(raw, this.parseErrors); - - return res || this.options.defaultConfig; - } catch (error) { - return this.options.defaultConfig; // Ignore parsing errors - } - } - - private registerWatcher(): void { - - // Watch the parent of the path so that we detect ADD and DELETES - const parentFolder = dirname(this._path); - this.watch(parentFolder, true); - - // Check if the path is a symlink and watch its target if so - this.handleSymbolicLink().then(undefined, () => { /* ignore error */ }); - } - - private async handleSymbolicLink(): Promise { - const { stat, symbolicLink } = await statLink(this._path); - if (symbolicLink && !stat.isDirectory()) { - const realPath = await realpath(this._path); - - this.watch(realPath, false); - } - } - - private watch(path: string, isFolder: boolean): void { - if (this.disposed) { - return; // avoid watchers that will never get disposed by checking for being disposed - } - - if (isFolder) { - this._register(watchFolder(path, (type, path) => path === this._path ? this.onConfigFileChange() : undefined, error => this.options.onError(error))); - } else { - this._register(watchFile(path, () => this.onConfigFileChange(), error => this.options.onError(error))); - } - } - - private onConfigFileChange(): void { - if (this.timeoutHandle) { - global.clearTimeout(this.timeoutHandle); - this.timeoutHandle = null; - } - - // we can get multiple change events for one change, so we buffer through a timeout - this.timeoutHandle = global.setTimeout(() => this.reload(), this.options.changeBufferDelay || 0); - } - - reload(callback?: (config: T) => void): void { - this.loadAsync(currentConfig => { - if (!objects.equals(currentConfig, this.cache)) { - this.updateCache(currentConfig); - - this._onDidUpdateConfiguration.fire({ config: currentConfig }); - } - - if (callback) { - return callback(currentConfig); - } - }); - } - - getConfig(): T { - this.ensureLoaded(); - - return this.cache!; - } - - private ensureLoaded(): void { - if (!this.loaded) { - this.updateCache(this.loadSync()); - } - } - - dispose(): void { - this.disposed = true; - super.dispose(); - } -} diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 08fc6ca613..499c75b28c 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -52,7 +52,7 @@ export function toDecodeStream(readable: Readable, options: IDecodeStreamOptions private bufferedChunks: Buffer[] = []; private bytesBuffered = 0; - _write(chunk: Buffer, encoding: string, callback: (error: Error | null) => void): void { + _write(chunk: Buffer, encoding: string, callback: (error: Error | null | undefined) => void): void { if (!Buffer.isBuffer(chunk)) { return callback(new Error('toDecodeStream(): data must be a buffer')); } @@ -84,7 +84,7 @@ export function toDecodeStream(readable: Readable, options: IDecodeStreamOptions } } - _startDecodeStream(callback: (error: Error | null) => void): void { + _startDecodeStream(callback: (error: Error | null | undefined) => void): void { // detect encoding from buffer this.decodeStreamPromise = Promise.resolve(detectEncodingFromBuffer({ diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 30724dc30f..c9b993643f 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -12,7 +12,6 @@ import { mkdirp, rimraf } from 'vs/base/node/pfs'; import { open as _openZip, Entry, ZipFile } from 'yauzl'; import * as yazl from 'yazl'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; export interface IExtractOptions { overwrite?: boolean; @@ -80,7 +79,7 @@ function extractEntry(stream: Readable, fileName: string, mode: number, targetPa let istream: WriteStream; - Event.once(token.onCancellationRequested)(() => { + token.onCancellationRequested(() => { if (istream) { istream.destroy(); } @@ -107,7 +106,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok let last = createCancelablePromise(() => Promise.resolve()); let extractedEntriesCount = 0; - Event.once(token.onCancellationRequested)(() => { + token.onCancellationRequested(() => { last.cancel(); zipfile.close(); }); diff --git a/src/vs/base/parts/tree/browser/treeModel.ts b/src/vs/base/parts/tree/browser/treeModel.ts index d29542bf51..dd1fb978f0 100644 --- a/src/vs/base/parts/tree/browser/treeModel.ts +++ b/src/vs/base/parts/tree/browser/treeModel.ts @@ -141,8 +141,8 @@ export class ItemRegistry { readonly onDidCollapseItem: Event = this._onDidCollapseItem.event; private _onDidAddTraitItem = new EventMultiplexer(); readonly onDidAddTraitItem: Event = this._onDidAddTraitItem.event; - private _onDidRemoveTraitItem = new EventMultiplexer(); - readonly onDidRemoveTraitItem: Event = this._onDidRemoveTraitItem.event; + private _onDidRemoveTraitItem = new EventMultiplexer(); + readonly onDidRemoveTraitItem: Event = this._onDidRemoveTraitItem.event; private _onDidRefreshItem = new EventMultiplexer(); readonly onDidRefreshItem: Event = this._onDidRefreshItem.event; private _onRefreshItemChildren = new EventMultiplexer(); @@ -273,8 +273,8 @@ export class Item { readonly onDidCollapse: Event = this._onDidCollapse.event; private readonly _onDidAddTrait = new Emitter(); readonly onDidAddTrait: Event = this._onDidAddTrait.event; - private readonly _onDidRemoveTrait = new Emitter(); - readonly onDidRemoveTrait: Event = this._onDidRemoveTrait.event; + private readonly _onDidRemoveTrait = new Emitter(); + readonly onDidRemoveTrait: Event = this._onDidRemoveTrait.event; private readonly _onDidRefresh = new Emitter(); readonly onDidRefresh: Event = this._onDidRefresh.event; private readonly _onRefreshChildren = new Emitter(); @@ -895,8 +895,8 @@ export class TreeModel { readonly onDidCollapseItem: Event = this._onDidCollapseItem.event; private _onDidAddTraitItem = new Relay(); readonly onDidAddTraitItem: Event = this._onDidAddTraitItem.event; - private _onDidRemoveTraitItem = new Relay(); - readonly onDidRemoveTraitItem: Event = this._onDidRemoveTraitItem.event; + private _onDidRemoveTraitItem = new Relay(); + readonly onDidRemoveTraitItem: Event = this._onDidRemoveTraitItem.event; private _onDidRefreshItem = new Relay(); readonly onDidRefreshItem: Event = this._onDidRefreshItem.event; private _onRefreshItemChildren = new Relay(); diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index c1e1efa294..b70335d62a 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -557,7 +557,7 @@ export class TreeView extends HeightMap { this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'MSGestureTap', (e) => this.onMsGestureTap(e))); // these events come too fast, we throttle them - this.viewListeners.push(DOM.addDisposableThrottledListener(this.wrapper, 'MSGestureChange', (e) => this.onThrottledMsGestureChange(e), (lastEvent: IThrottledGestureEvent, event: MSGestureEvent): IThrottledGestureEvent => { + this.viewListeners.push(DOM.addDisposableThrottledListener(this.wrapper, 'MSGestureChange', e => this.onThrottledMsGestureChange(e), (lastEvent, event) => { event.stopPropagation(); event.preventDefault(); diff --git a/src/vs/base/test/browser/linkedText.test.ts b/src/vs/base/test/browser/linkedText.test.ts new file mode 100644 index 0000000000..d1e3bbef14 --- /dev/null +++ b/src/vs/base/test/browser/linkedText.test.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { parseLinkedText } from 'vs/base/browser/linkedText'; + +suite('LinkedText', () => { + test('parses correctly', () => { + assert.deepEqual(parseLinkedText(''), []); + assert.deepEqual(parseLinkedText('hello'), ['hello']); + assert.deepEqual(parseLinkedText('hello there'), ['hello there']); + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href).'), [ + 'Some message with ', + { label: 'link text', href: 'http://link.href' }, + '.' + ]); + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href "and a title").'), [ + 'Some message with ', + { label: 'link text', href: 'http://link.href', title: 'and a title' }, + '.' + ]); + assert.deepEqual(parseLinkedText('Some message with [link text](random stuff).'), [ + 'Some message with [link text](random stuff).' + ]); + assert.deepEqual(parseLinkedText('Some message with [https link](https://link.href).'), [ + 'Some message with ', + { label: 'https link', href: 'https://link.href' }, + '.' + ]); + assert.deepEqual(parseLinkedText('Some message with [https link](https:).'), [ + 'Some message with [https link](https:).' + ]); + assert.deepEqual(parseLinkedText('Some message with [a command](command:foobar).'), [ + 'Some message with ', + { label: 'a command', href: 'command:foobar' }, + '.' + ]); + assert.deepEqual(parseLinkedText('Some message with [a command](command:).'), [ + 'Some message with [a command](command:).' + ]); + assert.deepEqual(parseLinkedText('link [one](command:foo "nice") and link [two](http://foo)...'), [ + 'link ', + { label: 'one', href: 'command:foo', title: 'nice' }, + ' and link ', + { label: 'two', href: 'http://foo' }, + '...' + ]); + assert.deepEqual(parseLinkedText('link\n[one](command:foo "nice")\nand link [two](http://foo)...'), [ + 'link\n', + { label: 'one', href: 'command:foo', title: 'nice' }, + '\nand link ', + { label: 'two', href: 'http://foo' }, + '...' + ]); + }); +}); diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index 449d4cac11..39d91ed1f5 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -108,6 +108,7 @@ suite('Resources', () => { assert.equal(joinPath(URI.file('/foo/bar'), '/./file.js').toString(), 'file:///foo/bar/file.js'); assert.equal(joinPath(URI.file('/foo/bar'), '../file.js').toString(), 'file:///foo/file.js'); } + assert.equal(joinPath(URI.parse('foo://a/foo/bar')).toString(), 'foo://a/foo/bar'); assert.equal(joinPath(URI.parse('foo://a/foo/bar'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); assert.equal(joinPath(URI.parse('foo://a/foo/bar'), 'file.js').toString(), 'foo://a/foo/bar/file.js'); assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); diff --git a/src/vs/base/test/node/config.test.ts b/src/vs/base/test/node/config.test.ts deleted file mode 100644 index 98743764b3..0000000000 --- a/src/vs/base/test/node/config.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as os from 'os'; - -import * as path from 'vs/base/common/path'; -import * as fs from 'fs'; -import * as uuid from 'vs/base/common/uuid'; -import { ConfigWatcher } from 'vs/base/node/config'; -import { testFile } from 'vs/base/test/node/utils'; - -suite('Config', () => { - - test('defaults', () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'config', id); - const testFile = path.join(newDir, 'config.json'); - - let watcher = new ConfigWatcher<{}>(testFile); - - let config = watcher.getConfig(); - assert.ok(config); - assert.equal(Object.keys(config), 0); - - watcher.dispose(); - - let watcher2 = new ConfigWatcher(testFile, { defaultConfig: ['foo'], onError: console.error }); - - let config2 = watcher2.getConfig(); - assert.ok(Array.isArray(config2)); - assert.equal(config2.length, 1); - - watcher.dispose(); - }); - - test('getConfig / getValue', function () { - return testFile('config', 'config.json').then(res => { - fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "bar" }'); - - let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); - - let config = watcher.getConfig(); - assert.ok(config); - assert.equal(config.foo, 'bar'); - assert.ok(!watcher.hasParseErrors); - - watcher.dispose(); - - return res.cleanUp(); - }); - }); - - test('getConfig / getValue - broken JSON', function () { - return testFile('config', 'config.json').then(res => { - fs.writeFileSync(res.testFile, '// my comment\n "foo": "bar ... '); - - let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); - - let config = watcher.getConfig(); - assert.ok(config); - assert.ok(!config.foo); - - assert.ok(watcher.hasParseErrors); - - watcher.dispose(); - - return res.cleanUp(); - }); - }); - - // test('watching', function (done) { - // this.timeout(10000); // watching is timing intense - - // testFile('config', 'config.json').then(res => { - // fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "bar" }'); - - // let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); - // watcher.getConfig(); // ensure we are in sync - - // fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "changed" }'); - - // watcher.onDidUpdateConfiguration(event => { - // assert.ok(event); - // assert.equal(event.config.foo, 'changed'); - // assert.equal(watcher.getValue('foo'), 'changed'); - - // watcher.dispose(); - - // res.cleanUp().then(done, done); - // }); - // }, done); - // }); - - // test('watching also works when file created later', function (done) { - // this.timeout(10000); // watching is timing intense - - // testFile('config', 'config.json').then(res => { - // let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); - // watcher.getConfig(); // ensure we are in sync - - // fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "changed" }'); - - // watcher.onDidUpdateConfiguration(event => { - // assert.ok(event); - // assert.equal(event.config.foo, 'changed'); - // assert.equal(watcher.getValue('foo'), 'changed'); - - // watcher.dispose(); - - // res.cleanUp().then(done, done); - // }); - // }, done); - // }); - - // test('watching detects the config file getting deleted', function (done) { - // this.timeout(10000); // watching is timing intense - - // testFile('config', 'config.json').then(res => { - // fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "bar" }'); - - // let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); - // watcher.getConfig(); // ensure we are in sync - - // watcher.onDidUpdateConfiguration(event => { - // assert.ok(event); - - // watcher.dispose(); - - // res.cleanUp().then(done, done); - // }); - - // fs.unlinkSync(res.testFile); - // }, done); - // }); - - test('reload', function (done) { - testFile('config', 'config.json').then(res => { - fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "bar" }'); - - let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile, { changeBufferDelay: 100, onError: console.error, defaultConfig: { foo: 'bar' } }); - watcher.getConfig(); // ensure we are in sync - - fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "changed" }'); - - // still old values because change is not bubbling yet - assert.equal(watcher.getConfig().foo, 'bar'); - - // force a load from disk - watcher.reload(config => { - assert.equal(config.foo, 'changed'); - assert.equal(watcher.getConfig().foo, 'changed'); - - watcher.dispose(); - - res.cleanUp().then(done, done); - }); - }, done); - }); -}); \ No newline at end of file diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 4864b9255e..47773b730c 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkbenchConstructionOptions, create, URI, Event, Emitter, UriComponents, ICredentialsProvider, IURLCallbackProvider, IWorkspaceProvider, IWorkspace } from 'vs/workbench/workbench.web.api'; +import { IWorkbenchConstructionOptions, create, URI, Event, Emitter, UriComponents, ICredentialsProvider, IURLCallbackProvider, IWorkspaceProvider, IWorkspace, IApplicationLinkProvider, IApplicationLink } from 'vs/workbench/workbench.web.api'; import { generateUuid } from 'vs/base/common/uuid'; import { CancellationToken } from 'vs/base/common/cancellation'; import { streamToBuffer } from 'vs/base/common/buffer'; @@ -12,6 +12,10 @@ import { request } from 'vs/base/parts/request/browser/request'; import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows'; import { isEqual } from 'vs/base/common/resources'; import { isStandalone } from 'vs/base/browser/browser'; +import product from 'vs/platform/product/common/product'; +import { Schemas } from 'vs/base/common/network'; +import { posix } from 'vs/base/common/path'; +import { localize } from 'vs/nls'; interface ICredential { service: string; @@ -275,6 +279,39 @@ class WorkspaceProvider implements IWorkspaceProvider { } } +class ApplicationLinkProvider { + + private links: IApplicationLink[] | undefined = undefined; + + constructor(workspace: IWorkspace) { + this.computeLink(workspace); + } + + private computeLink(workspace: IWorkspace): void { + if (!workspace) { + return; // not for empty workspaces + } + + const workspaceUri = isWorkspaceToOpen(workspace) ? workspace.workspaceUri : isFolderToOpen(workspace) ? workspace.folderUri : undefined; + if (workspaceUri) { + this.links = [{ + uri: URI.from({ + scheme: product.quality === 'stable' ? 'vscode' : 'vscode-insiders', + authority: Schemas.vscodeRemote, + path: posix.join(posix.sep, workspaceUri.authority, workspaceUri.path), + query: workspaceUri.query, + fragment: workspaceUri.fragment, + }), + label: localize('openInDesktop', "Open in Desktop") + }]; + } + } + + get provider(): IApplicationLinkProvider { + return () => this.links; + } +} + (function () { // Find config by checking for DOM @@ -343,6 +380,7 @@ class WorkspaceProvider implements IWorkspaceProvider { ...config, workspaceProvider: new WorkspaceProvider(workspace, payload), urlCallbackProvider: new PollingURLCallbackProvider(), - credentialsProvider: new LocalStorageCredentialsProvider() + credentialsProvider: new LocalStorageCredentialsProvider(), + applicationLinkProvider: new ApplicationLinkProvider(workspace).provider }); })(); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index d2457db13a..00ac8234cb 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -120,10 +120,20 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const mainProcessService = new MainProcessService(server, mainRouter); services.set(IMainProcessService, mainProcessService); - const configurationService = new ConfigurationService(environmentService.settingsResource); + // Files + const fileService = new FileService(logService); + services.set(IFileService, fileService); + disposables.add(fileService); + const diskFileSystemProvider = new DiskFileSystemProvider(logService); + disposables.add(diskFileSystemProvider); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + + // Configuration + const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); disposables.add(configurationService); await configurationService.initialize(); + // Storage const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService); await storageService.initialize(); services.set(IStorageService, storageService); @@ -136,19 +146,9 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IRequestService, new SyncDescriptor(RequestService)); services.set(ILoggerService, new SyncDescriptor(LoggerService)); - const electronService = createChannelSender(mainProcessService.getChannel('electron'), { context: configuration.windowId }); services.set(IElectronService, electronService); - // Files - const fileService = new FileService(logService); - services.set(IFileService, fileService); - disposables.add(fileService); - - const diskFileSystemProvider = new DiskFileSystemProvider(logService); - disposables.add(diskFileSystemProvider); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); - services.set(IDownloadService, new SyncDescriptor(DownloadService)); const instantiationService = new InstantiationService(services); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 28911ff396..2a927b74be 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -55,7 +55,7 @@ import { MenubarMainService } from 'vs/platform/menubar/electron-main/menubarMai import { RunOnceScheduler } from 'vs/base/common/async'; import { registerContextMenuListener } from 'vs/base/parts/contextmenu/electron-main/contextmenu'; import { homedir } from 'os'; -import { join, sep } from 'vs/base/common/path'; +import { join, sep, posix } from 'vs/base/common/path'; import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap'; @@ -70,9 +70,6 @@ import { WorkspacesMainService, IWorkspacesMainService } from 'vs/platform/works import { statSync } from 'fs'; import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsIpc'; import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { IFileService } from 'vs/platform/files/common/files'; -import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { IElectronMainService, ElectronMainService } from 'vs/platform/electron/electron-main/electronMainService'; import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService'; @@ -434,12 +431,6 @@ export class CodeApplication extends Disposable { private async createServices(machineId: string, trueMachineId: string | undefined, sharedProcess: SharedProcess, sharedProcessReady: Promise>): Promise { const services = new ServiceCollection(); - const fileService = this._register(new FileService(this.logService)); - services.set(IFileService, fileService); - - const diskFileSystemProvider = this._register(new DiskFileSystemProvider(this.logService)); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); - switch (process.platform) { case 'win32': services.set(IUpdateService, new SyncDescriptor(Win32UpdateService)); @@ -600,14 +591,42 @@ export class CodeApplication extends Disposable { urlService.registerHandler({ async handleURL(uri: URI, options?: IOpenURLOptions): Promise { - // Catch file URLs - if (uri.authority === Schemas.file && !!uri.path) { + // Catch file/remote URLs + if ((uri.authority === Schemas.file || uri.authority === Schemas.vscodeRemote) && !!uri.path) { const cli = assign(Object.create(null), environmentService.args); - const urisToOpen = [{ fileUri: URI.file(uri.fsPath) }]; + const urisToOpen: IWindowOpenable[] = []; - windowsMainService.open({ context: OpenContext.API, cli, urisToOpen, gotoLineMode: true }); + // File path + if (uri.authority === Schemas.file) { + // we configure as fileUri, but later validation will + // make sure to open as folder or workspace if possible + urisToOpen.push({ fileUri: URI.file(uri.fsPath) }); + } - return true; + // Remote path + else { + // Example conversion: + // From: vscode://vscode-remote/wsl+ubuntu/mnt/c/GitDevelopment/monaco + // To: vscode-remote://wsl+ubuntu/mnt/c/GitDevelopment/monaco + const secondSlash = uri.path.indexOf(posix.sep, 1 /* skip over the leading slash */); + if (secondSlash !== -1) { + const authority = uri.path.substring(1, secondSlash); + const path = uri.path.substring(secondSlash); + const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query: uri.query, fragment: uri.fragment }); + + if (hasWorkspaceFileExtension(path)) { + urisToOpen.push({ workspaceUri: remoteUri }); + } else { + urisToOpen.push({ folderUri: remoteUri }); + } + } + } + + if (urisToOpen.length > 0) { + windowsMainService.open({ context: OpenContext.API, cli, urisToOpen, gotoLineMode: true }); + + return true; + } } return false; diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 79f9874681..5681e2c83a 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -42,6 +42,10 @@ import { once } from 'vs/base/common/functional'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsIpc'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { Schemas } from 'vs/base/common/network'; +import { IFileService } from 'vs/platform/files/common/files'; class ExpectedError extends Error { readonly isExpected = true; @@ -118,12 +122,16 @@ class CodeMain { const environmentService = accessor.get(IEnvironmentService); const logService = accessor.get(ILogService); const lifecycleMainService = accessor.get(ILifecycleMainService); + const fileService = accessor.get(IFileService); const configurationService = accessor.get(IConfigurationService); const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleMainService, instantiationService, true); bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel()); - once(lifecycleMainService.onWillShutdown)(() => (configurationService as ConfigurationService).dispose()); + once(lifecycleMainService.onWillShutdown)(() => { + fileService.dispose(); + (configurationService as ConfigurationService).dispose(); + }); return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup(); }); @@ -143,7 +151,12 @@ class CodeMain { process.once('exit', () => logService.dispose()); services.set(ILogService, logService); - services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource)); + const fileService = new FileService(logService); + services.set(IFileService, fileService); + const diskFileSystemProvider = new DiskFileSystemProvider(logService); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + + services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource, fileService)); services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService)); services.set(IStateService, new SyncDescriptor(StateService)); services.set(IRequestService, new SyncDescriptor(RequestMainService)); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 3d8474347b..ec69823e65 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -306,16 +306,6 @@ export async function main(argv: ParsedArgs): Promise { await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath] .map((path): undefined | Promise => path ? mkdirp(path) : undefined)); - const configurationService = new ConfigurationService(environmentService.settingsResource); - disposables.add(configurationService); - await configurationService.initialize(); - - services.set(IEnvironmentService, environmentService); - services.set(ILogService, logService); - services.set(IConfigurationService, configurationService); - services.set(IStateService, new SyncDescriptor(StateService)); - services.set(IProductService, { _serviceBrand: undefined, ...product }); - // Files const fileService = new FileService(logService); disposables.add(fileService); @@ -325,6 +315,16 @@ export async function main(argv: ParsedArgs): Promise { disposables.add(diskFileSystemProvider); fileService.registerProvider(Schemas.file, diskFileSystemProvider); + const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); + disposables.add(configurationService); + await configurationService.initialize(); + + services.set(IEnvironmentService, environmentService); + services.set(ILogService, logService); + services.set(IConfigurationService, configurationService); + services.set(IStateService, new SyncDescriptor(StateService)); + services.set(IProductService, { _serviceBrand: undefined, ...product }); + const instantiationService: IInstantiationService = new InstantiationService(services); return instantiationService.invokeFunction(async accessor => { diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 772e75ae42..16cffd848d 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -103,7 +103,15 @@ export class OpenerService implements IOpenerService { // Default external opener is going through window.open() this._externalOpener = { openExternal: href => { - dom.windowOpenNoOpener(href); + // ensure to open HTTP/HTTPS links into new windows + // to not trigger a navigation. Any other link is + // safe to be set as HREF to prevent a blank window + // from opening. + if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) { + dom.windowOpenNoOpener(href); + } else { + window.location.href = href; + } return Promise.resolve(true); } }; diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index 77c6b2d307..0330103d59 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -118,7 +118,7 @@ export class View extends ViewEventHandler { this._context = new ViewContext(configuration, themeService.getTheme(), model, this.eventDispatcher); this._register(themeService.onThemeChange(theme => { - this._context.theme = theme; + this._context.theme.update(theme); this.eventDispatcher.emit(new viewEvents.ViewThemeChangedEvent()); this.render(true, false); })); diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index bf3cea82f9..a743f70487 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -22,11 +22,11 @@ import { MinimapCharRenderer } from 'vs/editor/browser/viewParts/minimap/minimap import { Constants } from 'vs/editor/browser/viewParts/minimap/minimapCharSheet'; import { MinimapTokensColorTracker } from 'vs/editor/common/viewModel/minimapTokensColorTracker'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; -import { ViewContext } from 'vs/editor/common/view/viewContext'; +import { ViewContext, EditorTheme } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { minimapSelection, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, minimapBackground } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant, ITheme } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel'; import { Selection } from 'vs/editor/common/core/selection'; import { Color } from 'vs/base/common/color'; @@ -109,7 +109,7 @@ class MinimapOptions { public readonly backgroundColor: RGBA8; - constructor(configuration: IConfiguration, theme: ITheme, tokensColorTracker: MinimapTokensColorTracker) { + constructor(configuration: IConfiguration, theme: EditorTheme, tokensColorTracker: MinimapTokensColorTracker) { const options = configuration.options; const pixelRatio = options.get(EditorOption.pixelRatio); const layoutInfo = options.get(EditorOption.layoutInfo); @@ -137,7 +137,7 @@ class MinimapOptions { this.backgroundColor = MinimapOptions._getMinimapBackground(theme, tokensColorTracker); } - private static _getMinimapBackground(theme: ITheme, tokensColorTracker: MinimapTokensColorTracker): RGBA8 { + private static _getMinimapBackground(theme: EditorTheme, tokensColorTracker: MinimapTokensColorTracker): RGBA8 { const themeColor = theme.getColor(minimapBackground); if (themeColor) { return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, themeColor.rgba.a); diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index fe2a89b63c..efedcc7411 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -12,9 +12,8 @@ import { IConfiguration } from 'vs/editor/common/editorCommon'; import { TokenizationRegistry } from 'vs/editor/common/modes'; import { editorCursorForeground, editorOverviewRulerBorder } from 'vs/editor/common/view/editorColorRegistry'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; -import { ViewContext } from 'vs/editor/common/view/viewContext'; +import { ViewContext, EditorTheme } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { ITheme } from 'vs/platform/theme/common/themeService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; class Settings { @@ -42,7 +41,7 @@ class Settings { public readonly x: number[]; public readonly w: number[]; - constructor(config: IConfiguration, theme: ITheme) { + constructor(config: IConfiguration, theme: EditorTheme) { const options = config.options; this.lineHeight = options.get(EditorOption.lineHeight); this.pixelRatio = options.get(EditorOption.pixelRatio); diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index dd438c146e..edabe24555 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -29,12 +29,13 @@ import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageCo import { NULL_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/nullMode'; import { ignoreBracketsInToken } from 'vs/editor/common/modes/supports'; import { BracketsUtils, RichEditBracket, RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets'; -import { ITheme, ThemeColor } from 'vs/platform/theme/common/themeService'; +import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer'; import { TokensStore, MultilineTokens, countEOL, MultilineTokens2, TokensStore2 } from 'vs/editor/common/model/tokensStore'; import { Color } from 'vs/base/common/color'; import { Constants } from 'vs/base/common/uint'; +import { EditorTheme } from 'vs/editor/common/view/viewContext'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -2945,7 +2946,7 @@ export class ModelDecorationOverviewRulerOptions extends DecorationOptions { this.position = (typeof options.position === 'number' ? options.position : model.OverviewRulerLane.Center); } - public getColor(theme: ITheme): string { + public getColor(theme: EditorTheme): string { if (!this._resolvedColor) { if (theme.type !== 'light' && this.darkColor) { this._resolvedColor = this._resolveColor(this.darkColor, theme); @@ -2960,7 +2961,7 @@ export class ModelDecorationOverviewRulerOptions extends DecorationOptions { this._resolvedColor = null; } - private _resolveColor(color: string | ThemeColor, theme: ITheme): string { + private _resolveColor(color: string | ThemeColor, theme: EditorTheme): string { if (typeof color === 'string') { return color; } @@ -2982,7 +2983,7 @@ export class ModelDecorationMinimapOptions extends DecorationOptions { this.position = options.position; } - public getColor(theme: ITheme): Color | undefined { + public getColor(theme: EditorTheme): Color | undefined { if (!this._resolvedColor) { if (theme.type !== 'light' && this.darkColor) { this._resolvedColor = this._resolveColor(this.darkColor, theme); @@ -2998,7 +2999,7 @@ export class ModelDecorationMinimapOptions extends DecorationOptions { this._resolvedColor = undefined; } - private _resolveColor(color: string | ThemeColor, theme: ITheme): Color | undefined { + private _resolveColor(color: string | ThemeColor, theme: EditorTheme): Color | undefined { if (typeof color === 'string') { return Color.fromHex(color); } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 3beda583a2..282f0641f2 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -615,7 +615,9 @@ export interface CodeActionProvider { /** * Optional list of CodeActionKinds that this provider returns. */ - providedCodeActionKinds?: ReadonlyArray; + readonly providedCodeActionKinds?: ReadonlyArray; + + readonly documentation?: ReadonlyArray<{ readonly kind: string, readonly command: Command }>; /** * @internal diff --git a/src/vs/editor/common/view/viewContext.ts b/src/vs/editor/common/view/viewContext.ts index 1d9a57383d..3e31d7a1fc 100644 --- a/src/vs/editor/common/view/viewContext.ts +++ b/src/vs/editor/common/view/viewContext.ts @@ -7,7 +7,30 @@ import { IConfiguration } from 'vs/editor/common/editorCommon'; import { ViewEventDispatcher } from 'vs/editor/common/view/viewEventDispatcher'; import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; import { IViewLayout, IViewModel } from 'vs/editor/common/viewModel/viewModel'; -import { ITheme } from 'vs/platform/theme/common/themeService'; +import { ITheme, ThemeType } from 'vs/platform/theme/common/themeService'; +import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; +import { Color } from 'vs/base/common/color'; + +export class EditorTheme { + + private _theme: ITheme; + + public get type(): ThemeType { + return this._theme.type; + } + + constructor(theme: ITheme) { + this._theme = theme; + } + + public update(theme: ITheme): void { + this._theme = theme; + } + + public getColor(color: ColorIdentifier): Color | undefined { + return this._theme.getColor(color); + } +} export class ViewContext { @@ -15,8 +38,7 @@ export class ViewContext { public readonly model: IViewModel; public readonly viewLayout: IViewLayout; public readonly privateViewEventBus: ViewEventDispatcher; - - public theme: ITheme; // will be updated + public readonly theme: EditorTheme; constructor( configuration: IConfiguration, @@ -25,7 +47,7 @@ export class ViewContext { privateViewEventBus: ViewEventDispatcher ) { this.configuration = configuration; - this.theme = theme; + this.theme = new EditorTheme(theme); this.model = model; this.viewLayout = model.viewLayout; this.privateViewEventBus = privateViewEventBus; diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index edffb78a46..69d268457d 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -13,9 +13,9 @@ import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/ import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer'; import { ICoordinatesConverter, IOverviewRulerDecorations, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; -import { ITheme } from 'vs/platform/theme/common/themeService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; +import { EditorTheme } from 'vs/editor/common/view/viewContext'; export class OutputPosition { outputLineIndex: number; @@ -131,7 +131,7 @@ export interface IViewModelLinesCollection extends IDisposable { getViewLineData(viewLineNumber: number): ViewLineData; getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): Array; - getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): IOverviewRulerDecorations; + getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: EditorTheme): IOverviewRulerDecorations; getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): IModelDecoration[]; } @@ -940,7 +940,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); } - public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): IOverviewRulerDecorations { + public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: EditorTheme): IOverviewRulerDecorations { const decorations = this.model.getOverviewRulerDecorations(ownerId, filterOutValidation); const result = new OverviewRulerDecorations(); for (const decoration of decorations) { @@ -1561,7 +1561,7 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return result; } - public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): IOverviewRulerDecorations { + public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: EditorTheme): IOverviewRulerDecorations { const decorations = this.model.getOverviewRulerDecorations(ownerId, filterOutValidation); const result = new OverviewRulerDecorations(); for (const decoration of decorations) { diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index ac08c4318a..edd17ac3e8 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -14,7 +14,7 @@ import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, T import { IViewEventListener } from 'vs/editor/common/view/viewEvents'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; -import { ITheme } from 'vs/platform/theme/common/themeService'; +import { EditorTheme } from 'vs/editor/common/view/viewContext'; export interface IViewWhitespaceViewportData { readonly id: string; @@ -127,7 +127,7 @@ export interface IViewModel { getLineMaxColumn(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; - getAllOverviewRulerDecorations(theme: ITheme): IOverviewRulerDecorations; + getAllOverviewRulerDecorations(theme: EditorTheme): IOverviewRulerDecorations; invalidateOverviewRulerColorCache(): void; invalidateMinimapColorCache(): void; getValueInRange(range: Range, eol: EndOfLinePreference): string; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index dcb77051f9..bca4ad2bd9 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -21,9 +21,9 @@ import { ViewLayout } from 'vs/editor/common/viewLayout/viewLayout'; import { IViewModelLinesCollection, IdentityLinesCollection, SplitLinesCollection, ILineBreaksComputerFactory } from 'vs/editor/common/viewModel/splitLinesCollection'; import { ICoordinatesConverter, IOverviewRulerDecorations, IViewModel, MinimapLinesRenderingData, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel'; import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations'; -import { ITheme } from 'vs/platform/theme/common/themeService'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as platform from 'vs/base/common/platform'; +import { EditorTheme } from 'vs/editor/common/view/viewContext'; const USE_IDENTITY_LINES_COLLECTION = true; @@ -595,7 +595,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ); } - public getAllOverviewRulerDecorations(theme: ITheme): IOverviewRulerDecorations { + public getAllOverviewRulerDecorations(theme: EditorTheme): IOverviewRulerDecorations { return this.lines.getAllOverviewRulerDecorations(this.editorId, filterValidationDecorations(this.configuration.options), theme); } diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index 87a66792e6..d4aa4d0fd1 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals, flatten, isNonEmptyArray, mergeSort } from 'vs/base/common/arrays'; +import { equals, flatten, isNonEmptyArray, mergeSort, coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { illegalArgument, isPromiseCanceledError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; @@ -27,6 +27,8 @@ export interface CodeActionSet extends IDisposable { readonly validActions: readonly modes.CodeAction[]; readonly allActions: readonly modes.CodeAction[]; readonly hasAutoFix: boolean; + + readonly documentation: readonly modes.Command[]; } class ManagedCodeActionSet extends Disposable implements CodeActionSet { @@ -48,7 +50,11 @@ class ManagedCodeActionSet extends Disposable implements CodeActionSet { public readonly validActions: readonly modes.CodeAction[]; public readonly allActions: readonly modes.CodeAction[]; - public constructor(actions: readonly modes.CodeAction[], disposables: DisposableStore) { + public constructor( + actions: readonly modes.CodeAction[], + public readonly documentation: readonly modes.Command[], + disposables: DisposableStore, + ) { super(); this._register(disposables); this.allActions = mergeSort([...actions], ManagedCodeActionSet.codeActionsComparator); @@ -80,17 +86,23 @@ export function getCodeActions( const promises = providers.map(async provider => { try { const providedCodeActions = await provider.provideCodeActions(model, rangeOrSelection, codeActionContext, cts.token); - if (cts.token.isCancellationRequested || !providedCodeActions) { - return []; + if (providedCodeActions) { + disposables.add(providedCodeActions); } - disposables.add(providedCodeActions); - return providedCodeActions.actions.filter(action => action && filtersAction(filter, action)); + + if (cts.token.isCancellationRequested) { + return { actions: [] as modes.CodeAction[], documentation: undefined }; + } + + const filteredActions = (providedCodeActions?.actions || []).filter(action => action && filtersAction(filter, action)); + const documentation = getDocumentation(provider, filteredActions, filter.include); + return { actions: filteredActions, documentation }; } catch (err) { if (isPromiseCanceledError(err)) { throw err; } onUnexpectedExternalError(err); - return []; + return { actions: [] as modes.CodeAction[], documentation: undefined }; } }); @@ -101,9 +113,11 @@ export function getCodeActions( } }); - return Promise.all(promises) - .then(flatten) - .then(actions => new ManagedCodeActionSet(actions, disposables)) + return Promise.all(promises).then(actions => { + const allActions = flatten(actions.map(x => x.actions)); + const allDocumentation = coalesce(actions.map(x => x.documentation)); + return new ManagedCodeActionSet(allActions, allDocumentation, disposables); + }) .finally(() => { listener.dispose(); cts.dispose(); @@ -125,6 +139,52 @@ function getCodeActionProviders( }); } +function getDocumentation( + provider: modes.CodeActionProvider, + providedCodeActions: readonly modes.CodeAction[], + only?: CodeActionKind +): modes.Command | undefined { + if (!provider.documentation) { + return undefined; + } + + const documentation = provider.documentation.map(entry => ({ kind: new CodeActionKind(entry.kind), command: entry.command })); + + if (only) { + let currentBest: { readonly kind: CodeActionKind, readonly command: modes.Command } | undefined; + for (const entry of documentation) { + if (entry.kind.contains(only)) { + if (!currentBest) { + currentBest = entry; + } else { + // Take best match + if (currentBest.kind.contains(entry.kind)) { + currentBest = entry; + } + } + } + } + if (currentBest) { + return currentBest?.command; + } + } + + // Otherwise, check to see if any of the provided actions match. + for (const action of providedCodeActions) { + if (!action.kind) { + continue; + } + + for (const entry of documentation) { + if (entry.kind.contains(new CodeActionKind(action.kind))) { + return entry.command; + } + } + } + + return undefined; +} + registerLanguageCommand('_executeCodeActionProvider', async function (accessor, args): Promise> { const { resource, rangeOrSelection, kind } = args; if (!(resource instanceof URI)) { diff --git a/src/vs/editor/contrib/codeAction/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/codeActionMenu.ts index dbe87f123d..a073034b41 100644 --- a/src/vs/editor/contrib/codeAction/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/codeActionMenu.ts @@ -14,9 +14,9 @@ import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { CodeAction, CodeActionProviderRegistry } from 'vs/editor/common/modes'; +import { CodeAction, CodeActionProviderRegistry, Command } from 'vs/editor/common/modes'; import { codeActionCommandId, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/codeAction'; -import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; +import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionTrigger, CodeActionKind } from 'vs/editor/contrib/codeAction/types'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -84,7 +84,7 @@ export class CodeActionMenu extends Disposable { this._visible = true; this._showingActions.value = codeActions; - const menuActions = this.getMenuActions(trigger, actionsToShow); + const menuActions = this.getMenuActions(trigger, actionsToShow, codeActions.documentation); const anchor = Position.isIPosition(at) ? this._toCoords(at) : at || { x: 0, y: 0 }; const resolver = this._keybindingResolver.getResolver(); @@ -101,28 +101,34 @@ export class CodeActionMenu extends Disposable { }); } - private getMenuActions(trigger: CodeActionTrigger, actionsToShow: readonly CodeAction[]): IAction[] { + private getMenuActions( + trigger: CodeActionTrigger, + actionsToShow: readonly CodeAction[], + documentation: readonly Command[] + ): IAction[] { const toCodeActionAction = (action: CodeAction): CodeActionAction => new CodeActionAction(action, () => this._delegate.onSelectCodeAction(action)); const result: IAction[] = actionsToShow .map(toCodeActionAction); + const allDocumentation: Command[] = [...documentation]; const model = this._editor.getModel(); if (model && result.length) { for (const provider of CodeActionProviderRegistry.all(model)) { if (provider._getAdditionalMenuItems) { - const items = provider._getAdditionalMenuItems({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow); - if (items.length) { - result.push(new Separator(), ...items.map(command => toCodeActionAction({ - title: command.title, - command: command, - }))); - } + allDocumentation.push(...provider._getAdditionalMenuItems({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow)); } } } + if (allDocumentation.length) { + result.push(new Separator(), ...allDocumentation.map(command => toCodeActionAction({ + title: command.title, + command: command, + }))); + } + return result; } diff --git a/src/vs/editor/contrib/gotoSymbol/goToSymbol.ts b/src/vs/editor/contrib/gotoSymbol/goToSymbol.ts index a4249e9f42..c3a4eeb2b1 100644 --- a/src/vs/editor/contrib/gotoSymbol/goToSymbol.ts +++ b/src/vs/editor/contrib/gotoSymbol/goToSymbol.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten, coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { registerModelAndPositionCommand } from 'vs/editor/browser/editorExtensions'; @@ -28,9 +27,18 @@ function getLocationLinks( return undefined; }); }); - return Promise.all(promises) - .then(flatten) - .then(coalesce); + + return Promise.all(promises).then(values => { + const result: LocationLink[] = []; + for (let value of values) { + if (Array.isArray(value)) { + result.push(...value); + } else if (value) { + result.push(value); + } + } + return result; + }); } diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 318f804d27..6248016047 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -181,6 +181,8 @@ export interface SelectionEvent { readonly element?: Location; } +class ReferencesTree extends WorkbenchAsyncDataTree { } + /** * ZoneWidget that is shown inside the editor */ @@ -195,7 +197,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { private readonly _onDidSelectReference = new Emitter(); readonly onDidSelectReference = this._onDidSelectReference.event; - private _tree!: WorkbenchAsyncDataTree; + private _tree!: ReferencesTree; private _treeContainer!: HTMLElement; private _splitView!: SplitView; private _preview!: ICodeEditor; @@ -316,8 +318,8 @@ export class ReferenceWidget extends peekView.PeekViewWidget { listBackground: peekView.peekViewResultsBackground } }; - this._tree = this._instantiationService.createInstance>( - WorkbenchAsyncDataTree, + this._tree = this._instantiationService.createInstance( + ReferencesTree, 'ReferencesWidget', this._treeContainer, new Delegate(), diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.css b/src/vs/editor/contrib/parameterHints/parameterHints.css index 9d3c535eaa..adb6ec6b11 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/parameterHints.css @@ -33,6 +33,7 @@ .monaco-editor .parameter-hints-widget .monaco-scrollable-element, .monaco-editor .parameter-hints-widget .body { display: flex; + flex: 1; flex-direction: column; min-height: 100%; } diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 852b9f65d4..aff07a2b39 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -137,8 +137,6 @@ border-bottom-style: solid; padding: 0 8px 0 4px; - - box-shadow: 0 -.5px 3px #ddd; } .monaco-editor .suggest-widget.list-right.docs-side > .suggest-status-bar { @@ -177,6 +175,11 @@ opacity: 0.7; } +.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left > .signature-label { + overflow: auto; + text-overflow: ellipsis; +} + .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left > .qualifier-label { margin-left: 4px; opacity: 0.4; @@ -213,8 +216,8 @@ /** Details: if using CompletionItemLabel#details, always show **/ -.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right.always-show-details > .details-label, -.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused > .contents > .main > .right.always-show-details > .details-label { +.monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .right > .details-label, +.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused:not(.string-label) > .contents > .main > .right > .details-label { display: inline; } @@ -228,6 +231,12 @@ overflow: hidden; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left > .monaco-icon-label { + flex-shrink: 0; +} +.monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .left > .monaco-icon-label { + max-width: 80%; +} +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label > .contents > .main > .left > .monaco-icon-label { flex-shrink: 1; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right { @@ -253,11 +262,11 @@ } /** Do NOT display ReadMore when using plain CompletionItemLabel (details/documentation might not be resolved) **/ -.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right:not(.always-show-details) > .readMore { +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label > .contents > .main > .right > .readMore { display: none; } /** Focused item can show ReadMore, but can't when docs is side/below **/ -.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused > .contents > .main > .right:not(.always-show-details) > .readMore { +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused.string-label > .contents > .main > .right > .readMore { display: inline-block; } diff --git a/src/vs/editor/contrib/suggest/media/suggestStatusBar.css b/src/vs/editor/contrib/suggest/media/suggestStatusBar.css index eb05ba733a..b21d5ecc6f 100644 --- a/src/vs/editor/contrib/suggest/media/suggestStatusBar.css +++ b/src/vs/editor/contrib/suggest/media/suggestStatusBar.css @@ -26,7 +26,7 @@ } .monaco-editor .suggest-widget.with-status-bar .monaco-list .monaco-list-row > .contents > .main > .right > .readMore, -.monaco-editor .suggest-widget.with-status-bar .monaco-list .monaco-list-row.focused > .contents > .main > .right:not(.always-show-details) > .readMore { +.monaco-editor .suggest-widget.with-status-bar .monaco-list .monaco-list-row.focused.string-label > .contents > .main > .right > .readMore { display: none; } diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index da63de0b90..b19693cbe4 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -26,6 +26,7 @@ export const Context = { MakesTextEdit: new RawContextKey('suggestionMakesTextEdit', true), AcceptSuggestionsOnEnter: new RawContextKey('acceptSuggestionOnEnter', true), HasInsertAndReplaceRange: new RawContextKey('suggestionHasInsertAndReplaceRange', false), + CanResolve: new RawContextKey('suggestionCanResolve', false), }; export const suggestWidgetStatusbarMenu = new MenuId('suggestWidgetStatusBar'); @@ -34,9 +35,12 @@ export class CompletionItem { _brand!: 'ISuggestionItem'; + private static readonly _defaultResolve = () => Promise.resolve(); + readonly resolve: (token: CancellationToken) => Promise; isResolved: boolean = false; + // readonly editStart: IPosition; readonly editInsertEnd: IPosition; @@ -87,13 +91,13 @@ export class CompletionItem { // create the suggestion resolver const { resolveCompletionItem } = provider; if (typeof resolveCompletionItem !== 'function') { - this.resolve = () => Promise.resolve(); + this.resolve = CompletionItem._defaultResolve; this.isResolved = true; } else { let cached: Promise | undefined; this.resolve = (token) => { if (!cached) { - cached = Promise.resolve(resolveCompletionItem.call(provider, model, position, completion, token)).then(value => { + cached = Promise.resolve(resolveCompletionItem.call(provider, model, Position.lift(position), completion, token)).then(value => { assign(completion, value); this.isResolved = true; }, err => { diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 691ae91a3f..6ceb0eb162 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -139,10 +139,12 @@ export class SuggestController implements IEditorContribution { // Wire up makes text edit context key const ctxMakesTextEdit = SuggestContext.MakesTextEdit.bindTo(this._contextKeyService); const ctxHasInsertAndReplace = SuggestContext.HasInsertAndReplaceRange.bindTo(this._contextKeyService); + const ctxCanResolve = SuggestContext.CanResolve.bindTo(this._contextKeyService); this._toDispose.add(toDisposable(() => { ctxMakesTextEdit.reset(); ctxHasInsertAndReplace.reset(); + ctxCanResolve.reset(); })); this._toDispose.add(widget.onDidFocus(({ item }) => { @@ -172,6 +174,9 @@ export class SuggestController implements IEditorContribution { // (ctx: hasInsertAndReplaceRange) ctxHasInsertAndReplace.set(!Position.equals(item.editInsertEnd, item.editReplaceEnd)); + + // (ctx: canResolve) + ctxCanResolve.set(Boolean(item.provider.resolveCompletionItem) || Boolean(item.completion.documentation) || item.completion.detail !== item.completion.label); })); this._toDispose.add(widget.onDetailsKeyDown(e => { @@ -695,13 +700,13 @@ registerEditorCommand(new SuggestCommand({ menuId: suggestWidgetStatusbarMenu, group: 'right', order: 1, - when: SuggestContext.DetailsVisible, + when: ContextKeyExpr.and(SuggestContext.DetailsVisible, SuggestContext.CanResolve), title: nls.localize('detail.more', "show less") }, { menuId: suggestWidgetStatusbarMenu, group: 'right', order: 1, - when: SuggestContext.DetailsVisible.toNegated(), + when: ContextKeyExpr.and(SuggestContext.DetailsVisible.toNegated(), SuggestContext.CanResolve), title: nls.localize('detail.less', "show more") }] })); diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index c61ed33c3f..bc5fd02f1c 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -244,12 +244,12 @@ class ItemRenderer implements IListRenderer() { + onModelRemoved = Event.None; + getModel(uri: URI) { + return uri.toString() === model.uri.toString() ? model : null; + } + }; + + let service = new class extends EditorWorkerServiceImpl { + + private _worker = new EditorSimpleWorker(new class extends mock() { }, null); + + constructor() { + super(modelService, new class extends mock() { }, new NullLogService()); + this._worker.acceptNewModel({ + url: model.uri.toString(), + lines: model.getLinesContent(), + EOL: model.getEOL(), + versionId: model.getVersionId() + }); + model.onDidChangeContent(e => this._worker.acceptModelChanged(model.uri.toString(), e)); + } + computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> { + return this._worker.computeWordRanges(resource.toString(), range, DEFAULT_WORD_REGEXP.source, DEFAULT_WORD_REGEXP.flags); + } + }; + + distance = await WordDistance.create(service, editor); + + disposables.add(mode); + disposables.add(model); + disposables.add(editor); + }); + + function createSuggestItem(label: string, overwriteBefore: number, position: IPosition): CompletionItem { + const suggestion: modes.CompletionItem = { + label, + range: { startLineNumber: position.lineNumber, startColumn: position.column - overwriteBefore, endLineNumber: position.lineNumber, endColumn: position.column }, + insertText: label, + kind: 0 + }; + const container: modes.CompletionList = { + suggestions: [suggestion] + }; + const provider: modes.CompletionItemProvider = { + provideCompletionItems(): any { + return; + } + }; + return new CompletionItem(position, suggestion, container, provider, undefined!); + } + + test('Suggest locality bonus can boost current word #90515', function () { + this.skip(); + + const pos = { lineNumber: 2, column: 2 }; + const d1 = distance.distance(pos, createSuggestItem('a', 1, pos).completion); + const d2 = distance.distance(pos, createSuggestItem('aa', 1, pos).completion); + const d3 = distance.distance(pos, createSuggestItem('ab', 1, pos).completion); + + assert.ok(d1 > d2); + assert.ok(d2 === d3); + }); +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 6aeac0f5b2..abbad9ba71 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -54,12 +54,19 @@ declare namespace monaco { } export interface CancellationToken { + /** + * A flag signalling is cancellation has been requested. + */ readonly isCancellationRequested: boolean; /** - * An event emitted when cancellation is requested + * An event which fires when cancellation is requested. This event + * only ever fires `once` as cancellation can only happen once. Listeners + * that are registered after cancellation will be called (next event loop run), + * but also only once. + * * @event */ - readonly onCancellationRequested: IEvent; + readonly onCancellationRequested: (listener: (e: any) => any, thisArgs?: any, disposables?: IDisposable[]) => IDisposable; } /** * Uniform Resource Identifier (Uri) http://tools.ietf.org/html/rfc3986. diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 0e0ec3e08f..c41cfb0ceb 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -13,6 +13,10 @@ import { OVERRIDE_PROPERTY_PATTERN, ConfigurationScope, IConfigurationRegistry, import { IOverrides, overrideIdentifierFromKey, addToValueTree, toValuesTree, IConfigurationModel, getConfigurationValue, IConfigurationOverrides, IConfigurationData, getDefaultValues, getConfigurationKeys, removeFromValueTree, toOverrides, IConfigurationValue, ConfigurationTarget, compare, IConfigurationChangeEvent, IConfigurationChange } from 'vs/platform/configuration/common/configuration'; import { Workspace } from 'vs/platform/workspace/common/workspace'; import { Registry } from 'vs/platform/registry/common/platform'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IFileService } from 'vs/platform/files/common/files'; +import { dirname } from 'vs/base/common/resources'; export class ConfigurationModel implements IConfigurationModel { @@ -335,6 +339,40 @@ export class ConfigurationModelParser { } } +export class UserSettings extends Disposable { + + private readonly parser: ConfigurationModelParser; + protected readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + constructor( + private readonly userSettingsResource: URI, + private readonly scopes: ConfigurationScope[] | undefined, + private readonly fileService: IFileService + ) { + super(); + this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes); + this._register(this.fileService.watch(dirname(this.userSettingsResource))); + this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire())); + } + + async loadConfiguration(): Promise { + try { + const content = await this.fileService.readFile(this.userSettingsResource); + this.parser.parseContent(content.value.toString() || '{}'); + return this.parser.configurationModel; + } catch (e) { + return new ConfigurationModel(); + } + } + + reprocess(): ConfigurationModel { + this.parser.parse(); + return this.parser.configurationModel; + } +} + + export class Configuration { private _workspaceConsolidatedConfiguration: ConfigurationModel | null = null; diff --git a/src/vs/platform/configuration/node/configurationService.ts b/src/vs/platform/configuration/node/configurationService.ts index f0e6bd5b71..8fb26a0b87 100644 --- a/src/vs/platform/configuration/node/configurationService.ts +++ b/src/vs/platform/configuration/node/configurationService.ts @@ -7,54 +7,39 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationService, IConfigurationChangeEvent, IConfigurationOverrides, ConfigurationTarget, isConfigurationOverrides, IConfigurationData, IConfigurationValue, IConfigurationChange } from 'vs/platform/configuration/common/configuration'; -import { DefaultConfigurationModel, Configuration, ConfigurationModel, ConfigurationModelParser, ConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; +import { DefaultConfigurationModel, Configuration, ConfigurationModel, ConfigurationChangeEvent, UserSettings } from 'vs/platform/configuration/common/configurationModels'; import { Event, Emitter } from 'vs/base/common/event'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { ConfigWatcher } from 'vs/base/node/config'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; +import { IFileService } from 'vs/platform/files/common/files'; +import { RunOnceScheduler } from 'vs/base/common/async'; export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable { _serviceBrand: undefined; private configuration: Configuration; - private userConfigModelWatcher: ConfigWatcher | undefined; + private userConfiguration: UserSettings; + private readonly reloadConfigurationScheduler: RunOnceScheduler; private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; constructor( - private readonly settingsResource: URI + private readonly settingsResource: URI, + fileService: IFileService ) { super(); + this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, fileService)); this.configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel()); + + this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50)); this._register(Registry.as(Extensions.Configuration).onDidUpdateConfiguration(configurationProperties => this.onDidDefaultConfigurationChange(configurationProperties))); + this._register(this.userConfiguration.onDidChange(() => this.reloadConfigurationScheduler.schedule())); } - initialize(): Promise { - if (this.userConfigModelWatcher) { - this.userConfigModelWatcher.dispose(); - } - - if (this.settingsResource.scheme !== Schemas.file) { - return Promise.resolve(); - } - return new Promise((c, e) => { - this.userConfigModelWatcher = this._register(new ConfigWatcher(this.settingsResource.fsPath, { - changeBufferDelay: 300, onError: error => onUnexpectedError(error), defaultConfig: new ConfigurationModelParser(this.settingsResource.fsPath), parse: (content: string, parseErrors: any[]) => { - const userConfigModelParser = new ConfigurationModelParser(this.settingsResource.fsPath); - userConfigModelParser.parseContent(content); - parseErrors = [...userConfigModelParser.errors]; - return userConfigModelParser; - }, initCallback: () => { - this.configuration = new Configuration(new DefaultConfigurationModel(), this.userConfigModelWatcher!.getConfig().configurationModel); - this._register(this.userConfigModelWatcher!.onDidUpdateConfiguration(() => this.onDidChangeUserConfiguration(this.userConfigModelWatcher!.getConfig().configurationModel))); - c(); - } - })); - }); + async initialize(): Promise { + const userConfiguration = await this.userConfiguration.loadConfiguration(); + this.configuration = new Configuration(new DefaultConfigurationModel(), userConfiguration); } getConfigurationData(): IConfigurationData { @@ -92,14 +77,9 @@ export class ConfigurationService extends Disposable implements IConfigurationSe return this.configuration.keys(undefined); } - reloadConfiguration(folder?: IWorkspaceFolder): Promise { - if (this.userConfigModelWatcher) { - return new Promise(c => this.userConfigModelWatcher!.reload(userConfigModelParser => { - this.onDidChangeUserConfiguration(userConfigModelParser.configurationModel); - c(); - })); - } - return this.initialize(); + async reloadConfiguration(): Promise { + const configurationModel = await this.userConfiguration.loadConfiguration(); + this.onDidChangeUserConfiguration(configurationModel); } private onDidChangeUserConfiguration(userConfigurationModel: ConfigurationModel): void { diff --git a/src/vs/platform/configuration/test/node/configurationService.test.ts b/src/vs/platform/configuration/test/node/configurationService.test.ts index 31e5b56548..278138bc8d 100644 --- a/src/vs/platform/configuration/test/node/configurationService.test.ts +++ b/src/vs/platform/configuration/test/node/configurationService.test.ts @@ -16,14 +16,32 @@ import { testFile } from 'vs/base/test/node/utils'; import { URI } from 'vs/base/common/uri'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { Event } from 'vs/base/common/event'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { Schemas } from 'vs/base/common/network'; +import { IFileService } from 'vs/platform/files/common/files'; suite('ConfigurationService - Node', () => { + let fileService: IFileService; + const disposables: IDisposable[] = []; + + setup(() => { + const logService = new NullLogService(); + fileService = new FileService(logService); + disposables.push(fileService); + const diskFileSystemProvider = new DiskFileSystemProvider(logService); + disposables.push(diskFileSystemProvider); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + }); + test('simple', async () => { const res = await testFile('config', 'config.json'); fs.writeFileSync(res.testFile, '{ "foo": "bar" }'); - const service = new ConfigurationService(URI.file(res.testFile)); + const service = new ConfigurationService(URI.file(res.testFile), fileService); await service.initialize(); const config = service.getValue<{ foo: string; @@ -41,7 +59,7 @@ suite('ConfigurationService - Node', () => { fs.writeFileSync(res.testFile, '{ "testworkbench.editor.tabs": true }'); - const service = new ConfigurationService(URI.file(res.testFile)); + const service = new ConfigurationService(URI.file(res.testFile), fileService); await service.initialize(); const config = service.getValue<{ testworkbench: { @@ -64,7 +82,7 @@ suite('ConfigurationService - Node', () => { fs.writeFileSync(res.testFile, ',,,,'); - const service = new ConfigurationService(URI.file(res.testFile)); + const service = new ConfigurationService(URI.file(res.testFile), fileService); await service.initialize(); const config = service.getValue<{ foo: string; @@ -81,7 +99,7 @@ suite('ConfigurationService - Node', () => { const newDir = path.join(parentDir, 'config', id); const testFile = path.join(newDir, 'config.json'); - const service = new ConfigurationService(URI.file(testFile)); + const service = new ConfigurationService(URI.file(testFile), fileService); await service.initialize(); const config = service.getValue<{ foo: string }>(); @@ -90,10 +108,10 @@ suite('ConfigurationService - Node', () => { service.dispose(); }); - test('trigger configuration change event', async () => { + test('trigger configuration change event when file does not exist', async () => { const res = await testFile('config', 'config.json'); - const service = new ConfigurationService(URI.file(res.testFile)); + const service = new ConfigurationService(URI.file(res.testFile), fileService); await service.initialize(); return new Promise((c, e) => { const disposable = Event.filter(service.onDidChangeConfiguration, e => e.source === ConfigurationTarget.USER)(async (e) => { @@ -108,12 +126,31 @@ suite('ConfigurationService - Node', () => { }); + test('trigger configuration change event when file exists', async () => { + const res = await testFile('config', 'config.json'); + + const service = new ConfigurationService(URI.file(res.testFile), fileService); + fs.writeFileSync(res.testFile, '{ "foo": "bar" }'); + await service.initialize(); + return new Promise((c, e) => { + const disposable = Event.filter(service.onDidChangeConfiguration, e => e.source === ConfigurationTarget.USER)(async (e) => { + disposable.dispose(); + assert.equal(service.getValue('foo'), 'barz'); + service.dispose(); + await res.cleanUp(); + c(); + }); + fs.writeFileSync(res.testFile, '{ "foo": "barz" }'); + }); + + }); + test('reloadConfiguration', async () => { const res = await testFile('config', 'config.json'); fs.writeFileSync(res.testFile, '{ "foo": "bar" }'); - const service = new ConfigurationService(URI.file(res.testFile)); + const service = new ConfigurationService(URI.file(res.testFile), fileService); await service.initialize(); let config = service.getValue<{ foo: string; @@ -162,7 +199,7 @@ suite('ConfigurationService - Node', () => { } }); - let serviceWithoutFile = new ConfigurationService(URI.file('__testFile')); + let serviceWithoutFile = new ConfigurationService(URI.file('__testFile'), fileService); await serviceWithoutFile.initialize(); let setting = serviceWithoutFile.getValue(); @@ -172,7 +209,7 @@ suite('ConfigurationService - Node', () => { return testFile('config', 'config.json').then(async res => { fs.writeFileSync(res.testFile, '{ "testworkbench.editor.tabs": true }'); - const service = new ConfigurationService(URI.file(res.testFile)); + const service = new ConfigurationService(URI.file(res.testFile), fileService); let setting = service.getValue(); @@ -205,7 +242,7 @@ suite('ConfigurationService - Node', () => { }); const r = await testFile('config', 'config.json'); - const service = new ConfigurationService(URI.file(r.testFile)); + const service = new ConfigurationService(URI.file(r.testFile), fileService); service.initialize(); let res = service.inspect('something.missing'); @@ -243,7 +280,7 @@ suite('ConfigurationService - Node', () => { }); const r = await testFile('config', 'config.json'); - const service = new ConfigurationService(URI.file(r.testFile)); + const service = new ConfigurationService(URI.file(r.testFile), fileService); service.initialize(); let res = service.inspect('lookup.service.testNullSetting'); diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index 327cb41aaf..d72eea3654 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ipcMain as ipc, app } from 'electron'; +import { ipcMain as ipc, app, BrowserWindow } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { IStateService } from 'vs/platform/state/node/state'; import { Event, Emitter } from 'vs/base/common/event'; @@ -12,7 +12,7 @@ import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Barrier } from 'vs/base/common/async'; +import { Barrier, timeout } from 'vs/base/common/async'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; export const ILifecycleMainService = createDecorator('lifecycleMainService'); @@ -106,7 +106,7 @@ export interface ILifecycleMainService { /** * Forcefully shutdown the application. No livecycle event handlers are triggered. */ - kill(code?: number): void; + kill(code?: number): Promise; /** * Returns a promise that resolves when a certain lifecycle phase @@ -550,9 +550,45 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe this.quit().then(veto => quitVetoed = veto); } - kill(code?: number): void { + async kill(code?: number): Promise { this.logService.trace('Lifecycle#kill()'); + // The kill() method is only used in 2 situations: + // - when an instance fails to start at all + // - when extension tests run from CLI to report proper exit code + // + // From extension tests we have seen issues where calling app.exit() + // with an opened window can lead to native crashes (Linux) when webviews + // are involved. As such, we should make sure to destroy any opened + // window before calling app.exit(). + // + // Note: Electron implements a similar logic here: + // https://github.com/electron/electron/blob/fe5318d753637c3903e23fc1ed1b263025887b6a/spec-main/window-helpers.ts#L5 + + await Promise.race([ + + // still do not block more than 1s + timeout(1000), + + // destroy any opened window + (async () => { + for (const window of BrowserWindow.getAllWindows()) { + if (window && !window.isDestroyed()) { + let whenWindowClosed: Promise; + if (window.webContents && !window.webContents.isDestroyed()) { + whenWindowClosed = new Promise(c => window.once('closed', c)); + } else { + whenWindowClosed = Promise.resolve(); + } + + window.destroy(); + await whenWindowClosed; + } + } + })() + ]); + + // Now exit either after 1s or all windows destroyed app.exit(code); } } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index eb462c2cf6..535e5e23cb 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -243,11 +243,13 @@ export class WorkbenchList extends List { readonly contextKeyService: IContextKeyService; private readonly configurationService: IConfigurationService; + private readonly themeService: IThemeService; private listHasSelectionOrFocus: IContextKey; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; + private _styler: IDisposable | undefined; private _useAltAsMultipleSelectionModifier: boolean; constructor( @@ -278,6 +280,7 @@ export class WorkbenchList extends List { this.contextKeyService = createScopedContextKeyService(contextKeyService, this); this.configurationService = configurationService; + this.themeService = themeService; const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); @@ -292,7 +295,7 @@ export class WorkbenchList extends List { this.disposables.add((listService as ListService).register(this)); if (options.overrideStyles) { - this.disposables.add(attachListStyler(this, themeService, options.overrideStyles)); + this.updateStyles(options.overrideStyles); } this.disposables.add(this.onSelectionChange(() => { @@ -313,6 +316,29 @@ export class WorkbenchList extends List { this.registerListeners(); } + updateOptions(options: IWorkbenchListOptions): void { + super.updateOptions(options); + + if (options.overrideStyles) { + this.updateStyles(options.overrideStyles); + } + } + + dispose(): void { + super.dispose(); + if (this._styler) { + this._styler.dispose(); + } + } + + private updateStyles(styles: IColorMapping): void { + if (this._styler) { + this._styler.dispose(); + } + + this._styler = attachListStyler(this, this.themeService, styles); + } + private registerListeners(): void { this.disposables.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(multiSelectModifierSettingKey)) { @@ -738,7 +764,7 @@ export class WorkbenchObjectTree, TFilterData = void> @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble>(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); super(user, container, delegate, renderers, treeOptions); this.disposables.add(disposable); this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); @@ -769,12 +795,20 @@ export class WorkbenchCompressibleObjectTree, TFilter @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble>(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); super(user, container, delegate, renderers, treeOptions); this.disposables.add(disposable); this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); this.disposables.add(this.internals); } + + updateOptions(options: IWorkbenchAsyncDataTreeOptions = {}): void { + super.updateOptions(options); + + if (options.overrideStyles) { + this.internals.updateStyleOverrides(options.overrideStyles); + } + } } export interface IWorkbenchDataTreeOptions extends IDataTreeOptions { @@ -801,7 +835,7 @@ export class WorkbenchDataTree extends DataTree>(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); super(user, container, delegate, renderers, dataSource, treeOptions); this.disposables.add(disposable); this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); @@ -841,7 +875,7 @@ export class WorkbenchAsyncDataTree extends Async @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble>(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); super(user, container, delegate, renderers, dataSource, treeOptions); this.disposables.add(disposable); this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); @@ -882,7 +916,7 @@ export class WorkbenchCompressibleAsyncDataTree e @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble>(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); super(user, container, virtualDelegate, compressionDelegate, renderers, dataSource, treeOptions); this.disposables.add(disposable); this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 9bb9e6f115..151e2bc548 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -64,7 +64,7 @@ class MarkerStats implements MarkerStatistics { this._data = undefined; } - private _update(resources: URI[]): void { + private _update(resources: readonly URI[]): void { if (!this._data) { return; } @@ -343,7 +343,7 @@ export class MarkerService implements IMarkerService { private static _dedupeMap: { [uri: string]: boolean }; - private static _debouncer(last: URI[], event: URI[]): URI[] { + private static _debouncer(last: URI[] | undefined, event: readonly URI[]): URI[] { if (!last) { MarkerService._dedupeMap = Object.create(null); last = []; diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts new file mode 100644 index 0000000000..b9987fddd1 --- /dev/null +++ b/src/vs/platform/opener/browser/link.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { $ } from 'vs/base/browser/dom'; +import { domEvent } from 'vs/base/browser/event'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Color } from 'vs/base/common/color'; + +export interface ILinkDescriptor { + readonly label: string; + readonly href: string; + readonly title?: string; +} + +export interface ILinkStyles { + readonly textLinkForeground?: Color; +} + +export class Link extends Disposable { + + readonly el: HTMLAnchorElement; + private styles: ILinkStyles = { + textLinkForeground: Color.fromHex('#006AB1') + }; + + constructor( + link: ILinkDescriptor, + @IOpenerService openerService: IOpenerService + ) { + super(); + + this.el = $('a', { + tabIndex: 0, + href: link.href, + title: link.title + }, link.label); + + const onClick = domEvent(this.el, 'click'); + const onEnterPress = Event.chain(domEvent(this.el, 'keypress')) + .map(e => new StandardKeyboardEvent(e)) + .filter(e => e.keyCode === KeyCode.Enter) + .event; + const onOpen = Event.any(onClick, onEnterPress); + + this._register(onOpen(_ => openerService.open(link.href))); + + this.applyStyles(); + } + + style(styles: ILinkStyles): void { + this.styles = styles; + this.applyStyles(); + } + + private applyStyles(): void { + this.el.style.color = this.styles.textLinkForeground?.toString() || ''; + } +} diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index b030c31162..02e8e1783b 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, darken, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; +import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, darken, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; import { IThemable, styleFn } from 'vs/base/common/styler'; @@ -269,6 +269,16 @@ export function attachButtonStyler(widget: IThemable, themeService: IThemeServic } as IButtonStyleOverrides, widget); } +export interface ILinkStyleOverrides extends IStyleOverrides { + textLinkForeground?: ColorIdentifier; +} + +export function attachLinkStyler(widget: IThemable, themeService: IThemeService, style?: ILinkStyleOverrides): IDisposable { + return attachStyler(themeService, { + textLinkForeground: (style && style.textLinkForeground) || textLinkForeground, + } as ILinkStyleOverrides, widget); +} + export interface IProgressBarStyleOverrides extends IStyleOverrides { progressBarBackground?: ColorIdentifier; } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 3e9fa9d8e9..b508bdc40e 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -4930,6 +4930,21 @@ declare module 'vscode' { */ readonly creationOptions: Readonly; + /** + * The exit status of the terminal, this will be undefined while the terminal is active. + * + * **Example:** Show a notification with the exit code when the terminal exits with a + * non-zero exit code. + * ```typescript + * window.onDidCloseTerminal(t => { + * if (t.exitStatus && t.exitStatus.code) { + * vscode.window.showInformationMessage(`Exit code: ${t.exitStatus.code}`); + * } + * }); + * ``` + */ + readonly exitStatus: TerminalExitStatus | undefined; + /** * Send text to the terminal. The text is written to the stdin of the underlying pty process * (shell) of the terminal. @@ -7746,6 +7761,20 @@ declare module 'vscode' { readonly rows: number; } + /** + * Represents how a terminal exited. + */ + export interface TerminalExitStatus { + /** + * The exit code that a terminal exited with, it can have the following values: + * - Zero: the terminal process or custom execution succeeded. + * - Non-zero: the terminal process or custom execution failed. + * - `undefined`: the user forcibly closed the terminal or a custom execution exited + * without providing an exit code. + */ + readonly code: number | undefined; + } + /** * A location in the editor at which progress information can be shown. It depends on the * location how progress is visually represented. @@ -8406,7 +8435,7 @@ declare module 'vscode' { * List of workspace folders or `undefined` when no folder is open. * *Note* that the first entry corresponds to the value of `rootPath`. */ - export const workspaceFolders: WorkspaceFolder[] | undefined; + export const workspaceFolders: ReadonlyArray | undefined; /** * The name of the workspace. `undefined` when no folder @@ -8580,7 +8609,7 @@ declare module 'vscode' { /** * All text documents currently known to the system. */ - export const textDocuments: TextDocument[]; + export const textDocuments: ReadonlyArray; /** * Opens a document. Will return early if this document is already open. Otherwise diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 72fe5ec72a..55986bcf4a 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1025,38 +1025,6 @@ declare module 'vscode' { //#endregion - //#region Terminal exit status https://github.com/microsoft/vscode/issues/62103 - - export interface TerminalExitStatus { - /** - * The exit code that a terminal exited with, it can have the following values: - * - Zero: the terminal process or custom execution succeeded. - * - Non-zero: the terminal process or custom execution failed. - * - `undefined`: the user forcibly closed the terminal or a custom execution exited - * without providing an exit code. - */ - readonly code: number | undefined; - } - - export interface Terminal { - /** - * The exit status of the terminal, this will be undefined while the terminal is active. - * - * **Example:** Show a notification with the exit code when the terminal exits with a - * non-zero exit code. - * ```typescript - * window.onDidCloseTerminal(t => { - * if (t.exitStatus && t.exitStatus.code) { - * vscode.window.showInformationMessage(`Exit code: ${t.exitStatus.code}`); - * } - * }); - * ``` - */ - readonly exitStatus: TerminalExitStatus | undefined; - } - - //#endregion - //#region Terminal dimensions property and change event https://github.com/microsoft/vscode/issues/55718 /** @@ -1643,5 +1611,34 @@ declare module 'vscode' { asExtensionUri(relativePath: string): Uri; } + export interface Extension { + /** + * Get the uri of a resource contained in the extension. + * + * @param relativePath A relative path to a resource contained in the extension. + * @return The uri of the resource. + */ + asExtensionUri(relativePath: string): Uri; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/86788 + + export interface CodeActionProviderMetadata { + /** + * Static documentation for a class of code actions. + * + * The documentation is shown in the code actions menu if either: + * + * - Code actions of `kind` are requested by VS Code. Note that in this case, we always pick the most specific + * documentation. For example, if documentation for both `Refactor` and `RefactorExtract` is provided, and we + * request code actions for `RefactorExtract`, we prefer the more specific documentation for `RefactorExtract`. + * + * - Any code actions of `kind` are returned by the provider. + */ + readonly documentation?: ReadonlyArray<{ readonly kind: CodeActionKind, readonly command: Command }>; + } + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 55f37a6c3f..204830ae74 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -14,12 +14,13 @@ import * as modes from 'vs/editor/common/modes'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { Extensions as PanelExtensions, PanelDescriptor, PanelRegistry } from 'vs/workbench/browser/panel'; import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; -import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsPanel'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; import { CommentProviderFeatures, ExtHostCommentsShape, ExtHostContext, IExtHostContext, MainContext, MainThreadCommentsShape, CommentThreadChanges } from '../common/extHost.protocol'; -import { COMMENTS_PANEL_ID, COMMENTS_PANEL_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; export class MainThreadCommentThread implements modes.CommentThread { @@ -343,13 +344,14 @@ export class MainThreadComments extends Disposable implements MainThreadComments private _activeCommentThread?: MainThreadCommentThread; private readonly _activeCommentThreadDisposables = this._register(new DisposableStore()); - private _openPanelListener: IDisposable | null = null; + private _openViewListener: IDisposable | null = null; constructor( extHostContext: IExtHostContext, @ICommentService private readonly _commentService: ICommentService, - @IPanelService private readonly _panelService: IPanelService + @IViewsService private readonly _viewsService: IViewsService, + @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); @@ -376,10 +378,10 @@ export class MainThreadComments extends Disposable implements MainThreadComments this._commentService.registerCommentController(providerId, provider); this._commentControllers.set(handle, provider); - const commentsPanelAlreadyConstructed = this._panelService.getPanels().some(panel => panel.id === COMMENTS_PANEL_ID); + const commentsPanelAlreadyConstructed = !!this._viewDescriptorService.getViewDescriptor(COMMENTS_VIEW_ID); if (!commentsPanelAlreadyConstructed) { - this.registerPanel(commentsPanelAlreadyConstructed); - this.registerOpenPanelListener(commentsPanelAlreadyConstructed); + this.registerView(commentsPanelAlreadyConstructed); + this.registerViewOpenedListener(commentsPanelAlreadyConstructed); } this._commentService.setWorkspaceComments(String(handle), []); } @@ -444,27 +446,36 @@ export class MainThreadComments extends Disposable implements MainThreadComments return provider.deleteCommentThread(commentThreadHandle); } - private registerPanel(commentsPanelAlreadyConstructed: boolean) { - if (!commentsPanelAlreadyConstructed) { - Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( - CommentsPanel, - COMMENTS_PANEL_ID, - COMMENTS_PANEL_TITLE, - 'commentsPanel', - 10 - )); + private registerView(commentsViewAlreadyRegistered: boolean) { + if (!commentsViewAlreadyRegistered) { + const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: COMMENTS_VIEW_ID, + name: COMMENTS_VIEW_TITLE, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + order: 10, + }, ViewContainerLocation.Panel); + + Registry.as(ViewExtensions.ViewsRegistry).registerViews([{ + id: COMMENTS_VIEW_ID, + name: COMMENTS_VIEW_TITLE, + canToggleVisibility: false, + ctorDescriptor: new SyncDescriptor(CommentsPanel), + focusCommand: { + id: 'workbench.action.focusCommentsPanel' + } + }], VIEW_CONTAINER); } } /** - * If the comments panel has never been opened, the constructor for it has not yet run so it has - * no listeners for comment threads being set or updated. Listen for the panel opening for the + * If the comments view has never been opened, the constructor for it has not yet run so it has + * no listeners for comment threads being set or updated. Listen for the view opening for the * first time and send it comments then. */ - private registerOpenPanelListener(commentsPanelAlreadyConstructed: boolean) { - if (!commentsPanelAlreadyConstructed && !this._openPanelListener) { - this._openPanelListener = this._panelService.onDidPanelOpen(e => { - if (e.panel.getId() === COMMENTS_PANEL_ID) { + private registerViewOpenedListener(commentsPanelAlreadyConstructed: boolean) { + if (!commentsPanelAlreadyConstructed && !this._openViewListener) { + this._openViewListener = this._viewsService.onDidChangeViewVisibility(e => { + if (e.id === COMMENTS_VIEW_ID && e.visible) { keys(this._commentControllers).forEach(handle => { let threads = this._commentControllers.get(handle)!.getAllComments(); @@ -474,9 +485,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments } }); - if (this._openPanelListener) { - this._openPanelListener.dispose(); - this._openPanelListener = null; + if (this._openViewListener) { + this._openViewListener.dispose(); + this._openViewListener = null; } } }); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 62dc156ef0..31d5117fad 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -11,7 +11,7 @@ import * as search from 'vs/workbench/contrib/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto } from '../common/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -55,11 +55,11 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha //#region --- revive functions - private static _reviveLocationDto(data: ILocationDto): modes.Location; - private static _reviveLocationDto(data: ILocationDto[]): modes.Location[]; - private static _reviveLocationDto(data: ILocationDto | ILocationDto[]): modes.Location | modes.Location[] { + private static _reviveLocationDto(data?: ILocationDto): modes.Location; + private static _reviveLocationDto(data?: ILocationDto[]): modes.Location[]; + private static _reviveLocationDto(data: ILocationDto | ILocationDto[] | undefined): modes.Location | modes.Location[] | undefined { if (!data) { - return data; + return undefined; // {{SQL CARBON EDIT}} strict-null-checks } else if (Array.isArray(data)) { data.forEach(l => MainThreadLanguageFeatures._reviveLocationDto(l)); return data; @@ -235,7 +235,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- quick fix - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], providedCodeActionKinds?: string[]): void { + $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto): void { this._registrations.set(handle, modes.CodeActionProviderRegistry.register(selector, { provideCodeActions: async (model: ITextModel, rangeOrSelection: EditorRange | Selection, context: modes.CodeActionContext, token: CancellationToken): Promise => { const listDto = await this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context, token); @@ -251,7 +251,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } }; }, - providedCodeActionKinds + providedCodeActionKinds: metadata.providedKinds, + documentation: metadata.documentation })); } diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 06403d89af..74e929e4ce 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -309,15 +309,16 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape return true; } - private _onRequestAvailableShells(request: IAvailableShellsRequest): void { + private async _onRequestAvailableShells(req: IAvailableShellsRequest): Promise { if (this._isPrimaryExtHost()) { - this._proxy.$requestAvailableShells().then(e => request(e)); + req.callback(await this._proxy.$getAvailableShells()); } } - private _onRequestDefaultShellAndArgs(request: IDefaultShellAndArgsRequest): void { + private async _onRequestDefaultShellAndArgs(req: IDefaultShellAndArgsRequest): Promise { if (this._isPrimaryExtHost()) { - this._proxy.$requestDefaultShellAndArgs(request.useAutomationShell).then(e => request.callback(e.shell, e.args)); + const res = await this._proxy.$getDefaultShellAndArgs(req.useAutomationShell); + req.callback(res.shell, res.args); } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 06d512be16..f8e2c8dbfe 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -53,7 +53,7 @@ import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyId import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as vscode from 'vscode'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { originalFSPath } from 'vs/base/common/resources'; +import { originalFSPath, joinPath } from 'vs/base/common/resources'; import { values } from 'vs/base/common/collections'; import { ExtHostEditorInsets } from 'vs/workbench/api/common/extHostCodeInsets'; import { ExtHostLabelService } from 'vs/workbench/api/common/extHostLabelService'; @@ -478,22 +478,19 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onDidChangeWindowState(listener, thisArg?, disposables?) { return extHostWindow.onDidChangeWindowState(listener, thisArg, disposables); }, - // {{SQL CARBON EDIT}} Typing needs to be disabled; enabled strict null checks allows the typing once we get there - showInformationMessage(message, first, ...rest) { - return extHostMessageService.showMessage(extension, Severity.Info, message, first, rest); + showInformationMessage(message: string, ...rest: Array) { + return >extHostMessageService.showMessage(extension, Severity.Info, message, rest[0], >rest.slice(1)); }, - // {{SQL CARBON EDIT}} Typing needs to be disabled; enabled strict null checks allows the typing once we get there - showWarningMessage(message, first, ...rest) { - return extHostMessageService.showMessage(extension, Severity.Warning, message, first, rest); + showWarningMessage(message: string, ...rest: Array) { + return >extHostMessageService.showMessage(extension, Severity.Warning, message, rest[0], >rest.slice(1)); }, - // {{SQL CARBON EDIT}} Typing needs to be disabled; enabled strict null checks allows the typing once we get there - showErrorMessage(message, first, ...rest) { - return extHostMessageService.showMessage(extension, Severity.Error, message, first, rest); + showErrorMessage(message: string, ...rest: Array) { + return >extHostMessageService.showMessage(extension, Severity.Error, message, rest[0], >rest.slice(1)); }, - showQuickPick(items: any, options: vscode.QuickPickOptions, token?: vscode.CancellationToken): any { + showQuickPick(items: any, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): any { return extHostQuickOpen.showQuickPick(items, !!extension.enableProposedApi, options, token); }, - showWorkspaceFolderPick(options: vscode.WorkspaceFolderPickOptions) { + showWorkspaceFolderPick(options?: vscode.WorkspaceFolderPickOptions) { return extHostQuickOpen.showWorkspaceFolderPick(options); }, showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken) { @@ -539,10 +536,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createOutputChannel(name: string): vscode.OutputChannel { return extHostOutputService.createOutputChannel(name); }, - createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { + createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options?: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { return extHostWebviews.createWebviewPanel(extension, viewType, title, showOptions, options); }, - createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options: vscode.WebviewOptions): vscode.WebviewEditorInset { + createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): vscode.WebviewEditorInset { checkProposedApiEnabled(extension); return extHostEditorInsets.createWebviewEditorInset(editor, line, height, options, extension); }, @@ -754,7 +751,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, openTunnel: (forward: vscode.TunnelOptions) => { checkProposedApiEnabled(extension); - return extHostTunnelService.openTunnel(forward); + return extHostTunnelService.openTunnel(forward).then(value => { + if (!value) { + throw new Error('cannot open tunnel'); + } + return value; + }); }, get tunnels() { checkProposedApiEnabled(extension); @@ -1025,6 +1027,11 @@ class Extension implements vscode.Extension { this.extensionKind = kind; } + asExtensionUri(relativePath: string): URI { + checkProposedApiEnabled(this.packageJSON); + return joinPath(this.packageJSON.extensionLocation, relativePath); + } + get isActive(): boolean { return this._extensionService.isActivated(this._identifier); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7b8a6656c9..1c9c9e640d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -359,7 +359,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerHoverProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], supportedKinds?: string[]): void; + $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; @@ -1151,6 +1151,11 @@ export interface ICodeActionListDto { actions: ReadonlyArray; } +export interface ICodeActionProviderMetadataDto { + readonly providedKinds?: readonly string[]; + readonly documentation?: ReadonlyArray<{ readonly kind: string, readonly command: ICommandDto }>; +} + export type CacheId = number; export type ChainedCacheId = [CacheId, CacheId]; @@ -1293,8 +1298,8 @@ export interface ExtHostTerminalServiceShape { $acceptProcessRequestCwd(id: number): void; $acceptProcessRequestLatency(id: number): number; $acceptWorkspacePermissionsChanged(isAllowed: boolean): void; - $requestAvailableShells(): Promise; - $requestDefaultShellAndArgs(useAutomationShell: boolean): Promise; + $getAvailableShells(): Promise; + $getDefaultShellAndArgs(useAutomationShell: boolean): Promise; } export interface ExtHostSCMShape { diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index d66c0b2acb..6e782ce878 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -9,7 +9,6 @@ import type * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { IRawColorInfo, IWorkspaceEditDto, ICallHierarchyItemDto, IIncomingCallDto, IOutgoingCallDto } from 'vs/workbench/api/common/extHost.protocol'; -import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import * as search from 'vs/workbench/contrib/search/common/search'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; @@ -128,17 +127,17 @@ const newCommands: ApiCommand[] = [ new ApiCommand( 'vscode.executeFormatDocumentProvider', '_executeFormatDocumentProvider', 'Execute document format provider.', [ApiCommandArgument.Uri, new ApiCommandArgument('options', 'Formatting options', _ => true, v => v)], - new ApiCommandResult('A promise that resolves to an array of TextEdits.', tryMapWith(typeConverters.TextEdit.to)) + new ApiCommandResult('A promise that resolves to an array of TextEdits.', tryMapWith(typeConverters.TextEdit.to)) ), new ApiCommand( 'vscode.executeFormatRangeProvider', '_executeFormatRangeProvider', 'Execute range format provider.', [ApiCommandArgument.Uri, ApiCommandArgument.Range, new ApiCommandArgument('options', 'Formatting options', _ => true, v => v)], - new ApiCommandResult('A promise that resolves to an array of TextEdits.', tryMapWith(typeConverters.TextEdit.to)) + new ApiCommandResult('A promise that resolves to an array of TextEdits.', tryMapWith(typeConverters.TextEdit.to)) ), new ApiCommand( 'vscode.executeFormatOnTypeProvider', '_executeFormatOnTypeProvider', 'Execute format on type provider.', [ApiCommandArgument.Uri, ApiCommandArgument.Position, new ApiCommandArgument('ch', 'Trigger character', v => typeof v === 'string', v => v), new ApiCommandArgument('options', 'Formatting options', _ => true, v => v)], - new ApiCommandResult('A promise that resolves to an array of TextEdits.', tryMapWith(typeConverters.TextEdit.to)) + new ApiCommandResult('A promise that resolves to an array of TextEdits.', tryMapWith(typeConverters.TextEdit.to)) ), // -- go to symbol (definition, type definition, declaration, impl, references) new ApiCommand( diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index dbed2f7dfc..9bd95ab04c 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -230,6 +230,8 @@ export class CommandsConverter { this._commands.registerCommand(true, this._delegatingCommandId, this._executeConvertedCommand, this); } + toInternal(command: vscode.Command, disposables: DisposableStore): ICommandDto; + toInternal(command: vscode.Command | undefined, disposables: DisposableStore): ICommandDto | undefined; toInternal(command: vscode.Command | undefined, disposables: DisposableStore): ICommandDto | undefined { if (!command) { diff --git a/src/vs/workbench/api/common/extHostCustomers.ts b/src/vs/workbench/api/common/extHostCustomers.ts index 07c3aefea0..48f63ac4d2 100644 --- a/src/vs/workbench/api/common/extHostCustomers.ts +++ b/src/vs/workbench/api/common/extHostCustomers.ts @@ -14,12 +14,12 @@ export type IExtHostCustomerCtor = IConstructorSignature1 export function extHostNamedCustomer(id: ProxyIdentifier) { return function (ctor: { new(context: IExtHostContext, ...services: Services): T }): void { - ExtHostCustomersRegistryImpl.INSTANCE.registerNamedCustomer(id, ctor); + ExtHostCustomersRegistryImpl.INSTANCE.registerNamedCustomer(id, ctor as IExtHostCustomerCtor); }; } export function extHostCustomer(ctor: { new(context: IExtHostContext, ...services: Services): T }): void { - ExtHostCustomersRegistryImpl.INSTANCE.registerCustomer(ctor); + ExtHostCustomersRegistryImpl.INSTANCE.registerCustomer(ctor as IExtHostCustomerCtor); } export namespace ExtHostCustomersRegistry { diff --git a/src/vs/workbench/api/common/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts index fefd2abacd..3213606e21 100644 --- a/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -224,7 +224,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { private readonly _collections = new Map(); private readonly _onDidChangeDiagnostics = new Emitter<(vscode.Uri | string)[]>(); - static _debouncer(last: (vscode.Uri | string)[], current: (vscode.Uri | string)[]): (vscode.Uri | string)[] { + static _debouncer(last: (vscode.Uri | string)[] | undefined, current: (vscode.Uri | string)[]): (vscode.Uri | string)[] { if (!last) { return current; } else { diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 7abe27d350..4e442cc86f 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -199,7 +199,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ const edits: WorkspaceEdit[] = []; - await emitter.fireAsync(data, token, async (thenable, listener: IExtensionListener) => { + await emitter.fireAsync(data, token, async (thenable, listener) => { // ignore all results except for WorkspaceEdits. Those are stored in an array. const now = Date.now(); const result = await Promise.resolve(thenable); @@ -208,7 +208,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ } if (Date.now() - now > timeout) { - this._logService.warn('SLOW file-participant', listener.extension?.identifier); + this._logService.warn('SLOW file-participant', (>listener).extension?.identifier); } }); diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 0886b3cc64..dbfc10d3b4 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -183,8 +183,13 @@ class CodeLensAdapter { } } -function convertToLocationLinks(value: vscode.Definition): modes.LocationLink[] { - return value ? asArray(value).map(typeConvert.DefinitionLink.from) : []; +function convertToLocationLinks(value: vscode.Location | vscode.Location[] | vscode.LocationLink[] | undefined | null): modes.LocationLink[] { + if (Array.isArray(value)) { + return (value).map(typeConvert.DefinitionLink.from); + } else if (value) { + return [typeConvert.DefinitionLink.from(value)]; + } + return []; } class DefinitionAdapter { @@ -1571,9 +1576,17 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- quick fix registerCodeActionProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { + const store = new DisposableStore(); const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider, this._logService, extension, this._apiDeprecation), extension); - this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector), (metadata && metadata.providedCodeActionKinds) ? metadata.providedCodeActionKinds.map(kind => kind.value) : undefined); - return this._createDisposable(handle); + this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector), { + providedKinds: metadata?.providedCodeActionKinds?.map(kind => kind.value), + documentation: metadata?.documentation?.map(x => ({ + kind: x.kind.value, + command: this._commands.converter.toInternal(x.command, store), + })) + }); + store.add(this._createDisposable(handle)); + return store; } diff --git a/src/vs/workbench/api/common/extHostLanguages.ts b/src/vs/workbench/api/common/extHostLanguages.ts index a4eb1fb091..cbc0c83627 100644 --- a/src/vs/workbench/api/common/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -24,10 +24,12 @@ export class ExtHostLanguages { return this._proxy.$getLanguages(); } - changeLanguage(uri: vscode.Uri, languageId: string): Promise { - return this._proxy.$changeLanguage(uri, languageId).then(() => { - const data = this._documents.getDocumentData(uri); - return data ? data.document : undefined; - }); + async changeLanguage(uri: vscode.Uri, languageId: string): Promise { + await this._proxy.$changeLanguage(uri, languageId); + const data = this._documents.getDocumentData(uri); + if (!data) { + throw new Error(`document '${uri.toString}' NOT found`); + } + return data.document; } } diff --git a/src/vs/workbench/api/common/extHostMessageService.ts b/src/vs/workbench/api/common/extHostMessageService.ts index 745904bc90..7f4627f9ab 100644 --- a/src/vs/workbench/api/common/extHostMessageService.ts +++ b/src/vs/workbench/api/common/extHostMessageService.ts @@ -24,10 +24,11 @@ export class ExtHostMessageService { this._proxy = mainContext.getProxy(MainContext.MainThreadMessageService); } - showMessage(extension: IExtensionDescription, severity: Severity, message: string, optionsOrFirstItem: vscode.MessageOptions | string, rest: string[]): Promise; - showMessage(extension: IExtensionDescription, severity: Severity, message: string, optionsOrFirstItem: vscode.MessageOptions | vscode.MessageItem, rest: vscode.MessageItem[]): Promise; - showMessage(extension: IExtensionDescription, severity: Severity, message: string, optionsOrFirstItem: vscode.MessageOptions | vscode.MessageItem | string, rest: Array): Promise; - showMessage(extension: IExtensionDescription, severity: Severity, message: string, optionsOrFirstItem: vscode.MessageOptions | string | vscode.MessageItem, rest: Array): Promise { + + showMessage(extension: IExtensionDescription, severity: Severity, message: string, optionsOrFirstItem: vscode.MessageOptions | string | undefined, rest: string[]): Promise; + showMessage(extension: IExtensionDescription, severity: Severity, message: string, optionsOrFirstItem: vscode.MessageOptions | vscode.MessageItem | undefined, rest: vscode.MessageItem[]): Promise; + showMessage(extension: IExtensionDescription, severity: Severity, message: string, optionsOrFirstItem: vscode.MessageOptions | vscode.MessageItem | string | undefined, rest: Array): Promise; + showMessage(extension: IExtensionDescription, severity: Severity, message: string, optionsOrFirstItem: vscode.MessageOptions | string | vscode.MessageItem | undefined, rest: Array): Promise { const options: MainThreadMessageOptions = { extension }; let items: (string | vscode.MessageItem)[]; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 795ad5000a..bb667b3a2b 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -325,8 +325,8 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ public abstract getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string; public abstract getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string; public abstract $spawnExtHostProcess(id: number, shellLaunchConfigDto: IShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise; - public abstract $requestAvailableShells(): Promise; - public abstract $requestDefaultShellAndArgs(useAutomationShell: boolean): Promise; + public abstract $getAvailableShells(): Promise; + public abstract $getDefaultShellAndArgs(useAutomationShell: boolean): Promise; public abstract $acceptWorkspacePermissionsChanged(isAllowed: boolean): void; public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { @@ -606,11 +606,11 @@ export class WorkerExtHostTerminalService extends BaseExtHostTerminalService { throw new Error('Not implemented'); } - public $requestAvailableShells(): Promise { + public $getAvailableShells(): Promise { throw new Error('Not implemented'); } - public async $requestDefaultShellAndArgs(useAutomationShell: boolean): Promise { + public async $getDefaultShellAndArgs(useAutomationShell: boolean): Promise { throw new Error('Not implemented'); } diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 881e4bcea1..4706f105b5 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -96,10 +96,8 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { // shellArgs: this._terminalService._getDefaultShellArgs(configProvider), cwd: args.cwd, name: args.title || nls.localize('debug.terminal.title', "debuggee"), - env: args.env }; delete args.cwd; - delete args.env; this._integratedTerminalInstance = this._terminalService.createTerminalFromOptions(options); } diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 5e66335bb0..661c91a38b 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -67,8 +67,8 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // already patched to use`process.send()` const nativeProcessSend = process.send!; const mainThreadConsole = this._extHostContext.getProxy(MainContext.MainThreadConsole); - process.send = (...args: any[]) => { - if (args.length === 0 || !args[0] || args[0].type !== '__$console') { + process.send = (...args) => { + if ((args as unknown[]).length === 0 || !args[0] || args[0].type !== '__$console') { return nativeProcessSend.apply(process, args); } mainThreadConsole.$logExtensionHostMessage(args[0]); diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 37495bef35..a53a6ed9be 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -180,7 +180,7 @@ export class ExtHostTask extends ExtHostTaskBase { } public $getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }> { - return this._terminalService.$requestDefaultShellAndArgs(true); + return this._terminalService.$getDefaultShellAndArgs(true); } public async $jsonTasksSupported(): Promise { diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 5feaeaefb5..1f69a9684d 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -200,16 +200,16 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { this._setupExtHostProcessListeners(id, new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, enableConpty, this._logService)); } - public $requestAvailableShells(): Promise { + public $getAvailableShells(): Promise { return detectAvailableShells(); } - public async $requestDefaultShellAndArgs(useAutomationShell: boolean): Promise { + public async $getDefaultShellAndArgs(useAutomationShell: boolean): Promise { const configProvider = await this._extHostConfiguration.getConfigProvider(); - return Promise.resolve({ + return { shell: this.getDefaultShell(useAutomationShell, configProvider), args: this.getDefaultShellArgs(useAutomationShell, configProvider) - }); + }; } public $acceptWorkspacePermissionsChanged(isAllowed: boolean): void { diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 0e905b6884..60035f7b0f 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -134,8 +134,14 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe } const ports: { host: string, port: number, detail: string }[] = []; - const tcp: string = fs.readFileSync('/proc/net/tcp', 'utf8'); - const tcp6: string = fs.readFileSync('/proc/net/tcp6', 'utf8'); + let tcp: string = ''; + let tcp6: string = ''; + try { + tcp = fs.readFileSync('/proc/net/tcp', 'utf8'); + tcp6 = fs.readFileSync('/proc/net/tcp6', 'utf8'); + } catch (e) { + // File reading error. No additional handling needed. + } const procSockets: string = await (new Promise(resolve => { exec('ls -l /proc/[0-9]*/fd/[0-9]* | grep socket:', (error, stdout, stderr) => { resolve(stdout); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 255edcaf0f..51d7a020ee 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -238,9 +238,12 @@ export class ResourcesDropHandler { private async handleDirtyEditorDrop(droppedDirtyEditor: IDraggedEditor): Promise { - // Untitled: always ensure that we open a new untitled for each file we drop + // Untitled: always ensure that we open a new untitled editor for each file we drop if (droppedDirtyEditor.resource.scheme === Schemas.untitled) { - droppedDirtyEditor.resource = this.textFileService.untitled.create({ mode: droppedDirtyEditor.mode, encoding: droppedDirtyEditor.encoding }).getResource(); + const untitledEditorResource = this.editorService.createInput({ mode: droppedDirtyEditor.mode, encoding: droppedDirtyEditor.encoding, forceUntitled: true }).getResource(); + if (untitledEditorResource) { + droppedDirtyEditor.resource = untitledEditorResource; + } } // File: ensure the file is not dirty or opened already diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 03aa9cbb8a..a6e02852cc 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -105,8 +105,7 @@ Registry.as(EditorExtensions.Editors).registerEditor( ); interface ISerializedUntitledTextEditorInput { - resource: string; - resourceJSON: object; + resourceJSON: UriComponents; modeId: string | undefined; encoding: string | undefined; } @@ -131,12 +130,11 @@ class UntitledTextEditorInputFactory implements IEditorInputFactory { const untitledTextEditorInput = editorInput; let resource = untitledTextEditorInput.getResource(); - if (untitledTextEditorInput.hasAssociatedFilePath) { + if (untitledTextEditorInput.model.hasAssociatedFilePath) { resource = toLocalResource(resource, this.environmentService.configuration.remoteAuthority); // untitled with associated file path use the local schema } const serialized: ISerializedUntitledTextEditorInput = { - resource: resource.toString(), // Keep for backwards compatibility resourceJSON: resource.toJSON(), modeId: untitledTextEditorInput.getMode(), encoding: untitledTextEditorInput.getEncoding() @@ -148,7 +146,7 @@ class UntitledTextEditorInputFactory implements IEditorInputFactory { deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): UntitledTextEditorInput { return instantiationService.invokeFunction(accessor => { const deserialized: ISerializedUntitledTextEditorInput = JSON.parse(serializedEditorInput); - const resource = !!deserialized.resourceJSON ? URI.revive(deserialized.resourceJSON) : URI.parse(deserialized.resource); + const resource = URI.revive(deserialized.resourceJSON); const mode = deserialized.modeId; const encoding = deserialized.encoding; diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 44357208d8..565633a654 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { GroupIdentifier, IWorkbenchEditorConfiguration, EditorOptions, TextEditorOptions, IEditorInput, IEditorIdentifier, IEditorCloseEvent, IEditor, IEditorPartOptions, IEditorPartOptionsChangeEvent } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IWorkbenchEditorConfiguration, EditorOptions, TextEditorOptions, IEditorInput, IEditorIdentifier, IEditorCloseEvent, IEditor, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorInput } from 'vs/workbench/common/editor'; import { EditorGroup } from 'vs/workbench/common/editor/editorGroup'; import { IEditorGroup, GroupDirection, IAddGroupOptions, IMergeGroupOptions, GroupsOrder, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -14,7 +14,7 @@ import { IConfigurationChangeEvent } from 'vs/platform/configuration/common/conf import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; export const EDITOR_TITLE_HEIGHT = 35; @@ -156,4 +156,9 @@ export interface EditorServiceImpl extends IEditorService { * Emitted when the list of most recently active editors change. */ readonly onDidMostRecentlyActiveEditorsChange: Event; + + /** + * Override to return a typed `EditorInput`. + */ + createInput(input: IResourceEditor): EditorInput; } diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index c12ab0f305..f3e9bcf35b 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -18,11 +18,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { RunOnceScheduler } from 'vs/base/common/async'; import { find } from 'vs/base/common/arrays'; import { DataTransfers } from 'vs/base/browser/dnd'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { VSBuffer } from 'vs/base/common/buffer'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; interface IDropOperation { splitDirection?: GroupDirection; @@ -48,8 +48,8 @@ class DropOverlay extends Themable { private groupView: IEditorGroupView, @IThemeService themeService: IThemeService, @IInstantiationService private instantiationService: IInstantiationService, - @ITextFileService private textFileService: ITextFileService, - @IFileDialogService private readonly fileDialogService: IFileDialogService + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IEditorService private readonly editorService: IEditorService ) { super(themeService); @@ -308,8 +308,11 @@ class DropOverlay extends Themable { } // Open as untitled file with the provided contents - const contents = VSBuffer.wrap(new Uint8Array(event.target.result)).toString(); - const untitledEditor = this.textFileService.untitled.create({ associatedResource: proposedFilePath, initialValue: contents }); + const untitledEditor = this.editorService.createInput({ + resource: proposedFilePath, + forceUntitled: true, + contents: VSBuffer.wrap(new Uint8Array(event.target.result)).toString() + }); if (!targetGroup) { targetGroup = ensureTargetGroup(); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index df561b7ba1..82a43746b9 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -31,8 +31,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; -import { IEditorGroupsAccessor, IEditorGroupView, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorGroupsAccessor, IEditorGroupView, getActiveTextEditorOptions, IEditorOpeningEvent, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ActionRunner, IAction, Action } from 'vs/base/common/actions'; @@ -43,7 +42,7 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { isErrorWithActions, IErrorWithActions } from 'vs/base/common/errorsWithActions'; -import { IVisibleEditor } from 'vs/workbench/services/editor/common/editorService'; +import { IVisibleEditor, IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types'; import { hash } from 'vs/base/common/hash'; import { guessMimeTypes } from 'vs/base/common/mime'; @@ -129,12 +128,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @ITextFileService private readonly textFileService: ITextFileService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IMenuService private readonly menuService: IMenuService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IFileDialogService private readonly fileDialogService: IFileDialogService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IEditorService private readonly editorService: EditorServiceImpl ) { super(themeService); @@ -256,7 +255,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (this.isEmpty) { EventHelper.stop(e); - this.openEditor(this.textFileService.untitled.create(), EditorOptions.create({ pinned: true })); + this.openEditor(this.editorService.createInput({ forceUntitled: true }), EditorOptions.create({ pinned: true })); } })); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index e550b49de3..f208965845 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -31,6 +31,7 @@ import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/bro import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { MementoObject } from 'vs/workbench/common/memento'; import { assertIsDefined } from 'vs/base/common/types'; +import { IBoundarySashes } from 'vs/base/browser/ui/grid/gridview'; interface IEditorPartUIState { serializedGrid: ISerializedGrid; @@ -947,11 +948,15 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro } private doSetGridWidget(gridWidget: SerializableGrid): void { + let boundarySashes: IBoundarySashes = {}; + if (this.gridWidget) { + boundarySashes = this.gridWidget.boundarySashes; this.gridWidget.dispose(); } this.gridWidget = gridWidget; + this.gridWidget.boundarySashes = boundarySashes; this.gridWidgetView.gridWidget = gridWidget; this._onDidSizeConstraintsChange.input = gridWidget.onDidChange; @@ -972,6 +977,11 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro return this.groupViews.size === 1 && this._activeGroup.isEmpty; } + setBoundarySashes(sashes: IBoundarySashes): void { + this.gridWidget.boundarySashes = sashes; + this.centeredLayoutWidget.boundarySashes = sashes; + } + layout(width: number, height: number): void { // Layout contents diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 703a082152..1a049fa246 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -34,14 +34,14 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { addClass, addDisposableListener, hasClass, EventType, EventHelper, removeClass, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { BreadcrumbsControl } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; import { IFileService } from 'vs/platform/files/common/files'; import { withNullAsUndefined, assertAllDefined, assertIsDefined } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { basenameOrAuthority } from 'vs/base/common/resources'; // {{SQL CARBON EDIT}} -- Display the editor's tab color @@ -80,7 +80,6 @@ export class TabsTitleControl extends TitleControl { group: IEditorGroupView, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService instantiationService: IInstantiationService, - @ITextFileService private readonly textFileService: ITextFileService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @ITelemetryService telemetryService: ITelemetryService, @@ -91,7 +90,8 @@ export class TabsTitleControl extends TitleControl { @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, @IFileService fileService: IFileService, - @ILabelService labelService: ILabelService + @ILabelService labelService: ILabelService, + @IEditorService private readonly editorService: EditorServiceImpl ) { super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService, configurationService, fileService, labelService); @@ -198,7 +198,13 @@ export class TabsTitleControl extends TitleControl { EventHelper.stop(e); - this.group.openEditor(this.textFileService.untitled.create(), { pinned: true /* untitled is always pinned */, index: this.group.count /* always at the end */ }); + this.group.openEditor(this.editorService.createInput({ + forceUntitled: true, + options: { + pinned: true, // untitled is always pinned + index: this.group.count // always at the end + } + })); })); }); diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 180677f44f..22c25774c5 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -214,7 +214,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { } private onDidEditorPaste(e: IPasteEvent, codeEditor: ICodeEditor): void { - if (e.range.startLineNumber !== 1 && e.range.startColumn !== 1) { + if (e.range.startLineNumber !== 1 || e.range.startColumn !== 1) { return; // only when pasting into first line, first column (= empty document) } diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 7dab812e5a..0b2e895ab4 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -156,14 +156,14 @@ export abstract class MenubarControl extends Disposable { return label; } - protected getOpenRecentActions(): IAction[] { + protected getOpenRecentActions(): (Separator | IAction & { uri: URI })[] { if (!this.recentlyOpened) { return []; } const { workspaces, files } = this.recentlyOpened; - const result: IAction[] = []; + const result = []; if (workspaces.length > 0) { for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) { diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 41db8d5103..7b2590bc13 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -26,7 +26,7 @@ import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { URI } from 'vs/base/common/uri'; import { dirname, basename } from 'vs/base/common/resources'; -import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon, IThemeService } from 'vs/platform/theme/common/themeService'; import { FileKind } from 'vs/platform/files/common/files'; import { WorkbenchAsyncDataTree, TreeResourceNavigator } from 'vs/platform/list/browser/listService'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; @@ -42,6 +42,7 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class CustomTreeViewPane extends ViewPane { @@ -56,8 +57,10 @@ export class CustomTreeViewPane extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IInstantiationService instantiationService: IInstantiationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(options.id)); this.treeView = treeView; this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this)); @@ -73,6 +76,8 @@ export class CustomTreeViewPane extends ViewPane { } renderBody(container: HTMLElement): void { + super.renderBody(container); + if (this.treeView instanceof CustomTreeView) { this.treeView.show(container); } @@ -113,6 +118,8 @@ class Root implements ITreeItem { const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data."); +class CustomTree extends WorkbenchAsyncDataTree { } + export class CustomTreeView extends Disposable implements ITreeView { private isVisible: boolean = false; @@ -127,7 +134,7 @@ export class CustomTreeView extends Disposable implements ITreeView { private _messageValue: string | undefined; private _canSelectMany: boolean = false; private messageElement!: HTMLDivElement; - private tree: WorkbenchAsyncDataTree | undefined; + private tree: CustomTree | undefined; private treeLabels: ResourceLabels | undefined; private root: ITreeItem; @@ -351,7 +358,7 @@ export class CustomTreeView extends Disposable implements ITreeView { const aligner = new Aligner(this.themeService); const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner); - this.tree = this._register(this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'CustomView', this.treeContainer, new CustomTreeDelegate(), [renderer], + this.tree = this._register(this.instantiationService.createInstance(CustomTree, 'CustomView', this.treeContainer, new CustomTreeDelegate(), [renderer], dataSource, { identityProvider: new CustomViewIdentityProvider(), accessibilityProvider: { diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 1afd8230dc..8d731197b6 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -71,6 +71,26 @@ display: none; } +.monaco-workbench .pane > .pane-body > .empty-view { + width: 100%; + height: 100%; + padding: 0 20px 0 20px; + position: absolute; + box-sizing: border-box; +} + +.monaco-workbench .pane > .pane-body:not(.empty) > .empty-view, +.monaco-workbench .pane > .pane-body.empty > :not(.empty-view) { + display: none; +} + +.monaco-workbench .pane > .pane-body > .empty-view .monaco-button { + max-width: 260px; + margin-left: auto; + margin-right: auto; + display: block; +} + .customview-tree .monaco-list-row .monaco-tl-contents.align-icon-with-twisty::before { display: none; } diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 91cfc24757..a73f88bf05 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -7,10 +7,10 @@ import 'vs/css!./media/paneviewlet'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; -import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler'; +import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler } from 'vs/platform/theme/common/styler'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener } from 'vs/base/browser/dom'; -import { IDisposable, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass } from 'vs/base/browser/dom'; +import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { firstIndex } from 'vs/base/common/arrays'; import { IAction, IActionRunner, ActionRunner } from 'vs/base/common/actions'; import { IActionViewItem, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -25,7 +25,7 @@ import { PaneView, IPaneViewOptions, IPaneOptions, Pane, DefaultPaneDndControlle import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewContainersRegistry, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer } from 'vs/workbench/common/views'; +import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewContainersRegistry, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry } from 'vs/workbench/common/views'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { assertIsDefined } from 'vs/base/common/types'; @@ -38,6 +38,10 @@ import { Component } from 'vs/workbench/common/component'; import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; +import { parseLinkedText } from 'vs/base/browser/linkedText'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Link } from 'vs/platform/opener/browser/link'; export interface IPaneColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -54,6 +58,8 @@ export interface IViewPaneOptions extends IPaneOptions { titleMenuId?: MenuId; } +const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); + export abstract class ViewPane extends Pane implements IView { private static readonly AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions'; @@ -70,6 +76,9 @@ export abstract class ViewPane extends Pane implements IView { protected _onDidChangeTitleArea = this._register(new Emitter()); readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; + protected _onDidChangeEmptyState = this._register(new Emitter()); + readonly onDidChangeEmptyState: Event = this._onDidChangeEmptyState.event; + private focusedViewContextKey: IContextKey; private _isVisible: boolean = false; @@ -79,12 +88,16 @@ export abstract class ViewPane extends Pane implements IView { private readonly menuActions: ViewMenuActions; protected actionRunner?: IActionRunner; - protected toolbar?: ToolBar; + private toolbar?: ToolBar; private readonly showActionsAlways: boolean = false; private headerContainer?: HTMLElement; private titleContainer?: HTMLElement; protected twistiesContainer?: HTMLElement; + private bodyContainer!: HTMLElement; + private emptyViewContainer!: HTMLElement; + private emptyViewDisposable: IDisposable = Disposable.None; + constructor( options: IViewPaneOptions, @IKeybindingService protected keybindingService: IKeybindingService, @@ -93,6 +106,8 @@ export abstract class ViewPane extends Pane implements IView { @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, @IInstantiationService protected instantiationService: IInstantiationService, + @IOpenerService protected openerService: IOpenerService, + @IThemeService protected themeService: IThemeService, ) { super(options); @@ -189,6 +204,22 @@ export abstract class ViewPane extends Pane implements IView { this._onDidChangeTitleArea.fire(); } + protected renderBody(container: HTMLElement): void { + this.bodyContainer = container; + this.emptyViewContainer = append(container, $('.empty-view', { tabIndex: 0 })); + + // we should update our empty state whenever + const onEmptyViewContentChange = Event.any( + // the registry changes + Event.map(Event.filter(viewsRegistry.onDidChangeEmptyViewContent, id => id === this.id), () => this.isEmpty()), + // or the view's empty state changes + Event.latch(Event.map(this.onDidChangeEmptyState, () => this.isEmpty())) + ); + + this._register(onEmptyViewContentChange(this.updateEmptyState, this)); + this.updateEmptyState(this.isEmpty()); + } + protected getProgressLocation(): string { return this.viewDescriptorService.getViewContainer(this.id)!.id; } @@ -254,6 +285,66 @@ export abstract class ViewPane extends Pane implements IView { saveState(): void { // Subclasses to implement for saving state } + + private updateEmptyState(isEmpty: boolean): void { + this.emptyViewDisposable.dispose(); + + if (!isEmpty) { + removeClass(this.bodyContainer, 'empty'); + this.emptyViewContainer.innerHTML = ''; + return; + } + + const contents = viewsRegistry.getEmptyViewContent(this.id); + + if (contents.length === 0) { + removeClass(this.bodyContainer, 'empty'); + this.emptyViewContainer.innerHTML = ''; + return; + } + + const disposables = new DisposableStore(); + addClass(this.bodyContainer, 'empty'); + this.emptyViewContainer.innerHTML = ''; + + for (const { content } of contents) { + const lines = content.split('\n'); + + for (let line of lines) { + line = line.trim(); + + if (!line) { + continue; + } + + const p = append(this.emptyViewContainer, $('p')); + const linkedText = parseLinkedText(line); + + for (const node of linkedText) { + if (typeof node === 'string') { + append(p, document.createTextNode(node)); + } else if (linkedText.length === 1) { + const button = new Button(p, { title: node.title }); + button.label = node.label; + button.onDidClick(_ => this.openerService.open(node.href), null, disposables); + disposables.add(button); + disposables.add(attachButtonStyler(button, this.themeService)); + } else { + const link = this.instantiationService.createInstance(Link, node); + append(p, link.el); + disposables.add(link); + disposables.add(attachLinkStyler(link, this.themeService)); + } + } + } + } + + this.emptyViewDisposable = disposables; + } + + isEmpty(): boolean { + return false; + } } export interface IViewPaneContainerOptions extends IPaneViewOptions { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index f3e8db66db..3dbc206834 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -191,7 +191,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'type': 'string', 'enum': ['left', 'bottom', 'right'], 'default': 'bottom', - 'description': nls.localize('panelDefaultLocation', "Controls the default location of the panel (terminal, debug console, output, problems). It can either show at the bottom or on the right of the workbench.") + 'description': nls.localize('panelDefaultLocation', "Controls the default location of the panel (terminal, debug console, output, problems). It can either show at the bottom, right, or left of the workbench.") }, 'workbench.statusBar.visible': { 'type': 'boolean', @@ -210,7 +210,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio }, 'workbench.view.experimental.allowMovingToNewContainer': { 'type': 'boolean', - 'default': false, + 'default': true, 'description': nls.localize('movingViewContainer', "Controls whether specific views will have a context menu entry allowing them to be moved to a new container. Currently, this setting only affects the outline view and views contributed by extensions.") }, 'workbench.fontAliasing': { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 9d0e61604a..1d6eb56183 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -230,22 +230,22 @@ export interface IUntitledTextResourceInput extends IBaseResourceInput { * force use the provided resource as associated path. As such, the resource will be used when saving * the untitled editor. */ - resource?: URI; + readonly resource?: URI; /** * Optional language of the untitled resource. */ - mode?: string; + readonly mode?: string; /** * Optional contents of the untitled resource. */ - contents?: string; + readonly contents?: string; /** * Optional encoding of the untitled resource. */ - encoding?: string; + readonly encoding?: string; } export interface IResourceDiffInput extends IBaseResourceInput { @@ -253,12 +253,12 @@ export interface IResourceDiffInput extends IBaseResourceInput { /** * The left hand side URI to open inside a diff editor. */ - leftResource: URI; + readonly leftResource: URI; /** * The right hand side URI to open inside a diff editor. */ - rightResource: URI; + readonly rightResource: URI; } export interface IResourceSideBySideInput extends IBaseResourceInput { @@ -266,12 +266,12 @@ export interface IResourceSideBySideInput extends IBaseResourceInput { /** * The right hand side URI to open inside a side by side editor. */ - masterResource: URI; + readonly masterResource: URI; /** * The left hand side URI to open inside a side by side editor. */ - detailResource: URI; + readonly detailResource: URI; } export const enum Verbosity { @@ -314,17 +314,17 @@ export interface ISaveOptions { * Forces to save the contents of the working copy * again even if the working copy is not dirty. */ - force?: boolean; + readonly force?: boolean; /** * Instructs the save operation to skip any save participants. */ - skipSaveParticipants?: boolean; + readonly skipSaveParticipants?: boolean; /** * A hint as to which file systems should be available for saving. */ - availableFileSystems?: string[]; + readonly availableFileSystems?: string[]; } export interface IRevertOptions { @@ -333,7 +333,7 @@ export interface IRevertOptions { * Forces to load the contents of the working copy * again even if the working copy is not dirty. */ - force?: boolean; + readonly force?: boolean; /** * A soft revert will clear dirty state of a working copy @@ -342,7 +342,7 @@ export interface IRevertOptions { * This option may be used in scenarios where an editor is * closed and where we do not require to load the contents. */ - soft?: boolean; + readonly soft?: boolean; } export interface IEditorInput extends IDisposable { diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 3c678d14fc..60ff1dc25a 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -141,13 +141,15 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel /** * Updates the text editor model with the provided value. If the value is the same as the model has, this is a no-op. */ - protected updateTextEditorModel(newValue: ITextBufferFactory, preferredMode?: string): void { + protected updateTextEditorModel(newValue?: ITextBufferFactory, preferredMode?: string): void { if (!this.isResolved()) { return; } // contents - this.modelService.updateModel(this.textEditorModel, newValue); + if (newValue) { + this.modelService.updateModel(this.textEditorModel, newValue); + } // mode (only if specific and changed) if (preferredMode && preferredMode !== PLAINTEXT_MODE_ID && this.textEditorModel.getModeId() !== preferredMode) { diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 15e9c28f30..6e016988d8 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -10,7 +10,7 @@ import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/con import { localize } from 'vs/nls'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { values, keys, getOrSet } from 'vs/base/common/map'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -19,6 +19,7 @@ import { IAction, IActionViewItem } from 'vs/base/common/actions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { flatten } from 'vs/base/common/arrays'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { SetMap } from 'vs/base/common/collections'; export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; @@ -210,6 +211,10 @@ export interface IViewDescriptorCollection extends IDisposable { readonly allViewDescriptors: IViewDescriptor[]; } +export interface IViewContentDescriptor { + readonly content: string; +} + export interface IViewsRegistry { readonly onViewsRegistered: Event<{ views: IViewDescriptor[], viewContainer: ViewContainer }>; @@ -229,6 +234,10 @@ export interface IViewsRegistry { getView(id: string): IViewDescriptor | null; getViewContainer(id: string): ViewContainer | null; + + readonly onDidChangeEmptyViewContent: Event; + registerEmptyViewContent(id: string, viewContent: IViewContentDescriptor): IDisposable; + getEmptyViewContent(id: string): IViewContentDescriptor[]; } class ViewsRegistry extends Disposable implements IViewsRegistry { @@ -242,8 +251,12 @@ class ViewsRegistry extends Disposable implements IViewsRegistry { private readonly _onDidChangeContainer: Emitter<{ views: IViewDescriptor[], from: ViewContainer, to: ViewContainer }> = this._register(new Emitter<{ views: IViewDescriptor[], from: ViewContainer, to: ViewContainer }>()); readonly onDidChangeContainer: Event<{ views: IViewDescriptor[], from: ViewContainer, to: ViewContainer }> = this._onDidChangeContainer.event; + private readonly _onDidChangeEmptyViewContent: Emitter = this._register(new Emitter()); + readonly onDidChangeEmptyViewContent: Event = this._onDidChangeEmptyViewContent.event; + private _viewContainers: ViewContainer[] = []; private _views: Map = new Map(); + private _emptyViewContents = new SetMap(); registerViews(views: IViewDescriptor[], viewContainer: ViewContainer): void { this.addViews(views, viewContainer); @@ -293,6 +306,22 @@ class ViewsRegistry extends Disposable implements IViewsRegistry { return null; } + registerEmptyViewContent(id: string, viewContent: IViewContentDescriptor): IDisposable { + this._emptyViewContents.add(id, viewContent); + this._onDidChangeEmptyViewContent.fire(id); + + return toDisposable(() => { + this._emptyViewContents.delete(id, viewContent); + this._onDidChangeEmptyViewContent.fire(id); + }); + } + + getEmptyViewContent(id: string): IViewContentDescriptor[] { + const result: IViewContentDescriptor[] = []; + this._emptyViewContents.forEach(id, descriptor => result.push(descriptor)); + return result; + } + private addViews(viewDescriptors: IViewDescriptor[], viewContainer: ViewContainer): void { let views = this._views.get(viewContainer); if (!views) { diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index b3add79628..292fabdfff 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -20,7 +20,7 @@ import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput, IUntitledTextResourceInput } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; @@ -32,7 +32,6 @@ import { toResource } from 'vs/base/test/common/utils'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; -import { INewUntitledTextEditorOptions } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files'; import { ShutdownReason, ILifecycleService, BeforeShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -43,6 +42,8 @@ import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker' import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { IModelService } from 'vs/editor/common/services/modelService'; import { TestTextFileService, TestElectronService, workbenchInstantiationService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); const backupHome = path.join(userdataDir, 'Backups'); @@ -133,7 +134,7 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); }); - async function createTracker(): Promise<[ServiceAccessor, EditorPart, BackupTracker]> { + async function createTracker(): Promise<[ServiceAccessor, EditorPart, BackupTracker, IInstantiationService]> { const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); const instantiationService = workbenchInstantiationService(); instantiationService.stub(IBackupFileService, backupFileService); @@ -153,18 +154,17 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests const tracker = instantiationService.createInstance(TestBackupTracker); - return [accessor, part, tracker]; + return [accessor, part, tracker, instantiationService]; } - async function untitledBackupTest(options?: INewUntitledTextEditorOptions): Promise { + async function untitledBackupTest(untitled: IUntitledTextResourceInput = {}): Promise { const [accessor, part, tracker] = await createTracker(); - const untitledEditor = accessor.textFileService.untitled.create(options); - await accessor.editorService.openEditor(untitledEditor, { pinned: true }); + const untitledEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; const untitledModel = await untitledEditor.resolve(); - if (!options?.initialValue) { + if (!untitled?.contents) { untitledModel.textEditorModel.setValue('Super Good'); } @@ -191,7 +191,7 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests test('Track backups (untitled with initial contents)', function () { this.timeout(20000); - return untitledBackupTest({ initialValue: 'Foo Bar' }); + return untitledBackupTest({ contents: 'Foo Bar' }); }); test.skip('Track backups (file)', async function () { // {{SQL CARBON EDIT}} tabcolorfailure diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts index ecdfd19492..965b68e978 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts @@ -9,7 +9,7 @@ import { WorkspaceEdit } from 'vs/editor/common/modes'; import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, BulkEditAriaProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditTree'; import { FuzzyScore } from 'vs/base/common/filters'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, ITheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; import { diffInserted, diffRemoved } from 'vs/platform/theme/common/colorRegistry'; import { localize } from 'vs/nls'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -37,6 +37,7 @@ import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import type { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const enum State { Data = 'data', @@ -83,10 +84,12 @@ export class BulkEditPane extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { super( { ...options, titleMenuId: MenuId.BulkEditTitle }, - keybindingService, contextMenuService, configurationService, _contextKeyService, viewDescriptorService, _instaService + keybindingService, contextMenuService, configurationService, _contextKeyService, viewDescriptorService, _instaService, openerService, themeService ); this.element.classList.add('bulk-edit-panel', 'show-file-icons'); @@ -101,6 +104,7 @@ export class BulkEditPane extends ViewPane { } protected renderBody(parent: HTMLElement): void { + super.renderBody(parent); const resourceLabels = this._instaService.createInstance( ResourceLabels, diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index 5a02027ce3..69b1d83786 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -82,13 +82,15 @@ class LayoutInfo { ) { } } +class CallHierarchyTree extends WorkbenchAsyncDataTree{ } + export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { private _changeDirectionAction?: ChangeHierarchyDirectionAction; private _parent!: HTMLElement; private _message!: HTMLElement; private _splitView!: SplitView; - private _tree!: WorkbenchAsyncDataTree; + private _tree!: CallHierarchyTree; private _treeViewStates = new Map(); private _editor!: EmbeddedCodeEditorWidget; private _dim!: Dimension; @@ -204,8 +206,8 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { listBackground: peekView.peekViewResultsBackground } }; - this._tree = this._instantiationService.createInstance>( - WorkbenchAsyncDataTree, + this._tree = this._instantiationService.createInstance( + CallHierarchyTree, 'CallHierarchyPeek', treeContainer, new callHTree.VirtualDelegate(), diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts index 3be91346a8..e8a4c290fd 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts @@ -7,7 +7,6 @@ import * as nls from 'vs/nls'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IUntitledTextResourceInput } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; @@ -29,7 +28,7 @@ class InspectKeyMap extends EditorAction { const keybindingService = accessor.get(IKeybindingService); const editorService = accessor.get(IEditorService); - editorService.openEditor({ contents: keybindingService._dumpDebugInfo(), options: { pinned: true } } as IUntitledTextResourceInput); + editorService.openEditor({ contents: keybindingService._dumpDebugInfo(), options: { pinned: true } }); } } @@ -49,7 +48,7 @@ class InspectKeyMapJSON extends Action { } public run(): Promise { - return this._editorService.openEditor({ contents: this._keybindingService._dumpDebugInfoJSON(), options: { pinned: true } } as IUntitledTextResourceInput); + return this._editorService.openEditor({ contents: this._keybindingService._dumpDebugInfoJSON(), options: { pinned: true } }); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index ddebf8db93..3376773299 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -22,8 +22,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; -export const COMMENTS_PANEL_ID = 'workbench.panel.comments'; -export const COMMENTS_PANEL_TITLE = 'Comments'; +export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; +export const COMMENTS_VIEW_TITLE = 'Comments'; export class CommentsAsyncDataSource implements IAsyncDataSource { hasChildren(element: any): boolean { @@ -176,7 +176,7 @@ export class CommentsList extends WorkbenchAsyncDataTree { renderers, dataSource, { - ariaLabel: COMMENTS_PANEL_TITLE, + ariaLabel: COMMENTS_VIEW_TITLE, keyboardSupport: true, identityProvider: { getId: (element: any) => { diff --git a/src/vs/workbench/contrib/comments/browser/commentsPanel.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts similarity index 76% rename from src/vs/workbench/contrib/comments/browser/commentsPanel.ts rename to src/vs/workbench/contrib/comments/browser/commentsView.ts index 91ae9535b6..faf93aac53 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsPanel.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -11,22 +11,25 @@ import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TreeResourceNavigator } from 'vs/platform/list/browser/listService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { Panel } from 'vs/workbench/browser/panel'; import { CommentNode, CommentsModel, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsEditorContribution'; -import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/browser/commentService'; +import { IWorkspaceCommentThreadsEvent, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IStorageService } from 'vs/platform/storage/common/storage'; import { ResourceLabels } from 'vs/workbench/browser/labels'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { CommentsList, COMMENTS_PANEL_ID, COMMENTS_PANEL_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { CommentsList, COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; -export class CommentsPanel extends Panel { +export class CommentsPanel extends ViewPane { private treeLabels!: ResourceLabels; private tree!: CommentsList; private treeContainer!: HTMLElement; @@ -35,43 +38,50 @@ export class CommentsPanel extends Panel { private commentsModel!: CommentsModel; private collapseAllAction?: IAction; + readonly onDidChangeVisibility = this.onDidChangeBodyVisibility; + constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ICommentService private readonly commentService: ICommentService, + options: IViewPaneOptions, + @IInstantiationService readonly instantiationService: IInstantiationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IEditorService private readonly editorService: IEditorService, - @ITelemetryService telemetryService: ITelemetryService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextMenuService contextMenuService: IContextMenuService, + @IKeybindingService keybindingService: IKeybindingService, + @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService + @ICommentService private readonly commentService: ICommentService ) { - super(COMMENTS_PANEL_ID, telemetryService, themeService, storageService); + super({ ...(options as IViewPaneOptions), id: COMMENTS_VIEW_ID, ariaHeaderLabel: COMMENTS_VIEW_TITLE }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); } - public create(parent: HTMLElement): void { - super.create(parent); + public renderBody(container: HTMLElement): void { + super.renderBody(container); - dom.addClass(parent, 'comments-panel'); + dom.addClass(container, 'comments-panel'); - let container = dom.append(parent, dom.$('.comments-panel-container')); - this.treeContainer = dom.append(container, dom.$('.tree-container')); + let domContainer = dom.append(container, dom.$('.comments-panel-container')); + this.treeContainer = dom.append(domContainer, dom.$('.tree-container')); this.commentsModel = new CommentsModel(); this.createTree(); - this.createMessageBox(container); + this.createMessageBox(domContainer); this._register(this.commentService.onDidSetAllCommentThreads(this.onAllCommentsChanged, this)); this._register(this.commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this)); - const styleElement = dom.createStyleSheet(parent); + const styleElement = dom.createStyleSheet(container); this.applyStyles(styleElement); this._register(this.themeService.onThemeChange(_ => this.applyStyles(styleElement))); - this._register(this.onDidChangeVisibility(visible => { + this._register(this.onDidChangeBodyVisibility(visible => { if (visible) { this.refresh(); } })); - this.render(); + this.renderComments(); } private applyStyles(styleElement: HTMLStyleElement) { @@ -101,7 +111,7 @@ export class CommentsPanel extends Panel { styleElement.innerHTML = content.join('\n'); } - private async render(): Promise { + private async renderComments(): Promise { dom.toggleClass(this.treeContainer, 'hidden', !this.commentsModel.hasCommentThreads()); await this.tree.setInput(this.commentsModel); this.renderMessage(); @@ -116,12 +126,12 @@ export class CommentsPanel extends Panel { return [this.collapseAllAction]; } - public layout(dimensions: dom.Dimension): void { - this.tree.layout(dimensions.height, dimensions.width); + public layoutBody(height: number, width: number): void { + this.tree.layout(height, width); } public getTitle(): string { - return COMMENTS_PANEL_TITLE; + return COMMENTS_VIEW_TITLE; } private createMessageBox(parent: HTMLElement): void { @@ -224,10 +234,7 @@ export class CommentsPanel extends Panel { CommandsRegistry.registerCommand({ id: 'workbench.action.focusCommentsPanel', handler: async (accessor) => { - const panelService = accessor.get(IPanelService); - const panels = panelService.getPanels(); - if (panels.some(panelIdentifier => panelIdentifier.id === COMMENTS_PANEL_ID)) { - await panelService.openPanel(COMMENTS_PANEL_ID, true); - } + const viewsService = accessor.get(IViewsService); + viewsService.openView(COMMENTS_VIEW_ID, true); } }); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index d768edbbd8..2255cfa88f 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -31,10 +31,10 @@ import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/ import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { ILabelService } from 'vs/platform/label/common/label'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { Gesture } from 'vs/base/browser/touch'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const $ = dom.$; @@ -64,20 +64,23 @@ export class BreakpointsView extends ViewPane { @IDebugService private readonly debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, - @IThemeService private readonly themeService: IThemeService, + @IThemeService themeService: IThemeService, @IEditorService private readonly editorService: IEditorService, @IContextViewService private readonly contextViewService: IContextViewService, @IConfigurationService configurationService: IConfigurationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IContextKeyService contextKeyService: IContextKeyService, + @IOpenerService openerService: IOpenerService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.minimumBodySize = this.maximumBodySize = getExpandedBodySize(this.debugService.getModel()); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); } public renderBody(container: HTMLElement): void { + super.renderBody(container); + dom.addClass(container, 'debug-breakpoints'); const delegate = new BreakpointsDelegate(this.debugService); @@ -98,7 +101,7 @@ export class BreakpointsView extends ViewPane { isChecked: (breakpoint: IEnablement) => breakpoint.enabled }, overrideStyles: { - listBackground: SIDE_BAR_BACKGROUND + listBackground: this.getBackgroundColor() } }); @@ -150,6 +153,12 @@ export class BreakpointsView extends ViewPane { this.onBreakpointsChange(); } })); + + this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { + if (views.some(v => v.id === this.id)) { + this.list.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); + } + })); } public focus(): void { diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 5c86a5953a..c95b19ebad 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -34,8 +34,9 @@ import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; import { STOP_ID, STOP_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, RESTART_SESSION_ID, RESTART_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STEP_INTO_LABEL, STEP_INTO_ID, STEP_OUT_LABEL, STEP_OUT_ID, PAUSE_ID, PAUSE_LABEL, CONTINUE_ID, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { CollapseAction } from 'vs/workbench/browser/viewlet'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const $ = dom.$; @@ -98,8 +99,10 @@ export class CallStackView extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IMenuService menuService: IMenuService, @IContextKeyService readonly contextKeyService: IContextKeyService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('callstackSection', "Call Stack Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('callstackSection', "Call Stack Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.callStackItemType = CONTEXT_CALLSTACK_ITEM_TYPE.bindTo(contextKeyService); this.contributedContextMenu = menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); @@ -116,16 +119,11 @@ export class CallStackView extends ViewPane { this.pauseMessageLabel.title = thread.stoppedDetails.text || ''; dom.toggleClass(this.pauseMessageLabel, 'exception', thread.stoppedDetails.reason === 'exception'); this.pauseMessage.hidden = false; - if (this.toolbar) { - this.toolbar.setActions([])(); - } + this.updateActions(); } else { this.pauseMessage.hidden = true; - if (this.toolbar) { - const collapseAction = new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all'); - this.toolbar.setActions([collapseAction])(); - } + this.updateActions(); } this.needsRefresh = false; @@ -150,7 +148,17 @@ export class CallStackView extends ViewPane { this.pauseMessageLabel = dom.append(this.pauseMessage, $('span.label')); } + getActions(): IAction[] { + if (this.pauseMessage.hidden) { + return [new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all')]; + } + + return []; + } + renderBody(container: HTMLElement): void { + super.renderBody(container); + dom.addClass(container, 'debug-call-stack'); const treeContainer = renderViewTree(container); @@ -197,7 +205,7 @@ export class CallStackView extends ViewPane { }, expandOnlyOnTwistieClick: true, overrideStyles: { - listBackground: SIDE_BAR_BACKGROUND + listBackground: this.getBackgroundColor() } }); @@ -290,6 +298,12 @@ export class CallStackView extends ViewPane { this.parentSessionToExpand.add(s.parentSession); } })); + + this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { + if (views.some(v => v.id === this.id)) { + this.tree.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); + } + })); } layoutBody(height: number, width: number): void { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 8e4f81a447..ef7d40d056 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -72,7 +72,7 @@ const viewContainer = Registry.as(ViewExtensions.ViewCo id: VIEWLET_ID, name: nls.localize('run', "Run"), ctorDescriptor: new SyncDescriptor(DebugViewPaneContainer), - icon: 'codicon-debug-alt', + icon: 'codicon-debug-alt-2', order: 13 // {{SQL CARBON EDIT}} }, ViewContainerLocation.Sidebar); @@ -104,12 +104,12 @@ Registry.as(ViewExtensions.ViewsRegistry).registerViews([{ // Register default debug views const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); -viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), ctorDescriptor: new SyncDescriptor(VariablesView), order: 10, weight: 40, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusVariablesView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), ctorDescriptor: new SyncDescriptor(WatchExpressionsView), order: 20, weight: 10, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), ctorDescriptor: new SyncDescriptor(CallStackView), order: 30, weight: 30, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), ctorDescriptor: new SyncDescriptor(BreakpointsView), order: 40, weight: 20, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: ContextKeyExpr.or(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); +viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), ctorDescriptor: new SyncDescriptor(VariablesView), order: 10, weight: 40, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusVariablesView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), ctorDescriptor: new SyncDescriptor(WatchExpressionsView), order: 20, weight: 10, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), ctorDescriptor: new SyncDescriptor(CallStackView), order: 30, weight: 30, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), ctorDescriptor: new SyncDescriptor(BreakpointsView), order: 40, weight: 20, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: ContextKeyExpr.or(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); viewsRegistry.registerViews([{ id: StartView.ID, name: StartView.LABEL, ctorDescriptor: new SyncDescriptor(StartView), order: 10, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], viewContainer); -viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), ctorDescriptor: new SyncDescriptor(LoadedScriptsView), order: 35, weight: 5, canToggleVisibility: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); +viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), ctorDescriptor: new SyncDescriptor(LoadedScriptsView), order: 35, weight: 5, canToggleVisibility: true, canMoveView: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); registerCommands(); @@ -262,8 +262,8 @@ configurationRegistry.registerConfiguration({ default: true }, 'debug.onTaskErrors': { - enum: ['debugAnyway', 'showErrors', 'prompt'], - enumDescriptions: [nls.localize('debugAnyway', "Ignore task errors and start debugging."), nls.localize('showErrors', "Show the Problems view and do not start debugging."), nls.localize('prompt', "Prompt user.")], + enum: ['debugAnyway', 'showErrors', 'prompt', 'cancel'], + enumDescriptions: [nls.localize('debugAnyway', "Ignore task errors and start debugging."), nls.localize('showErrors', "Show the Problems view and do not start debugging."), nls.localize('prompt', "Prompt user."), nls.localize('cancel', "Cancel debugging.")], description: nls.localize('debug.onTaskErrors', "Controls what to do when errors are encountered after running a preLaunchTask."), default: 'prompt' }, diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 99fe807f45..3ae8d9dcb0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -72,6 +72,9 @@ export class DebugTaskRunner { await this.viewsService.openView(Constants.MARKERS_VIEW_ID); return Promise.resolve(TaskRunResult.Failure); } + if (onTaskErrors === 'cancel') { + return Promise.resolve(TaskRunResult.Failure); + } const taskLabel = typeof taskId === 'string' ? taskId : taskId ? taskId.name : ''; const message = errorCount > 1 @@ -89,12 +92,15 @@ export class DebugTaskRunner { cancelId: 2 }); - if (result.choice === 2) { - return Promise.resolve(TaskRunResult.Failure); - } + const debugAnyway = result.choice === 0; + const cancel = result.choice = 2; if (result.checkboxChecked) { - this.configurationService.updateValue('debug.onTaskErrors', debugAnyway ? 'debugAnyway' : 'showErrors'); + this.configurationService.updateValue('debug.onTaskErrors', result.choice === 0 ? 'debugAnyway' : cancel ? 'cancel' : 'showErrors'); + } + + if (cancel) { + return Promise.resolve(TaskRunResult.Failure); } if (debugAnyway) { return TaskRunResult.Success; diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index 0e788a79f4..3ecffec357 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -34,10 +34,11 @@ import { dispose } from 'vs/base/common/lifecycle'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { ILabelService } from 'vs/platform/label/common/label'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import type { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import type { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; const NEW_STYLE_COMPRESS = true; @@ -423,13 +424,17 @@ export class LoadedScriptsView extends ViewPane { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IDebugService private readonly debugService: IDebugService, - @ILabelService private readonly labelService: ILabelService + @ILabelService private readonly labelService: ILabelService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.loadedScriptsItemType = CONTEXT_LOADED_SCRIPTS_ITEM_TYPE.bindTo(contextKeyService); } renderBody(container: HTMLElement): void { + super.renderBody(container); + dom.addClass(container, 'debug-loaded-scripts'); dom.addClass(container, 'show-file-icons'); @@ -466,7 +471,7 @@ export class LoadedScriptsView extends ViewPane { accessibilityProvider: new LoadedSciptsAccessibilityProvider(), ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'loadedScriptsAriaLabel' }, "Debug Loaded Scripts"), overrideStyles: { - listBackground: SIDE_BAR_BACKGROUND + listBackground: this.getBackgroundColor() } } ); @@ -568,6 +573,12 @@ export class LoadedScriptsView extends ViewPane { } })); + this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { + if (views.some(v => v.id === this.id)) { + this.tree.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); + } + })); + // feature: expand all nodes when filtering (not when finding) let viewState: IViewState | undefined; this._register(this.tree.onDidChangeTypeFilterPattern(pattern => { diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 7a11c460ba..527b2d5cbb 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -56,6 +56,7 @@ import { localize } from 'vs/nls'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const $ = dom.$; @@ -96,7 +97,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { @IDebugService private readonly debugService: IDebugService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, - @IThemeService protected themeService: IThemeService, + @IThemeService themeService: IThemeService, @IModelService private readonly modelService: IModelService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ICodeEditorService codeEditorService: ICodeEditorService, @@ -106,9 +107,10 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, @IClipboardService private readonly clipboardService: IClipboardService, @IEditorService private readonly editorService: IEditorService, - @IKeybindingService keybindingService: IKeybindingService + @IKeybindingService keybindingService: IKeybindingService, + @IOpenerService openerService: IOpenerService, ) { - super({ ...(options as IViewPaneOptions), id: REPL_VIEW_ID, ariaHeaderLabel: localize('debugConsole', "Debug Console") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), id: REPL_VIEW_ID, ariaHeaderLabel: localize('debugConsole', "Debug Console") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); codeEditorService.registerDecorationType(DECORATION_KEY, {}); @@ -431,6 +433,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { // --- Creation protected renderBody(parent: HTMLElement): void { + super.renderBody(parent); + this.container = dom.append(parent, $('.repl')); const treeContainer = dom.append(this.container, $('.repl-tree')); this.createReplInput(this.container); diff --git a/src/vs/workbench/contrib/debug/browser/startView.ts b/src/vs/workbench/contrib/debug/browser/startView.ts index ce48ffab4e..c66c6e3e04 100644 --- a/src/vs/workbench/contrib/debug/browser/startView.ts +++ b/src/vs/workbench/contrib/debug/browser/startView.ts @@ -26,6 +26,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const $ = dom.$; interface DebugStartMetrics { @@ -63,7 +64,7 @@ export class StartView extends ViewPane { constructor( options: IViewletViewOptions, - @IThemeService private readonly themeService: IThemeService, + @IThemeService themeService: IThemeService, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @@ -75,9 +76,10 @@ export class StartView extends ViewPane { @IFileDialogService private readonly dialogService: IFileDialogService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IOpenerService openerService: IOpenerService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: localize('debugStart', "Debug Start Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: localize('debugStart', "Debug Start Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this._register(editorService.onDidActiveEditorChange(() => this.updateView())); this._register(this.debugService.getConfigurationManager().onDidRegisterDebugger(() => this.updateView())); } @@ -156,6 +158,8 @@ export class StartView extends ViewPane { } protected renderBody(container: HTMLElement): void { + super.renderBody(container); + this.firstMessageContainer = $('.top-section'); container.appendChild(this.firstMessageContainer); diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 08cf9d5c2f..0051ac6b6b 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -30,8 +30,9 @@ import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabe import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; const $ = dom.$; let forgetScopes = true; @@ -54,9 +55,11 @@ export class VariablesView extends ViewPane { @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IClipboardService private readonly clipboardService: IClipboardService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('variablesSection', "Variables Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('variablesSection', "Variables Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); // Use scheduler to prevent unnecessary flashing this.onFocusStackFrameScheduler = new RunOnceScheduler(async () => { @@ -85,6 +88,8 @@ export class VariablesView extends ViewPane { } renderBody(container: HTMLElement): void { + super.renderBody(container); + dom.addClass(container, 'debug-variables'); const treeContainer = renderViewTree(container); @@ -96,7 +101,7 @@ export class VariablesView extends ViewPane { identityProvider: { getId: (element: IExpression | IScope) => element.getId() }, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression | IScope) => e }, overrideStyles: { - listBackground: SIDE_BAR_BACKGROUND + listBackground: this.getBackgroundColor() } }); @@ -104,10 +109,6 @@ export class VariablesView extends ViewPane { CONTEXT_VARIABLES_FOCUSED.bindTo(this.tree.contextKeyService); - if (this.toolbar) { - const collapseAction = new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all'); - this.toolbar.setActions([collapseAction])(); - } this.tree.updateChildren(); this._register(this.debugService.getViewModel().onDidFocusStackFrame(sf => { @@ -142,6 +143,15 @@ export class VariablesView extends ViewPane { this.tree.rerender(e); } })); + this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { + if (views.some(v => v.id === this.id)) { + this.tree.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); + } + })); + } + + getActions(): IAction[] { + return [new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all')]; } layoutBody(width: number, height: number): void { diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 556eead50f..a6920ca611 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -30,8 +30,9 @@ import { IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel import { variableSetEmitter, VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; let ignoreVariableSetEmitter = false; @@ -52,8 +53,10 @@ export class WatchExpressionsView extends ViewPane { @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.onWatchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { this.needsRefresh = false; @@ -62,6 +65,8 @@ export class WatchExpressionsView extends ViewPane { } renderBody(container: HTMLElement): void { + super.renderBody(container); + dom.addClass(container, 'debug-watch'); const treeContainer = renderViewTree(container); @@ -83,20 +88,13 @@ export class WatchExpressionsView extends ViewPane { }, dnd: new WatchExpressionsDragAndDrop(this.debugService), overrideStyles: { - listBackground: SIDE_BAR_BACKGROUND + listBackground: this.getBackgroundColor() } }); this.tree.setInput(this.debugService); CONTEXT_WATCH_EXPRESSIONS_FOCUSED.bindTo(this.tree.contextKeyService); - if (this.toolbar) { - const addWatchExpressionAction = new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService); - const collapseAction = new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all'); - const removeAllWatchExpressionsAction = new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService); - this.toolbar.setActions([addWatchExpressionAction, collapseAction, removeAllWatchExpressionsAction])(); - } - this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); this._register(this.debugService.getModel().onDidChangeWatchExpressions(async we => { @@ -140,6 +138,11 @@ export class WatchExpressionsView extends ViewPane { this.tree.rerender(e); } })); + this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { + if (views.some(v => v.id === this.id)) { + this.tree.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); + } + })); } layoutBody(height: number, width: number): void { @@ -150,6 +153,14 @@ export class WatchExpressionsView extends ViewPane { this.tree.domFocus(); } + getActions(): IAction[] { + return [ + new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService), + new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all'), + new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService) + ]; + } + private onMouseDblClick(e: ITreeMouseEvent): void { if ((e.browserEvent.target as HTMLElement).className.indexOf('twistie') >= 0) { // Ignore double click events on twistie diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index eb185ca412..108fc68c2d 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -467,7 +467,7 @@ export interface IDebugConfiguration { closeOnEnd: boolean; }; focusWindowOnBreak: boolean; - onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt'; + onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt' | 'cancel'; showBreakpointsInOverviewRuler: boolean; showInlineBreakpointCandidates: boolean; } diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index ef4024fa87..8dfe002e01 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -105,38 +105,43 @@ export function isUri(s: string | undefined): boolean { return !!(s && s.match(_schemePattern)); } -function stringToUri(path: string): string { - if (typeof path === 'string') { - if (isUri(path)) { - return uri.parse(path); +function stringToUri(source: PathContainer): string | undefined { + if (typeof source.path === 'string') { + if (typeof source.sourceReference === 'number' && source.sourceReference > 0) { + // if there is a source reference, don't touch path } else { - // assume path - if (isAbsolute(path)) { - return uri.file(path); + if (isUri(source.path)) { + return uri.parse(source.path); } else { - // leave relative path as is + // assume path + if (isAbsolute(source.path)) { + return uri.file(source.path); + } else { + // leave relative path as is + } } } } - return path; + return source.path; } -function uriToString(path: string): string { - if (typeof path === 'object') { - const u = uri.revive(path); +function uriToString(source: PathContainer): string | undefined { + if (typeof source.path === 'object') { + const u = uri.revive(source.path); if (u.scheme === 'file') { return u.fsPath; } else { return u.toString(); } } - return path; + return source.path; } // path hooks helpers interface PathContainer { path?: string; + sourceReference?: number; } export function convertToDAPaths(message: DebugProtocol.ProtocolMessage, toUri: boolean): DebugProtocol.ProtocolMessage { @@ -148,7 +153,7 @@ export function convertToDAPaths(message: DebugProtocol.ProtocolMessage, toUri: convertPaths(msg, (toDA: boolean, source: PathContainer | undefined) => { if (toDA && source) { - source.path = source.path ? fixPath(source.path) : undefined; + source.path = fixPath(source); } }); return msg; @@ -163,7 +168,7 @@ export function convertToVSCPaths(message: DebugProtocol.ProtocolMessage, toUri: convertPaths(msg, (toDA: boolean, source: PathContainer | undefined) => { if (!toDA && source) { - source.path = source.path ? fixPath(source.path) : undefined; + source.path = fixPath(source); } }); return msg; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index c4a1866092..2ad5f0b677 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -50,6 +50,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IMenuService } from 'vs/platform/actions/common/actions'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; @@ -96,7 +97,7 @@ export class ExtensionsListView extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService protected instantiationService: IInstantiationService, - @IThemeService private readonly themeService: IThemeService, + @IThemeService themeService: IThemeService, @IExtensionService private readonly extensionService: IExtensionService, @IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService, @IEditorService private readonly editorService: IEditorService, @@ -111,8 +112,9 @@ export class ExtensionsListView extends ViewPane { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IMenuService private readonly menuService: IMenuService, + @IOpenerService openerService: IOpenerService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.server = options.server; } @@ -125,6 +127,8 @@ export class ExtensionsListView extends ViewPane { } renderBody(container: HTMLElement): void { + super.renderBody(container); + const extensionsList = append(container, $('.extensions-list')); const messageContainer = append(container, $('.message-container')); const messageSeverityIcon = append(messageContainer, $('')); @@ -953,7 +957,6 @@ export class ServerExtensionsView extends ExtensionsListView { @IContextMenuService contextMenuService: IContextMenuService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, @IExtensionService extensionService: IExtensionService, @IEditorService editorService: IEditorService, @IExtensionTipsService tipsService: IExtensionTipsService, @@ -967,9 +970,11 @@ export class ServerExtensionsView extends ExtensionsListView { @IProductService productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService menuService: IMenuService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { options.server = server; - super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService); + super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService, openerService); this._register(onDidChangeTitle(title => this.updateTitle(title))); } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts index dd8d405bc7..61f3628aed 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts @@ -496,7 +496,7 @@ suite.skip('ExtensionsTipsService Test', () => { // {{SQL CARBON EDIT}} skip sui }); }); - test('test global extensions are modified and recommendation change event is fired when an extension is ignored', () => { + test('test global extensions are modified and recommendation change event is fired when an extension is ignored', async () => { const storageSetterTarget = sinon.spy(); const changeHandlerTarget = sinon.spy(); const ignoredExtensionId = 'Some.Extension'; @@ -508,15 +508,15 @@ suite.skip('ExtensionsTipsService Test', () => { // {{SQL CARBON EDIT}} skip sui } }); - return setUpFolderWorkspace('myFolder', []).then(() => { - testObject = instantiationService.createInstance(ExtensionTipsService); - testObject.onRecommendationChange(changeHandlerTarget); - testObject.toggleIgnoredRecommendation(ignoredExtensionId, true); + await setUpFolderWorkspace('myFolder', []); + testObject = instantiationService.createInstance(ExtensionTipsService); + testObject.onRecommendationChange(changeHandlerTarget); + testObject.toggleIgnoredRecommendation(ignoredExtensionId, true); + await testObject.loadWorkspaceConfigPromise; - assert.ok(changeHandlerTarget.calledOnce); - assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: 'Some.Extension', isRecommended: false })); - assert.ok(storageSetterTarget.calledWithExactly('extensionsAssistant/ignored_recommendations', `["ms-vscode.vscode","${ignoredExtensionId.toLowerCase()}"]`, StorageScope.GLOBAL)); - }); + assert.ok(changeHandlerTarget.calledOnce); + assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: 'Some.Extension', isRecommended: false })); + assert.ok(storageSetterTarget.calledWithExactly('extensionsAssistant/ignored_recommendations', `["ms-vscode.vscode","${ignoredExtensionId.toLowerCase()}"]`, StorageScope.GLOBAL)); }); test('ExtensionTipsService: Get file based recommendations from storage (old format)', () => { diff --git a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts b/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts index ae15fee962..7239d3e2d0 100644 --- a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts +++ b/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts @@ -301,29 +301,28 @@ export class LinuxExternalTerminalService implements IExternalTerminalService { private static _DEFAULT_TERMINAL_LINUX_READY: Promise; - public static getDefaultTerminalLinuxReady(): Promise { + public static async getDefaultTerminalLinuxReady(): Promise { if (!LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY) { - LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise(c => { + LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise(async r => { if (env.isLinux) { - Promise.all([pfs.exists('/etc/debian_version'), Promise.resolve(process.lazyEnv) || Promise.resolve(undefined)]).then(([isDebian]) => { - if (isDebian) { - c('x-terminal-emulator'); - } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { - c('gnome-terminal'); - } else if (process.env.DESKTOP_SESSION === 'kde-plasma') { - c('konsole'); - } else if (process.env.COLORTERM) { - c(process.env.COLORTERM); - } else if (process.env.TERM) { - c(process.env.TERM); - } else { - c('xterm'); - } - }); - return; + const isDebian = await pfs.exists('/etc/debian_version'); + await process.lazyEnv; + if (isDebian) { + r('x-terminal-emulator'); + } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { + r('gnome-terminal'); + } else if (process.env.DESKTOP_SESSION === 'kde-plasma') { + r('konsole'); + } else if (process.env.COLORTERM) { + r(process.env.COLORTERM); + } else if (process.env.TERM) { + r(process.env.TERM); + } else { + r('xterm'); + } + } else { + r('xterm'); } - - c('xterm'); }); } return LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY; diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 9942a7472c..2e53285b03 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -114,8 +114,7 @@ Registry.as(EditorInputExtensions.EditorInputFactor }); interface ISerializedFileInput { - resource: string; - resourceJSON: object; + resourceJSON: UriComponents; encoding?: string; modeId?: string; } @@ -131,7 +130,6 @@ class FileEditorInputFactory implements IEditorInputFactory { const fileEditorInput = editorInput; const resource = fileEditorInput.getResource(); const fileInput: ISerializedFileInput = { - resource: resource.toString(), // Keep for backwards compatibility resourceJSON: resource.toJSON(), encoding: fileEditorInput.getEncoding(), modeId: fileEditorInput.getPreferredMode() // only using the preferred user associated mode here if available to not store redundant data @@ -143,7 +141,7 @@ class FileEditorInputFactory implements IEditorInputFactory { deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): FileEditorInput { return instantiationService.invokeFunction(accessor => { const fileInput: ISerializedFileInput = JSON.parse(serializedEditorInput); - const resource = !!fileInput.resourceJSON ? URI.revive(fileInput.resourceJSON) : URI.parse(fileInput.resource); + const resource = URI.revive(fileInput.resourceJSON); const encoding = fileInput.encoding; const mode = fileInput.modeId; diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index da6e279072..775657afcf 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -26,6 +26,7 @@ import { Schemas } from 'vs/base/common/network'; import { isWeb } from 'vs/base/common/platform'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class EmptyView extends ViewPane { @@ -37,7 +38,7 @@ export class EmptyView extends ViewPane { constructor( options: IViewletViewOptions, - @IThemeService private readonly themeService: IThemeService, + @IThemeService themeService: IThemeService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IInstantiationService instantiationService: IInstantiationService, @IKeybindingService keybindingService: IKeybindingService, @@ -46,14 +47,17 @@ export class EmptyView extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService, @ILabelService private labelService: ILabelService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IOpenerService openerService: IOpenerService ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this._register(this.contextService.onDidChangeWorkbenchState(() => this.setLabels())); this._register(this.labelService.onDidChangeFormatters(() => this.setLabels())); } protected renderBody(container: HTMLElement): void { + super.renderBody(container); + DOM.addClass(container, 'explorer-empty-view'); container.tabIndex = 0; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 940dfb6495..4b2c3dbfc8 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -56,6 +56,7 @@ import { ColorValue, listDropBackground } from 'vs/platform/theme/common/colorRe import { Color } from 'vs/base/common/color'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; interface IExplorerViewColors extends IColorMapping { listDropBackground?: ColorValue | undefined; @@ -162,15 +163,16 @@ export class ExplorerView extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IDecorationsService private readonly decorationService: IDecorationsService, @ILabelService private readonly labelService: ILabelService, - @IThemeService private readonly themeService: IWorkbenchThemeService, + @IThemeService protected themeService: IWorkbenchThemeService, @IMenuService private readonly menuService: IMenuService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IExplorerService private readonly explorerService: IExplorerService, @IStorageService private readonly storageService: IStorageService, @IClipboardService private clipboardService: IClipboardService, - @IFileService private readonly fileService: IFileService + @IFileService private readonly fileService: IFileService, + @IOpenerService openerService: IOpenerService, ) { - super({ ...(options as IViewPaneOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.resourceContext = instantiationService.createInstance(ResourceContextKey); this._register(this.resourceContext); @@ -239,6 +241,8 @@ export class ExplorerView extends ViewPane { } renderBody(container: HTMLElement): void { + super.renderBody(container); + const treeContainer = DOM.append(container, DOM.$('.explorer-folders-view')); this.styleElement = DOM.createStyleSheet(treeContainer); @@ -246,10 +250,6 @@ export class ExplorerView extends ViewPane { this.createTree(treeContainer); - if (this.toolbar) { - this.toolbar.setActions(this.getActions(), this.getSecondaryActions())(); - } - this._register(this.labelService.onDidChangeFormatters(() => { this._onDidChangeTitleArea.fire(); })); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 800d02159c..17b125395e 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -45,6 +45,7 @@ import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/w import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const $ = dom.$; @@ -76,16 +77,17 @@ export class OpenEditorsView extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IThemeService private readonly themeService: IThemeService, + @IThemeService themeService: IThemeService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IMenuService private readonly menuService: IMenuService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IOpenerService openerService: IOpenerService, ) { super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize({ key: 'openEditosrSection', comment: ['Open is an adjective'] }, "Open Editors Section"), - }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.structuralRefreshDelay = 0; this.listRefreshScheduler = new RunOnceScheduler(() => { @@ -202,6 +204,8 @@ export class OpenEditorsView extends ViewPane { } renderBody(container: HTMLElement): void { + super.renderBody(container); + dom.addClass(container, 'explorer-open-editors'); dom.addClass(container, 'show-file-icons'); diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index 7efe909e88..2cc54420c7 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -77,7 +77,7 @@ export class ExplorerModel implements IDisposable { } export class ExplorerItem { - private _isDirectoryResolved: boolean; + protected _isDirectoryResolved: boolean; private _isDisposed: boolean; public isError = false; @@ -402,5 +402,6 @@ export class ExplorerItem { export class NewExplorerItem extends ExplorerItem { constructor(fileService: IFileService, parent: ExplorerItem, isDirectory: boolean) { super(URI.file(''), fileService, parent, isDirectory); + this._isDirectoryResolved = true; } } diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts index 68362466e8..0ff2fe5682 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts @@ -23,6 +23,8 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileE import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; class ServiceAccessor { constructor( @@ -80,7 +82,7 @@ suite('Files - FileEditorTracker', () => { (accessor.textFileService.files).dispose(); }); - async function createTracker(): Promise<[EditorPart, ServiceAccessor, FileEditorTracker]> { + async function createTracker(): Promise<[EditorPart, ServiceAccessor, FileEditorTracker, IInstantiationService, IEditorService]> { const instantiationService = workbenchInstantiationService(); const part = instantiationService.createInstance(EditorPart); @@ -98,7 +100,7 @@ suite('Files - FileEditorTracker', () => { const tracker = instantiationService.createInstance(FileEditorTracker); - return [part, accessor, tracker]; + return [part, accessor, tracker, instantiationService, editorService]; } test.skip('dirty text file model opens as editor', async function () { // {{SQL CARBON EDIT}} tabcolormode failure @@ -121,9 +123,9 @@ suite('Files - FileEditorTracker', () => { }); test.skip('dirty untitled text file model opens as editor', async function () { // {{SQL CARBON EDIT}} tabcolormode failure - const [part, accessor, tracker] = await createTracker(); + const [part, accessor, tracker, , editorService] = await createTracker(); - const untitledEditor = accessor.textFileService.untitled.create(); + const untitledEditor = editorService.createInput({ forceUntitled: true }) as UntitledTextEditorInput; const model = await untitledEditor.resolve(); assert.ok(!accessor.editorService.isOpen(untitledEditor)); diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 6f9cff54af..658fb09fd6 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -49,6 +49,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { editorLightBulbForeground, editorLightBulbAutoFixForeground } from 'vs/platform/theme/common/colorRegistry'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterator> { const markersIt = Iterator.fromArray(resourceMarkers.markers); @@ -104,8 +105,10 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { @IMenuService private readonly menuService: IMenuService, @IKeybindingService keybindingService: IKeybindingService, @IStorageService storageService: IStorageService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super({ ...(options as IViewPaneOptions), id: Constants.MARKERS_VIEW_ID, ariaHeaderLabel: Messages.MARKERS_PANEL_TITLE_PROBLEMS }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...(options as IViewPaneOptions), id: Constants.MARKERS_VIEW_ID, ariaHeaderLabel: Messages.MARKERS_PANEL_TITLE_PROBLEMS }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.panelFoucusContextKey = Constants.MarkerViewFocusContextKey.bindTo(contextKeyService); this.panelState = new Memento(Constants.MARKERS_VIEW_STORAGE_ID, storageService).getMemento(StorageScope.WORKSPACE); this.markersViewModel = this._register(instantiationService.createInstance(MarkersViewModel, this.panelState['multiline'])); @@ -129,6 +132,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } public renderBody(parent: HTMLElement): void { + super.renderBody(parent); dom.addClass(parent, 'markers-panel'); @@ -185,7 +189,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { return; } - if (this.isEmpty() && this.messageBoxContainer) { + if (this.hasNoProblems() && this.messageBoxContainer) { this.messageBoxContainer.focus(); } else if (this.tree) { this.tree.getHTMLElement().focus(); @@ -525,7 +529,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } } - private isEmpty(): boolean { + private hasNoProblems(): boolean { const { total, filtered } = this.getFilterStats(); return total === 0 || filtered === 0; } @@ -534,7 +538,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this.cachedFilterStats = undefined; this.resetTree(); if (this.tree) { - this.tree.toggleVisibility(this.isEmpty()); + this.tree.toggleVisibility(this.hasNoProblems()); } this.renderMessage(); } diff --git a/src/vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution.ts b/src/vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution.ts new file mode 100644 index 0000000000..2a38da8ba5 --- /dev/null +++ b/src/vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution.ts @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Action } from 'vs/base/common/actions'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IApplicationLink } from 'vs/workbench/workbench.web.api'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + +export class OpenInDesktopIndicator extends Disposable implements IWorkbenchContribution { + + constructor( + @IStatusbarService private readonly statusbarService: IStatusbarService, + @IWorkspaceContextService workspaceService: IWorkspaceContextService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService + ) { + super(); + + const links = environmentService.options?.applicationLinkProvider?.(); + if (Array.isArray(links) && links?.length > 0) { + this.installOpenInDesktopIndicator(links); + } + } + + private installOpenInDesktopIndicator(links: IApplicationLink[]): void { + + // Register action to trigger "Open In Desktop" + const registry = Registry.as(ActionExtensions.WorkbenchActions); + registry.registerWorkbenchAction(SyncActionDescriptor.create(OpenInDesktopAction, OpenInDesktopAction.ID, OpenInDesktopAction.LABEL), 'Open Workspace in Desktop'); + + // Show in status bar + const properties: IStatusbarEntry = { + backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), + color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND), + text: links.length === 1 ? links[0].label : localize('openInDesktop', "Open in Desktop..."), + command: 'workbench.web.openWorkspaceInDesktop' + }; + + this.statusbarService.addEntry(properties, 'status.openInDesktop', properties.text, StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */); + } +} + +const workbenchContributionsRegistry = Registry.as(WorkbenchContributionsExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(OpenInDesktopIndicator, LifecyclePhase.Starting); + +export class OpenInDesktopAction extends Action { + static readonly ID = 'workbench.web.openWorkspaceInDesktop'; + static readonly LABEL = localize('openWorkspaceInDesktop', "Open Workspace in Desktop"); + + constructor( + id: string, + label: string, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IOpenerService private readonly openerService: IOpenerService + ) { + super(id, label); + } + + async run(): Promise { + const links = this.environmentService.options?.applicationLinkProvider?.(); + if (Array.isArray(links)) { + if (links.length === 1) { + return this.openApplicationLink(links[0]); + } + + return this.runWithPicker(links); + } + + return true; + } + + private async runWithPicker(links: IApplicationLink[]): Promise { + + // Show a picker with choices + const quickPick = this.quickInputService.createQuickPick(); + quickPick.items = links; + quickPick.placeholder = OpenInDesktopAction.LABEL; + quickPick.canSelectMany = false; + quickPick.onDidAccept(() => { + const selectedItems = quickPick.selectedItems; + if (selectedItems.length === 1) { + this.openApplicationLink(selectedItems[0]); + } + quickPick.hide(); + }); + + quickPick.show(); + + return true; + } + + private async openApplicationLink(link: IApplicationLink): Promise { + this.openerService.open(link.uri, { openExternal: true }); + + return true; + } +} diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 4347f091b6..adb987c7e1 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -48,6 +48,7 @@ import { IDataSource } from 'vs/base/browser/ui/tree/tree'; import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; class RequestState { @@ -266,8 +267,10 @@ export class OutlinePane extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super(options, keybindingService, contextMenuService, _configurationService, contextKeyService, viewDescriptorService, _instantiationService); + super(options, keybindingService, contextMenuService, _configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService); this._outlineViewState.restore(this._storageService); this._contextKeyFocused = OutlineViewFocused.bindTo(contextKeyService); this._contextKeyFiltered = OutlineViewFiltered.bindTo(contextKeyService); @@ -295,6 +298,8 @@ export class OutlinePane extends ViewPane { } protected renderBody(container: HTMLElement): void { + super.renderBody(container); + this._domNode = container; this._domNode.tabIndex = 0; dom.addClass(container, 'outline-pane'); diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 3286cd5825..7456339521 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -29,6 +29,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class OutputViewPane extends ViewPane { @@ -46,8 +47,10 @@ export class OutputViewPane extends ViewPane { @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IInstantiationService instantiationService: IInstantiationService, @IOutputService private readonly outputService: IOutputService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.editor = instantiationService.createInstance(OutputEditor); this._register(this.editor.onTitleAreaUpdate(() => { this.updateTitle(this.editor.getTitle()); diff --git a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts index 6e2027427f..4c64d9ce69 100644 --- a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts @@ -10,7 +10,7 @@ import { Language } from 'vs/base/common/platform'; import { Action, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { Mode, IEntryRunContext, IAutoFocus, IModel, IQuickNavigateConfiguration } from 'vs/base/parts/quickopen/common/quickOpen'; import { QuickOpenEntryGroup, IHighlight, QuickOpenModel, QuickOpenEntry } from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { QuickOpenHandler, IWorkbenchQuickOpenConfiguration } from 'vs/workbench/browser/quickopen'; import { IEditorAction } from 'vs/editor/common/editorCommon'; @@ -471,7 +471,9 @@ export class CommandsHandler extends QuickOpenHandler implements IDisposable { // Other Actions const menu = this.editorService.invokeWithinEditorContext(accessor => this.menuService.createMenu(MenuId.CommandPalette, accessor.get(IContextKeyService))); - const menuActions = menu.getActions().reduce((r, [, actions]) => [...r, ...actions], []).filter(action => action instanceof MenuItemAction) as MenuItemAction[]; + const menuActions = menu.getActions() + .reduce((r, [, actions]) => [...r, ...actions], >[]) + .filter(action => action instanceof MenuItemAction) as MenuItemAction[]; const commandEntries = this.menuItemActionsToEntries(menuActions, searchValue); menu.dispose(); this.disposeOnClose.add(toDisposable(() => dispose(menuActions))); diff --git a/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts b/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts index 910b7a5951..bb170cfb4d 100644 --- a/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts @@ -77,7 +77,7 @@ class OutlineModel extends QuickOpenModel { const searchValuePos = searchValue.indexOf(SCOPE_PREFIX) === 0 ? 1 : 0; // Check for match and update visibility and group label - this.entries.forEach((entry: SymbolEntry) => { + (>this.entries).forEach(entry => { // Clear all state first entry.setGroupLabel(undefined); @@ -98,7 +98,7 @@ class OutlineModel extends QuickOpenModel { }); // select comparator based on the presence of the colon-prefix - this.entries.sort(searchValuePos === 0 + (>this.entries).sort(searchValuePos === 0 ? SymbolEntry.compareByRank : SymbolEntry.compareByKindAndRank ); diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 5b045d163c..d66c9cd168 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -368,16 +368,19 @@ class HelpPanel extends ViewPane { @IConfigurationService protected configurationService: IConfigurationService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IOpenerService protected openerService: IOpenerService, + @IOpenerService openerService: IOpenerService, @IQuickInputService protected quickInputService: IQuickInputService, @ICommandService protected commandService: ICommandService, @IRemoteExplorerService protected readonly remoteExplorerService: IRemoteExplorerService, - @IWorkbenchEnvironmentService protected readonly workbenchEnvironmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService protected readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IThemeService themeService: IThemeService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); } protected renderBody(container: HTMLElement): void { + super.renderBody(container); + dom.addClass(container, 'remote-help'); const treeContainer = document.createElement('div'); dom.addClass(treeContainer, 'remote-help-content'); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index bdb27ee7f1..4503ecbf5d 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -426,10 +426,12 @@ const TunnelViewSelectionKeyName = 'tunnelViewSelection'; const TunnelViewSelectionContextKey = new RawContextKey(TunnelViewSelectionKeyName, undefined); const PortChangableContextKey = new RawContextKey('portChangable', false); +class TunnelDataTree extends WorkbenchAsyncDataTree { } + export class TunnelPanel extends ViewPane { static readonly ID = '~remote.forwardedPorts'; static readonly TITLE = nls.localize('remote.tunnel', "Forwarded Ports"); - private tree!: WorkbenchAsyncDataTree; + private tree!: TunnelDataTree; private tunnelTypeContext: IContextKey; private tunnelCloseableContext: IContextKey; private tunnelViewFocusContext: IContextKey; @@ -448,16 +450,16 @@ export class TunnelPanel extends ViewPane { @IConfigurationService protected configurationService: IConfigurationService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IOpenerService protected openerService: IOpenerService, + @IOpenerService openerService: IOpenerService, @IQuickInputService protected quickInputService: IQuickInputService, @ICommandService protected commandService: ICommandService, @IMenuService private readonly menuService: IMenuService, @INotificationService private readonly notificationService: INotificationService, @IContextViewService private readonly contextViewService: IContextViewService, - @IThemeService private readonly themeService: IThemeService, + @IThemeService themeService: IThemeService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService); this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService); this.tunnelViewFocusContext = TunnelViewFocusContextKey.bindTo(contextKeyService); @@ -484,13 +486,15 @@ export class TunnelPanel extends ViewPane { } protected renderBody(container: HTMLElement): void { + super.renderBody(container); + const panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view')); const treeContainer = dom.append(panelContainer, dom.$('.customview-tree')); dom.addClass(treeContainer, 'file-icon-themable-tree'); dom.addClass(treeContainer, 'show-file-icons'); const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService); - this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, + this.tree = this.instantiationService.createInstance(TunnelDataTree, 'RemoteTunnels', treeContainer, new TunnelTreeVirtualDelegate(), diff --git a/src/vs/workbench/contrib/scm/browser/mainPane.ts b/src/vs/workbench/contrib/scm/browser/mainPane.ts index 977392163e..82951159b2 100644 --- a/src/vs/workbench/contrib/scm/browser/mainPane.ts +++ b/src/vs/workbench/contrib/scm/browser/mainPane.ts @@ -32,6 +32,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IViewDescriptor, IViewDescriptorService } from 'vs/workbench/common/views'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export interface ISpliceEvent { index: number; @@ -186,12 +187,16 @@ export class MainPane extends ViewPane { @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); } protected renderBody(container: HTMLElement): void { + super.renderBody(container); + const delegate = new ProvidersListDelegate(); const renderer = this.instantiationService.createInstance(ProviderRenderer); const identityProvider = { getId: (r: ISCMRepository) => r.provider.id }; diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts index a3e812745a..4359467732 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts @@ -63,8 +63,11 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import * as platform from 'vs/base/common/platform'; import { format } from 'vs/base/common/strings'; import { inputPlaceholderForeground, inputValidationInfoBorder, inputValidationWarningBorder, inputValidationErrorBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBackground, inputValidationErrorForeground, inputBackground, inputForeground, inputBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { Schemas } from 'vs/base/common/network'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; type TreeElement = ISCMResourceGroup | IResourceNode | ISCMResource; @@ -612,6 +615,8 @@ export class RepositoryPane extends ViewPane { protected contextKeyService: IContextKeyService; private commitTemplate = ''; + isEmpty() { return true; } + constructor( readonly repository: ISCMRepository, options: IViewPaneOptions, @@ -629,8 +634,9 @@ export class RepositoryPane extends ViewPane { @IMenuService protected menuService: IMenuService, @IStorageService private storageService: IStorageService, @IModelService private modelService: IModelService, + @IOpenerService openerService: IOpenerService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.menus = instantiationService.createInstance(SCMMenus, this.repository.provider); this._register(this.menus); @@ -663,6 +669,8 @@ export class RepositoryPane extends ViewPane { } protected renderBody(container: HTMLElement): void { + super.renderBody(container); + const focusTracker = trackFocus(container); this._register(focusTracker.onDidFocus(() => this.repository.focus())); this._register(focusTracker); @@ -721,16 +729,15 @@ export class RepositoryPane extends ViewPane { fontFamily: ' -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif', wrappingStrategy: 'advanced', wrappingIndent: 'none', - // suggest: { - // showWords: false - // } + padding: { top: 3, bottom: 3 }, + suggest: { showWords: false } }; const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { isSimpleWidget: true, contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - // SuggestController.ID, - // SnippetController2.ID, + SuggestController.ID, + SnippetController2.ID, MenuPreventer.ID, SelectionClipboardContributionID, ContextMenuController.ID, @@ -761,10 +768,6 @@ export class RepositoryPane extends ViewPane { this.inputModel = this.modelService.getModel(uri) || this.modelService.createModel('', null, uri); this.inputEditor.setModel(this.inputModel); - this.inputEditor.changeViewZones(accessor => { - accessor.addZone({ afterLineNumber: 0, domNode: $('div'), heightInPx: 3 }); - }); - this._register(this.inputEditor.onDidChangeCursorPosition(triggerValidation)); // Keep model in sync with API @@ -844,7 +847,7 @@ export class RepositoryPane extends ViewPane { this._register(Event.chain(this.tree.onDidOpen) .map(e => e.elements[0]) - .filter(e => !!e && !isSCMResourceGroup(e) && !ResourceTree.isResourceNode(e)) + .filter((e): e is ISCMResource => !!e && !isSCMResourceGroup(e) && !ResourceTree.isResourceNode(e)) .on(this.open, this)); this._register(Event.chain(this.tree.onDidPin) @@ -916,7 +919,7 @@ export class RepositoryPane extends ViewPane { removeClass(this.inputContainer, 'hidden'); const editorContentHeight = this.inputEditor.getContentHeight(); - const editorHeight = Math.min(editorContentHeight + 3, 134); + const editorHeight = Math.min(editorContentHeight, 134); this.inputEditor.layout({ height: editorHeight, width: width! - 12 - 16 - 2 }); this.validationContainer.style.top = `${editorHeight + 1}px`; @@ -997,7 +1000,7 @@ export class RepositoryPane extends ViewPane { } } - private onListContextMenu(e: ITreeContextMenuEvent): void { + private onListContextMenu(e: ITreeContextMenuEvent): void { if (!e.element) { return; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index c267b49d83..f83b53f2c1 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -30,7 +30,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { RepositoryPane, RepositoryViewDescriptor } from 'vs/workbench/contrib/scm/browser/repositoryPane'; import { MainPaneDescriptor, MainPane, IViewModel } from 'vs/workbench/contrib/scm/browser/mainPane'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import type { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; +import type { IAddedViewDescriptorRef, IViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { debounce } from 'vs/base/common/decorators'; export interface ISpliceEvent { @@ -169,7 +169,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode } } - private onDidHideView(e: IAddedViewDescriptorRef[]): void { + private onDidHideView(e: IViewDescriptorRef[]): void { for (const ref of e) { if (ref.viewDescriptor instanceof RepositoryViewDescriptor) { ref.viewDescriptor.repository.setSelected(false); diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index e14c5d8fbf..214ea9f2e9 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -162,16 +162,17 @@ export class SearchView extends ViewPane { @IReplaceService private readonly replaceService: IReplaceService, @ITextFileService private readonly textFileService: ITextFileService, @IPreferencesService private readonly preferencesService: IPreferencesService, - @IThemeService protected themeService: IThemeService, + @IThemeService themeService: IThemeService, @ISearchHistoryService private readonly searchHistoryService: ISearchHistoryService, @IContextMenuService contextMenuService: IContextMenuService, @IMenuService private readonly menuService: IMenuService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IKeybindingService keybindingService: IKeybindingService, @IStorageService storageService: IStorageService, - @IOpenerService private readonly openerService: IOpenerService) { + @IOpenerService openerService: IOpenerService, + ) { - super({ ...options, id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super({ ...options, id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.container = dom.$('.search-view'); @@ -254,6 +255,7 @@ export class SearchView extends ViewPane { } renderBody(parent: HTMLElement): void { + super.renderBody(parent); this.container = dom.append(parent, dom.$('.search-view')); this.searchWidgetsContainerElement = dom.append(this.container, $('.search-widgets-container')); diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 1dc76a1a93..e94569c345 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -448,8 +448,9 @@ export class SearchWidget extends Widget { } setValue(value: string, skipSearchOnChange: boolean) { - this.temporarilySkipSearchOnChange = skipSearchOnChange || this.temporarilySkipSearchOnChange; + this.temporarilySkipSearchOnChange = skipSearchOnChange; this.searchInput.setValue(value); + this.temporarilySkipSearchOnChange = false; } setReplaceAllActionState(enabled: boolean): void { @@ -491,9 +492,7 @@ export class SearchWidget extends Widget { this.setReplaceAllActionState(false); if (this.searchConfiguration.searchOnType) { - if (this.temporarilySkipSearchOnChange) { - this.temporarilySkipSearchOnChange = false; - } else { + if (!this.temporarilySkipSearchOnChange) { this._onSearchCancel.fire({ focus: false }); if (this.searchInput.getRegex()) { try { diff --git a/src/vs/workbench/contrib/searchEditor/browser/constants.ts b/src/vs/workbench/contrib/searchEditor/browser/constants.ts index ebf304cac4..4d3e23bd87 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/constants.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/constants.ts @@ -17,3 +17,5 @@ export const EnableSearchEditorPreview = new RawContextKey('previewSear export const InSearchEditor = new RawContextKey('inSearchEditor', false); export const SearchEditorScheme = 'search-editor'; + +export const SearchEditorFindMatchClass = 'seaarchEditorFindMatch'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 17a3b103a3..dc87f1bf0a 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -106,18 +106,17 @@ class SearchEditorInputFactory implements IEditorInputFactory { const config = input.getConfigSync(); const dirty = input.isDirty(); - const highlights = input.highlights; + const matchRanges = input.getMatchRanges(); - return JSON.stringify({ resource, dirty, config, viewState: input.viewState, name: input.getName(), highlights }); + return JSON.stringify({ resource, dirty, config, name: input.getName(), matchRanges }); } deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): SearchEditorInput | undefined { - const { resource, dirty, config, viewState, highlights } = JSON.parse(serializedEditorInput); + const { resource, dirty, config, matchRanges } = JSON.parse(serializedEditorInput); if (config && (config.query !== undefined)) { const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config, uri: URI.parse(resource) }); - input.viewState = viewState; input.setDirty(dirty); - input.setHighlights(highlights); + input.setMatchRanges(matchRanges); return input; } return undefined; @@ -167,8 +166,7 @@ const category = localize('search', "Search Editor"); registry.registerWorkbenchAction( SyncActionDescriptor.create(ReRunSearchEditorSearchAction, ReRunSearchEditorSearchAction.ID, ReRunSearchEditorSearchAction.LABEL), - 'Search Editor: Rerun search', category, ContextKeyExpr.and(SearchEditorConstants.InSearchEditor) -); + 'Search Editor: Rerun search', category, ContextKeyExpr.and(SearchEditorConstants.InSearchEditor)); registry.registerWorkbenchAction( SyncActionDescriptor.create(OpenResultsInEditorAction, OpenResultsInEditorAction.ID, OpenResultsInEditorAction.LABEL, diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index bc0a060664..6dc60fcbaa 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -11,15 +11,12 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/searchEditor'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; -import type { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { Range } from 'vs/editor/common/core/range'; -import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ReferencesController } from 'vs/editor/contrib/gotoSymbol/peek/referencesController'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -31,7 +28,6 @@ import { inputBorder, registerColor, searchEditorFindMatch, searchEditorFindMatc import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; @@ -39,18 +35,29 @@ import { InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search'; import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; -import { InSearchEditor } from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { InSearchEditor, SearchEditorFindMatchClass } from 'vs/workbench/contrib/searchEditor/browser/constants'; import type { SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { extractSearchQuery, serializeSearchConfiguration, serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IPatternInfo, ISearchConfigurationProperties, ITextQuery } from 'vs/workbench/services/search/common/search'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ICodeEditorViewState } from 'vs/editor/common/editorCommon'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; +import { assertIsDefined } from 'vs/base/common/types'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/; const FILE_LINE_REGEX = /^(\S.*):$/; -export class SearchEditor extends BaseEditor { +type SearchEditorViewState = ICodeEditorViewState & { focused: 'input' | 'editor' }; + +export class SearchEditor extends BaseTextEditor { static readonly ID: string = 'workbench.editor.searchEditor'; + static readonly SEARCH_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'searchEditorViewState'; + private queryEditorWidget!: SearchWidget; private searchResultEditor!: CodeEditorWidget; private queryEditorContainer!: HTMLElement; @@ -78,16 +85,20 @@ export class SearchEditor extends BaseEditor { @IModelService private readonly modelService: IModelService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ILabelService private readonly labelService: ILabelService, - @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService readonly instantiationService: IInstantiationService, @IContextViewService private readonly contextViewService: IContextViewService, @ICommandService private readonly commandService: ICommandService, @IContextKeyService readonly contextKeyService: IContextKeyService, @IEditorProgressService readonly progressService: IEditorProgressService, + @ITextResourceConfigurationService textResourceService: ITextResourceConfigurationService, + @IEditorGroupsService protected editorGroupService: IEditorGroupsService, + @IEditorService protected editorService: IEditorService, + @IConfigurationService protected configurationService: IConfigurationService, ) { - super(SearchEditor.ID, telemetryService, themeService, storageService); + super(SearchEditor.ID, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService); this.container = DOM.$('.search-editor'); + const scopedContextKeyService = contextKeyService.createScoped(this.container); this.instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); @@ -186,18 +197,9 @@ export class SearchEditor extends BaseEditor { private createResultsEditor(parent: HTMLElement) { const searchResultContainer = DOM.append(parent, DOM.$('.search-results')); - const getSearchEditorOptions = () => this.configurationService.getValue('editor', { overrideIdentifier: 'search-result' }); - const configuration: IEditorOptions = getSearchEditorOptions(); - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('editor')) { - this.searchResultEditor.updateOptions(getSearchEditorOptions()); - } - })); - - const options: ICodeEditorWidgetOptions = {}; - this.searchResultEditor = this._register(this.instantiationService.createInstance(CodeEditorWidget, searchResultContainer, configuration, options)); + super.createEditor(searchResultContainer); + this.searchResultEditor = super.getControl() as CodeEditorWidget; this.searchResultEditor.onMouseUp(e => { - if (e.event.detail === 2) { const behaviour = this.configurationService.getValue('search').searchEditorPreview.doubleClickBehaviour; const position = e.target.position; @@ -227,14 +229,13 @@ export class SearchEditor extends BaseEditor { }); } - getControl() { return this.searchResultEditor; } focus() { - const input = this.getInput(); - if (input && input.viewState && input.viewState.focused === 'editor') { + const viewState = this.loadViewState(); + if (viewState && viewState.focused === 'editor') { this.searchResultEditor.focus(); } else { this.queryEditorWidget.focus(); @@ -296,23 +297,17 @@ export class SearchEditor extends BaseEditor { if (!this.pauseSearching) { await this.runSearchDelayer.trigger(async () => { await this.doRunSearch(); + this.toggleRunAgainMessage(false); if (resetCursor) { this.searchResultEditor.setSelection(new Range(1, 1, 1, 1)); + this.searchResultEditor.setScrollPosition({ scrollTop: 0, scrollLeft: 0 }); } }, instant ? 0 : undefined); } } - private async doRunSearch() { - const startInput = this.input; - - this.searchHistoryDelayer.trigger(() => { - this.queryEditorWidget.searchInput.onSearchSubmit(); - this.inputPatternExcludes.onSearchSubmit(); - this.inputPatternIncludes.onSearchSubmit(); - }); - - const config: SearchConfiguration = { + private readConfigFromWidget() { + return { caseSensitive: this.queryEditorWidget.searchInput.getCaseSensitive(), contextLines: this.queryEditorWidget.contextLines(), excludes: this.inputPatternExcludes.getValue(), @@ -323,6 +318,18 @@ export class SearchEditor extends BaseEditor { useIgnores: this.inputPatternExcludes.useExcludesAndIgnoreFiles(), showIncludesExcludes: this.showingIncludesExcludes }; + } + + private async doRunSearch() { + const startInput = this.getInput(); + + this.searchHistoryDelayer.trigger(() => { + this.queryEditorWidget.searchInput.onSearchSubmit(); + this.inputPatternExcludes.onSearchSubmit(); + this.inputPatternIncludes.onSearchSubmit(); + }); + + const config: SearchConfiguration = this.readConfigFromWidget(); if (!config.query) { return; } @@ -364,7 +371,10 @@ export class SearchEditor extends BaseEditor { this.searchOperation.start(500); await searchModel.search(query).finally(() => this.searchOperation.stop()); const input = this.getInput(); - if (!input || input !== startInput) { + if (!input || + input !== startInput || + JSON.stringify(config) !== JSON.stringify(this.readConfigFromWidget())) { + searchModel.dispose(); return; } @@ -378,8 +388,7 @@ export class SearchEditor extends BaseEditor { header.setValue(serializeSearchConfiguration(config)); input.setDirty(input.resource.scheme !== 'search-editor'); - input.setHighlights(results.matchRanges.map(range => - ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }))); + input.setMatchRanges(results.matchRanges); searchModel.dispose(); } @@ -454,28 +463,34 @@ export class SearchEditor extends BaseEditor { this.reLayout(); } - getModel() { - return this.searchResultEditor.getModel(); + saveState() { + this.saveViewState(); + super.saveState(); } private saveViewState() { - const input = this.getInput(); - if (!input) { return; } + const resource = this.getInput()?.resource; + if (resource) { this.saveTextEditorViewState(resource); } + } - if (this.searchResultEditor.hasWidgetFocus()) { - const viewState = this.searchResultEditor.saveViewState(); - if (viewState) { - input.viewState = { focused: 'editor', state: viewState }; - } - } else { - input.viewState = { focused: 'input' }; - } + protected retrieveTextEditorViewState(resource: URI): SearchEditorViewState | null { + const control = this.getControl(); + const editorViewState = control.saveViewState(); + if (!editorViewState) { return null; } + if (resource.toString() !== this.getInput()?.resource.toString()) { return null; } + + return { ...editorViewState, focused: this.searchResultEditor.hasWidgetFocus() ? 'editor' : 'input' }; + } + + private loadViewState() { + const resource = assertIsDefined(this.input?.getResource()); + return this.loadTextEditorViewState(resource) as SearchEditorViewState; } private restoreViewState() { - const input = this.getInput(); - if (input && input.viewState && input.viewState.focused === 'editor') { - this.searchResultEditor.restoreViewState(input.viewState.state); + const viewState = this.loadViewState(); + if (viewState) { this.searchResultEditor.restoreViewState(viewState); } + if (viewState && viewState.focused === 'editor') { this.searchResultEditor.focus(); } else { this.queryEditorWidget.focus(); @@ -486,14 +501,18 @@ export class SearchEditor extends BaseEditor { this.saveViewState(); super.clearInput(); } + + getAriaLabel() { + return this.getInput()?.getName() ?? localize('searchEditor', "Search Editor"); + } } registerThemingParticipant((theme, collector) => { - collector.addRule(`.monaco-editor .searchEditorFindMatch { background-color: ${theme.getColor(searchEditorFindMatch)}; }`); + collector.addRule(`.monaco-editor .${SearchEditorFindMatchClass} { background-color: ${theme.getColor(searchEditorFindMatch)}; }`); const findMatchHighlightBorder = theme.getColor(searchEditorFindMatchBorder); if (findMatchHighlightBorder) { - collector.addRule(`.monaco-editor .searchEditorFindMatch { border: 1px ${theme.type === 'hc' ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`); + collector.addRule(`.monaco-editor .${SearchEditorFindMatchClass} { border: 1px ${theme.type === 'hc' ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`); } }); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 7cb2c1bfdb..c54c5a4ab0 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -4,11 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Action } from 'vs/base/common/actions'; -import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/searchEditor'; import { ICodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -138,6 +136,7 @@ const openNewSearchEditor = const editorService = accessor.get(IEditorService); const telemetryService = accessor.get(ITelemetryService); const instantiationService = accessor.get(IInstantiationService); + const configurationService = accessor.get(IConfigurationService); const activeEditor = editorService.activeTextEditorWidget; let activeModel: ICodeEditor | undefined; @@ -164,7 +163,11 @@ const openNewSearchEditor = telemetryService.publicLog2('searchEditor/openNewSearchEditor'); const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected } }); - await editorService.openEditor(input, { pinned: true }); + const editor = await editorService.openEditor(input, { pinned: true }) as SearchEditor; + + if (selected && configurationService.getValue('search').searchOnType) { + editor.runSearch(true, true); + } }; export const createEditorFromSearchResult = @@ -187,7 +190,6 @@ export const createEditorFromSearchResult = const { text, matchRanges } = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter, true); const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { text }); - const editor = await editorService.openEditor(input, { pinned: true }) as SearchEditor; - const model = assertIsDefined(editor.getModel()); - model.deltaDecorations([], matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }))); + await editorService.openEditor(input, { pinned: true }); + input.setMatchRanges(matchRanges); }; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index 0ae2c831ee..5b8cd30234 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -9,8 +9,8 @@ import { basename } from 'vs/base/common/path'; import { isEqual, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/searchEditor'; -import type { ICodeEditorViewState } from 'vs/editor/common/editorCommon'; -import { IModelDeltaDecoration, ITextModel, DefaultEndOfLine } from 'vs/editor/common/model'; +import { Range } from 'vs/editor/common/core/range'; +import { DefaultEndOfLine, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; @@ -18,16 +18,16 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorInput, GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; +import { SearchEditorFindMatchClass, SearchEditorScheme } from 'vs/workbench/contrib/searchEditor/browser/constants'; import { extractSearchQuery, serializeSearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { ITextFileSaveOptions, ITextFileService, snapshotToString, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { SearchEditorScheme } from 'vs/workbench/contrib/searchEditor/browser/constants'; -import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; export type SearchConfiguration = { @@ -42,24 +42,18 @@ export type SearchConfiguration = { showIncludesExcludes: boolean, }; -type SearchEditorViewState = - | { focused: 'input' } - | { focused: 'editor', state: ICodeEditorViewState }; - export class SearchEditorInput extends EditorInput { static readonly ID: string = 'workbench.editorinputs.searchEditorInput'; private dirty: boolean = false; private readonly contentsModel: Promise; private readonly headerModel: Promise; + private _cachedContentsModel: ITextModel | undefined; private _cachedConfig?: SearchConfiguration; private readonly _onDidChangeContent = new Emitter(); readonly onDidChangeContent: Event = this._onDidChangeContent.event; - viewState: SearchEditorViewState = { focused: 'input' }; - - private _highlights: IModelDeltaDecoration[] | undefined; private oldDecorationsIDs: string[] = []; constructor( @@ -93,9 +87,11 @@ export class SearchEditorInput extends EditorInput { })); this._cachedConfig = extractSearchQuery(headerModel); + this._cachedContentsModel = contentsModel; this._register(contentsModel); this._register(headerModel); + this._onDidChangeLabel.fire(); return { contentsModel, headerModel }; }); @@ -154,7 +150,7 @@ export class SearchEditorInput extends EditorInput { this.setDirty(false); if (!isEqual(path, this.resource)) { const input = this.instantiationService.invokeFunction(getOrMakeSearchEditorInput, { uri: path }); - input.setHighlights(this.highlights); + input.setMatchRanges(this.getMatchRanges()); return input; } return this; @@ -241,14 +237,15 @@ export class SearchEditorInput extends EditorInput { return false; } - public get highlights(): IModelDeltaDecoration[] { - return (this._highlights ?? []).map(({ range, options }) => ({ range, options })); + public getMatchRanges(): Range[] { + return (this._cachedContentsModel?.getAllDecorations() ?? []) + .filter(decoration => decoration.options.className === SearchEditorFindMatchClass) + .map(({ range }) => range); } - public async setHighlights(value: IModelDeltaDecoration[]) { - if (!value) { return; } - this.oldDecorationsIDs = (await this.contentsModel).deltaDecorations(this.oldDecorationsIDs, value); - this._highlights = value; + public async setMatchRanges(ranges: Range[]) { + this.oldDecorationsIDs = (await this.contentsModel).deltaDecorations(this.oldDecorationsIDs, ranges.map(range => + ({ range, options: { className: SearchEditorFindMatchClass, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }))); } async revert(group: GroupIdentifier, options?: IRevertOptions) { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index cd1d3d3aef..b5dec3258b 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -207,13 +207,15 @@ export const extractSearchQuery = (model: ITextModel | string): SearchConfigurat export const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string, includeHeader: boolean): { matchRanges: Range[], text: string } => { - const header = (includeHeader + const header = includeHeader ? contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines) - : []); + : []; - const resultCount = searchResult.count() ? localize('resultCount', "{0} results in {1} files", searchResult.count(), searchResult.fileCount()) : localize('noResults', "No Results"); - header.push(resultCount); - header.push(''); + const info = [ + searchResult.count() + ? localize('resultCount', "{0} results in {1} files", searchResult.count(), searchResult.fileCount()) + : localize('noResults', "No Results"), + '']; const allResults = flattenSearchResultSerializations( @@ -223,8 +225,8 @@ export const serializeSearchResultForEditor = .map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter))))); return { - matchRanges: allResults.matchRanges.map(translateRangeLines(header.length)), - text: header.concat(allResults.text).join(lineDelimiter) + matchRanges: allResults.matchRanges.map(translateRangeLines(info.length)), + text: header.concat(info).concat(allResults.text).join(lineDelimiter) }; }; diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index e4b5e8cdc2..22e29a69e4 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -154,7 +154,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { }); } - resolveCompletionItem?(model: ITextModel, position: Position, item: CompletionItem): CompletionItem { + resolveCompletionItem(_model: ITextModel, _position: Position, item: CompletionItem): CompletionItem { return (item instanceof SnippetCompletion) ? item.resolve() : item; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 39f87496f0..5c03f1f623 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -332,11 +332,6 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, - 'terminal.integrated.experimentalRefreshOnResume': { - description: nls.localize('terminal.integrated.experimentalRefreshOnResume', "An experimental setting that will refresh the terminal renderer when the system is resumed."), - type: 'boolean', - default: false - }, 'terminal.integrated.experimentalUseTitleEvent': { description: nls.localize('terminal.integrated.experimentalUseTitleEvent', "An experimental setting that will use the terminal title event for the dropdown title. This setting will only apply to new terminals."), type: 'boolean', diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 46d6f56852..76d6c9d518 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -392,12 +392,6 @@ export interface ITerminalInstance { */ sendText(text: string, addNewLine: boolean): void; - /** - * Write text directly to the terminal, skipping the process if it exists. - * @param text The text to write. - */ - write(text: string): void; - /** Scroll the terminal buffer down 1 line. */ scrollDownLine(): void; /** Scroll the terminal buffer down 1 page. */ diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 1f3251a260..cc35698cb7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -38,28 +38,26 @@ import { Action2 } from 'vs/platform/actions/common/actions'; export const TERMINAL_PICKER_PREFIX = 'term '; -function getCwdForSplit(configHelper: ITerminalConfigHelper, instance: ITerminalInstance, folders?: IWorkspaceFolder[], commandService?: ICommandService): Promise { +async function getCwdForSplit(configHelper: ITerminalConfigHelper, instance: ITerminalInstance, folders?: IWorkspaceFolder[], commandService?: ICommandService): Promise { switch (configHelper.config.splitCwd) { case 'workspaceRoot': - let pathPromise: Promise = Promise.resolve(''); if (folders !== undefined && commandService !== undefined) { if (folders.length === 1) { - pathPromise = Promise.resolve(folders[0].uri); + return folders[0].uri; } else if (folders.length > 1) { // Only choose a path when there's more than 1 folder const options: IPickOptions = { placeHolder: nls.localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") }; - pathPromise = commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]).then(workspace => { - if (!workspace) { - // Don't split the instance if the workspace picker was canceled - return undefined; - } - return Promise.resolve(workspace.uri); - }); + const workspace = await commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]); + if (!workspace) { + // Don't split the instance if the workspace picker was canceled + return undefined; + } + return Promise.resolve(workspace.uri); } } - return pathPromise; + return ''; case 'initial': return instance.getInitialCwd(); case 'inherited': @@ -133,12 +131,13 @@ export class QuickKillTerminalAction extends Action { super(id, label, 'terminal-action kill'); } - public run(event?: any): Promise { + public async run(event?: any): Promise { const instance = this.terminalEntry.instance; if (instance) { instance.dispose(true); } - return Promise.resolve(timeout(50)).then(result => this.quickOpenService.show(TERMINAL_PICKER_PREFIX, undefined)); + await timeout(50); + return this.quickOpenService.show(TERMINAL_PICKER_PREFIX, undefined); } } @@ -329,43 +328,35 @@ export class CreateNewTerminalAction extends Action { super(id, label, 'terminal-action codicon-add'); } - public run(event?: any): Promise { + public async run(event?: any): Promise { const folders = this.workspaceContextService.getWorkspace().folders; if (event instanceof MouseEvent && (event.altKey || event.ctrlKey)) { const activeInstance = this.terminalService.getActiveInstance(); if (activeInstance) { - return getCwdForSplit(this.terminalService.configHelper, activeInstance).then(cwd => { - this.terminalService.splitInstance(activeInstance, { cwd }); - return Promise.resolve(null); - }); + const cwd = await getCwdForSplit(this.terminalService.configHelper, activeInstance); + this.terminalService.splitInstance(activeInstance, { cwd }); + return undefined; } } - let instancePromise: Promise; + let instance: ITerminalInstance | undefined; if (folders.length <= 1) { // Allow terminal service to handle the path when there is only a // single root - instancePromise = Promise.resolve(this.terminalService.createTerminal(undefined)); + instance = this.terminalService.createTerminal(undefined); } else { const options: IPickOptions = { placeHolder: nls.localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") }; - instancePromise = this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]).then(workspace => { - if (!workspace) { - // Don't create the instance if the workspace picker was canceled - return null; - } - return this.terminalService.createTerminal({ cwd: workspace.uri }); - }); - } - - return instancePromise.then(instance => { - if (!instance) { - return Promise.resolve(undefined); + const workspace = await this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]); + if (!workspace) { + // Don't create the instance if the workspace picker was canceled + return undefined; } - this.terminalService.setActiveInstance(instance); - return this.terminalService.showPanel(true); - }); + instance = this.terminalService.createTerminal({ cwd: workspace.uri }); + } + this.terminalService.setActiveInstance(instance); + return this.terminalService.showPanel(true); } } @@ -405,19 +396,17 @@ export class SplitTerminalAction extends Action { super(id, label, 'terminal-action codicon-split-horizontal'); } - public run(event?: any): Promise { + public async run(event?: any): Promise { const instance = this._terminalService.getActiveInstance(); if (!instance) { return Promise.resolve(undefined); } - return getCwdForSplit(this._terminalService.configHelper, instance, this.workspaceContextService.getWorkspace().folders, this.commandService).then(cwd => { - if (cwd || (cwd === '')) { - this._terminalService.splitInstance(instance, { cwd }); - return this._terminalService.showPanel(true); - } else { - return undefined; - } - }); + const cwd = await getCwdForSplit(this._terminalService.configHelper, instance, this.workspaceContextService.getWorkspace().folders, this.commandService); + if (cwd === undefined) { + return undefined; + } + this._terminalService.splitInstance(instance, { cwd }); + return this._terminalService.showPanel(true); } } @@ -432,15 +421,14 @@ export class SplitInActiveWorkspaceTerminalAction extends Action { super(id, label); } - public run(event?: any): Promise { + public async run(event?: any): Promise { const instance = this._terminalService.getActiveInstance(); if (!instance) { return Promise.resolve(undefined); } - return getCwdForSplit(this._terminalService.configHelper, instance).then(cwd => { - this._terminalService.splitInstance(instance, { cwd }); - return this._terminalService.showPanel(true); - }); + const cwd = await getCwdForSplit(this._terminalService.configHelper, instance); + this._terminalService.splitInstance(instance, { cwd }); + return this._terminalService.showPanel(true); } } @@ -697,7 +685,7 @@ export class RunActiveFileInTerminalAction extends Action { super(id, label); } - public run(event?: any): Promise { + public async run(event?: any): Promise { const instance = this.terminalService.getActiveOrCreateInstance(); if (!instance) { return Promise.resolve(undefined); @@ -712,10 +700,9 @@ export class RunActiveFileInTerminalAction extends Action { return Promise.resolve(undefined); } - return this.terminalService.preparePathForTerminalAsync(uri.fsPath, instance.shellLaunchConfig.executable, instance.title, instance.shellType).then(path => { - instance.sendText(path, true); - return this.terminalService.showPanel(); - }); + const path = await this.terminalService.preparePathForTerminalAsync(uri.fsPath, instance.shellLaunchConfig.executable, instance.title, instance.shellType); + instance.sendText(path, true); + return this.terminalService.showPanel(); } } @@ -1040,19 +1027,18 @@ export class RenameTerminalAction extends Action { super(id, label); } - public run(entry?: TerminalEntry): Promise { + public async run(entry?: TerminalEntry): Promise { const terminalInstance = entry ? entry.instance : this.terminalService.getActiveInstance(); if (!terminalInstance) { return Promise.resolve(undefined); } - return this.quickInputService.input({ + const name = await this.quickInputService.input({ value: terminalInstance.title, prompt: nls.localize('workbench.action.terminal.rename.prompt', "Enter terminal name"), - }).then(name => { - if (name) { - terminalInstance.setTitle(name, TitleEventSource.Api); - } }); + if (name) { + terminalInstance.setTitle(name, TitleEventSource.Api); + } } } export class RenameWithArgTerminalAction extends Action2 { @@ -1168,12 +1154,11 @@ export class RenameTerminalQuickOpenAction extends RenameTerminalAction { this.class = 'codicon codicon-gear'; } - public run(): Promise { - super.run(this.terminal) - // This timeout is needed to make sure the previous quickOpen has time to close before we show the next one - .then(() => timeout(50)) - .then(result => this.quickOpenService.show(TERMINAL_PICKER_PREFIX, undefined)); - return Promise.resolve(null); + public async run(): Promise { + await super.run(this.terminal); + // This timeout is needed to make sure the previous quickOpen has time to close before we show the next one + await timeout(50); + await this.quickOpenService.show(TERMINAL_PICKER_PREFIX, undefined); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index 503fb2510f..a3af40a1b4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -312,9 +312,8 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { } } - private isExtensionInstalled(id: string): Promise { - return this._extensionManagementService.getInstalled(ExtensionType.User).then(extensions => { - return extensions.some(e => e.identifier.id === id); - }); + private async isExtensionInstalled(id: string): Promise { + const extensions = await this._extensionManagementService.getInstalled(ExtensionType.User); + return extensions.some(e => e.identifier.id === id); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 13a465d037..ab9d80dd14 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -560,136 +560,136 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._container.appendChild(this._wrapperElement); } - public _attachToElement(container: HTMLElement): void { - this._xtermReadyPromise.then(xterm => { - if (this._wrapperElement) { - throw new Error('The terminal instance has already been attached to a container'); - } + public async _attachToElement(container: HTMLElement): Promise { + const xterm = await this._xtermReadyPromise; - this._container = container; - this._wrapperElement = document.createElement('div'); - dom.addClass(this._wrapperElement, 'terminal-wrapper'); - this._xtermElement = document.createElement('div'); + if (this._wrapperElement) { + throw new Error('The terminal instance has already been attached to a container'); + } - // Attach the xterm object to the DOM, exposing it to the smoke tests - this._wrapperElement.xterm = this._xterm; + this._container = container; + this._wrapperElement = document.createElement('div'); + dom.addClass(this._wrapperElement, 'terminal-wrapper'); + this._xtermElement = document.createElement('div'); - this._wrapperElement.appendChild(this._xtermElement); - this._container.appendChild(this._wrapperElement); - xterm.open(this._xtermElement); - if (this._configHelper.config.rendererType === 'experimentalWebgl') { - this._terminalInstanceService.getXtermWebglConstructor().then(Addon => { - xterm.loadAddon(new Addon()); - }); - } + // Attach the xterm object to the DOM, exposing it to the smoke tests + this._wrapperElement.xterm = this._xterm; - if (!xterm.element || !xterm.textarea) { - throw new Error('xterm elements not set after open'); - } - - xterm.textarea.addEventListener('focus', () => this._onFocus.fire(this)); - xterm.attachCustomKeyEventHandler((event: KeyboardEvent): boolean => { - // Disable all input if the terminal is exiting - if (this._isExiting) { - return false; - } - - // Skip processing by xterm.js of keyboard events that resolve to commands described - // within commandsToSkipShell - const standardKeyboardEvent = new StandardKeyboardEvent(event); - const resolveResult = this._keybindingService.softDispatch(standardKeyboardEvent, standardKeyboardEvent.target); - // Respect chords if the allowChords setting is set and it's not Escape. Escape is - // handled specially for Zen Mode's Escape, Escape chord, plus it's important in - // terminals generally - const allowChords = resolveResult && resolveResult.enterChord && this._configHelper.config.allowChords && event.key !== 'Escape'; - if (allowChords || resolveResult && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) { - event.preventDefault(); - return false; - } - - // If tab focus mode is on, tab is not passed to the terminal - if (TabFocus.getTabFocusMode() && event.keyCode === 9) { - return false; - } - - // Always have alt+F4 skip the terminal on Windows and allow it to be handled by the - // system - if (platform.isWindows && event.altKey && event.key === 'F4' && !event.ctrlKey) { - return false; - } - - return true; + this._wrapperElement.appendChild(this._xtermElement); + this._container.appendChild(this._wrapperElement); + xterm.open(this._xtermElement); + if (this._configHelper.config.rendererType === 'experimentalWebgl') { + this._terminalInstanceService.getXtermWebglConstructor().then(Addon => { + xterm.loadAddon(new Addon()); }); - this._register(dom.addDisposableListener(xterm.element, 'mousedown', () => { - // We need to listen to the mouseup event on the document since the user may release - // the mouse button anywhere outside of _xterm.element. - const listener = dom.addDisposableListener(document, 'mouseup', () => { - // Delay with a setTimeout to allow the mouseup to propagate through the DOM - // before evaluating the new selection state. - setTimeout(() => this._refreshSelectionContextKey(), 0); - listener.dispose(); - }); - })); + } - // xterm.js currently drops selection on keyup as we need to handle this case. - this._register(dom.addDisposableListener(xterm.element, 'keyup', () => { - // Wait until keyup has propagated through the DOM before evaluating - // the new selection state. - setTimeout(() => this._refreshSelectionContextKey(), 0); - })); + if (!xterm.element || !xterm.textarea) { + throw new Error('xterm elements not set after open'); + } - const xtermHelper: HTMLElement = xterm.element.querySelector('.xterm-helpers'); - const focusTrap: HTMLElement = document.createElement('div'); - focusTrap.setAttribute('tabindex', '0'); - dom.addClass(focusTrap, 'focus-trap'); - this._register(dom.addDisposableListener(focusTrap, 'focus', () => { - let currentElement = focusTrap; - while (!dom.hasClass(currentElement, 'part')) { - currentElement = currentElement.parentElement!; - } - const hidePanelElement = currentElement.querySelector('.hide-panel-action'); - hidePanelElement.focus(); - })); - xtermHelper.insertBefore(focusTrap, xterm.textarea); - - this._register(dom.addDisposableListener(xterm.textarea, 'focus', () => { - this._terminalFocusContextKey.set(true); - this._onFocused.fire(this); - })); - this._register(dom.addDisposableListener(xterm.textarea, 'blur', () => { - this._terminalFocusContextKey.reset(); - this._refreshSelectionContextKey(); - })); - this._register(dom.addDisposableListener(xterm.element, 'focus', () => { - this._terminalFocusContextKey.set(true); - })); - this._register(dom.addDisposableListener(xterm.element, 'blur', () => { - this._terminalFocusContextKey.reset(); - this._refreshSelectionContextKey(); - })); - - const widgetManager = new TerminalWidgetManager(this._wrapperElement, this._openerService); - this._widgetManager = widgetManager; - this._processManager.onProcessReady(() => this._linkHandler?.setWidgetManager(widgetManager)); - - const computedStyle = window.getComputedStyle(this._container); - const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10); - const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10); - this.layout(new dom.Dimension(width, height)); - this.setVisible(this._isVisible); - this.updateConfig(); - - // If IShellLaunchConfig.waitOnExit was true and the process finished before the terminal - // panel was initialized. - if (xterm.getOption('disableStdin')) { - this._attachPressAnyKeyToCloseListener(xterm); + xterm.textarea.addEventListener('focus', () => this._onFocus.fire(this)); + xterm.attachCustomKeyEventHandler((event: KeyboardEvent): boolean => { + // Disable all input if the terminal is exiting + if (this._isExiting) { + return false; } - const neverMeasureRenderTime = this._storageService.getBoolean(NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, StorageScope.GLOBAL, false); - if (!neverMeasureRenderTime && this._configHelper.config.rendererType === 'auto') { - this._measureRenderTime(); + // Skip processing by xterm.js of keyboard events that resolve to commands described + // within commandsToSkipShell + const standardKeyboardEvent = new StandardKeyboardEvent(event); + const resolveResult = this._keybindingService.softDispatch(standardKeyboardEvent, standardKeyboardEvent.target); + // Respect chords if the allowChords setting is set and it's not Escape. Escape is + // handled specially for Zen Mode's Escape, Escape chord, plus it's important in + // terminals generally + const allowChords = resolveResult && resolveResult.enterChord && this._configHelper.config.allowChords && event.key !== 'Escape'; + if (allowChords || resolveResult && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) { + event.preventDefault(); + return false; } + + // If tab focus mode is on, tab is not passed to the terminal + if (TabFocus.getTabFocusMode() && event.keyCode === 9) { + return false; + } + + // Always have alt+F4 skip the terminal on Windows and allow it to be handled by the + // system + if (platform.isWindows && event.altKey && event.key === 'F4' && !event.ctrlKey) { + return false; + } + + return true; }); + this._register(dom.addDisposableListener(xterm.element, 'mousedown', () => { + // We need to listen to the mouseup event on the document since the user may release + // the mouse button anywhere outside of _xterm.element. + const listener = dom.addDisposableListener(document, 'mouseup', () => { + // Delay with a setTimeout to allow the mouseup to propagate through the DOM + // before evaluating the new selection state. + setTimeout(() => this._refreshSelectionContextKey(), 0); + listener.dispose(); + }); + })); + + // xterm.js currently drops selection on keyup as we need to handle this case. + this._register(dom.addDisposableListener(xterm.element, 'keyup', () => { + // Wait until keyup has propagated through the DOM before evaluating + // the new selection state. + setTimeout(() => this._refreshSelectionContextKey(), 0); + })); + + const xtermHelper: HTMLElement = xterm.element.querySelector('.xterm-helpers'); + const focusTrap: HTMLElement = document.createElement('div'); + focusTrap.setAttribute('tabindex', '0'); + dom.addClass(focusTrap, 'focus-trap'); + this._register(dom.addDisposableListener(focusTrap, 'focus', () => { + let currentElement = focusTrap; + while (!dom.hasClass(currentElement, 'part')) { + currentElement = currentElement.parentElement!; + } + const hidePanelElement = currentElement.querySelector('.hide-panel-action'); + hidePanelElement.focus(); + })); + xtermHelper.insertBefore(focusTrap, xterm.textarea); + + this._register(dom.addDisposableListener(xterm.textarea, 'focus', () => { + this._terminalFocusContextKey.set(true); + this._onFocused.fire(this); + })); + this._register(dom.addDisposableListener(xterm.textarea, 'blur', () => { + this._terminalFocusContextKey.reset(); + this._refreshSelectionContextKey(); + })); + this._register(dom.addDisposableListener(xterm.element, 'focus', () => { + this._terminalFocusContextKey.set(true); + })); + this._register(dom.addDisposableListener(xterm.element, 'blur', () => { + this._terminalFocusContextKey.reset(); + this._refreshSelectionContextKey(); + })); + + const widgetManager = new TerminalWidgetManager(this._wrapperElement, this._openerService); + this._widgetManager = widgetManager; + this._processManager.onProcessReady(() => this._linkHandler?.setWidgetManager(widgetManager)); + + const computedStyle = window.getComputedStyle(this._container); + const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10); + const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10); + this.layout(new dom.Dimension(width, height)); + this.setVisible(this._isVisible); + this.updateConfig(); + + // If IShellLaunchConfig.waitOnExit was true and the process finished before the terminal + // panel was initialized. + if (xterm.getOption('disableStdin')) { + this._attachPressAnyKeyToCloseListener(xterm); + } + + const neverMeasureRenderTime = this._storageService.getBoolean(NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, StorageScope.GLOBAL, false); + if (!neverMeasureRenderTime && this._configHelper.config.rendererType === 'auto') { + this._measureRenderTime(); + } } private async _measureRenderTime(): Promise { @@ -744,6 +744,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } public deregisterLinkMatcher(linkMatcherId: number): void { + // TODO: Move this into TerminalLinkHandler to avoid the promise check this._xtermReadyPromise.then(xterm => xterm.deregisterLinkMatcher(linkMatcherId)); } @@ -846,15 +847,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (!this._xterm) { return; } - if (this._configHelper.config.experimentalRefreshOnResume) { - if (this._xterm.getOption('rendererType') !== 'dom') { - this._xterm.setOption('rendererType', 'dom'); - // Do this asynchronously to clear our the texture atlas as all terminals will not - // be using canvas - const xterm = this._xterm; - setTimeout(() => xterm.setOption('rendererType', 'canvas'), 0); - } - } this._xterm.refresh(0, this._xterm.rows - 1); } @@ -872,8 +864,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - public focusWhenReady(force?: boolean): Promise { - return this._xtermReadyPromise.then(() => this.focus(force)); + public async focusWhenReady(force?: boolean): Promise { + await this._xtermReadyPromise; + this.focus(force); } public async paste(): Promise { @@ -883,17 +876,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.focus(); this._xterm.paste(await this._clipboardService.readText()); } - - public write(text: string): void { - this._xtermReadyPromise.then(() => { - if (!this._xterm) { - return; - } - this._xterm.write(text); - }); - } - - public sendText(text: string, addNewLine: boolean): void { + public async sendText(text: string, addNewLine: boolean): Promise { // Normalize line endings to 'enter' press. text = text.replace(TerminalInstance.EOL_REGEX, '\r'); if (addNewLine && text.substr(text.length - 1) !== '\r') { @@ -901,7 +884,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } // Send it to the process - this._processManager.ptyProcessReady.then(() => this._processManager.write(text)); + await this._processManager.ptyProcessReady; + this._processManager.write(text); } public setVisible(visible: boolean): void { @@ -1341,7 +1325,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } @debounce(50) - private _resize(): void { + private async _resize(): Promise { let cols = this.cols; let rows = this.rows; @@ -1391,7 +1375,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(cols, rows)); + await this._processManager.ptyProcessReady; + this._processManager.setDimensions(cols, rows); } public setShellType(shellType: TerminalShellType) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 97e0f620fb..a9461856de 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -14,7 +14,6 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IFileService } from 'vs/platform/files/common/files'; import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -29,6 +28,7 @@ import { isWindows, isMacintosh, OperatingSystem } from 'vs/base/common/platform import { basename } from 'vs/base/common/path'; import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; import { find } from 'vs/base/common/arrays'; +import { timeout } from 'vs/base/common/async'; import { IViewsService } from 'vs/workbench/common/views'; interface IExtHostReadyEntry { @@ -97,7 +97,6 @@ export class TerminalService implements ITerminalService { @IDialogService private _dialogService: IDialogService, @IInstantiationService private _instantiationService: IInstantiationService, @IExtensionService private _extensionService: IExtensionService, - @IFileService private _fileService: IFileService, @IRemoteAgentService private _remoteAgentService: IRemoteAgentService, @IQuickInputService private _quickInputService: IQuickInputService, @IConfigurationService private _configurationService: IConfigurationService, @@ -110,7 +109,7 @@ export class TerminalService implements ITerminalService { this._activeTabIndex = 0; this._isShuttingDown = false; this._findState = new FindReplaceState(); - lifecycleService.onBeforeShutdown(event => event.veto(this._onBeforeShutdown())); + lifecycleService.onBeforeShutdown(async event => event.veto(await this._onBeforeShutdown())); lifecycleService.onShutdown(() => this._onShutdown()); if (this._terminalNativeService) { this._terminalNativeService.onOpenFileRequest(e => this._onOpenFileRequest(e)); @@ -143,15 +142,14 @@ export class TerminalService implements ITerminalService { return activeInstance ? activeInstance : this.createTerminal(undefined); } - public requestSpawnExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): void { - this._extensionService.whenInstalledExtensionsRegistered().then(async () => { - // Wait for the remoteAuthority to be ready (and listening for events) before firing - // the event to spawn the ext host process - const conn = this._remoteAgentService.getConnection(); - const remoteAuthority = conn ? conn.remoteAuthority : 'null'; - await this._whenExtHostReady(remoteAuthority); - this._onInstanceRequestSpawnExtHostProcess.fire({ proxy, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, isWorkspaceShellAllowed }); - }); + public async requestSpawnExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { + await this._extensionService.whenInstalledExtensionsRegistered(); + // Wait for the remoteAuthority to be ready (and listening for events) before firing + // the event to spawn the ext host process + const conn = this._remoteAgentService.getConnection(); + const remoteAuthority = conn ? conn.remoteAuthority : 'null'; + await this._whenExtHostReady(remoteAuthority); + this._onInstanceRequestSpawnExtHostProcess.fire({ proxy, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, isWorkspaceShellAllowed }); } public requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): void { @@ -178,7 +176,7 @@ export class TerminalService implements ITerminalService { this._extHostsReady[remoteAuthority] = { promise, resolve }; } - private _onBeforeShutdown(): boolean | Promise { + private async _onBeforeShutdown(): Promise { if (this.terminalInstances.length === 0) { // No terminal instances, don't veto return false; @@ -186,12 +184,11 @@ export class TerminalService implements ITerminalService { if (this.configHelper.config.confirmOnExit) { // veto if configured to show confirmation and the user choosed not to exit - return this._showTerminalCloseConfirmation().then(veto => { - if (!veto) { - this._isShuttingDown = true; - } - return veto; - }); + const veto = await this._showTerminalCloseConfirmation(); + if (!veto) { + this._isShuttingDown = true; + } + return veto; } this._isShuttingDown = true; @@ -204,20 +201,19 @@ export class TerminalService implements ITerminalService { this.terminalInstances.forEach(instance => instance.dispose(true)); } - private _onOpenFileRequest(request: IOpenFileRequest): void { + private async _onOpenFileRequest(request: IOpenFileRequest): Promise { // if the request to open files is coming in from the integrated terminal (identified though // the termProgram variable) and we are instructed to wait for editors close, wait for the // marker file to get deleted and then focus back to the integrated terminal. - if (request.termProgram === 'vscode' && request.filesToWait) { + if (request.termProgram === 'vscode' && request.filesToWait && this._terminalNativeService) { const waitMarkerFileUri = URI.revive(request.filesToWait.waitMarkerFileUri); - this._terminalNativeService?.whenFileDeleted(waitMarkerFileUri).then(() => { - if (this.terminalInstances.length > 0) { - const terminal = this.getActiveInstance(); - if (terminal) { - terminal.focus(); - } + await this._terminalNativeService.whenFileDeleted(waitMarkerFileUri); + if (this.terminalInstances.length > 0) { + const terminal = this.getActiveInstance(); + if (terminal) { + terminal.focus(); } - }); + } } } @@ -421,43 +417,20 @@ export class TerminalService implements ITerminalService { return find(this._terminalTabs, tab => tab.terminalInstances.indexOf(instance) !== -1); } - public showPanel(focus?: boolean): Promise { - return new Promise(async (complete) => { - const pane = this._viewsService.getActiveViewWithId(TERMINAL_VIEW_ID) as TerminalViewPane; - if (!pane) { - await this._panelService.openPanel(TERMINAL_VIEW_ID, focus); - if (focus) { - // Do the focus call asynchronously as going through the - // command palette will force editor focus - setTimeout(() => { - const instance = this.getActiveInstance(); - if (instance) { - instance.focusWhenReady(true).then(() => complete(undefined)); - } else { - complete(undefined); - } - }, 0); - } else { - complete(undefined); - } - } else { - if (focus) { - // Do the focus call asynchronously as going through the - // command palette will force editor focus - setTimeout(() => { - const instance = this.getActiveInstance(); - if (instance) { - instance.focusWhenReady(true).then(() => complete(undefined)); - } else { - complete(undefined); - } - }, 0); - } else { - complete(undefined); - } + public async showPanel(focus?: boolean): Promise { + const pane = this._viewsService.getActiveViewWithId(TERMINAL_VIEW_ID) as TerminalViewPane; + if (!pane) { + await this._panelService.openPanel(TERMINAL_VIEW_ID, focus); + } + if (focus) { + // Do the focus call asynchronously as going through the + // command palette will force editor focus + await timeout(0); + const instance = this.getActiveInstance(); + if (instance) { + await instance.focusWhenReady(true); } - return undefined; - }); + } } private _getIndexFromId(terminalId: number): number { @@ -497,22 +470,6 @@ export class TerminalService implements ITerminalService { return !res.confirmed; } - protected _validateShellPaths(label: string, potentialPaths: string[]): Promise<[string, string] | null> { - if (potentialPaths.length === 0) { - return Promise.resolve(null); - } - const current = potentialPaths.shift(); - if (current! === '') { - return this._validateShellPaths(label, potentialPaths); - } - return this._fileService.exists(URI.file(current!)).then(exists => { - if (!exists) { - return this._validateShellPaths(label, potentialPaths); - } - return [label, current] as [string, string]; - }); - } - public preparePathForTerminalAsync(originalPath: string, executable: string, title: string, shellType: TerminalShellType): Promise { return new Promise(c => { if (!executable) { @@ -575,34 +532,31 @@ export class TerminalService implements ITerminalService { }); } - public selectDefaultWindowsShell(): Promise { - return this._detectWindowsShells().then(shells => { - const options: IPickOptions = { - placeHolder: nls.localize('terminal.integrated.chooseWindowsShell', "Select your preferred terminal shell, you can change this later in your settings") - }; - const quickPickItems = shells.map((s): IQuickPickItem => { - return { label: s.label, description: s.path }; - }); - return this._quickInputService.pick(quickPickItems, options).then(async value => { - if (!value) { - return undefined; - } - const shell = value.description; - const env = await this._remoteAgentService.getEnvironment(); - let platformKey: string; - if (env) { - platformKey = env.os === OperatingSystem.Windows ? 'windows' : (env.os === OperatingSystem.Macintosh ? 'osx' : 'linux'); - } else { - platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); - } - await this._configurationService.updateValue(`terminal.integrated.shell.${platformKey}`, shell, ConfigurationTarget.USER).then(() => shell); - return Promise.resolve(); - }); + public async selectDefaultWindowsShell(): Promise { + const shells = await this._detectWindowsShells(); + const options: IPickOptions = { + placeHolder: nls.localize('terminal.integrated.chooseWindowsShell', "Select your preferred terminal shell, you can change this later in your settings") + }; + const quickPickItems = shells.map((s): IQuickPickItem => { + return { label: s.label, description: s.path }; }); + const value = await this._quickInputService.pick(quickPickItems, options); + if (!value) { + return undefined; + } + const shell = value.description; + const env = await this._remoteAgentService.getEnvironment(); + let platformKey: string; + if (env) { + platformKey = env.os === OperatingSystem.Windows ? 'windows' : (env.os === OperatingSystem.Macintosh ? 'osx' : 'linux'); + } else { + platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); + } + await this._configurationService.updateValue(`terminal.integrated.shell.${platformKey}`, shell, ConfigurationTarget.USER); } private _detectWindowsShells(): Promise { - return new Promise(r => this._onRequestAvailableShells.fire(r)); + return new Promise(r => this._onRequestAvailableShells.fire({ callback: r })); } @@ -655,12 +609,11 @@ export class TerminalService implements ITerminalService { this._onInstancesChanged.fire(); } - public focusFindWidget(): Promise { - return this.showPanel(false).then(() => { - const pane = this._viewsService.getActiveViewWithId(TERMINAL_VIEW_ID) as TerminalViewPane; - pane.focusFindWidget(); - this._findWidgetVisible.set(true); - }); + public async focusFindWidget(): Promise { + await this.showPanel(false); + const pane = this._viewsService.getActiveViewWithId(TERMINAL_VIEW_ID) as TerminalViewPane; + pane.focusFindWidget(); + this._findWidgetVisible.set(true); } public hideFindWidget(): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index b3ae2b465c..22368eb002 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -28,6 +28,7 @@ import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/vie import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const FIND_FOCUS_CLASS = 'find-focused'; @@ -53,9 +54,10 @@ export class TerminalViewPane extends ViewPane { @IThemeService protected readonly themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, @INotificationService private readonly _notificationService: INotificationService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IOpenerService openerService: IOpenerService, ) { - super(options, keybindingService, _contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instantiationService); + super(options, keybindingService, _contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService); } protected renderBody(container: HTMLElement): void { @@ -296,9 +298,8 @@ export class TerminalViewPane extends ViewPane { const terminal = this._terminalService.getActiveInstance(); if (terminal) { - return this._terminalService.preparePathForTerminalAsync(path, terminal.shellLaunchConfig.executable, terminal.title, terminal.shellType).then(preparedPath => { - terminal.sendText(preparedPath, false); - }); + const preparedPath = await this._terminalService.preparePathForTerminalAsync(path, terminal.shellLaunchConfig.executable, terminal.title, terminal.shellType); + terminal.sendText(preparedPath, false); } } })); diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index df5d245877..04f06255dd 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -119,7 +119,6 @@ export interface ITerminalConfiguration { showExitAlert: boolean; splitCwd: 'workspaceRoot' | 'initial' | 'inherited'; windowsEnableConpty: boolean; - experimentalRefreshOnResume: boolean; experimentalUseTitleEvent: boolean; enableFileLinks: boolean; unicodeVersion: '6' | '11'; @@ -360,7 +359,7 @@ export interface IStartExtensionTerminalRequest { } export interface IAvailableShellsRequest { - (shells: IShellDefinition[]): void; + callback: (shells: IShellDefinition[]) => void; } export interface IDefaultShellAndArgsRequest { diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeService.ts index a0dd76f725..aa44b6bdfe 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeService.ts @@ -44,17 +44,16 @@ export class TerminalNativeService implements ITerminalNativeService { // Complete when wait marker file is deleted return new Promise(resolve => { let running = false; - const interval = setInterval(() => { + const interval = setInterval(async () => { if (!running) { running = true; - this._fileService.exists(path).then(exists => { - running = false; + const exists = await this._fileService.exists(path); + running = false; - if (!exists) { - clearInterval(interval); - resolve(undefined); - } - }); + if (!exists) { + clearInterval(interval); + resolve(undefined); + } } }, 1000); }); diff --git a/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts b/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts index d0f038a7fa..665343e281 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts @@ -10,6 +10,7 @@ import { Terminal as XTermTerminal } from 'xterm'; import * as WindowsProcessTreeType from 'windows-process-tree'; import { Disposable } from 'vs/base/common/lifecycle'; import { ITerminalInstance, TerminalShellType, WindowsShellType } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { timeout } from 'vs/base/common/async'; const SHELL_EXECUTABLES = [ 'cmd.exe', @@ -46,36 +47,40 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe this._isDisposed = false; - (import('windows-process-tree')).then(mod => { - if (this._isDisposed) { - return; - } + this._startMonitoringShell(); + } - windowsProcessTree = mod; - // The debounce is necessary to prevent multiple processes from spawning when - // the enter key or output is spammed - Event.debounce(this._onCheckShell.event, (l, e) => e, 150, true)(() => { - setTimeout(() => { - this.checkShell(); - }, 50); - }); + private async _startMonitoringShell(): Promise { + if (!windowsProcessTree) { + windowsProcessTree = await import('windows-process-tree'); + } - // We want to fire a new check for the shell on a linefeed, but only - // when parsing has finished which is indicated by the cursormove event. - // If this is done on every linefeed, parsing ends up taking - // significantly longer due to resetting timers. Note that this is - // private API. - this._xterm.onLineFeed(() => this._newLineFeed = true); - this._xterm.onCursorMove(() => { - if (this._newLineFeed) { - this._onCheckShell.fire(undefined); - this._newLineFeed = false; - } - }); + if (this._isDisposed) { + return; + } - // Fire a new check for the shell when any key is pressed. - this._xterm.onKey(() => this._onCheckShell.fire(undefined)); + // The debounce is necessary to prevent multiple processes from spawning when + // the enter key or output is spammed + Event.debounce(this._onCheckShell.event, (l, e) => e, 150, true)(async () => { + await timeout(50); + this.checkShell(); }); + + // We want to fire a new check for the shell on a linefeed, but only + // when parsing has finished which is indicated by the cursormove event. + // If this is done on every linefeed, parsing ends up taking + // significantly longer due to resetting timers. Note that this is + // private API. + this._xterm.onLineFeed(() => this._newLineFeed = true); + this._xterm.onCursorMove(() => { + if (this._newLineFeed) { + this._onCheckShell.fire(undefined); + this._newLineFeed = false; + } + }); + + // Fire a new check for the shell when any key is pressed. + this._xterm.onKey(() => this._onCheckShell.fire(undefined)); } private checkShell(): void { diff --git a/src/vs/workbench/contrib/terminal/node/terminal.ts b/src/vs/workbench/contrib/terminal/node/terminal.ts index 85397c1610..558655ad13 100644 --- a/src/vs/workbench/contrib/terminal/node/terminal.ts +++ b/src/vs/workbench/contrib/terminal/node/terminal.ts @@ -64,18 +64,17 @@ function getSystemShellWindows(): string { let detectedDistro = LinuxDistro.Unknown; if (platform.isLinux) { const file = '/etc/os-release'; - fileExists(file).then(exists => { + fileExists(file).then(async exists => { if (!exists) { return; } - readFile(file).then(b => { - const contents = b.toString(); - if (/NAME="?Fedora"?/.test(contents)) { - detectedDistro = LinuxDistro.Fedora; - } else if (/NAME="?Ubuntu"?/.test(contents)) { - detectedDistro = LinuxDistro.Ubuntu; - } - }); + const buffer = await readFile(file); + const contents = buffer.toString(); + if (/NAME="?Fedora"?/.test(contents)) { + detectedDistro = LinuxDistro.Fedora; + } else if (/NAME="?Ubuntu"?/.test(contents)) { + detectedDistro = LinuxDistro.Ubuntu; + } }); } @@ -128,8 +127,8 @@ async function detectAvailableWindowsShells(): Promise { }; const promises: PromiseLike[] = []; Object.keys(expectedLocations).forEach(key => promises.push(validateShellPaths(key, expectedLocations[key]))); - - return Promise.all(promises).then(coalesce); + const shells = await Promise.all(promises); + return coalesce(shells); } async function detectAvailableUnixShells(): Promise { diff --git a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts index 7af8e2cae0..636f1ff98f 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts @@ -70,6 +70,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess conptyInheritCursor: useConpty && !!shellLaunchConfig.initialText }; + // TODO: Pull verification out into its own function const cwdVerification = stat(cwd).then(async stat => { if (!stat.isDirectory()) { return Promise.reject(SHELL_CWD_INVALID_EXIT_CODE); @@ -178,26 +179,25 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._closeTimeout = setTimeout(() => this._kill(), 250); } - private _kill(): void { + private async _kill(): Promise { // Wait to kill to process until the start up code has run. This prevents us from firing a process exit before a // process start. - this._processStartupComplete!.then(() => { - if (this._isDisposed) { - return; + await this._processStartupComplete; + if (this._isDisposed) { + return; + } + // Attempt to kill the pty, it may have already been killed at this + // point but we want to make sure + try { + if (this._ptyProcess) { + this._logService.trace('IPty#kill'); + this._ptyProcess.kill(); } - // Attempt to kill the pty, it may have already been killed at this - // point but we want to make sure - try { - if (this._ptyProcess) { - this._logService.trace('IPty#kill'); - this._ptyProcess.kill(); - } - } catch (ex) { - // Swallow, the pty has already been killed - } - this._onProcessExit.fire(this._exitCode || 0); - this.dispose(); - }); + } catch (ex) { + // Swallow, the pty has already been killed + } + this._onProcessExit.fire(this._exitCode || 0); + this.dispose(); } private _sendProcessId(ptyProcess: pty.IPty) { diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index 0214ca10ae..f1f390666d 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -230,7 +230,7 @@ function isItem(i: QuickPickInput): i is ThemeItem { } function toEntries(themes: Array, label?: string): QuickPickInput[] { - const toEntry = (theme: IColorTheme): ThemeItem => ({ id: theme.id, label: theme.label, description: theme.description }); + const toEntry = (theme: IColorTheme | IFileIconTheme): ThemeItem => ({ id: theme.id, label: theme.label, description: theme.description }); const sorter = (t1: ThemeItem, t2: ThemeItem) => t1.label.localeCompare(t2.label); let entries: QuickPickInput[] = themes.map(toEntry).sort(sorter); if (entries.length > 0 && label) { diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index e14c3010ec..908cfd9fdd 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -30,6 +30,7 @@ import { basename } from 'vs/base/common/path'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { debounce } from 'vs/base/common/decorators'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; type TreeElement = TimelineItem; @@ -62,9 +63,11 @@ export class TimelinePane extends ViewPane { @IEditorService protected editorService: IEditorService, @ICommandService protected commandService: ICommandService, @IProgressService private readonly progressService: IProgressService, - @ITimelineService protected timelineService: ITimelineService + @ITimelineService protected timelineService: ITimelineService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); scopedContextKeyService.createKey('view', TimelinePane.ID); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index bce029f9e5..5e8ecf29bf 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -8,7 +8,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { UserDataSyncWorkbenchContribution } from 'vs/workbench/contrib/userDataSync/browser/userDataSync'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncEnablementService, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync'; class UserDataSyncSettingsMigrationContribution implements IWorkbenchContribution { @@ -16,22 +16,24 @@ class UserDataSyncSettingsMigrationContribution implements IWorkbenchContributio @IConfigurationService private readonly configurationService: IConfigurationService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, ) { - if (!configurationService.getValue('sync.enableSettings')) { - userDataSyncEnablementService.setResourceEnablement('settings', false); + if (getUserDataSyncStore(configurationService)) { + if (!configurationService.getValue('sync.enableSettings')) { + userDataSyncEnablementService.setResourceEnablement('settings', false); + } + if (!configurationService.getValue('sync.enableKeybindings')) { + userDataSyncEnablementService.setResourceEnablement('keybindings', false); + } + if (!configurationService.getValue('sync.enableUIState')) { + userDataSyncEnablementService.setResourceEnablement('globalState', false); + } + if (!configurationService.getValue('sync.enableExtensions')) { + userDataSyncEnablementService.setResourceEnablement('extensions', false); + } + if (configurationService.getValue('sync.enable')) { + userDataSyncEnablementService.setEnablement(true); + } + this.removeFromConfiguration(); } - if (!configurationService.getValue('sync.enableKeybindings')) { - userDataSyncEnablementService.setResourceEnablement('keybindings', false); - } - if (!configurationService.getValue('sync.enableUIState')) { - userDataSyncEnablementService.setResourceEnablement('globalState', false); - } - if (!configurationService.getValue('sync.enableExtensions')) { - userDataSyncEnablementService.setResourceEnablement('extensions', false); - } - if (configurationService.getValue('sync.enable')) { - userDataSyncEnablementService.setEnablement(true); - } - this.removeFromConfiguration(); } private async removeFromConfiguration(): Promise { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index c802eb3f80..65d6a642ec 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -3,47 +3,48 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IUserDataSyncService, SyncStatus, SyncSource, CONTEXT_SYNC_STATE, IUserDataSyncStore, registerConfiguration, getUserDataSyncStore, ISyncConfiguration, IUserDataAuthTokenService, IUserDataAutoSyncService, USER_DATA_SYNC_SCHEME, toRemoteContentResource, getSyncSourceFromRemoteContentResource, UserDataSyncErrorCode, UserDataSyncError, getSyncSourceFromPreviewResource, IUserDataSyncEnablementService, ResourceKey } from 'vs/platform/userDataSync/common/userDataSync'; -import { localize } from 'vs/nls'; -import { Disposable, MutableDisposable, toDisposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { MenuRegistry, MenuId, IMenuItem } from 'vs/platform/actions/common/actions'; -import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey, ContextKeyRegexExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; -import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { URI } from 'vs/base/common/uri'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { Event } from 'vs/base/common/event'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { isEqual } from 'vs/base/common/resources'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { isWeb } from 'vs/base/common/platform'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; +import { Action } from 'vs/base/common/actions'; import { timeout } from 'vs/base/common/async'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; -import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { AuthenticationSession } from 'vs/editor/common/modes'; -import { isPromiseCanceledError, canceled } from 'vs/base/common/errors'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; +import { canceled, isPromiseCanceledError } from 'vs/base/common/errors'; +import { Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { isWeb } from 'vs/base/common/platform'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import type { IEditorContribution } from 'vs/editor/common/editorCommon'; +import type { ITextModel } from 'vs/editor/common/model'; +import { AuthenticationSession } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; -import type { ITextModel } from 'vs/editor/common/model'; -import type { IEditorContribution } from 'vs/editor/common/editorCommon'; -import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import type { IEditorInput } from 'vs/workbench/common/editor'; -import { Action } from 'vs/base/common/actions'; -import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { IMenuItem, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey, ContextKeyRegexExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { CONTEXT_SYNC_STATE, getSyncSourceFromRemoteContentResource, getUserDataSyncStore, ISyncConfiguration, IUserDataAuthTokenService, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncSource, SyncStatus, toRemoteContentResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, ResourceKey, getSyncSourceFromPreviewResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import type { IEditorInput } from 'vs/workbench/common/editor'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; +import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; +import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; const enum AuthStatus { Initializing = 'Initializing', @@ -66,6 +67,11 @@ function getSyncAreaLabel(source: SyncSource): string { } } +type SyncConflictsClassification = { + source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; + action?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; +}; + type FirstTimeSyncClassification = { action: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; @@ -101,6 +107,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @ITextModelService textModelResolverService: ITextModelService, @IPreferencesService private readonly preferencesService: IPreferencesService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IFileService private readonly fileService: IFileService, ) { super(); this.userDataSyncStore = getUserDataSyncStore(configurationService); @@ -256,10 +263,24 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const conflictsArea = getSyncAreaLabel(conflictsSource); const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts in {0}. Please resolve them to continue.", conflictsArea), [ + { + label: localize('accept remote', "Accept Remote"), + run: () => { + this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: 'acceptRemote' }); + this.acceptRemote(conflictsSource); + } + }, + { + label: localize('accept local', "Accept Local"), + run: () => { + this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: 'acceptLocal' }); + this.acceptLocal(conflictsSource); + } + }, { label: localize('show conflicts', "Show Conflicts"), run: () => { - this.telemetryService.publicLog2('sync/showConflicts'); + this.telemetryService.publicLog2<{ source: string, action?: string }, SyncConflictsClassification>('sync/showConflicts', { source: conflictsSource }); this.handleConflicts(conflictsSource); } } @@ -291,6 +312,35 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } + private async acceptRemote(syncSource: SyncSource) { + try { + const contents = await this.userDataSyncService.getRemoteContent(syncSource, false); + if (contents) { + await this.userDataSyncService.accept(syncSource, contents); + } + } catch (e) { + this.notificationService.error(e); + } + } + + private async acceptLocal(syncSource: SyncSource): Promise { + try { + const previewResource = syncSource === SyncSource.Settings + ? this.workbenchEnvironmentService.settingsSyncPreviewResource + : syncSource === SyncSource.Keybindings + ? this.workbenchEnvironmentService.keybindingsSyncPreviewResource + : null; + if (previewResource) { + const fileContent = await this.fileService.readFile(previewResource); + if (fileContent) { + this.userDataSyncService.accept(syncSource, fileContent.value.toString()); + } + } + } catch (e) { + this.notificationService.error(e); + } + } + private onDidChangeEnablement(enabled: boolean) { this.syncEnablementContext.set(enabled); this.updateBadge(); @@ -743,11 +793,6 @@ class UserDataRemoteContentProvider implements ITextModelContentProvider { } } -type SyncConflictsClassification = { - source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; - action: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; -}; - class AcceptChangesContribution extends Disposable implements IEditorContribution { static get(editor: ICodeEditor): AcceptChangesContribution { diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index b42d1397db..5acc32f7c8 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -10,7 +10,7 @@ import * as errors from 'vs/base/common/errors'; import { Disposable, IDisposable, dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; +import { ConfigurationModel, ConfigurationModelParser, UserSettings } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { IStoredWorkspaceFolder, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; @@ -64,38 +64,6 @@ export class UserConfiguration extends Disposable { } } -class UserSettings extends Disposable { - - private readonly parser: ConfigurationModelParser; - protected readonly _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; - - constructor( - private readonly userSettingsResource: URI, - private readonly scopes: ConfigurationScope[] | undefined, - private readonly fileService: IFileService - ) { - super(); - this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes); - this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire())); - } - - async loadConfiguration(): Promise { - try { - const content = await this.fileService.readFile(this.userSettingsResource); - this.parser.parseContent(content.value.toString() || '{}'); - return this.parser.configurationModel; - } catch (e) { - return new ConfigurationModel(); - } - } - - reprocess(): ConfigurationModel { - this.parser.parse(); - return this.parser.configurationModel; - } -} - class FileServiceBasedConfigurationWithNames extends Disposable { private _folderSettingsModelParser: ConfigurationModelParser; diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 6f937d8209..e2d9ef5c10 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -175,7 +175,7 @@ class FileDecorationChangeEvent implements IResourceDecorationChangeEvent { return this._data.get(uri.toString()) || this._data.findSuperstr(uri.toString()) !== undefined; } - static debouncer(last: FileDecorationChangeEvent, current: URI | URI[]) { + static debouncer(last: FileDecorationChangeEvent | undefined, current: URI | URI[]) { if (!last) { last = new FileDecorationChangeEvent(); } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 2849af4a27..886c5674e1 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -28,16 +28,16 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { withNullAsUndefined } from 'vs/base/common/types'; import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; +import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -type CachedEditorInput = ResourceEditorInput | IFileEditorInput; +type CachedEditorInput = ResourceEditorInput | IFileEditorInput | UntitledTextEditorInput; type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE; export class EditorService extends Disposable implements EditorServiceImpl { _serviceBrand: undefined; - private static CACHE = new ResourceMap(); - //#region events private readonly _onDidActiveEditorChange = this._register(new Emitter()); @@ -64,6 +64,8 @@ export class EditorService extends Disposable implements EditorServiceImpl { private readonly editorsObserver = this._register(this.instantiationService.createInstance(EditorsObserver)); + private readonly editorInputCache = new ResourceMap(); + constructor( @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, @@ -575,14 +577,30 @@ export class EditorService extends Disposable implements EditorServiceImpl { }; // Untitled resource: use as hint for an existing untitled editor + let untitledModel: IUntitledTextEditorModel; if (untitledInput.resource?.scheme === Schemas.untitled) { - return this.untitledTextEditorService.create({ untitledResource: untitledInput.resource, ...untitledOptions }); + untitledModel = this.untitledTextEditorService.create({ untitledResource: untitledInput.resource, ...untitledOptions }); } // Other resource: use as hint for associated filepath else { - return this.untitledTextEditorService.create({ associatedResource: untitledInput.resource, ...untitledOptions }); + untitledModel = this.untitledTextEditorService.create({ associatedResource: untitledInput.resource, ...untitledOptions }); } + + return this.createOrGetCached(untitledModel.resource, () => { + + // Factory function for new untitled editor + const input = this.instantiationService.createInstance(UntitledTextEditorInput, untitledModel); + + // We dispose the untitled model once the editor + // is being disposed. Even though we may have not + // created the model initially, the lifecycle for + // untitled is tightly coupled with the editor + // lifecycle for now. + Event.once(input.onDispose)(() => untitledModel.dispose()); + + return input; + }) as EditorInput; } // Resource Editor Support @@ -593,54 +611,69 @@ export class EditorService extends Disposable implements EditorServiceImpl { label = basename(resourceInput.resource); // derive the label from the path } - return this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding, resourceInput.mode, resourceInput.forceFile) as EditorInput; + return this.createOrGetCached(resourceInput.resource, () => { + + // File + if (resourceInput.forceFile /* fix for https://github.com/Microsoft/vscode/issues/48275 */ || this.fileService.canHandleResource(resourceInput.resource)) { + return this.fileInputFactory.createFileInput(resourceInput.resource, resourceInput.encoding, resourceInput.mode, this.instantiationService); + } + + // Resource + return this.instantiationService.createInstance(ResourceEditorInput, resourceInput.label, resourceInput.description, resourceInput.resource, resourceInput.mode); + }, cachedInput => { + + // Untitled + if (cachedInput instanceof UntitledTextEditorInput) { + return; + } + + // Files + else if (!(cachedInput instanceof ResourceEditorInput)) { + if (resourceInput.encoding) { + cachedInput.setPreferredEncoding(resourceInput.encoding); + } + + if (resourceInput.mode) { + cachedInput.setPreferredMode(resourceInput.mode); + } + } + + // Resources + else { + if (label) { + cachedInput.setName(label); + } + + if (resourceInput.description) { + cachedInput.setDescription(resourceInput.description); + } + + if (resourceInput.mode) { + cachedInput.setPreferredMode(resourceInput.mode); + } + } + }) as EditorInput; } throw new Error('Unknown input type'); } - private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string | undefined, description: string | undefined, encoding: string | undefined, mode: string | undefined, forceFile: boolean | undefined): CachedEditorInput { - if (EditorService.CACHE.has(resource)) { - const input = EditorService.CACHE.get(resource)!; - if (input instanceof ResourceEditorInput) { - if (label) { - input.setName(label); - } + private createOrGetCached(resource: URI, factoryFn: () => CachedEditorInput, cachedFn?: (input: CachedEditorInput) => void): CachedEditorInput { - if (description) { - input.setDescription(description); - } - - if (mode) { - input.setPreferredMode(mode); - } - } else { - if (encoding) { - input.setPreferredEncoding(encoding); - } - - if (mode) { - input.setPreferredMode(mode); - } + // Return early if already cached + let input = this.editorInputCache.get(resource); + if (input) { + if (cachedFn) { + cachedFn(input); } return input; } - // File - let input: CachedEditorInput; - if (forceFile /* fix for https://github.com/Microsoft/vscode/issues/48275 */ || this.fileService.canHandleResource(resource)) { - input = this.fileInputFactory.createFileInput(resource, encoding, mode, instantiationService); - } - - // Resource - else { - input = instantiationService.createInstance(ResourceEditorInput, label, description, resource, mode); - } - - // Add to cache and remove when input gets disposed - EditorService.CACHE.set(resource, input); - Event.once(input.onDispose)(() => EditorService.CACHE.delete(resource)); + // Otherwise create and add to cache + input = factoryFn(); + this.editorInputCache.set(resource, input); + Event.once(input.onDispose)(() => this.editorInputCache.delete(resource)); return input; } @@ -710,7 +743,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Editors to save sequentially for (const { groupId, editor } of editorsToSaveSequentially) { if (editor.isDisposed()) { - continue; // might have been disposed from from the save already + continue; // might have been disposed from the save already } // Preserve view state by opening the editor first if the editor @@ -753,6 +786,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { } const result = await Promise.all(editors.map(async ({ groupId, editor }) => { + if (editor.isDisposed()) { + return true; // might have been disposed from from the revert already + } // Use revert as a hint to pin the editor this.editorGroupService.getGroup(groupId)?.pinEditor(editor); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index c2f8239c08..311197389a 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -338,19 +338,19 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite // Untyped Input (untitled with file path) input = service.createInput({ resource: URI.file('/some/path.txt'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } }); assert(input instanceof UntitledTextEditorInput); - assert.ok((input as UntitledTextEditorInput).hasAssociatedFilePath); + assert.ok((input as UntitledTextEditorInput).model.hasAssociatedFilePath); // Untyped Input (untitled with untitled resource) input = service.createInput({ resource: URI.parse('untitled://Untitled-1'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } }); assert(input instanceof UntitledTextEditorInput); - assert.ok(!(input as UntitledTextEditorInput).hasAssociatedFilePath); + assert.ok(!(input as UntitledTextEditorInput).model.hasAssociatedFilePath); // Untyped Input (untitled with custom resource) const provider = instantiationService.createInstance(FileServiceProvider, 'untitled-custom'); input = service.createInput({ resource: URI.parse('untitled-custom://some/path'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } }); assert(input instanceof UntitledTextEditorInput); - assert.ok((input as UntitledTextEditorInput).hasAssociatedFilePath); + assert.ok((input as UntitledTextEditorInput).model.hasAssociatedFilePath); provider.dispose(); diff --git a/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts b/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts index 7cce12ad21..a4f836f60b 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts @@ -19,7 +19,6 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import * as nls from 'vs/nls'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IUntitledTextResourceInput } from 'vs/workbench/common/editor'; import { StopWatch } from 'vs/base/common/stopwatch'; import { VSBuffer } from 'vs/base/common/buffer'; import { IExtensionHostStarter } from 'vs/workbench/services/extensions/common/extensions'; @@ -407,7 +406,7 @@ registerAction2(class MeasureExtHostLatencyAction extends Action2 { const editorService = accessor.get(IEditorService); const measurements = await Promise.all(getLatencyTestProviders().map(provider => provider.measure())); - editorService.openEditor({ contents: measurements.map(MeasureExtHostLatencyAction._print).join('\n\n'), options: { pinned: true } } as IUntitledTextResourceInput); + editorService.openEditor({ contents: measurements.map(MeasureExtHostLatencyAction._print).join('\n\n'), options: { pinned: true } }); } private static _print(m: ExtHostLatencyResult | null): string { diff --git a/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts index dd381a89cc..402117352a 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; @@ -153,7 +153,7 @@ export class RemoteFileSystemProvider extends Disposable implements // Support cancellation if (token) { - Event.once(token.onCancellationRequested)(() => { + token.onCancellationRequested(() => { // Ensure to end the stream properly with an error // to indicate the cancellation. diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 1c639c8ec5..ca9f187181 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -36,6 +36,7 @@ import { suggestFilename } from 'vs/base/common/mime'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; +import { isValidBasename } from 'vs/base/common/extpath'; /** * The workbench file service implementation implements the raw file service spec and adds additional methods on top. @@ -567,12 +568,19 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return toLocalResource(resource, remoteAuthority); } - // Untitled without associated file path + // Untitled without associated file path: use name + // of untitled model if it is a valid path name + let untitledName = model.name; + if (!isValidBasename(untitledName)) { + untitledName = basename(resource); + } + + // Add mode file extension if specified const mode = model.getMode(); - if (mode !== PLAINTEXT_MODE_ID) { // do not suggest when the mode ID is simple plain text - suggestedFilename = suggestFilename(mode, model.name); + if (mode !== PLAINTEXT_MODE_ID) { + suggestedFilename = suggestFilename(mode, untitledName); } else { - suggestedFilename = model.name; + suggestedFilename = untitledName; } } } diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index ea5f217e5f..843b74148d 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -68,6 +68,7 @@ suite('Files - TextFileService', () => { assert.ok(accessor.textFileService.isDirty(untitled.resource)); untitled.dispose(); + model.dispose(); }); test('save - file', async function () { diff --git a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts index 3eabd43708..a2337d9447 100644 --- a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts @@ -20,6 +20,7 @@ import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/commo import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { Event } from 'vs/base/common/event'; import { timeout } from 'vs/base/common/async'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; class ServiceAccessor { constructor( @@ -110,11 +111,13 @@ suite('Workbench - TextModelResolverService', () => { test('resolve untitled', async () => { const service = accessor.untitledTextEditorService; - const input = service.create(); + const untitledModel = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, untitledModel); await input.resolve(); const ref = await accessor.textModelResolverService.createModelReference(input.getResource()); const model = ref.object; + assert.equal(untitledModel, model); const editorModel = model.textEditorModel; assert.ok(editorModel); ref.dispose(); diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts index 88812a7e89..ab7d16e721 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts @@ -3,11 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; import { IEncodingSupport, EncodingMode, Verbosity, IModeSupport, TextResourceEditorInput } from 'vs/workbench/common/editor'; -import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Emitter } from 'vs/base/common/event'; +import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ILabelService } from 'vs/platform/label/common/label'; import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; @@ -23,22 +20,10 @@ export class UntitledTextEditorInput extends TextResourceEditorInput implements static readonly ID: string = 'workbench.editors.untitledEditorInput'; - private readonly _onDidModelChangeEncoding = this._register(new Emitter()); - readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event; - - private cachedModel: UntitledTextEditorModel | undefined = undefined; - - private modelResolve: Promise | undefined = undefined; - - private preferredMode: string | undefined; + private modelResolve: Promise | undefined = undefined; constructor( - resource: URI, - private readonly _hasAssociatedFilePath: boolean, - preferredMode: string | undefined, - private readonly initialValue: string | undefined, - private preferredEncoding: string | undefined, - @IInstantiationService private readonly instantiationService: IInstantiationService, + public readonly model: IUntitledTextEditorModel, @ITextFileService textFileService: ITextFileService, @ILabelService labelService: ILabelService, @IEditorService editorService: IEditorService, @@ -46,19 +31,19 @@ export class UntitledTextEditorInput extends TextResourceEditorInput implements @IFileService fileService: IFileService, @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService ) { - super(resource, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService); + super(model.resource, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService); - if (preferredMode) { - this.setMode(preferredMode); - } + this.registerModelListeners(model); } - get model(): UntitledTextEditorModel | undefined { - return this.cachedModel; - } + private registerModelListeners(model: IUntitledTextEditorModel): void { - get hasAssociatedFilePath(): boolean { - return this._hasAssociatedFilePath; + // re-emit some events from the model + this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + this._register(model.onDidChangeName(() => this._onDidChangeLabel.fire())); + + // a reverted untitled text editor model renders this input disposed + this._register(model.onDidRevert(() => this.dispose())); } getTypeId(): string { @@ -66,17 +51,13 @@ export class UntitledTextEditorInput extends TextResourceEditorInput implements } getName(): string { - if (this.cachedModel) { - return this.cachedModel.name; - } - - return super.getName(); + return this.model.name; } getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { // Without associated path: only use if name and description differ - if (!this.hasAssociatedFilePath) { + if (!this.model.hasAssociatedFilePath) { const descriptionCandidate = this.resource.path; if (descriptionCandidate !== this.getName()) { return descriptionCandidate; @@ -93,7 +74,7 @@ export class UntitledTextEditorInput extends TextResourceEditorInput implements // Without associated path: check if name and description differ to decide // if description should appear besides the name to distinguish better - if (!this.hasAssociatedFilePath) { + if (!this.model.hasAssociatedFilePath) { const name = this.getName(); const description = this.getDescription(); if (description && description !== name) { @@ -108,102 +89,37 @@ export class UntitledTextEditorInput extends TextResourceEditorInput implements } isDirty(): boolean { - - // Always trust the model first if existing - if (this.cachedModel) { - return this.cachedModel.isDirty(); - } - - // A disposed input is never dirty, even if it was restored from backup - if (this.isDisposed()) { - return false; - } - - // A input with initial value is always dirty - if (this.initialValue && this.initialValue.length > 0) { - return true; - } - - // A input with associated path is always dirty because it is the intent - // of the user to create a new file at that location through saving - return this.hasAssociatedFilePath; + return this.model.isDirty(); } getEncoding(): string | undefined { - if (this.cachedModel) { - return this.cachedModel.getEncoding(); - } - - return this.preferredEncoding; + return this.model.getEncoding(); } setEncoding(encoding: string, mode: EncodingMode /* ignored, we only have Encode */): void { - this.preferredEncoding = encoding; - - if (this.cachedModel) { - this.cachedModel.setEncoding(encoding); - } + this.model.setEncoding(encoding); } setMode(mode: string): void { - let actualMode: string | undefined = undefined; - if (mode === '${activeEditorLanguage}') { - // support the special '${activeEditorLanguage}' mode by - // looking up the language mode from the currently - // active text editor if any - actualMode = this.editorService.activeTextEditorMode; - } else { - actualMode = mode; - } - - this.preferredMode = actualMode; - - if (this.preferredMode && this.cachedModel) { - this.cachedModel.setMode(this.preferredMode); - } + this.model.setMode(mode); } getMode(): string | undefined { - if (this.cachedModel) { - return this.cachedModel.getMode(); - } - - return this.preferredMode; + return this.model.getMode(); } - resolve(): Promise { + resolve(): Promise { // Join a model resolve if we have had one before if (this.modelResolve) { return this.modelResolve; } - // Otherwise Create Model and load - this.cachedModel = this.createModel(); - this.modelResolve = this.cachedModel.load(); + this.modelResolve = this.model.load(); return this.modelResolve; } - private createModel(): UntitledTextEditorModel { - const model = this._register(this.instantiationService.createInstance(UntitledTextEditorModel, this.preferredMode, this.resource, this.hasAssociatedFilePath, this.initialValue, this.preferredEncoding)); - - this.registerModelListeners(model); - - return model; - } - - private registerModelListeners(model: UntitledTextEditorModel): void { - - // re-emit some events from the model - this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this._register(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.fire())); - this._register(model.onDidChangeName(() => this._onDidChangeLabel.fire())); - - // a disposed untitled text editor model renders this input disposed - this._register(model.onDispose(() => this.dispose())); - } - matches(otherInput: unknown): boolean { if (super.matches(otherInput) === true) { return true; @@ -218,7 +134,6 @@ export class UntitledTextEditorInput extends TextResourceEditorInput implements } dispose(): void { - this.cachedModel = undefined; this.modelResolve = undefined; super.dispose(); diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index d89e95f066..03a98b654b 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -8,7 +8,7 @@ import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel' import { URI } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { Emitter } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model'; @@ -20,13 +20,45 @@ import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvent import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; import { ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy { + /** + * Emits an event when the encoding of this untitled model changes. + */ + readonly onDidChangeEncoding: Event; + + /** + * Emits an event when the name of this untitled model changes. + */ + readonly onDidChangeName: Event; + + /** + * Emits an event when this untitled model is reverted. + */ + readonly onDidRevert: Event; + /** * Wether this untitled text model has an associated file path. */ readonly hasAssociatedFilePath: boolean; + + /** + * Sets the encoding to use for this untitled model. + */ + setEncoding(encoding: string): void; + + /** + * Load the untitled model. + */ + load(): Promise; + + /** + * Updates the value of the untitled model optionally allowing to ignore dirty. + * The model must be resolved for this method to work. + */ + setValue(this: IResolvedTextEditorModel, value: string, ignoreDirty?: boolean): void; } export class UntitledTextEditorModel extends BaseTextEditorModel implements IUntitledTextEditorModel { @@ -45,6 +77,9 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt private readonly _onDidChangeEncoding = this._register(new Emitter()); readonly onDidChangeEncoding = this._onDidChangeEncoding.event; + private readonly _onDidRevert = this._register(new Emitter()); + readonly onDidRevert = this._onDidRevert.event; + readonly capabilities = WorkingCopyCapabilities.Untitled; private cachedModelFirstLineWords: string | undefined = undefined; @@ -67,10 +102,10 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt private configuredEncoding: string | undefined; constructor( - private readonly preferredMode: string | undefined, public readonly resource: URI, public readonly hasAssociatedFilePath: boolean, private readonly initialValue: string | undefined, + private preferredMode: string | undefined, private preferredEncoding: string | undefined, @IModeService modeService: IModeService, @IModelService modelService: IModelService, @@ -78,13 +113,18 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt @ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @ITextFileService private readonly textFileService: ITextFileService, - @ILabelService private readonly labelService: ILabelService + @ILabelService private readonly labelService: ILabelService, + @IEditorService private readonly editorService: IEditorService ) { super(modelService, modeService); // Make known to working copy service this._register(this.workingCopyService.registerWorkingCopy(this)); + if (preferredMode) { + this.setMode(preferredMode); + } + this.registerListeners(); } @@ -110,6 +150,24 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return this.versionId; } + setMode(mode: string): void { + let actualMode: string | undefined = undefined; + if (mode === '${activeEditorLanguage}') { + // support the special '${activeEditorLanguage}' mode by + // looking up the language mode from the currently + // active text editor if any + actualMode = this.editorService.activeTextEditorMode; + } else { + actualMode = mode; + } + + this.preferredMode = actualMode; + + if (actualMode) { + super.setMode(actualMode); + } + } + getMode(): string | undefined { if (this.textEditorModel) { return this.textEditorModel.getModeId(); @@ -172,6 +230,9 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt async revert(): Promise { this.setDirty(false); + // Emit as event + this._onDidRevert.fire(); + // A reverted untitled model is invalid because it has // no actual source on disk to revert to. As such we // dispose the model. @@ -197,13 +258,17 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } // Create text editor model if not yet done + let createdUntitledModel = false; if (!this.textEditorModel) { this.createTextEditorModel(untitledContents, this.resource, this.preferredMode); + createdUntitledModel = true; } - // Otherwise update + // Otherwise: the untitled model already exists and we must assume + // that the value of the model was changed by the user. As such we + // do not update the contents, only the mode if configured. else { - this.updateTextEditorModel(untitledContents, this.preferredMode); + this.updateTextEditorModel(undefined, this.preferredMode); } // Figure out encoding now that model is present @@ -214,18 +279,23 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this._register(textEditorModel.onDidChangeContent(e => this.onModelContentChanged(textEditorModel, e))); this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange())); // mode change can have impact on config - // Name - if (backup || this.initialValue) { - this.updateNameFromFirstLine(); - } + // Only adjust name and dirty state etc. if we + // actually created the untitled model + if (createdUntitledModel) { - // Untitled associated to file path are dirty right away as well as untitled with content - this.setDirty(this.hasAssociatedFilePath || !!backup || !!this.initialValue); + // Name + if (backup || this.initialValue) { + this.updateNameFromFirstLine(); + } - // If we have initial contents, make sure to emit this - // as the appropiate events to the outside. - if (backup || this.initialValue) { - this._onDidChangeContent.fire(); + // Untitled associated to file path are dirty right away as well as untitled with content + this.setDirty(this.hasAssociatedFilePath || !!backup || !!this.initialValue); + + // If we have initial contents, make sure to emit this + // as the appropiate events to the outside. + if (backup || this.initialValue) { + this._onDidChangeContent.fire(); + } } return this as UntitledTextEditorModel & IResolvedTextEditorModel; diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts index afacca47d5..c3baad9ea0 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts @@ -5,14 +5,13 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { UntitledTextEditorModel, IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { IFilesConfiguration } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Event, Emitter } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; @@ -21,18 +20,18 @@ export const IUntitledTextEditorService = createDecorator; /** - * Creates a new untitled input with the provided options. If the `untitledResource` - * property is provided and the untitled input exists, it will return that existing + * Creates a new untitled editor model with the provided options. If the `untitledResource` + * property is provided and the untitled editor exists, it will return that existing * instance instead of creating a new one. */ - create(options?: INewUntitledTextEditorOptions): UntitledTextEditorInput; - create(options?: INewUntitledTextEditorWithAssociatedResourceOptions): UntitledTextEditorInput; - create(options?: IExistingUntitledTextEditorOptions): UntitledTextEditorInput; + create(options?: INewUntitledTextEditorOptions): IUntitledTextEditorModel; + create(options?: INewUntitledTextEditorWithAssociatedResourceOptions): IUntitledTextEditorModel; + create(options?: IExistingUntitledTextEditorOptions): IUntitledTextEditorModel; /** - * Returns an existing untitled model if already created before. + * Returns an existing untitled editor model if already created before. */ get(resource: URI): IUntitledTextEditorModel | undefined; /** * Resolves an untitled editor model from the provided options. If the `untitledResource` - * property is provided and the untitled input exists, it will return that existing + * property is provided and the untitled editor exists, it will return that existing * instance instead of creating a new one. */ resolve(options?: INewUntitledTextEditorOptions): Promise; @@ -130,7 +129,7 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe private readonly _onDidChangeLabel = this._register(new Emitter()); readonly onDidChangeLabel = this._onDidChangeLabel.event; - private readonly mapResourceToInput = new ResourceMap(); + private readonly mapResourceToModel = new ResourceMap(); constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -140,23 +139,23 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe } get(resource: URI): UntitledTextEditorModel | undefined { - return this.mapResourceToInput.get(resource)?.model; + return this.mapResourceToModel.get(resource); } resolve(options?: IInternalUntitledTextEditorOptions): Promise { - return this.doCreateOrGet(options).resolve(); + return this.doCreateOrGet(options).load(); } - create(options?: IInternalUntitledTextEditorOptions): UntitledTextEditorInput { + create(options?: IInternalUntitledTextEditorOptions): UntitledTextEditorModel { return this.doCreateOrGet(options); } - private doCreateOrGet(options: IInternalUntitledTextEditorOptions = Object.create(null)): UntitledTextEditorInput { + private doCreateOrGet(options: IInternalUntitledTextEditorOptions = Object.create(null)): UntitledTextEditorModel { const massagedOptions = this.massageOptions(options); // Return existing instance if asked for it - if (massagedOptions.untitledResource && this.mapResourceToInput.has(massagedOptions.untitledResource)) { - return this.mapResourceToInput.get(massagedOptions.untitledResource)!; + if (massagedOptions.untitledResource && this.mapResourceToModel.has(massagedOptions.untitledResource)) { + return this.mapResourceToModel.get(massagedOptions.untitledResource)!; } // Create new instance otherwise @@ -199,7 +198,7 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe return massagedOptions; } - private doCreate(options: IInternalUntitledTextEditorOptions): UntitledTextEditorInput { + private doCreate(options: IInternalUntitledTextEditorOptions): UntitledTextEditorModel { // Create a new untitled resource if none is provided let untitledResource = options.untitledResource; @@ -208,38 +207,36 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe do { untitledResource = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}` }); counter++; - } while (this.mapResourceToInput.has(untitledResource)); + } while (this.mapResourceToModel.has(untitledResource)); } - // Create new input with provided options - const input = this.instantiationService.createInstance(UntitledTextEditorInput, untitledResource, !!options.associatedResource, options.mode, options.initialValue, options.encoding); + // Create new model with provided options + const model = this._register(this.instantiationService.createInstance(UntitledTextEditorModel, untitledResource, !!options.associatedResource, options.initialValue, options.mode, options.encoding)); - this.register(input); + this.registerModel(model); - return input; + return model; } - private register(editor: UntitledTextEditorInput): void { - const dirtyListener = editor.onDidChangeDirty(() => this._onDidChangeDirty.fire(editor.getResource())); - const labelListener = editor.onDidChangeLabel(() => this._onDidChangeLabel.fire(editor.getResource())); - const encodingListener = editor.onDidModelChangeEncoding(() => this._onDidChangeEncoding.fire(editor.getResource())); - const disposeListener = editor.onDispose(() => this._onDidDisposeModel.fire(editor.getResource())); + private registerModel(model: UntitledTextEditorModel): void { + const modelDisposables = new DisposableStore(); + modelDisposables.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(model.resource))); + modelDisposables.add(model.onDidChangeName(() => this._onDidChangeLabel.fire(model.resource))); + modelDisposables.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(model.resource))); + modelDisposables.add(model.onDispose(() => this._onDidDisposeModel.fire(model.resource))); // Remove from cache on dispose - Event.once(editor.onDispose)(() => { + Event.once(model.onDispose)(() => { // Registry - this.mapResourceToInput.delete(editor.getResource()); + this.mapResourceToModel.delete(model.resource); // Listeners - dirtyListener.dispose(); - labelListener.dispose(); - encodingListener.dispose(); - disposeListener.dispose(); + modelDisposables.dispose(); }); // Add to cache - this.mapResourceToInput.set(editor.getResource(), editor); + this.mapResourceToModel.set(model.resource, model); } } diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index 016d720f1b..6ca2bf37ea 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -19,6 +19,7 @@ import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/working import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; class ServiceAccessor { constructor( @@ -48,15 +49,14 @@ suite('Untitled text editors', () => { const service = accessor.untitledTextEditorService; const workingCopyService = accessor.workingCopyService; - const input1 = service.create(); + const input1 = instantiationService.createInstance(UntitledTextEditorInput, service.create()); await input1.resolve(); - assert.equal(input1, service.create({ untitledResource: input1.getResource() })); assert.equal(service.get(input1.getResource()), input1.model); assert.ok(service.get(input1.getResource())); assert.ok(!service.get(URI.file('testing'))); - const input2 = service.create(); + const input2 = instantiationService.createInstance(UntitledTextEditorInput, service.create()); assert.equal(service.get(input2.getResource()), input2.model); // get() @@ -118,7 +118,7 @@ suite('Untitled text editors', () => { test('setValue()', async () => { const service = accessor.untitledTextEditorService; - const untitled = service.create(); + const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create()); const model = await untitled.resolve(); @@ -132,10 +132,10 @@ suite('Untitled text editors', () => { model.dispose(); }); - test('associated resource is dirty', () => { + test('associated resource is dirty', async () => { const service = accessor.untitledTextEditorService; const file = URI.file(join('C:\\', '/foo/file.txt')); - const untitled = service.create({ associatedResource: file }); + const untitled = await service.resolve({ associatedResource: file }); assert.ok(untitled.hasAssociatedFilePath); assert.equal(untitled.isDirty(), true); @@ -146,7 +146,7 @@ suite('Untitled text editors', () => { test('no longer dirty when content gets empty (not with associated resource)', async () => { const service = accessor.untitledTextEditorService; const workingCopyService = accessor.workingCopyService; - const input = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); // dirty const model = await input.resolve(); @@ -163,7 +163,7 @@ suite('Untitled text editors', () => { test('via create options', async () => { const service = accessor.untitledTextEditorService; - const model1 = await service.create().resolve(); + const model1 = await instantiationService.createInstance(UntitledTextEditorInput, service.create()).resolve(); model1.textEditorModel!.setValue('foo bar'); assert.ok(model1.isDirty()); @@ -171,17 +171,17 @@ suite('Untitled text editors', () => { model1.textEditorModel!.setValue(''); assert.ok(!model1.isDirty()); - const model2 = await service.create({ initialValue: 'Hello World' }).resolve(); + const model2 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Hello World' })).resolve(); assert.equal(snapshotToString(model2.createSnapshot()!), 'Hello World'); - const input = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); - const model3 = await service.create({ untitledResource: input.getResource() }).resolve(); + const model3 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ untitledResource: input.getResource() })).resolve(); assert.equal(model3.resource.toString(), input.getResource().toString()); const file = URI.file(join('C:\\', '/foo/file44.txt')); - const model4 = await service.create({ associatedResource: file }).resolve(); + const model4 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file })).resolve(); assert.ok(model4.hasAssociatedFilePath); assert.ok(model4.isDirty()); @@ -195,7 +195,7 @@ suite('Untitled text editors', () => { test('associated path remains dirty when content gets empty', async () => { const service = accessor.untitledTextEditorService; const file = URI.file(join('C:\\', '/foo/file.txt')); - const input = service.create({ associatedResource: file }); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file })); // dirty const model = await input.resolve(); @@ -211,8 +211,7 @@ suite('Untitled text editors', () => { const service = accessor.untitledTextEditorService; const workingCopyService = accessor.workingCopyService; - const untitled = service.create({ initialValue: 'Hello World' }); - assert.equal(untitled.isDirty(), true); + const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Hello World' })); let onDidChangeDirty: IWorkingCopy | undefined = undefined; const listener = workingCopyService.onDidChangeDirty(copy => { @@ -245,21 +244,21 @@ suite('Untitled text editors', () => { input.dispose(); }); - test('created with files.defaultLanguage setting (${activeEditorLanguage})', () => { + test('created with files.defaultLanguage setting (${activeEditorLanguage})', async () => { const config = accessor.testConfigurationService; config.setUserConfiguration('files', { 'defaultLanguage': '${activeEditorLanguage}' }); accessor.editorService.activeTextEditorMode = 'typescript'; const service = accessor.untitledTextEditorService; - const input = service.create(); + const model = service.create(); - assert.equal(input.getMode(), 'typescript'); + assert.equal(model.getMode(), 'typescript'); config.setUserConfiguration('files', { 'defaultLanguage': undefined }); accessor.editorService.activeTextEditorMode = undefined; - input.dispose(); + model.dispose(); }); test('created with mode overrides files.defaultLanguage setting', () => { @@ -286,7 +285,7 @@ suite('Untitled text editors', () => { }); const service = accessor.untitledTextEditorService; - const input = service.create({ mode }); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create({ mode })); assert.equal(input.getMode(), mode); @@ -303,7 +302,7 @@ suite('Untitled text editors', () => { test('service#onDidChangeEncoding', async () => { const service = accessor.untitledTextEditorService; - const input = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); let counter = 0; @@ -322,7 +321,7 @@ suite('Untitled text editors', () => { test('service#onDidChangeLabel', async () => { const service = accessor.untitledTextEditorService; - const input = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); let counter = 0; @@ -341,7 +340,7 @@ suite('Untitled text editors', () => { test('service#onDidDisposeModel', async () => { const service = accessor.untitledTextEditorService; - const input = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); let counter = 0; @@ -352,14 +351,13 @@ suite('Untitled text editors', () => { const model = await input.resolve(); assert.equal(counter, 0); - input.dispose(); - assert.equal(counter, 1); model.dispose(); + assert.equal(counter, 1); }); test('model#onDidChangeContent', async function () { const service = accessor.untitledTextEditorService; - const input = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); let counter = 0; @@ -383,26 +381,26 @@ suite('Untitled text editors', () => { model.dispose(); }); - test('model#onDispose when reverted', async function () { + test('model#onDidRevert and input disposed when reverted', async function () { const service = accessor.untitledTextEditorService; - const input = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); let counter = 0; const model = await input.resolve(); - model.onDispose(() => counter++); + model.onDidRevert(() => counter++); model.textEditorModel.setValue('foo'); await model.revert(); assert.ok(input.isDisposed()); - assert.ok(counter > 1); + assert.ok(counter === 1); }); test('model#onDidChangeName and input name', async function () { const service = accessor.untitledTextEditorService; - const input = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); let counter = 0; @@ -462,7 +460,7 @@ suite('Untitled text editors', () => { input.dispose(); model.dispose(); - const inputWithContents = service.create({ initialValue: 'Foo' }); + const inputWithContents = instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Foo' })); model = await inputWithContents.resolve(); assert.equal(inputWithContents.getName(), 'Foo'); @@ -473,7 +471,7 @@ suite('Untitled text editors', () => { test('model#onDidChangeDirty', async function () { const service = accessor.untitledTextEditorService; - const input = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); let counter = 0; @@ -493,7 +491,7 @@ suite('Untitled text editors', () => { test('model#onDidChangeEncoding', async function () { const service = accessor.untitledTextEditorService; - const input = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); let counter = 0; diff --git a/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts index 2c7e771779..a43e1aa633 100644 --- a/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts @@ -28,7 +28,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { IHostService } from 'vs/workbench/services/host/browser/host'; import { AbstractWorkspaceEditingService } from 'vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService'; import { IElectronService } from 'vs/platform/electron/node/electron'; -import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform'; +import { isMacintosh } from 'vs/base/common/platform'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService'; @@ -91,22 +91,14 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi CANCEL } - const save = { label: mnemonicButtonLabel(nls.localize('save', "Save")), result: ConfirmResult.SAVE }; - const dontSave = { label: mnemonicButtonLabel(nls.localize('doNotSave', "Don't Save")), result: ConfirmResult.DONT_SAVE }; - const cancel = { label: nls.localize('cancel', "Cancel"), result: ConfirmResult.CANCEL }; - - const buttons: { label: string; result: ConfirmResult; }[] = []; - if (isWindows) { - buttons.push(save, dontSave, cancel); - } else if (isLinux) { - buttons.push(dontSave, cancel, save); - } else { - buttons.push(save, cancel, dontSave); - } - + const buttons: { label: string; result: ConfirmResult; }[] = [ + { label: mnemonicButtonLabel(nls.localize('save', "Save")), result: ConfirmResult.SAVE }, + { label: mnemonicButtonLabel(nls.localize('doNotSave', "Don't Save")), result: ConfirmResult.DONT_SAVE }, + { label: nls.localize('cancel', "Cancel"), result: ConfirmResult.CANCEL } + ]; const message = nls.localize('saveWorkspaceMessage', "Do you want to save your workspace configuration as a file?"); const detail = nls.localize('saveWorkspaceDetail', "Save your workspace if you plan to open it again."); - const cancelId = buttons.indexOf(cancel); + const cancelId = 2; const { choice } = await this.dialogService.show(Severity.Warning, message, buttons.map(button => button.label), { detail, cancelId }); diff --git a/src/vs/workbench/test/browser/parts/editor/editor.test.ts b/src/vs/workbench/test/browser/parts/editor/editor.test.ts index 522a511ccb..2ebf640e48 100644 --- a/src/vs/workbench/test/browser/parts/editor/editor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editor.test.ts @@ -12,6 +12,7 @@ import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbe import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { Schemas } from 'vs/base/common/network'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; class ServiceAccessor { constructor(@IUntitledTextEditorService public untitledTextEditorService: UntitledTextEditorService) { } @@ -55,7 +56,7 @@ suite('Workbench editor', () => { assert.ok(!toResource(null!)); - const untitled = service.create(); + const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create()); assert.equal(toResource(untitled)!.toString(), untitled.getResource().toString()); assert.equal(toResource(untitled, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), untitled.getResource().toString()); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index c059c1ca1a..1c040077a8 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -87,13 +87,13 @@ import { BrowserTextFileService } from 'vs/workbench/services/textfile/browser/b import * as CommonWorkbenchTestServices from 'vs/workbench/test/common/workbenchTestServices'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; +import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; +import { Direction } from 'vs/base/browser/ui/grid/grid'; export import TestTextResourcePropertiesService = CommonWorkbenchTestServices.TestTextResourcePropertiesService; export import TestContextService = CommonWorkbenchTestServices.TestContextService; export import TestStorageService = CommonWorkbenchTestServices.TestStorageService; export import TestWorkingCopyService = CommonWorkbenchTestServices.TestWorkingCopyService; -import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; -import { Direction } from 'vs/base/browser/ui/grid/grid'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined); @@ -577,7 +577,7 @@ export class TestEditorService implements EditorServiceImpl { getOpened(_editor: IEditorInput | IResourceInput | IUntitledTextResourceInput): IEditorInput { throw new Error('not implemented'); } replaceEditors(_editors: any, _group: any) { return Promise.resolve(undefined); } invokeWithinEditorContext(fn: (accessor: ServicesAccessor) => T): T { throw new Error('not implemented'); } - createInput(_input: IResourceInput | IUntitledTextResourceInput | IResourceDiffInput | IResourceSideBySideInput): IEditorInput { throw new Error('not implemented'); } + createInput(_input: IResourceInput | IUntitledTextResourceInput | IResourceDiffInput | IResourceSideBySideInput): EditorInput { throw new Error('not implemented'); } save(editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { throw new Error('Method not implemented.'); } saveAll(options?: ISaveEditorsOptions): Promise { throw new Error('Method not implemented.'); } revert(editors: IEditorIdentifier[], options?: IRevertOptions): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index b7e95a815b..8a8cf155b1 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -58,6 +58,31 @@ interface IShowCandidate { (host: string, port: number, detail: string): Thenable; } +interface IApplicationLink { + + /** + * A link that is opened in the OS. If you want to open VSCode it must + * follow our expected structure of links: + * + * ://// + * + * For example: + * + * vscode://vscode-remote/vsonline+2005711d/home/vsonline/workspace for + * a remote folder in VSO or vscode://file/home/workspace for a local folder. + */ + uri: URI; + + /** + * A label for the link to display. + */ + label: string; +} + +interface IApplicationLinkProvider { + (): IApplicationLink[] | undefined +} + interface IWorkbenchConstructionOptions { /** @@ -138,6 +163,18 @@ interface IWorkbenchConstructionOptions { */ readonly showCandidate?: IShowCandidate; + /** + * Provide entries for the "Open in Desktop" feature. + * + * Depending on the returned elements the behaviour is: + * - no elements: there will not be a "Open in Desktop" affordance + * - 1 element: there will be a "Open in Desktop" affordance that opens on click + * and it will use the label provided by the link + * - N elements: there will be a "Open in Desktop" affordance that opens + * a picker on click to select which application to open. + */ + readonly applicationLinkProvider?: IApplicationLinkProvider; + /** * Current logging level. Default is `LogLevel.Info`. */ @@ -211,5 +248,9 @@ export { ICommontTelemetryPropertiesResolver, // External Uris - IExternalUriResolver + IExternalUriResolver, + + // Protocol Links + IApplicationLink, + IApplicationLinkProvider }; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index a6879eb39f..40c664f141 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -119,4 +119,7 @@ import 'vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.con // Issues import 'vs/workbench/contrib/issue/browser/issue.contribution'; +// Open In Desktop +import 'vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution'; + //#endregion diff --git a/test/automation/src/application.ts b/test/automation/src/application.ts index fcfe088ec2..7c1d87d821 100644 --- a/test/automation/src/application.ts +++ b/test/automation/src/application.ts @@ -133,8 +133,7 @@ export class Application { extraArgs, remote: this.options.remote, web: this.options.web, - browser: this.options.browser, - headless: this.options.headless + browser: this.options.browser }); this._workbench = new Workbench(this._code, this.userDataPath); diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 43679ea996..a903e430b7 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -103,8 +103,6 @@ export interface SpawnOptions { web?: boolean; /** A specific browser to use (requires web: true) */ browser?: 'chromium' | 'webkit' | 'firefox'; - /** Run in headless mode (only applies when web is true) */ - headless?: boolean; } async function createDriverHandle(): Promise { @@ -127,7 +125,7 @@ export async function spawn(options: SpawnOptions): Promise { if (options.web) { await launch(options.userDataDir, options.workspacePath, options.codePath); - connectDriver = connectPlaywrightDriver.bind(connectPlaywrightDriver, !!options.headless, options.browser); + connectDriver = connectPlaywrightDriver.bind(connectPlaywrightDriver, options.browser); } else { const env = process.env; diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index f97c64544d..8cd69fbcb9 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -137,12 +137,11 @@ function waitForEndpoint(): Promise { }); } -export function connect(headless: boolean, engine: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> { +export function connect(engine: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> { return new Promise(async (c) => { const browser = await playwright[engine].launch({ // Run in Edge dev on macOS - // executablePath: '/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev', - headless + // executablePath: '/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev' }); const page = (await browser.defaultContext().pages())[0]; await page.setViewport({ width, height }); diff --git a/test/integration/browser/README.md b/test/integration/browser/README.md index dd8b6b74d4..6140aab9ac 100644 --- a/test/integration/browser/README.md +++ b/test/integration/browser/README.md @@ -14,6 +14,8 @@ All integration tests run in an Electron instance. You can specify to run the te ## Run (inside browser) - resources/server/test/test-web-integration.[sh|bat] --browser [chromium|webkit] + resources/server/test/test-web-integration.[sh|bat] --browser [chromium|webkit] [--debug] All integration tests run in a browser instance as specified by the command line arguments. + +Add the `--debug` flag to see a browser window with the tests running. diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 622065f96c..9cdbfad551 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -29,7 +29,8 @@ const width = 1200; const height = 800; async function runTestsInBrowser(browserType: string, endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { - const browser = await playwright[browserType].launch({ headless: !Boolean(optimist.argv.debug) }); + const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros + const browser = await playwright[browserType].launch({ headless: !Boolean(optimist.argv.debug), dumpio: true, args }); const page = (await browser.defaultContext().pages())[0]; await page.setViewport({ width, height }); diff --git a/test/smoke/src/areas/explorer/explorer.test.ts b/test/smoke/src/areas/explorer/explorer.test.ts deleted file mode 100644 index 7a7b04c380..0000000000 --- a/test/smoke/src/areas/explorer/explorer.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Application } from '../../../../automation'; - -export function setup() { - describe('Explorer', () => { - it('quick open search produces correct result', async function () { - const app = this.app as Application; - const expectedNames = [ - '.eslintrc.json', - 'tasks.json', - 'app.js', - 'index.js', - 'users.js', - 'package.json', - 'jsconfig.json' - ]; - - await app.workbench.quickopen.openQuickOpen('.js'); - await app.workbench.quickopen.waitForQuickOpenElements(names => expectedNames.every(n => names.some(m => n === m))); - await app.code.dispatchKeybinding('escape'); - }); - - it('quick open respects fuzzy matching', async function () { - const app = this.app as Application; - const expectedNames = [ - 'tasks.json', - 'app.js', - 'package.json' - ]; - - await app.workbench.quickopen.openQuickOpen('a.s'); - await app.workbench.quickopen.waitForQuickOpenElements(names => expectedNames.every(n => names.some(m => n === m))); - await app.code.dispatchKeybinding('escape'); - }); - }); -} diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index 8af936fcdb..8703d7e4d3 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -56,4 +56,36 @@ export function setup() { await app.workbench.search.waitForNoResultText(); }); }); + + describe('Quick Open', () => { + it('quick open search produces correct result', async function () { + const app = this.app as Application; + const expectedNames = [ + '.eslintrc.json', + 'tasks.json', + 'app.js', + 'index.js', + 'users.js', + 'package.json', + 'jsconfig.json' + ]; + + await app.workbench.quickopen.openQuickOpen('.js'); + await app.workbench.quickopen.waitForQuickOpenElements(names => expectedNames.every(n => names.some(m => n === m))); + await app.code.dispatchKeybinding('escape'); + }); + + it('quick open respects fuzzy matching', async function () { + const app = this.app as Application; + const expectedNames = [ + 'tasks.json', + 'app.js', + 'package.json' + ]; + + await app.workbench.quickopen.openQuickOpen('a.s'); + await app.workbench.quickopen.waitForQuickOpenElements(names => expectedNames.every(n => names.some(m => n === m))); + await app.code.dispatchKeybinding('escape'); + }); + }); } diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 2f85f49f19..1024cbf125 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -29,7 +29,6 @@ import { setup as runQueryEditorTests } from './sql/queryEditor/queryEditor.test /* import { setup as setupDataMigrationTests } from './areas/workbench/data-migration.test'; import { setup as setupDataLossTests } from './areas/workbench/data-loss.test'; -import { setup as setupDataExplorerTests } from './areas/explorer/explorer.test'; import { setup as setupDataPreferencesTests } from './areas/preferences/preferences.test'; import { setup as setupDataSearchTests } from './areas/search/search.test'; import { setup as setupDataCSSTests } from './areas/css/css.test'; @@ -65,7 +64,6 @@ const opts = minimist(args, { 'verbose', 'remote', 'web', - 'headless', 'ci' ], default: { @@ -253,8 +251,7 @@ function createOptions(): ApplicationOptions { screenshotsPath, remote: opts.remote, web: opts.web, - browser: opts.browser, - headless: opts.headless + browser: opts.browser }; } @@ -331,7 +328,6 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { else { /*if (!opts.web) { setupDataMigrationTests(opts['stable-build'], testDataPath); } {{SQL CARBON EDIT}} comment out tests if (!opts.web) { setupDataLossTests(); } - setupDataExplorerTests(); if (!opts.web) { setupDataPreferencesTests(); } setupDataSearchTests(); setupDataCSSTests(); diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 6e1703c708..41c92f4261 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -118,8 +118,8 @@ const testModules = (async function () { async function runTestsInBrowser(testModules, browserType) { - - const browser = await playwright[browserType].launch({ headless: !Boolean(argv.debug) }); + const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros + const browser = await playwright[browserType].launch({ headless: !Boolean(argv.debug), dumpio: true, args }); const page = (await browser.defaultContext().pages())[0] const target = url.pathToFileURL(path.join(__dirname, 'renderer.html')); if (argv.build) { diff --git a/yarn.lock b/yarn.lock index df4c9d386f..3dac5caf6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10197,10 +10197,10 @@ xterm-addon-webgl@0.5.0: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0.tgz#c1031dc7599cce3509824643ab5f15361c928e3e" integrity sha512-hQrvabKCnwXFaEZ+YtoJM9Pm0CIBXL5KSwoU+RiGStU3KYTAcqYP2GsH3dWdvKX6kTWhWLS81dtDsGkfbOciuA== -xterm@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0.tgz#5915d3c4c8800fadbcf555a0a603c672ab9df589" - integrity sha512-JGIpigWM3EBWvnS3rtBuefkiToIILSK1HYMXy4BCsUpO+O4UeeV+/U1AdAXgCB6qJrnPNb7yLgBsVCQUNMteig== +xterm@4.5.0-beta.4: + version "4.5.0-beta.4" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.5.0-beta.4.tgz#701f05553b643236d3fcd8bb7f14045bd4537c92" + integrity sha512-Yv1Bf60LTLBMaig1rv033hPz8hQGXZN6VYW2oe/409t2NbJXPg5xZgf47qyaWFV7a5k1BFiwjayJCWaL2nYBew== y18n@^3.2.1: version "3.2.1"