diff --git a/build/azure-pipelines/darwin/build.sh b/build/azure-pipelines/darwin/build.sh new file mode 100755 index 0000000000..af558b8c14 --- /dev/null +++ b/build/azure-pipelines/darwin/build.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e +yarn gulp vscode-darwin-min +yarn gulp upload-vscode-sourcemaps \ No newline at end of file diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 55ee286e85..f5d5ef626b 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -18,9 +18,15 @@ steps: password $(VSCODE_MIXIN_PASSWORD) EOF + git config user.email "vscode@microsoft.com" + git config user.name "VSCode" + git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" + git fetch distro + git merge $(node -p "require('./package.json').distro") + yarn - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" yarn gulp -- mixin - yarn gulp -- hygiene + yarn gulp mixin + yarn gulp hygiene yarn monaco-compile-check node build/azure-pipelines/common/installDistro.js node build/lib/builtInExtensions.js @@ -30,10 +36,7 @@ steps: set -e VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ - yarn gulp -- vscode-darwin-min - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ - AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ - yarn gulp -- upload-vscode-sourcemaps + ./build/azure-pipelines/darwin/build.sh displayName: Build - script: | @@ -43,6 +46,11 @@ steps: # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-darwin/$APP_NAME" displayName: Run unit tests +- script: | + set -e + ./scripts/test-integration.sh --build --tfs "Integration Tests" + displayName: Run integration tests + - script: | set -e pushd ../VSCode-darwin && zip -r -X -y ../VSCode-darwin.zip * && popd @@ -69,30 +77,12 @@ steps: - script: | set -e - - # remove pkg from archive - zip -d ../VSCode-darwin.zip "*.pkg" - - # publish the build - PACKAGEJSON=`ls ../VSCode-darwin/*.app/Contents/Resources/app/package.json` - VERSION=`node -p "require(\"$PACKAGEJSON\").version"` + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - node build/azure-pipelines/common/publish.js \ - "$(VSCODE_QUALITY)" \ - darwin \ - archive \ - "VSCode-darwin-$(VSCODE_QUALITY).zip" \ - $VERSION \ - true \ - ../VSCode-darwin.zip - - # publish hockeyapp symbols - node build/azure-pipelines/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_MACOS)" - - # upload configuration AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ - yarn gulp -- upload-vscode-configuration + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + VSCODE_HOCKEYAPP_TOKEN="$(VSCODE_HOCKEYAPP_TOKEN)" \ + ./build/azure-pipelines/darwin/publish.sh displayName: Publish - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 diff --git a/build/azure-pipelines/darwin/publish.sh b/build/azure-pipelines/darwin/publish.sh new file mode 100755 index 0000000000..96d5967ea4 --- /dev/null +++ b/build/azure-pipelines/darwin/publish.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# remove pkg from archive +zip -d ../VSCode-darwin.zip "*.pkg" + +# publish the build +PACKAGEJSON=`ls ../VSCode-darwin/*.app/Contents/Resources/app/package.json` +VERSION=`node -p "require(\"$PACKAGEJSON\").version"` +node build/azure-pipelines/common/publish.js \ + "$VSCODE_QUALITY" \ + darwin \ + archive \ + "VSCode-darwin-$VSCODE_QUALITY.zip" \ + $VERSION \ + true \ + ../VSCode-darwin.zip + +# publish hockeyapp symbols +node build/azure-pipelines/common/symbols.js "$VSCODE_MIXIN_PASSWORD" "$VSCODE_HOCKEYAPP_TOKEN" "$VSCODE_ARCH" "$VSCODE_HOCKEYAPP_ID_MACOS" + +# upload configuration +yarn gulp upload-vscode-configuration diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml index b8ef20162b..dc55bce808 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -24,6 +24,7 @@ steps: git remote add distro "https://github.com/$VSCODE_MIXIN_REPO.git" git fetch distro + git push distro origin/master:refs/heads/master git merge $(node -p "require('./package.json').distro") - displayName: Merge Distro \ No newline at end of file + displayName: Sync & Merge Distro \ No newline at end of file diff --git a/build/azure-pipelines/linux/build.sh b/build/azure-pipelines/linux/build.sh new file mode 100755 index 0000000000..bd251ebc78 --- /dev/null +++ b/build/azure-pipelines/linux/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -e +yarn gulp "vscode-linux-$VSCODE_ARCH-min" \ No newline at end of file diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 3cea6670f0..342d72e496 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -22,87 +22,45 @@ steps: password $(VSCODE_MIXIN_PASSWORD) EOF + git config user.email "vscode@microsoft.com" + git config user.name "VSCode" + git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" + git fetch distro + git merge $(node -p "require('./package.json').distro") + CHILD_CONCURRENCY=1 yarn - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin - npm run gulp -- hygiene - npm run monaco-compile-check + yarn gulp mixin + yarn gulp hygiene + yarn monaco-compile-check node build/azure-pipelines/common/installDistro.js node build/lib/builtInExtensions.js + displayName: Prepare build - script: | set -e - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min - name: build + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ + ./build/azure-pipelines/linux/build.sh + displayName: Build - script: | set -e - npm run gulp -- "electron-$(VSCODE_ARCH)" + yarn gulp "electron-$(VSCODE_ARCH)" # xvfb seems to be crashing often, let's make sure it's always up service xvfb start DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" - name: test + displayName: Run unit tests - script: | set -e - REPO="$(pwd)" - ROOT="$REPO/.." - ARCH="$(VSCODE_ARCH)" - - # Publish tarball - PLATFORM_LINUX="linux-$(VSCODE_ARCH)" - [[ "$ARCH" == "ia32" ]] && DEB_ARCH="i386" || DEB_ARCH="amd64" - [[ "$ARCH" == "ia32" ]] && RPM_ARCH="i386" || RPM_ARCH="x86_64" - BUILDNAME="VSCode-$PLATFORM_LINUX" - BUILD="$ROOT/$BUILDNAME" - BUILD_VERSION="$(date +%s)" - [ -z "$VSCODE_QUALITY" ] && TARBALL_FILENAME="code-$BUILD_VERSION.tar.gz" || TARBALL_FILENAME="code-$VSCODE_QUALITY-$BUILD_VERSION.tar.gz" - TARBALL_PATH="$ROOT/$TARBALL_FILENAME" - PACKAGEJSON="$BUILD/resources/app/package.json" - VERSION=$(node -p "require(\"$PACKAGEJSON\").version") - - rm -rf $ROOT/code-*.tar.* - (cd $ROOT && tar -czf $TARBALL_PATH $BUILDNAME) - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - node build/azure-pipelines/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_LINUX" archive-unsigned "$TARBALL_FILENAME" "$VERSION" true "$TARBALL_PATH" - - # Publish hockeyapp symbols - node build/azure-pipelines/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX64)" - - # Publish DEB - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" - PLATFORM_DEB="linux-deb-$ARCH" - [[ "$ARCH" == "ia32" ]] && DEB_ARCH="i386" || DEB_ARCH="amd64" - DEB_FILENAME="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/)" - DEB_PATH="$REPO/.build/linux/deb/$DEB_ARCH/deb/$DEB_FILENAME" - - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - node build/azure-pipelines/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_DEB" package "$DEB_FILENAME" "$VERSION" true "$DEB_PATH" - - # Publish RPM - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" - PLATFORM_RPM="linux-rpm-$ARCH" - [[ "$ARCH" == "ia32" ]] && RPM_ARCH="i386" || RPM_ARCH="x86_64" - RPM_FILENAME="$(ls $REPO/.build/linux/rpm/$RPM_ARCH/ | grep .rpm)" - RPM_PATH="$REPO/.build/linux/rpm/$RPM_ARCH/$RPM_FILENAME" - - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - node build/azure-pipelines/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_RPM" package "$RPM_FILENAME" "$VERSION" true "$RPM_PATH" - - # Publish Snap - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-prepare-snap" - - # Pack snap tarball artifact, in order to preserve file perms - mkdir -p $REPO/.build/linux/snap-tarball - SNAP_TARBALL_PATH="$REPO/.build/linux/snap-tarball/snap-$(VSCODE_ARCH).tar.gz" - rm -rf $SNAP_TARBALL_PATH - (cd .build/linux && tar -czf $SNAP_TARBALL_PATH snap) + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ + VSCODE_HOCKEYAPP_TOKEN="$(VSCODE_HOCKEYAPP_TOKEN)" \ + ./build/azure-pipelines/linux/publish.sh + displayName: Publish - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 displayName: 'Component Detection' diff --git a/build/azure-pipelines/linux/publish.sh b/build/azure-pipelines/linux/publish.sh new file mode 100755 index 0000000000..ca338a6fc5 --- /dev/null +++ b/build/azure-pipelines/linux/publish.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -e +REPO="$(pwd)" +ROOT="$REPO/.." + +# Publish tarball +PLATFORM_LINUX="linux-$VSCODE_ARCH" +[[ "$VSCODE_ARCH" == "ia32" ]] && DEB_ARCH="i386" || DEB_ARCH="amd64" +[[ "$VSCODE_ARCH" == "ia32" ]] && RPM_ARCH="i386" || RPM_ARCH="x86_64" +BUILDNAME="VSCode-$PLATFORM_LINUX" +BUILD="$ROOT/$BUILDNAME" +BUILD_VERSION="$(date +%s)" +[ -z "$VSCODE_QUALITY" ] && TARBALL_FILENAME="code-$BUILD_VERSION.tar.gz" || TARBALL_FILENAME="code-$VSCODE_QUALITY-$BUILD_VERSION.tar.gz" +TARBALL_PATH="$ROOT/$TARBALL_FILENAME" +PACKAGEJSON="$BUILD/resources/app/package.json" +VERSION=$(node -p "require(\"$PACKAGEJSON\").version") + +rm -rf $ROOT/code-*.tar.* +(cd $ROOT && tar -czf $TARBALL_PATH $BUILDNAME) + +node build/azure-pipelines/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_LINUX" archive-unsigned "$TARBALL_FILENAME" "$VERSION" true "$TARBALL_PATH" + +# Publish hockeyapp symbols +node build/azure-pipelines/common/symbols.js "$VSCODE_MIXIN_PASSWORD" "$VSCODE_HOCKEYAPP_TOKEN" "$VSCODE_ARCH" "$VSCODE_HOCKEYAPP_ID_LINUX64" + +# Publish DEB +yarn gulp "vscode-linux-$VSCODE_ARCH-build-deb" +PLATFORM_DEB="linux-deb-$VSCODE_ARCH" +[[ "$VSCODE_ARCH" == "ia32" ]] && DEB_ARCH="i386" || DEB_ARCH="amd64" +DEB_FILENAME="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/)" +DEB_PATH="$REPO/.build/linux/deb/$DEB_ARCH/deb/$DEB_FILENAME" + +node build/azure-pipelines/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_DEB" package "$DEB_FILENAME" "$VERSION" true "$DEB_PATH" + +# Publish RPM +yarn gulp "vscode-linux-$VSCODE_ARCH-build-rpm" +PLATFORM_RPM="linux-rpm-$VSCODE_ARCH" +[[ "$VSCODE_ARCH" == "ia32" ]] && RPM_ARCH="i386" || RPM_ARCH="x86_64" +RPM_FILENAME="$(ls $REPO/.build/linux/rpm/$RPM_ARCH/ | grep .rpm)" +RPM_PATH="$REPO/.build/linux/rpm/$RPM_ARCH/$RPM_FILENAME" + +node build/azure-pipelines/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_RPM" package "$RPM_FILENAME" "$VERSION" true "$RPM_PATH" + +# Publish Snap +yarn gulp "vscode-linux-$VSCODE_ARCH-prepare-snap" + +# Pack snap tarball artifact, in order to preserve file perms +mkdir -p $REPO/.build/linux/snap-tarball +SNAP_TARBALL_PATH="$REPO/.build/linux/snap-tarball/snap-$VSCODE_ARCH.tar.gz" +rm -rf $SNAP_TARBALL_PATH +(cd .build/linux && tar -czf $SNAP_TARBALL_PATH snap) diff --git a/build/azure-pipelines/win32/build.ps1 b/build/azure-pipelines/win32/build.ps1 new file mode 100644 index 0000000000..60325ba54c --- /dev/null +++ b/build/azure-pipelines/win32/build.ps1 @@ -0,0 +1,4 @@ +. build/azure-pipelines/win32/exec.ps1 +$ErrorActionPreference = "Stop" +exec { yarn gulp "vscode-win32-$env:VSCODE_ARCH-min" } +exec { yarn gulp "vscode-win32-$env:VSCODE_ARCH-inno-updater" } \ No newline at end of file diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 7c1b5cd9a5..a49349150d 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -18,34 +18,47 @@ steps: "machine monacotools.visualstudio.com`npassword $(VSO_PAT)`nmachine github.com`nlogin vscode`npassword $(VSCODE_MIXIN_PASSWORD)" | Out-File "$env:USERPROFILE\_netrc" -Encoding ASCII $env:npm_config_arch="$(VSCODE_ARCH)" $env:CHILD_CONCURRENCY="1" - $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" + + exec { git config user.email "vscode@microsoft.com" } + exec { git config user.name "VSCode" } + exec { git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" } + exec { git fetch distro } + exec { git merge $(node -p "require('./package.json').distro") } + exec { yarn } - exec { npm run gulp -- mixin } - exec { npm run gulp -- hygiene } - exec { npm run monaco-compile-check } + exec { yarn gulp mixin } + exec { yarn gulp hygiene } + exec { yarn monaco-compile-check } exec { node build/azure-pipelines/common/installDistro.js } exec { node build/lib/builtInExtensions.js } + displayName: Prepare build - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" - exec { npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-min" } - exec { npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-inno-updater" } - name: build + .\build\azure-pipelines\win32\build.ps1 + displayName: Build - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { npm run gulp -- "electron-$(VSCODE_ARCH)" } + exec { yarn gulp "electron-$(VSCODE_ARCH)" } exec { .\scripts\test.bat --build --tfs "Unit Tests" } # yarn smoketest -- --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - name: test + displayName: Run unit tests + +- powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn gulp "electron-$(VSCODE_ARCH)" } + exec { .\scripts\test-integration.bat --build --tfs "Integration Tests" } + displayName: Run integration tests - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 inputs: ConnectedServiceName: 'ESRP CodeSign' - FolderPath: '$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)' + FolderPath: '$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH),$(agent.builddirectory)/vscode-reh-win32-$(VSCODE_ARCH)' Pattern: '*.dll,*.exe,*.node' signConfigType: inlineSignParams inlineOperation: | @@ -119,31 +132,11 @@ steps: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-archive" "vscode-win32-$(VSCODE_ARCH)-system-setup" "vscode-win32-$(VSCODE_ARCH)-user-setup" --sign } - - $Repo = "$(pwd)" - $Root = "$Repo\.." - $SystemExe = "$Repo\.build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe" - $UserExe = "$Repo\.build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe" - $Zip = "$Repo\.build\win32-$(VSCODE_ARCH)\archive\VSCode-win32-$(VSCODE_ARCH).zip" - $Build = "$Root\VSCode-win32-$(VSCODE_ARCH)" - - # get version - $PackageJson = Get-Content -Raw -Path "$Build\resources\app\package.json" | ConvertFrom-Json - $Version = $PackageJson.version - $Quality = "$env:VSCODE_QUALITY" $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(AZURE_STORAGE_ACCESS_KEY_2)" $env:AZURE_DOCUMENTDB_MASTERKEY = "$(AZURE_DOCUMENTDB_MASTERKEY)" - - $assetPlatform = if ("$(VSCODE_ARCH)" -eq "ia32") { "win32" } else { "win32-x64" } - - exec { node build/azure-pipelines/common/publish.js $Quality "$global:assetPlatform-archive" archive "VSCode-win32-$(VSCODE_ARCH)-$Version.zip" $Version true $Zip } - exec { node build/azure-pipelines/common/publish.js $Quality "$global:assetPlatform" setup "VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $SystemExe } - exec { node build/azure-pipelines/common/publish.js $Quality "$global:assetPlatform-user" setup "VSCodeUserSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $UserExe } - - # publish hockeyapp symbols - $hockeyAppId = if ("$(VSCODE_ARCH)" -eq "ia32") { "$(VSCODE_HOCKEYAPP_ID_WIN32)" } else { "$(VSCODE_HOCKEYAPP_ID_WIN64)" } - exec { node build/azure-pipelines/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" $hockeyAppId } + $env:VSCODE_HOCKEYAPP_TOKEN = "$(VSCODE_HOCKEYAPP_TOKEN)" + .\build\azure-pipelines\win32\publish.ps1 + displayName: Publish - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 displayName: 'Component Detection' diff --git a/build/azure-pipelines/win32/publish.ps1 b/build/azure-pipelines/win32/publish.ps1 new file mode 100644 index 0000000000..8e6fa988ad --- /dev/null +++ b/build/azure-pipelines/win32/publish.ps1 @@ -0,0 +1,28 @@ +. build/azure-pipelines/win32/exec.ps1 +$ErrorActionPreference = "Stop" + +$Arch = "$env:VSCODE_ARCH" + +exec { yarn gulp "vscode-win32-$Arch-archive" "vscode-win32-$Arch-system-setup" "vscode-win32-$Arch-user-setup" --sign } + +$Repo = "$(pwd)" +$Root = "$Repo\.." +$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" +$Build = "$Root\VSCode-win32-$Arch" + +# get version +$PackageJson = Get-Content -Raw -Path "$Build\resources\app\package.json" | ConvertFrom-Json +$Version = $PackageJson.version +$Quality = "$env:VSCODE_QUALITY" + +$AssetPlatform = if ("$Arch" -eq "ia32") { "win32" } else { "win32-x64" } + +exec { node build/azure-pipelines/common/publish.js $Quality "$AssetPlatform-archive" archive "VSCode-win32-$Arch-$Version.zip" $Version true $Zip } +exec { node build/azure-pipelines/common/publish.js $Quality "$AssetPlatform" setup "VSCodeSetup-$Arch-$Version.exe" $Version true $SystemExe } +exec { node build/azure-pipelines/common/publish.js $Quality "$AssetPlatform-user" setup "VSCodeUserSetup-$Arch-$Version.exe" $Version true $UserExe } + +# 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 } diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 987ddd4115..c52b3eefea 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -75,9 +75,10 @@ const vscodeResources = [ 'out-build/bootstrap-window.js', 'out-build/paths.js', 'out-build/vs/**/*.{svg,png,cur,html}', + '!out-build/vs/code/browser/**/*.html', 'out-build/vs/base/common/performance.js', 'out-build/vs/base/node/languagePacks.js', - 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh,cpuUsage.sh}', + 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh,cpuUsage.sh,ps.sh}', 'out-build/vs/base/browser/ui/octiconLabel/octicons/**', 'out-build/vs/workbench/browser/media/*-theme.css', 'out-build/vs/workbench/contrib/debug/**/*.json', @@ -431,6 +432,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op .pipe(replace('@@VERSION@@', version)) .pipe(replace('@@COMMIT@@', commit)) .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(replace('@@QUALITY@@', quality)) .pipe(rename(function (f) { f.basename = product.applicationName; f.extname = ''; }))); result = es.merge(result, gulp.src('resources/win32/VisualElementsManifest.xml', { base: 'resources/win32' }) diff --git a/build/lib/extensions.js b/build/lib/extensions.js index dc0923ed36..2cf9bca6d2 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -237,6 +237,7 @@ exports.fromMarketplace = fromMarketplace; const excludedExtensions = [ 'vscode-api-tests', 'vscode-colorize-tests', + 'vscode-test-resolver', 'ms-vscode.node-debug', 'ms-vscode.node-debug2', // {{SQL CARBON EDIT}} diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index d1a7aa30da..9cb9021270 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -283,6 +283,7 @@ interface IPackageExtensionsOptions { const excludedExtensions = [ 'vscode-api-tests', 'vscode-colorize-tests', + 'vscode-test-resolver', 'ms-vscode.node-debug', 'ms-vscode.node-debug2', // {{SQL CARBON EDIT}} diff --git a/cgmanifest.json b/cgmanifest.json index 927c66cbc3..01cdd8df5f 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -111,11 +111,11 @@ "git": { "name": "vscode-octicons-font", "repositoryUrl": "https://github.com/Microsoft/vscode-octicons-font", - "commitHash": "5095860bb929919670646e2dfa0ee47d9b93bcb9" + "commitHash": "4f69de3a233ed501c2098e33047e116ac2fbbf42" } }, "license": "MIT", - "version": "1.0.0" + "version": "1.1.0" }, { "component": { diff --git a/extensions/git/package.json b/extensions/git/package.json index 058fd6691a..4395d306bb 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1031,7 +1031,7 @@ ], "markdownDescription": "%config.path%", "default": null, - "scope": "application" + "scope": "machine" }, "git.autoRepositoryDetection": { "type": [ diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index c9c2b86b3d..6decfcc826 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -381,7 +381,14 @@ export class MarkdownPreview extends Disposable { clearTimeout(this.throttleTimer); this.throttleTimer = undefined; - const document = await vscode.workspace.openTextDocument(resource); + let document: vscode.TextDocument; + try { + document = await vscode.workspace.openTextDocument(resource); + } catch { + await this.showFileNotFoundError(); + return; + } + if (!this.forceUpdate && this.currentVersion && this.currentVersion.resource.fsPath === resource.fsPath && this.currentVersion.version === document.version) { if (this.line) { this.updateForView(resource, this.line); @@ -391,12 +398,9 @@ export class MarkdownPreview extends Disposable { this.forceUpdate = false; this.currentVersion = { resource, version: document.version }; - const content = await this._contentProvider.provideTextDocumentContent(document, this._previewConfigurations, this.line, this.state); if (this._resource === resource) { - this.editor.title = MarkdownPreview.getPreviewTitle(this._resource, this._locked); - this.editor.iconPath = this.iconPath; - this.editor.webview.options = MarkdownPreview.getWebviewOptions(resource, this._contributionProvider.contributions); - this.editor.webview.html = content; + const content = await this._contentProvider.provideTextDocumentContent(document, this._previewConfigurations, this.line, this.state); + this.setContent(content); } } @@ -456,7 +460,22 @@ export class MarkdownPreview extends Disposable { } } - vscode.workspace.openTextDocument(this._resource).then(vscode.window.showTextDocument); + vscode.workspace.openTextDocument(this._resource) + .then(vscode.window.showTextDocument) + .then(undefined, () => { + vscode.window.showErrorMessage(localize('preview.clickOpenFailed', 'Could not open {0}', this._resource.toString())); + }); + } + + private async showFileNotFoundError() { + this.setContent(this._contentProvider.provideFileNotFoundContent(this._resource)); + } + + private setContent(html: string): void { + this.editor.title = MarkdownPreview.getPreviewTitle(this._resource, this._locked); + this.editor.iconPath = this.iconPath; + this.editor.webview.options = MarkdownPreview.getWebviewOptions(this._resource, this._contributionProvider.contributions); + this.editor.webview.html = html; } private async onDidClickPreviewLink(path: string, fragment: string | undefined) { diff --git a/extensions/markdown-language-features/src/features/previewContentProvider.ts b/extensions/markdown-language-features/src/features/previewContentProvider.ts index 80f032b1b8..f2ad4a54e4 100644 --- a/extensions/markdown-language-features/src/features/previewContentProvider.ts +++ b/extensions/markdown-language-features/src/features/previewContentProvider.ts @@ -90,6 +90,19 @@ export class MarkdownContentProvider { `; } + public provideFileNotFoundContent( + resource: vscode.Uri, + ): string { + const resourcePath = path.basename(resource.fsPath); + const body = localize('preview.notFound', '{0} cannot be found', resourcePath); + return ` + + + ${body} + + `; + } + private extensionResourcePath(mediaFile: string): string { return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))) .with({ scheme: 'vscode-resource' }) diff --git a/extensions/package.json b/extensions/package.json index aee1adf2d3..724474ed96 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.4.3-insiders.20190408" + "typescript": "3.4.3" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/theme-defaults/themes/dark_plus.json b/extensions/theme-defaults/themes/dark_plus.json index 3e2c5ac607..c66e666e2d 100644 --- a/extensions/theme-defaults/themes/dark_plus.json +++ b/extensions/theme-defaults/themes/dark_plus.json @@ -68,7 +68,12 @@ }, { "name": "Control flow keywords", - "scope": "keyword.control", + "scope": [ + "keyword.control", + "keyword.operator.new.cpp", + "keyword.operator.delete.cpp", + "keyword.other.using" + ], "settings": { "foreground": "#C586C0" } diff --git a/extensions/theme-defaults/themes/hc_black.json b/extensions/theme-defaults/themes/hc_black.json index c34b0981fd..74bf098818 100644 --- a/extensions/theme-defaults/themes/hc_black.json +++ b/extensions/theme-defaults/themes/hc_black.json @@ -65,7 +65,12 @@ }, { "name": "Control flow keywords", - "scope": "keyword.control", + "scope": [ + "keyword.control", + "keyword.operator.new.cpp", + "keyword.operator.delete.cpp", + "keyword.other.using" + ], "settings": { "foreground": "#C586C0" } diff --git a/extensions/theme-defaults/themes/light_plus.json b/extensions/theme-defaults/themes/light_plus.json index 726cae4058..c58e8c6f90 100644 --- a/extensions/theme-defaults/themes/light_plus.json +++ b/extensions/theme-defaults/themes/light_plus.json @@ -68,7 +68,12 @@ }, { "name": "Control flow keywords", - "scope": "keyword.control", + "scope": [ + "keyword.control", + "keyword.operator.new.cpp", + "keyword.operator.delete.cpp", + "keyword.other.using" + ], "settings": { "foreground": "#AF00DB" } diff --git a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json index d4fbd2f7e2..8b2110255f 100644 --- a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json +++ b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json @@ -113,7 +113,10 @@ "name": "Keywords", "scope": [ "keyword", - "keyword.control" + "keyword.control", + "keyword.operator.new.cpp", + "keyword.operator.delete.cpp", + "keyword.other.using" ], "settings": { "foreground": "#98676a" diff --git a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json index b50041d6cd..e5088a0811 100644 --- a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json +++ b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json @@ -255,7 +255,12 @@ }, { "name": "Keyword Control", - "scope": "keyword.control", + "scope": [ + "keyword.control", + "keyword.operator.new.cpp", + "keyword.operator.delete.cpp", + "keyword.other.using" + ], "settings": { "fontStyle": "", "foreground": "#9872A2" diff --git a/extensions/typescript-language-features/src/features/smartSelect.ts b/extensions/typescript-language-features/src/features/smartSelect.ts new file mode 100644 index 0000000000..a9a0370bee --- /dev/null +++ b/extensions/typescript-language-features/src/features/smartSelect.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as Proto from '../protocol'; +import { ITypeScriptServiceClient } from '../typescriptService'; +import API from '../utils/api'; +import { VersionDependentRegistration } from '../utils/dependentRegistration'; +import * as typeConverters from '../utils/typeConverters'; + +class SmartSelection implements vscode.SelectionRangeProvider { + public static readonly minVersion = API.v350; + + public constructor( + private readonly client: ITypeScriptServiceClient + ) { } + + public async provideSelectionRanges( + document: vscode.TextDocument, + positions: vscode.Position[], + token: vscode.CancellationToken, + ): Promise { + const file = this.client.toOpenedFilePath(document); + if (!file) { + return undefined; + } + + const args: Proto.FileRequestArgs & { locations: Proto.Location[] } = { + file, + locations: positions.map(typeConverters.Position.toLocation) + }; + const response = await this.client.execute('selectionRange', args, token); + if (response.type !== 'response' || !response.body) { + return undefined; + } + return response.body.map(SmartSelection.convertSelectionRange); + } + + private static convertSelectionRange( + selectionRange: Proto.SelectionRange + ): vscode.SelectionRange { + return new vscode.SelectionRange( + typeConverters.Range.fromTextSpan(selectionRange.textSpan), + selectionRange.parent ? SmartSelection.convertSelectionRange(selectionRange.parent) : undefined, + ); + } +} + +export function register( + selector: vscode.DocumentSelector, + client: ITypeScriptServiceClient, +) { + return new VersionDependentRegistration(client, SmartSelection.minVersion, () => + vscode.languages.registerSelectionRangeProvider(selector, new SmartSelection(client))); +} \ No newline at end of file diff --git a/extensions/typescript-language-features/src/test/onEnter.test.ts b/extensions/typescript-language-features/src/test/onEnter.test.ts new file mode 100644 index 0000000000..c7d6c3a45d --- /dev/null +++ b/extensions/typescript-language-features/src/test/onEnter.test.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import 'mocha'; +import * as vscode from 'vscode'; +import { CURSOR, withRandomFileEditor } from './testUtils'; + +const onDocumentChange = (doc: vscode.TextDocument): Promise => { + return new Promise(resolve => { + const sub = vscode.workspace.onDidChangeTextDocument(e => { + if (e.document !== doc) { + return; + } + sub.dispose(); + resolve(e.document); + }); + }); +}; + +const type = async (document: vscode.TextDocument, text: string): Promise => { + const onChange = onDocumentChange(document); + await vscode.commands.executeCommand('type', { text }); + await onChange; + return document; +}; + +suite('OnEnter', () => { + test('should indent after if block with braces', () => { + return withRandomFileEditor(`if (true) {${CURSOR}`, 'js', async (_editor, document) => { + await type(document, '\nx'); + assert.strictEqual(document.getText(), `if (true) {\n x`); + }); + }); + + test('should indent within empty object literal', () => { + return withRandomFileEditor(`({${CURSOR}})`, 'js', async (_editor, document) => { + await type(document, '\nx'); + assert.strictEqual(document.getText(), `({\n x\n})`); + }); + }); + + test('should indent after simple jsx tag with attributes', () => { + return withRandomFileEditor(`const a =
${CURSOR}`, 'jsx', async (_editor, document) => { + await type(document, '\nx'); + assert.strictEqual(document.getText(), `const a =
\n x`); + }); + }); + + test('should indent after simple jsx tag with attributes', () => { + return withRandomFileEditor(`const a =
${CURSOR}`, 'jsx', async (_editor, document) => { + await type(document, '\nx'); + assert.strictEqual(document.getText(), `const a =
\n x`); + }); + }); +}); \ No newline at end of file diff --git a/extensions/typescript-language-features/src/test/testUtils.ts b/extensions/typescript-language-features/src/test/testUtils.ts new file mode 100644 index 0000000000..d8efdc3f9c --- /dev/null +++ b/extensions/typescript-language-features/src/test/testUtils.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as os from 'os'; +import { join } from 'path'; + +function rndName() { + return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10); +} + +export function createRandomFile(contents = '', fileExtension = 'txt'): Thenable { + return new Promise((resolve, reject) => { + const tmpFile = join(os.tmpdir(), rndName() + '.' + fileExtension); + fs.writeFile(tmpFile, contents, (error) => { + if (error) { + return reject(error); + } + + resolve(vscode.Uri.file(tmpFile)); + }); + }); +} + + +export function deleteFile(file: vscode.Uri): Thenable { + return new Promise((resolve, reject) => { + fs.unlink(file.fsPath, (err) => { + if (err) { + reject(err); + } else { + resolve(true); + } + }); + }); +} + +export const CURSOR = '$$CURSOR$$'; + +export function withRandomFileEditor( + contents: string, + fileExtension: string, + run: (editor: vscode.TextEditor, doc: vscode.TextDocument) => Thenable +): Thenable { + const cursorIndex = contents.indexOf(CURSOR); + return createRandomFile(contents.replace(CURSOR, ''), fileExtension).then(file => { + return vscode.workspace.openTextDocument(file).then(doc => { + return vscode.window.showTextDocument(doc).then((editor) => { + if (cursorIndex >= 0) { + const pos = doc.positionAt(cursorIndex); + editor.selection = new vscode.Selection(pos, pos); + } + return run(editor, doc).then(_ => { + if (doc.isDirty) { + return doc.save().then(() => { + return deleteFile(file); + }); + } else { + return deleteFile(file); + } + }); + }); + }); + }); +} \ No newline at end of file diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 9c39cd909a..8c1f7f117c 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.4.3-insiders.20190408: - version "3.4.3-insiders.20190408" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.3-insiders.20190408.tgz#18d98336c693a13dc8b2d5f39b70268c018c650b" - integrity sha512-5SI6EA+2u0ea/Uy0qCEczh8vBR0ByVaCFCyU0RdROROw8V5O4OIQHMFcnIdyg+nnfRGYp39PxvllGMDpsTFOOQ== +typescript@3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.3.tgz#0eb320e4ace9b10eadf5bc6103286b0f8b7c224f" + integrity sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ== diff --git a/package.json b/package.json index 98df3f5f77..733a15d15b 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "vscode-ripgrep": "^1.2.5", "vscode-sqlite3": "4.0.7", "vscode-textmate": "^4.0.1", - "vscode-xterm": "3.13.0-beta2", + "vscode-xterm": "3.13.0-beta3", "yauzl": "^2.9.1", "yazl": "^2.4.3", "zone.js": "^0.8.4" @@ -119,6 +119,7 @@ "gulp-eslint": "^5.0.0", "gulp-filter": "^5.1.0", "gulp-flatmap": "^1.0.2", + "gulp-gunzip": "^1.0.0", "gulp-json-editor": "^2.5.0", "gulp-plumber": "^1.2.0", "gulp-remote-src": "^0.4.4", @@ -128,6 +129,7 @@ "gulp-tsb": "2.0.7", "gulp-tslint": "^8.1.3", "gulp-uglify": "^3.0.0", + "gulp-untar": "^0.0.7", "gulp-vinyl-zip": "^2.1.2", "husky": "^0.13.1", "innosetup-compiler": "^5.5.60", @@ -177,8 +179,8 @@ "url": "https://github.com/Microsoft/azuredatastudio/issues" }, "optionalDependencies": { - "vscode-windows-registry": "1.0.1", "vscode-windows-ca-certs": "0.1.0", + "vscode-windows-registry": "1.0.1", "windows-foreground-love": "0.1.0", "windows-mutex": "0.2.1", "windows-process-tree": "0.2.3" @@ -189,4 +191,4 @@ "natives": "1.1.6", "@types/node": "10.12.12" } -} +} \ No newline at end of file diff --git a/resources/linux/bin/code.sh b/resources/linux/bin/code.sh index 47ea0587cc..32f19a7c99 100755 --- a/resources/linux/bin/code.sh +++ b/resources/linux/bin/code.sh @@ -3,6 +3,15 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Source EULA. See License.txt in the project root for license information. +# test that VSCode wasn't installed inside WSL +if grep -qi Microsoft /proc/version; then + echo "To use VS Code with the Windows Subsystem for Linux, please install VS Code in Windows and uninstall the Linux version in WSL. You can then use the '@@PRODNAME@@' command in a WSL terminal just as you would in a normal command prompt." 1>&2 + read -e -p "Do you want to continue anyways ? [y/N] " YN + + [[ $YN == "n" || $YN == "N" || $YN == "" ]] && exit 1 +fi + + # If root, ensure that --user-data-dir or --file-write is specified if [ "$(id -u)" = "0" ]; then for i in $@ diff --git a/resources/win32/bin/code.sh b/resources/win32/bin/code.sh index b95bf7983b..6cc47a93ae 100644 --- a/resources/win32/bin/code.sh +++ b/resources/win32/bin/code.sh @@ -2,28 +2,31 @@ # # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Source EULA. See License.txt in the project root for license information. +COMMIT="@@COMMIT@@" +APP_NAME="@@APPNAME@@" +QUALITY="@@QUALITY@@" -NAME="@@NAME@@" -VSCODE_PATH="$(dirname "$(dirname "$(realpath "$0")")")" -ELECTRON="$VSCODE_PATH/$NAME.exe" -if grep -q Microsoft /proc/version; then - if [ -x /bin/wslpath ]; then - # On recent WSL builds, we just need to set WSLENV so that - # ELECTRON_RUN_AS_NODE is visible to the win32 process - export WSLENV=ELECTRON_RUN_AS_NODE/w:$WSLENV - CLI=$(wslpath -m "$VSCODE_PATH/resources/app/out/cli.js") - else - # If running under older WSL, don't pass cli.js to Electron as - # environment vars cannot be transferred from WSL to Windows - # See: https://github.com/Microsoft/BashOnWindows/issues/1363 - # https://github.com/Microsoft/BashOnWindows/issues/1494 - "$ELECTRON" "$@" +set -e + +if grep -qi Microsoft /proc/version; then + # in a wsl shell + WIN_CODE_CMD=$(wslpath -w "$(dirname "$(realpath "$0")")/$APP_NAME.cmd") + + WSL_EXT_ID="ms-vscode.remote-wsl" + WSL_EXT_WLOC=$(cmd.exe /c "$WIN_CODE_CMD" --locate-extension $WSL_EXT_ID) + if ! [ -z "$WSL_EXT_WLOC" ]; then + # replace \r\n with \n in WSL_EXT_WLOC, get linux path for + WSL_CODE=$(wslpath -u "${WSL_EXT_WLOC%%[[:cntrl:]]}")/scripts/wslCode.sh + $WSL_CODE $COMMIT $QUALITY "$WIN_CODE_CMD" "$APP_NAME" "$@" exit $? fi -elif [ "$(expr substr $(uname -s) 1 9)" == "CYGWIN_NT" ]; then +fi + +if [ -x "$(command -v cygpath)" ]; then CLI=$(cygpath -m "$VSCODE_PATH/resources/app/out/cli.js") else CLI="$VSCODE_PATH/resources/app/out/cli.js" fi +ELECTRON="$VSCODE_PATH/$NAME.exe" ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@" exit $? diff --git a/scripts/code.sh b/scripts/code.sh index 8035059f65..ba4177c33e 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -e + if [[ "$OSTYPE" == "darwin"* ]]; then realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } ROOT=$(dirname "$(dirname "$(realpath "$0")")") @@ -9,6 +11,9 @@ if [[ "$OSTYPE" == "darwin"* ]]; then export ELECTRON_ENABLE_LOGGING=1 else ROOT=$(dirname "$(dirname "$(readlink -f $0)")") + if grep -qi Microsoft /proc/version; then + IN_WSL=true + fi fi function code() { @@ -50,4 +55,23 @@ function code() { exec "$CODE" . "$@" } -code "$@" +function code-wsl() +{ + # in a wsl shell + local WIN_CODE_CLI_CMD=$(wslpath -w "$ROOT/scripts/code-cli.bat") + + local WSL_EXT_ID="ms-vscode.remote-wsl" + local WSL_EXT_WLOC=$(cmd.exe /c "$WIN_CODE_CLI_CMD" --locate-extension $WSL_EXT_ID) + if ! [ -z "$WSL_EXT_WLOC" ]; then + # replace \r\n with \n in WSL_EXT_WLOC + local WSL_CODE=$(wslpath -u "${WSL_EXT_WLOC%%[[:cntrl:]]}")/scripts/wslCode-dev.sh + $WSL_CODE "$ROOT" "$@" + exit $? + fi +} + +if [ -z ${IN_WSL+x} ]; then + code "$@" +else + code-wsl "$@" +fi \ No newline at end of file diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index 191fde9e2b..41c57a40a0 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -11,6 +11,10 @@ const bootstrap = require('./bootstrap'); // Enable ASAR in our forked processes bootstrap.enableASARSupport(); +if (process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']) { + bootstrap.injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); +} + // Configure: pipe logging to parent process if (!!process.send && process.env.PIPE_LOGGING === 'true') { pipeLoggingToParent(); diff --git a/src/bootstrap.js b/src/bootstrap.js index 844960605c..1e8a03ec6c 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -20,6 +20,38 @@ process.on('SIGPIPE', () => { //#endregion +//#region Add support for redirecting the loading of node modules +exports.injectNodeModuleLookupPath = function (injectPath) { + if (!injectPath) { + throw new Error('Missing injectPath'); + } + + // @ts-ignore + const Module = require('module'); + const path = require('path'); + + const nodeModulesPath = path.join(__dirname, '../node_modules'); + + // @ts-ignore + const originalResolveLookupPaths = Module._resolveLookupPaths; + + // @ts-ignore + Module._resolveLookupPaths = function (moduleName, parent, newReturn) { + const result = originalResolveLookupPaths(moduleName, parent, newReturn); + + const paths = newReturn ? result : result[1]; + for (let i = 0, len = paths.length; i < len; i++) { + if (paths[i] === nodeModulesPath) { + paths.splice(i, 0, injectPath); + break; + } + } + + return result; + }; +}; +//#endregion + //#region Add support for using node_modules.asar /** * @param {string=} nodeModulesPath diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index d58e8a0f12..87ad0aa242 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -41,6 +41,7 @@ import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostConfiguration, ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; +import { ISchemeTransformer } from 'vs/workbench/api/common/extHostLanguageFeatures'; export interface ISqlExtensionApiFactory { vsCodeFactory(extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -58,9 +59,11 @@ export function createApiFactory( extHostConfiguration: ExtHostConfiguration, extensionService: ExtHostExtensionService, logService: ExtHostLogService, - extHostStorage: ExtHostStorage + extHostStorage: ExtHostStorage, + schemeTransformer: ISchemeTransformer | null, + outputChannelName: string ): ISqlExtensionApiFactory { - let vsCodeFactory = extHostApi.createApiFactory(initData, rpcProtocol, extHostWorkspace, extHostConfiguration, extensionService, logService, extHostStorage); + let vsCodeFactory = extHostApi.createApiFactory(initData, rpcProtocol, extHostWorkspace, extHostConfiguration, extensionService, logService, extHostStorage, schemeTransformer, outputChannelName); // Addressable instances const extHostAccountManagement = rpcProtocol.set(SqlExtHostContext.ExtHostAccountManagement, new ExtHostAccountManagement(rpcProtocol)); diff --git a/src/sql/workbench/services/telemetry/node/fileTelemetryService.ts b/src/sql/workbench/services/telemetry/node/fileTelemetryService.ts deleted file mode 100644 index f744c049d6..0000000000 --- a/src/sql/workbench/services/telemetry/node/fileTelemetryService.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; -const fs = require('fs'); - -/** - * Write telemetry into a file for test purposes - */ -export class FileTelemetryService implements ITelemetryService { - _serviceBrand: undefined; - private _isFirst = true; - - constructor(private _outputFile: string) { - } - - publicLog(eventName: string, data?: ITelemetryData) { - let telemetryData = JSON.stringify(Object.assign({ eventName: eventName, data: data })); - if (this._outputFile) { - if (this._isFirst) { - fs.open(this._outputFile, fs.O_WRONLY | fs.O_CREAT, (err, fr) => { - fs.writeFileSync(this._outputFile, telemetryData + '\n'); - this._isFirst = false; - }); - } else { - fs.appendFileSync(this._outputFile, telemetryData + '\n'); - } - } - return Promise.resolve(null); - } - isOptedIn: true; - getTelemetryInfo(): Promise { - return Promise.resolve({ - instanceId: 'someValue.instanceId', - sessionId: 'someValue.sessionId', - machineId: 'someValue.machineId' - }); - } -} diff --git a/src/sqltest/stubs/telemetryServiceStub.ts b/src/sqltest/stubs/telemetryServiceStub.ts index 937b44cf68..5edf619833 100644 --- a/src/sqltest/stubs/telemetryServiceStub.ts +++ b/src/sqltest/stubs/telemetryServiceStub.ts @@ -7,6 +7,9 @@ import { ITelemetryService, ITelemetryData, ITelemetryInfo } from 'vs/platform/t // Test stubs for commonly used objects export class TelemetryServiceStub implements ITelemetryService { + setEnabled(value: boolean): void { + // no op; + } _serviceBrand: any; @@ -23,4 +26,4 @@ export class TelemetryServiceStub implements ITelemetryService { } isOptedIn: boolean; -} \ No newline at end of file +} diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index 1303af547f..afcbcaff39 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -1,18 +1,16 @@ { + "extends": "./tsconfig.base.json", "compilerOptions": { "noEmit": true, + "types": [], + "paths": {}, "module": "amd", "moduleResolution": "classic", - "noImplicitAny": false, "removeComments": false, "preserveConstEnums": true, "target": "es5", "sourceMap": false, - "experimentalDecorators": true, "declaration": true, - "noImplicitReturns": true, - "baseUrl": ".", - "types": [] }, "include": [ "typings/require.d.ts", diff --git a/src/vs/base/browser/dnd.ts b/src/vs/base/browser/dnd.ts index 2485a9c1e7..23cc0ad948 100644 --- a/src/vs/base/browser/dnd.ts +++ b/src/vs/base/browser/dnd.ts @@ -68,7 +68,7 @@ export const DataTransfers = { FILES: 'Files', /** - * Typicaly transfer type for copy/paste transfers. + * Typically transfer type for copy/paste transfers. */ TEXT: 'text/plain' }; diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 9b4aa8ad96..02a6055c3a 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -467,28 +467,6 @@ export function getComputedStyle(el: HTMLElement): CSSStyleDeclaration { return document.defaultView!.getComputedStyle(el, null); } -// Adapted from WinJS -// Converts a CSS positioning string for the specified element to pixels. -const convertToPixels: (element: HTMLElement, value: string) => number = (function () { - return function (element: HTMLElement, value: string): number { - return parseFloat(value) || 0; - }; -})(); - -function getDimension(element: HTMLElement, cssPropertyName: string, jsPropertyName: string): number { - let computedStyle: CSSStyleDeclaration = getComputedStyle(element); - let value = '0'; - if (computedStyle) { - if (computedStyle.getPropertyValue) { - value = computedStyle.getPropertyValue(cssPropertyName); - } else { - // IE8 - value = (computedStyle).getAttribute(jsPropertyName); - } - } - return convertToPixels(element, value); -} - export function getClientArea(element: HTMLElement): Dimension { // Try with DOM clientWidth / clientHeight @@ -514,48 +492,66 @@ export function getClientArea(element: HTMLElement): Dimension { throw new Error('Unable to figure out browser width and height'); } -const sizeUtils = { +class SizeUtils { + // Adapted from WinJS + // Converts a CSS positioning string for the specified element to pixels. + private static convertToPixels(element: HTMLElement, value: string): number { + return parseFloat(value) || 0; + } - getBorderLeftWidth: function (element: HTMLElement): number { - return getDimension(element, 'border-left-width', 'borderLeftWidth'); - }, - getBorderRightWidth: function (element: HTMLElement): number { - return getDimension(element, 'border-right-width', 'borderRightWidth'); - }, - getBorderTopWidth: function (element: HTMLElement): number { - return getDimension(element, 'border-top-width', 'borderTopWidth'); - }, - getBorderBottomWidth: function (element: HTMLElement): number { - return getDimension(element, 'border-bottom-width', 'borderBottomWidth'); - }, + private static getDimension(element: HTMLElement, cssPropertyName: string, jsPropertyName: string): number { + let computedStyle: CSSStyleDeclaration = getComputedStyle(element); + let value = '0'; + if (computedStyle) { + if (computedStyle.getPropertyValue) { + value = computedStyle.getPropertyValue(cssPropertyName); + } else { + // IE8 + value = (computedStyle).getAttribute(jsPropertyName); + } + } + return SizeUtils.convertToPixels(element, value); + } - getPaddingLeft: function (element: HTMLElement): number { - return getDimension(element, 'padding-left', 'paddingLeft'); - }, - getPaddingRight: function (element: HTMLElement): number { - return getDimension(element, 'padding-right', 'paddingRight'); - }, - getPaddingTop: function (element: HTMLElement): number { - return getDimension(element, 'padding-top', 'paddingTop'); - }, - getPaddingBottom: function (element: HTMLElement): number { - return getDimension(element, 'padding-bottom', 'paddingBottom'); - }, + static getBorderLeftWidth(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'border-left-width', 'borderLeftWidth'); + } + static getBorderRightWidth(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'border-right-width', 'borderRightWidth'); + } + static getBorderTopWidth(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'border-top-width', 'borderTopWidth'); + } + static getBorderBottomWidth(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'border-bottom-width', 'borderBottomWidth'); + } - getMarginLeft: function (element: HTMLElement): number { - return getDimension(element, 'margin-left', 'marginLeft'); - }, - getMarginTop: function (element: HTMLElement): number { - return getDimension(element, 'margin-top', 'marginTop'); - }, - getMarginRight: function (element: HTMLElement): number { - return getDimension(element, 'margin-right', 'marginRight'); - }, - getMarginBottom: function (element: HTMLElement): number { - return getDimension(element, 'margin-bottom', 'marginBottom'); - }, - __commaSentinel: false -}; + static getPaddingLeft(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'padding-left', 'paddingLeft'); + } + static getPaddingRight(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'padding-right', 'paddingRight'); + } + static getPaddingTop(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'padding-top', 'paddingTop'); + } + static getPaddingBottom(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'padding-bottom', 'paddingBottom'); + } + + static getMarginLeft(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'margin-left', 'marginLeft'); + } + static getMarginTop(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'margin-top', 'marginTop'); + } + static getMarginRight(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'margin-right', 'marginRight'); + } + static getMarginBottom(element: HTMLElement): number { + return SizeUtils.getDimension(element, 'margin-bottom', 'marginBottom'); + } +} // ---------------------------------------------------------------------------------------- // Position & Dimension @@ -594,8 +590,8 @@ export function getTopLeftOffset(element: HTMLElement): { left: number; top: num } if (element === offsetParent) { - left += sizeUtils.getBorderLeftWidth(element); - top += sizeUtils.getBorderTopWidth(element); + left += SizeUtils.getBorderLeftWidth(element); + top += SizeUtils.getBorderTopWidth(element); top += element.offsetTop; left += element.offsetLeft; offsetParent = element.offsetParent; @@ -686,33 +682,33 @@ export const StandardWindow: IStandardWindow = new class implements IStandardWin // Adapted from WinJS // Gets the width of the element, including margins. export function getTotalWidth(element: HTMLElement): number { - let margin = sizeUtils.getMarginLeft(element) + sizeUtils.getMarginRight(element); + let margin = SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element); return element.offsetWidth + margin; } export function getContentWidth(element: HTMLElement): number { - let border = sizeUtils.getBorderLeftWidth(element) + sizeUtils.getBorderRightWidth(element); - let padding = sizeUtils.getPaddingLeft(element) + sizeUtils.getPaddingRight(element); + let border = SizeUtils.getBorderLeftWidth(element) + SizeUtils.getBorderRightWidth(element); + let padding = SizeUtils.getPaddingLeft(element) + SizeUtils.getPaddingRight(element); return element.offsetWidth - border - padding; } export function getTotalScrollWidth(element: HTMLElement): number { - let margin = sizeUtils.getMarginLeft(element) + sizeUtils.getMarginRight(element); + let margin = SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element); return element.scrollWidth + margin; } // Adapted from WinJS // Gets the height of the content of the specified element. The content height does not include borders or padding. export function getContentHeight(element: HTMLElement): number { - let border = sizeUtils.getBorderTopWidth(element) + sizeUtils.getBorderBottomWidth(element); - let padding = sizeUtils.getPaddingTop(element) + sizeUtils.getPaddingBottom(element); + let border = SizeUtils.getBorderTopWidth(element) + SizeUtils.getBorderBottomWidth(element); + let padding = SizeUtils.getPaddingTop(element) + SizeUtils.getPaddingBottom(element); return element.offsetHeight - border - padding; } // Adapted from WinJS // Gets the height of the element, including its margins. export function getTotalHeight(element: HTMLElement): number { - let margin = sizeUtils.getMarginTop(element) + sizeUtils.getMarginBottom(element); + let margin = SizeUtils.getMarginTop(element) + SizeUtils.getMarginBottom(element); return element.offsetHeight + margin; } @@ -836,53 +832,53 @@ export function isHTMLElement(o: any): o is HTMLElement { export const EventType = { // Mouse - CLICK: 'click' as 'click', - DBLCLICK: 'dblclick' as 'dblclick', - MOUSE_UP: 'mouseup' as 'mouseup', - MOUSE_DOWN: 'mousedown' as 'mousedown', - MOUSE_OVER: 'mouseover' as 'mouseover', - MOUSE_MOVE: 'mousemove' as 'mousemove', - MOUSE_OUT: 'mouseout' as 'mouseout', - MOUSE_ENTER: 'mouseenter' as 'mouseenter', - MOUSE_LEAVE: 'mouseleave' as 'mouseleave', - CONTEXT_MENU: 'contextmenu' as 'contextmenu', - WHEEL: 'wheel' as 'wheel', + CLICK: 'click', + DBLCLICK: 'dblclick', + MOUSE_UP: 'mouseup', + MOUSE_DOWN: 'mousedown', + MOUSE_OVER: 'mouseover', + MOUSE_MOVE: 'mousemove', + MOUSE_OUT: 'mouseout', + MOUSE_ENTER: 'mouseenter', + MOUSE_LEAVE: 'mouseleave', + CONTEXT_MENU: 'contextmenu', + WHEEL: 'wheel', // Keyboard - KEY_DOWN: 'keydown' as 'keydown', - KEY_PRESS: 'keypress' as 'keypress', - KEY_UP: 'keyup' as 'keyup', + KEY_DOWN: 'keydown', + KEY_PRESS: 'keypress', + KEY_UP: 'keyup', // HTML Document - LOAD: 'load' as 'load', - UNLOAD: 'unload' as 'unload', - ABORT: 'abort' as 'abort', - ERROR: 'error' as 'error', - RESIZE: 'resize' as 'resize', - SCROLL: 'scroll' as 'scroll', + LOAD: 'load', + UNLOAD: 'unload', + ABORT: 'abort', + ERROR: 'error', + RESIZE: 'resize', + SCROLL: 'scroll', // Form - SELECT: 'select' as 'select', - CHANGE: 'change' as 'change', - SUBMIT: 'submit' as 'submit', - RESET: 'reset' as 'reset', - FOCUS: 'focus' as 'focus', - FOCUS_IN: 'focusin' as 'focusin', - FOCUS_OUT: 'focusout' as 'focusout', - BLUR: 'blur' as 'blur', - INPUT: 'input' as 'input', + SELECT: 'select', + CHANGE: 'change', + SUBMIT: 'submit', + RESET: 'reset', + FOCUS: 'focus', + FOCUS_IN: 'focusin', + FOCUS_OUT: 'focusout', + BLUR: 'blur', + INPUT: 'input', // Local Storage - STORAGE: 'storage' as 'storage', + STORAGE: 'storage', // Drag - DRAG_START: 'dragstart' as 'dragstart', - DRAG: 'drag' as 'drag', - DRAG_ENTER: 'dragenter' as 'dragenter', - DRAG_LEAVE: 'dragleave' as 'dragleave', - DRAG_OVER: 'dragover' as 'dragover', - DROP: 'drop' as 'drop', - DRAG_END: 'dragend' as 'dragend', + DRAG_START: 'dragstart', + DRAG: 'drag', + DRAG_ENTER: 'dragenter', + DRAG_LEAVE: 'dragleave', + DRAG_OVER: 'dragover', + DROP: 'drop', + DRAG_END: 'dragend', // Animation ANIMATION_START: browser.isWebKit ? 'webkitAnimationStart' : 'animationstart', ANIMATION_END: browser.isWebKit ? 'webkitAnimationEnd' : 'animationend', ANIMATION_ITERATION: browser.isWebKit ? 'webkitAnimationIteration' : 'animationiteration' -}; +} as const; export interface EventLike { preventDefault(): void; diff --git a/src/vs/base/browser/touch.ts b/src/vs/base/browser/touch.ts index 56586c0156..34cee89d67 100644 --- a/src/vs/base/browser/touch.ts +++ b/src/vs/base/browser/touch.ts @@ -214,10 +214,10 @@ export class Gesture extends Disposable { } } - private newGestureEvent(type: string, intialTarget?: EventTarget): GestureEvent { + private newGestureEvent(type: string, initialTarget?: EventTarget): GestureEvent { let event = (document.createEvent('CustomEvent')); event.initEvent(type, false, true); - event.initialTarget = intialTarget; + event.initialTarget = initialTarget; return event; } diff --git a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts index 57fd3a5d4a..24f6dafa8a 100644 --- a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -126,7 +126,7 @@ export class BreadcrumbsWidget { this._pendingLayout.dispose(); } if (dim) { - // only meaure + // only measure this._pendingLayout = this._updateDimensions(dim); } else { this._pendingLayout = this._updateScrollbar(); diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 8f5d835974..c909dada79 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -15,6 +15,10 @@ align-items: center; } +.monaco-workbench .dialog-modal-block.dimmed { + background: rgba(0, 0, 0, 0.3); +} + /** Dialog: Container */ .monaco-workbench .dialog-box { display: flex; @@ -56,16 +60,22 @@ display: flex; flex-grow: 1; padding: 10px 15px 20px; + align-items: center; } .monaco-workbench .dialog-box .dialog-message-row .dialog-icon { flex: 0 0 30px; + height: 30px; padding-right: 4px; padding-left: 4px; background-position: center; background-repeat: no-repeat; } +.vs .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-pending { + background-image: url('pending.svg'); +} + .vs .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-info { background-image: url('info.svg'); } @@ -78,6 +88,10 @@ background-image: url('error.svg'); } +.vs-dark .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-pending { + background-image: url('pending-dark.svg'); +} + .vs-dark .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-info, .hc-black .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-info { background-image: url('info-inverse.svg'); @@ -93,6 +107,14 @@ background-image: url('error-inverse.svg'); } +.hc-black .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-pending { + background-image: url('pending-hc.svg'); +} + +.monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-pending { + background-size: 30px; +} + /** Dialog: Message Container */ .monaco-workbench .dialog-box .dialog-message-row .dialog-message-container { display: flex; diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 0ee6b1f655..7172974334 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -19,7 +19,7 @@ import { mnemonicButtonLabel } from 'vs/base/common/labels'; export interface IDialogOptions { cancelId?: number; detail?: string; - type?: 'none' | 'info' | 'error' | 'question' | 'warning'; + type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending'; } export interface IDialogStyles extends IButtonStyles { @@ -33,6 +33,7 @@ export class Dialog extends Disposable { private element: HTMLElement | undefined; private modal: HTMLElement | undefined; private buttonsContainer: HTMLElement | undefined; + private messageDetailElement: HTMLElement | undefined; private iconElement: HTMLElement | undefined; private toolbarContainer: HTMLElement | undefined; private buttonGroup: ButtonGroup | undefined; @@ -40,7 +41,7 @@ export class Dialog extends Disposable { constructor(private container: HTMLElement, private message: string, private buttons: string[], private options: IDialogOptions) { super(); - this.modal = this.container.appendChild($('.dialog-modal-block')); + this.modal = this.container.appendChild($(`.dialog-modal-block${options.type === 'pending' ? '.dimmed' : ''}`)); this.element = this.modal.appendChild($('.dialog-box')); hide(this.element); @@ -56,13 +57,19 @@ export class Dialog extends Disposable { messageElement.innerText = this.message; } - const messageDetailElement = messageContainer.appendChild($('.dialog-message-detail')); - messageDetailElement.innerText = this.options.detail ? this.options.detail : message; + this.messageDetailElement = messageContainer.appendChild($('.dialog-message-detail')); + this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message; const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row')); this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar')); } + updateMessage(message: string): void { + if (this.messageDetailElement) { + this.messageDetailElement.innerText = message; + } + } + async show(): Promise { return new Promise((resolve) => { if (!this.element || !this.buttonsContainer || !this.iconElement || !this.toolbarContainer) { @@ -129,6 +136,9 @@ export class Dialog extends Disposable { case 'warning': addClass(this.iconElement, 'icon-warning'); break; + case 'pending': + addClass(this.iconElement, 'icon-pending'); + break; case 'none': case 'info': case 'question': diff --git a/src/vs/base/browser/ui/dialog/pending-dark.svg b/src/vs/base/browser/ui/dialog/pending-dark.svg new file mode 100644 index 0000000000..bbf6e8d84c --- /dev/null +++ b/src/vs/base/browser/ui/dialog/pending-dark.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/src/vs/base/browser/ui/dialog/pending-hc.svg b/src/vs/base/browser/ui/dialog/pending-hc.svg new file mode 100644 index 0000000000..4d0b2a10c7 --- /dev/null +++ b/src/vs/base/browser/ui/dialog/pending-hc.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/src/vs/base/browser/ui/dialog/pending.svg b/src/vs/base/browser/ui/dialog/pending.svg new file mode 100644 index 0000000000..596cfdd60c --- /dev/null +++ b/src/vs/base/browser/ui/dialog/pending.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 5de959993c..95495e8e46 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -502,7 +502,7 @@ function isMouseRightClick(event: UIEvent): boolean { return event instanceof MouseEvent && event.button === 2; } -const DefaultMultipleSelectionContoller = { +const DefaultMultipleSelectionController = { isSelectionSingleChangeEvent, isSelectionRangeChangeEvent }; @@ -529,7 +529,7 @@ export class MouseController implements IDisposable { this.multipleSelectionSupport = !(list.options.multipleSelectionSupport === false); if (this.multipleSelectionSupport) { - this.multipleSelectionController = list.options.multipleSelectionController || DefaultMultipleSelectionContoller; + this.multipleSelectionController = list.options.multipleSelectionController || DefaultMultipleSelectionController; } this.openController = list.options.openController || DefaultOpenController; @@ -909,7 +909,7 @@ function getContiguousRangeContaining(range: number[], value: number): number[] /** * Given two sorted collections of numbers, returns the intersection - * betweem them (OR). + * between them (OR). */ function disjunction(one: number[], other: number[]): number[] { const result: number[] = []; diff --git a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css index d24cc0b525..d583733048 100644 --- a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css +++ b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css @@ -1,7 +1,7 @@ @font-face { font-family: "octicons"; - src: url("./octicons.ttf?4cd2299755e93a2430ba5703f4476584") format("truetype"), -url("./octicons.svg?4cd2299755e93a2430ba5703f4476584#octicons") format("svg"); + src: url("./octicons.ttf?91284a5a76ea88faeb754359b7f7cd03") format("truetype"), +url("./octicons.svg?91284a5a76ea88faeb754359b7f7cd03#octicons") format("svg"); } .octicon, .mega-octicon { @@ -240,4 +240,5 @@ url("./octicons.svg?4cd2299755e93a2430ba5703f4476584#octicons") format("svg"); .octicon-fold-up:before { content: "\f105" } .octicon-github-action:before { content: "\f106" } .octicon-play:before { content: "\f107" } -.octicon-request-changes:before { content: "\f108" } +.octicon-remote:before { content: "\f108" } +.octicon-request-changes:before { content: "\f109" } diff --git a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg index 4581795255..af83960978 100644 --- a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg +++ b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg @@ -421,6 +421,9 @@ + @@ -446,7 +449,7 @@ unicode="" horiz-adv-x="1000" d=" M437.5 320H562.5V195H437.5z M437.5 632.5H562.5V382.5H437.5z M937.5 757.5H62.5C25 757.5 0 732.5 0 695V132.5C0 95 25 70 62.5 70H187.5V-180L437.5 70H937.5C975 70 1000 95 1000 132.5V695C1000 732.5 975 757.5 937.5 757.5zM937.5 132.5H406.25L250 -23.75V132.5H62.5V695H937.5z" /> + horiz-adv-x="1000" d=" M998.75 330.625L938.125 702.5C927.5 788.75 820.625 820 750 820H355.625C343.125 820 331.8750000000001 816.875 322.5 811.25L232.5 757.5H125C58.75 757.5 0 698.75 0 632.5V382.5C0 316.25 58.75 256.25 125 257.5H250C306.875 257.5 336.875 229.375 399.3750000000001 160.625C456.2500000000001 98.125 454.3750000000001 48.1249999999999 438.7500000000001 -43.75C433.75 -75 442.5 -106.25 465 -132.5C489.375 -161.875 526.25 -180 562.5 -180C676.875 -180 750 51.875 750 133.125L748.75 194.375H876.25C948.75 194.375 998.1249999999998 244.3750000000001 1000 317.5000000000001C1000 324.3750000000001 998.75 330.6250000000001 998.75 330.6250000000001zM875.625 256.25H751.25C707.5 256.25 686.875 238.7500000000001 686.875 195.625L688.75 131.25C688.75 51.875 615.625 -118.75 563.75 -118.75C532.5 -118.75 496.2499999999999 -87.5 501.25 -56.25C516.875 42.5 522.5 117.5 445.625 202.5000000000001C381.875 273.125 335 320 250 320V695L354.375 757.5H750C795.625 757.5 871.875 738.125 875 695L876.25 693.75L938.75 318.75C936.875 278.75 914.9999999999998 256.25 876.25 256.25H875.625z" /> + horiz-adv-x="1000" d=" M998.75 309.375L938.125 -62.5000000000001C927.5 -148.75 820.625 -180 750 -180H355.625C343.125 -180 331.8750000000001 -176.875 322.5 -171.25L232.5 -117.5H125C58.75 -117.5 0 -58.75 0 7.5V257.5C0 323.7500000000001 58.75 383.75 125 382.5H250C306.875 382.5 336.875 410.625 399.3750000000001 479.375C456.2500000000001 541.875 454.3750000000001 591.875 438.7500000000001 683.75C433.75 715 442.5 746.25 465 772.5C489.375 801.875 526.25 820 562.5 820C676.875 820 750 588.125 750 506.875L748.75 445.625H876.25C948.75 445.625 998.1249999999998 395.625 1000 322.5C1000 315.625 998.75 309.375 998.75 309.375zM875.625 383.75H751.25C707.5 383.75 686.875 401.25 686.875 444.375L688.75 508.75C688.75 588.125 615.625 758.75 563.75 758.75C532.5 758.75 496.2499999999999 727.5 501.25 696.25C516.875 597.5 522.5 522.5 445.625 437.5C381.875 366.875 335 320 250 320V-55L354.375 -117.5H750C795.625 -117.5 871.875 -98.125 875 -55L876.25 -53.75L938.75 321.25C936.875 361.25 914.9999999999998 383.75 876.25 383.75H875.625z" /> @@ -546,10 +549,10 @@ horiz-adv-x="1000" d=" M750 318.75C750 250.625 721.875 188.125 676.875 141.875L635 183.75C669.375 218.75 690.625 265.625 690.625 318.75C690.625 371.875 669.375 419.3750000000001 635 453.7500000000001L676.875 495.6250000000001A249.375 249.375 0 0 0 750 318.75zM482.5 677.5L250 445H125C90.625 445 62.5 416.875 62.5 382.5V257.5C62.5 223.125 90.625 195 125 195H250L482.5000000000001 -37.5C511.8750000000001 -66.8750000000001 562.5 -46.2500000000001 562.5 -4.3750000000001V644.375C562.5 686.25 511.8749999999999 706.875 482.5 677.5zM853.75 672.5L811.875 630.625A437.24999999999994 437.24999999999994 0 0 0 940.625 319.375C940.625 198.125 891.8750000000001 88.125 811.875 8.125L853.75 -33.75A498.31249999999994 498.31249999999994 0 0 1 1000 320C1000 458.7500000000001 944.375 583.75 853.75 673.75V672.5zM765.625 584.375L722.5 542.5A315.62499999999994 315.62499999999994 0 0 0 815.0000000000001 318.75C815.0000000000001 231.875 780 152.5 722.5 96.25L765.625 54.375A373.18750000000006 373.18750000000006 0 0 1 875 318.75C875 421.8750000000001 833.125 516.25 765.625 584.375z" /> + horiz-adv-x="1000" d=" M979.375 378.375L911.875 462.125A93.75 93.75 0 0 0 892.5625 510.2500000000001L880.6875000000001 616.375A94.375 94.375 0 0 1 797.625 699.5L691.4375 711.375C672.6875 713.25 656.4375 721.3125 642.6875000000001 731.9375L559.0625 799.375A94 94 0 0 1 441.6875 799.375L357.9375 731.875A93.75 93.75 0 0 0 309.8125000000001 712.5L203.6875000000001 700.625C159.9375000000001 695.625 125.5625000000001 661.25 120.5625000000001 617.5625L108.6875 511.375C106.8125 492.625 98.75 476.3750000000001 88.1250000000001 462.625L20.625 379.0625A94 94 0 0 1 20.625 261.6875L88.125 177.9375C98.75 164.1875 105.625 147.9375 107.5 129.8125L119.375 23.6875C124.375 -20.0624999999999 158.75 -54.4375 202.4375 -59.4375L308.625 -71.3125C327.375 -73.1874999999999 343.625 -81.25 357.375 -91.875L441.0625 -159.375C475.4375 -186.8124999999999 524.1250000000001 -186.8124999999999 558.4375 -159.375L642.1875 -91.875C655.9375000000001 -81.25 672.1875 -74.375 690.3125 -72.5L796.4375 -60.625C840.1875 -55.625 874.5625 -21.2499999999999 879.5625 22.4375L891.4375 128.625C893.3125 147.3750000000001 901.375 163.6250000000001 912 177.375L979.5 261.0625A94 94 0 0 1 979.5 378.4375000000001zM562.5 101.25C562.5 83.75 548.75 70 531.25 70H468.75C451.875 70 437.5 83.75 437.5 101.25V163.75C437.5 181.25 451.875 195 468.75 195H531.25C548.75 195 562.5 181.25 562.5 163.75V101.25zM660 406.875C656.25 396.25 649.375 386.25 641.25 377.5C633.1249999999999 367.5 632.5 365.625 620.625 353.75C610.625 343.125 601.25 335 588.125 325.625C581.25 320 575.625 313.75 570.625 308.75C565.625 303.75 561.875 298.125 558.7500000000001 291.875C555.625 285.6250000000001 553.7500000000001 280.0000000000001 551.8750000000001 273.125C550.0000000000001 266.25 550.0000000000001 265 550.0000000000001 257.5H445.625C445.625 271.25 445.625 276.875 447.5 287.5C449.375 299.375 452.5 310 456.25 320C459.9999999999999 328.75 464.9999999999999 337.5 471.875 346.25C478.75 354.375 486.25 361.875 497.5 370C514.375 381.875 520 388.75 527.5 402.5C534.9999999999999 416.25 539.9999999999999 426.25 539.9999999999999 439.375C539.9999999999999 456.25 536.2499999999999 467.5 527.5 475.625C519.3749999999999 483.75 508.1249999999999 487.5000000000001 491.2499999999999 487.5000000000001C485.625 487.5000000000001 479.3749999999999 486.2500000000001 472.5 484.3750000000001C465.6249999999999 482.5000000000001 461.875 478.7500000000001 456.875 474.3750000000001C451.8749999999999 470 448.125 467.5 444.375 461.8750000000001A25.624999999999996 25.624999999999996 0 0 1 438.75 444.375H313.75C313.75 468.125 321.875 479.3750000000001 330.625 496.25C340.625 513.125 353.125 527.5 368.75 538.125C384.375 548.75 403.125 556.875 423.75 561.875C444.375 566.875 467.4999999999999 570 491.8749999999999 570C519.3749999999999 570 543.75 566.875 565 561.875C586.2499999999999 556.25 604.375 548.125 620 537.5C634.375 526.875 645.625 513.75 654.375 498.1250000000001C662.5000000000001 482.5000000000001 666.25 463.7500000000001 666.25 443.1250000000001C666.25 429.3750000000001 666.25 416.8750000000001 661.25 406.2500000000001L660 406.8750000000001z" /> + horiz-adv-x="1000" d=" M979.375 378.375L911.875 462.125A93.75 93.75 0 0 0 892.5625 510.2500000000001L880.6875000000001 616.375A94.375 94.375 0 0 1 797.625 699.5L691.4375 711.375C672.6875 713.25 656.4375 721.3125 642.6875000000001 731.9375L559.0625 799.375A94 94 0 0 1 441.6875 799.375L357.9375 731.875A93.75 93.75 0 0 0 309.8125000000001 712.5L203.6875000000001 700.625C159.9375000000001 695.625 125.5625000000001 661.25 120.5625000000001 617.5625L108.6875 511.375C106.8125 492.625 98.75 476.3750000000001 88.1250000000001 462.625L20.625 379.0625A94 94 0 0 1 20.625 261.6875L88.125 177.9375C98.75 164.1875 105.625 147.9375 107.5 129.8125L119.375 23.6875C124.375 -20.0624999999999 158.75 -54.4375 202.4375 -59.4375L308.625 -71.3125C327.375 -73.1874999999999 343.625 -81.25 357.375 -91.875L441.0625 -159.375C475.4375 -186.8124999999999 524.1250000000001 -186.8124999999999 558.4375 -159.375L642.1875 -91.875C655.9375000000001 -81.25 672.1875 -74.375 690.3125 -72.5L796.4375 -60.625C840.1875 -55.625 874.5625 -21.2499999999999 879.5625 22.4375L891.4375 128.625C893.3125 147.3750000000001 901.375 163.6250000000001 912 177.375L979.5 261.0625A94 94 0 0 1 979.5 378.4375000000001zM406.25 69.375L187.5 288.125L281.25 381.875L406.25 256.875L718.75 569.375L812.5 472.5L406.25 69.375z" /> diff --git a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.ttf b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.ttf index 12b561cd47..8c29d33830 100644 Binary files a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.ttf and b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.ttf differ diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 7d90894682..3bdf4ba502 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -523,7 +523,7 @@ export class DomScrollableElement extends ScrollableElement { } public scanDomNode(): void { - // widh, scrollLeft, scrollWidth, height, scrollTop, scrollHeight + // width, scrollLeft, scrollWidth, height, scrollTop, scrollHeight this.setScrollDimensions({ width: this._element.clientWidth, scrollWidth: this._element.scrollWidth, diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 1987721713..d67539192a 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -33,7 +33,7 @@ export const enum TreeVisibility { export interface ITreeFilterDataResult { /** - * Whether the node should be visibile. + * Whether the node should be visible. */ visibility: boolean | TreeVisibility; diff --git a/src/vs/base/common/jsonEdit.ts b/src/vs/base/common/jsonEdit.ts index 87a2fe2af7..dc8f902313 100644 --- a/src/vs/base/common/jsonEdit.ts +++ b/src/vs/base/common/jsonEdit.ts @@ -5,6 +5,7 @@ import { ParseError, Node, JSONPath, Segment, parseTree, findNodeAtLocation } from './json'; import { Edit, format, isEOL, FormattingOptions } from './jsonFormatter'; +import { mergeSort } from 'vs/base/common/arrays'; export function removeProperty(text: string, path: JSONPath, formattingOptions: FormattingOptions): Edit[] { @@ -158,6 +159,27 @@ export function applyEdit(text: string, edit: Edit): string { return text.substring(0, edit.offset) + edit.content + text.substring(edit.offset + edit.length); } +export function applyEdits(text: string, edits: Edit[]): string { + let sortedEdits = mergeSort(edits, (a, b) => { + const diff = a.offset - b.offset; + if (diff === 0) { + return a.length - b.length; + } + return diff; + }); + let lastModifiedOffset = text.length; + for (let i = sortedEdits.length - 1; i >= 0; i--) { + let e = sortedEdits[i]; + if (e.offset + e.length <= lastModifiedOffset) { + text = applyEdit(text, e); + } else { + throw new Error('Overlapping edit'); + } + lastModifiedOffset = e.offset; + } + return text; +} + export function isWS(text: string, offset: number) { return '\r\n \t'.indexOf(text.charAt(offset)) !== -1; } \ No newline at end of file diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index bea1c19f1f..f2501bf249 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -226,7 +226,7 @@ export class URI implements UriComponents { // ---- modify to new ------------------------- - public with(change: { scheme?: string; authority?: string | null; path?: string | null; query?: string | null; fragment?: string | null }): URI { + with(change: { scheme?: string; authority?: string | null; path?: string | null; query?: string | null; fragment?: string | null }): URI { if (!change) { return this; @@ -279,7 +279,7 @@ export class URI implements UriComponents { * * @param value A string which represents an URI (see `URI#toString`). */ - public static parse(value: string, _strict: boolean = false): URI { + static parse(value: string, _strict: boolean = false): URI { const match = _regexp.exec(value); if (!match) { return new _URI(_empty, _empty, _empty, _empty, _empty); @@ -315,7 +315,7 @@ export class URI implements UriComponents { * * @param path A file system path (see `URI#fsPath`) */ - public static file(path: string): URI { + static file(path: string): URI { let authority = _empty; @@ -342,7 +342,7 @@ export class URI implements UriComponents { return new _URI('file', authority, path, _empty, _empty); } - public static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { + static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { return new _URI( components.scheme, components.authority, @@ -365,17 +365,22 @@ export class URI implements UriComponents { * * @param skipEncoding Do not encode the result, default is `false` */ - public toString(skipEncoding: boolean = false): string { + toString(skipEncoding: boolean = false): string { return _asFormatted(this, skipEncoding); } - public toJSON(): object { + toJSON(): UriComponents { return this; } - static revive(data: UriComponents | any): URI { + static revive(data: UriComponents | URI): URI; + static revive(data: UriComponents | URI | undefined): URI | undefined; + static revive(data: UriComponents | URI | null): URI | null; + static revive(data: UriComponents | URI | undefined | null): URI | undefined | null; + static revive(data: UriComponents | URI | undefined | null): URI | undefined | null { if (!data) { - return data; + // {{SQL CARBON EDIT}} @todo chlafren change back to data when we enable strict null checks + return undefined; } else if (data instanceof URI) { return data; } else { @@ -415,7 +420,7 @@ class _URI extends URI { return this._fsPath; } - public toString(skipEncoding: boolean = false): string { + toString(skipEncoding: boolean = false): string { if (!skipEncoding) { if (!this._formatted) { this._formatted = _asFormatted(this, false); @@ -427,7 +432,7 @@ class _URI extends URI { } } - toJSON(): object { + toJSON(): UriComponents { const res = { $mid: 1 }; diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 2d38d1b3e9..f6524edb7f 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -14,6 +14,10 @@ export const UTF8_with_bom = 'utf8bom'; export const UTF16be = 'utf16be'; export const UTF16le = 'utf16le'; +export const UTF16be_BOM = [0xFE, 0xFF]; +export const UTF16le_BOM = [0xFF, 0xFE]; +export const UTF8_BOM = [0xEF, 0xBB, 0xBF]; + export interface IDecodeStreamOptions { guessEncoding?: boolean; minBytesRequiredForDetection?: number; @@ -150,12 +154,12 @@ export function detectEncodingByBOMFromBuffer(buffer: Buffer | null, bytesRead: const b1 = buffer.readUInt8(1); // UTF-16 BE - if (b0 === 0xFE && b1 === 0xFF) { + if (b0 === UTF16be_BOM[0] && b1 === UTF16be_BOM[1]) { return UTF16be; } // UTF-16 LE - if (b0 === 0xFF && b1 === 0xFE) { + if (b0 === UTF16le_BOM[0] && b1 === UTF16le_BOM[1]) { return UTF16le; } @@ -166,7 +170,7 @@ export function detectEncodingByBOMFromBuffer(buffer: Buffer | null, bytesRead: const b2 = buffer.readUInt8(2); // UTF-8 - if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) { + if (b0 === UTF8_BOM[0] && b1 === UTF8_BOM[1] && b2 === UTF8_BOM[2]) { return UTF8; } @@ -178,7 +182,7 @@ export function detectEncodingByBOMFromBuffer(buffer: Buffer | null, bytesRead: * If no BOM is detected, null will be passed to callback. */ export function detectEncodingByBOM(file: string): Promise { - return stream.readExactlyByFile(file, 3).then(({ buffer, bytesRead }) => detectEncodingByBOMFromBuffer(buffer, bytesRead)); + return stream.readExactlyByFile(file, 3).then(({ buffer, bytesRead }) => detectEncodingByBOMFromBuffer(buffer, bytesRead), error => null); } const MINIMUM_THRESHOLD = 0.2; diff --git a/src/vs/base/node/ps.sh b/src/vs/base/node/ps.sh new file mode 100755 index 0000000000..4fe31039c5 --- /dev/null +++ b/src/vs/base/node/ps.sh @@ -0,0 +1,39 @@ +#!/bin/sh +PAGESIZE=`getconf PAGESIZE`; +TOTAL_MEMORY=`cat /proc/meminfo | head -n 1 | awk '{print $2}'`; + +# Mimic the output of ps -ax -o pid=,ppid=,pcpu=,pmem=,command= +# Read all numeric subdirectories in /proc +for pid in `cd /proc && ls -d [0-9]*` + do { + if [ -e /proc/$pid/stat ] + then + echo $pid; + + # ppid is the word at index 4 in the stat file for the process + awk '{print $4}' /proc/$pid/stat; + + # pcpu - calculation will be done later, this is a placeholder value + echo "0.0" + + # pmem - ratio of the process's working set size to total memory. + # use the page size to convert to bytes, total memory is in KB + # multiplied by 100 to get percentage, extra 10 to be able to move + # the decimal over by one place + RESIDENT_SET_SIZE=`awk '{print $24}' /proc/$pid/stat`; + PERCENT_MEMORY=$(((1000 * $PAGESIZE * $RESIDENT_SET_SIZE) / ($TOTAL_MEMORY * 1024))); + if [ $PERCENT_MEMORY -lt 10 ] + then + # replace the last character with 0. the last character + echo $PERCENT_MEMORY | sed 's/.$/0.&/'; #pmem + else + # insert . before the last character + echo $PERCENT_MEMORY | sed 's/.$/.&/'; + fi + + # cmdline + xargs -0 < /proc/$pid/cmdline; + fi + } | tr "\n" "\t"; # Replace newlines with tab so that all info for a process is shown on one line + echo; # But add new lines between processes +done diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts index 934cb1cc90..f739096714 100644 --- a/src/vs/base/node/ps.ts +++ b/src/vs/base/node/ps.ts @@ -14,6 +14,7 @@ export function listProcesses(rootPid: number): Promise { let rootItem: ProcessItem | undefined; const map = new Map(); + function addToTree(pid: number, ppid: number, cmd: string, load: number, mem: number) { const parent = map.get(ppid); @@ -162,64 +163,87 @@ export function listProcesses(rootPid: number): Promise { }, windowsProcessTree.ProcessDataFlag.CommandLine | windowsProcessTree.ProcessDataFlag.Memory); }); } else { // OS X & Linux - - const CMD = '/bin/ps -ax -o pid=,ppid=,pcpu=,pmem=,command='; - const PID_CMD = /^\s*([0-9]+)\s+([0-9]+)\s+([0-9]+\.[0-9]+)\s+([0-9]+\.[0-9]+)\s+(.+)$/; - - // Set numeric locale to ensure '.' is used as the decimal separator - exec(CMD, { maxBuffer: 1000 * 1024, env: { LC_NUMERIC: 'en_US.UTF-8' } }, (err, stdout, stderr) => { - - if (err || stderr) { - reject(err || new Error(stderr.toString())); - } else { - - const lines = stdout.toString().split('\n'); - for (const line of lines) { - const matches = PID_CMD.exec(line.trim()); - if (matches && matches.length === 6) { - addToTree(parseInt(matches[1]), parseInt(matches[2]), matches[5], parseFloat(matches[3]), parseFloat(matches[4])); + function calculateLinuxCpuUsage() { + // Flatten rootItem to get a list of all VSCode processes + let processes = [rootItem]; + const pids: number[] = []; + while (processes.length) { + const process = processes.shift(); + if (process) { + pids.push(process.pid); + if (process.children) { + processes = processes.concat(process.children); } } + } - if (process.platform === 'linux') { - // Flatten rootItem to get a list of all VSCode processes - let processes = [rootItem]; - const pids: number[] = []; - while (processes.length) { - const process = processes.shift(); - if (process) { - pids.push(process.pid); - if (process.children) { - processes = processes.concat(process.children); - } - } + // The cpu usage value reported on Linux is the average over the process lifetime, + // recalculate the usage over a one second interval + // JSON.stringify is needed to escape spaces, https://github.com/nodejs/node/issues/6803 + let cmd = JSON.stringify(getPathFromAmdModule(require, 'vs/base/node/cpuUsage.sh')); + cmd += ' ' + pids.join(' '); + + exec(cmd, {}, (err, stdout, stderr) => { + if (err || stderr) { + reject(err || new Error(stderr.toString())); + } else { + const cpuUsage = stdout.toString().split('\n'); + for (let i = 0; i < pids.length; i++) { + const processInfo = map.get(pids[i])!; + processInfo.load = parseFloat(cpuUsage[i]); } - // The cpu usage value reported on Linux is the average over the process lifetime, - // recalculate the usage over a one second interval - // JSON.stringify is needed to escape spaces, https://github.com/nodejs/node/issues/6803 - let cmd = JSON.stringify(getPathFromAmdModule(require, 'vs/base/node/cpuUsage.sh')); - cmd += ' ' + pids.join(' '); + resolve(rootItem); + } + }); + } + exec('which ps', {}, (err, stdout, stderr) => { + if (err || stderr) { + if (process.platform !== 'linux') { + reject(err || new Error(stderr.toString())); + } else { + const cmd = JSON.stringify(getPathFromAmdModule(require, 'vs/base/node/ps.sh')); exec(cmd, {}, (err, stdout, stderr) => { if (err || stderr) { reject(err || new Error(stderr.toString())); } else { - const cpuUsage = stdout.toString().split('\n'); - for (let i = 0; i < pids.length; i++) { - const processInfo = map.get(pids[i])!; - processInfo.load = parseFloat(cpuUsage[i]); - } - - resolve(rootItem); + parsePsOutput(stdout, addToTree); + calculateLinuxCpuUsage(); } }); - } else { - resolve(rootItem); } + } else { + const ps = stdout.toString().trim(); + const args = '-ax -o pid=,ppid=,pcpu=,pmem=,command='; + // Set numeric locale to ensure '.' is used as the decimal separator + exec(`${ps} ${args}`, { maxBuffer: 1000 * 1024, env: { LC_NUMERIC: 'en_US.UTF-8' } }, (err, stdout, stderr) => { + if (err || stderr) { + reject(err || new Error(stderr.toString())); + } else { + parsePsOutput(stdout, addToTree); + + if (process.platform === 'linux') { + calculateLinuxCpuUsage(); + } else { + resolve(rootItem); + } + } + }); } }); } }); } + +function parsePsOutput(stdout: string, addToTree: (pid: number, ppid: number, cmd: string, load: number, mem: number) => void): void { + const PID_CMD = /^\s*([0-9]+)\s+([0-9]+)\s+([0-9]+\.[0-9]+)\s+([0-9]+\.[0-9]+)\s+(.+)$/; + const lines = stdout.toString().split('\n'); + for (const line of lines) { + const matches = PID_CMD.exec(line.trim()); + if (matches && matches.length === 6) { + addToTree(parseInt(matches[1]), parseInt(matches[2]), matches[5], parseFloat(matches[3]), parseFloat(matches[4])); + } + } +} \ No newline at end of file diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index fa3731b241..4e9f685ecd 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { isWindows } from 'vs/base/common/platform'; @@ -441,7 +441,7 @@ suite('URI', () => { // let c = 100000; // while (c-- > 0) { for (let value of values) { - let data = value.toJSON(); + let data = value.toJSON() as UriComponents; let clone = URI.revive(data); assert.equal(clone.scheme, value.scheme); diff --git a/src/vs/base/test/node/encoding/encoding.test.ts b/src/vs/base/test/node/encoding/encoding.test.ts index b8187fe5ab..aa1c6b4bb7 100644 --- a/src/vs/base/test/node/encoding/encoding.test.ts +++ b/src/vs/base/test/node/encoding/encoding.test.ts @@ -11,6 +11,14 @@ import { Readable } from 'stream'; import { getPathFromAmdModule } from 'vs/base/common/amd'; suite('Encoding', () => { + + test('detectBOM does not return error for non existing file', async () => { + const file = getPathFromAmdModule(require, './fixtures/not-exist.css'); + + const detectedEncoding = await encoding.detectEncodingByBOM(file); + assert.equal(detectedEncoding, null); + }); + test('detectBOM UTF-8', async () => { const file = getPathFromAmdModule(require, './fixtures/some_utf8.css'); diff --git a/src/vs/base/test/node/id.test.ts b/src/vs/base/test/node/id.test.ts index ee299dd01a..6bc2b1896f 100644 --- a/src/vs/base/test/node/id.test.ts +++ b/src/vs/base/test/node/id.test.ts @@ -8,7 +8,8 @@ import { getMachineId } from 'vs/base/node/id'; suite('ID', () => { - test('getMachineId', () => { + test('getMachineId', function () { + this.timeout(20000); return getMachineId().then(id => { assert.ok(id); }); diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 8d19680205..9beb31ab1a 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -60,6 +60,7 @@ export function startup(configuration: IssueReporterConfiguration) { const issueReporter = new IssueReporter(configuration); issueReporter.render(); document.body.style.display = 'block'; + issueReporter.setInitialFocus(); } export class IssueReporter extends Disposable { @@ -88,6 +89,8 @@ export class IssueReporter extends Disposable { os: `${os.type()} ${os.arch()} ${os.release()}${isSnap ? ' snap' : ''}` }, extensionsDisabled: !!this.environmentService.disableExtensions, + fileOnExtension: configuration.data.extensionId ? true : undefined, + selectedExtension: configuration.data.extensionId ? configuration.data.enabledExtensions.filter(extension => extension.id === configuration.data.extensionId)[0] : undefined }); const issueReporterElement = this.getElementById('issue-reporter'); @@ -140,6 +143,21 @@ export class IssueReporter extends Disposable { this.renderBlocks(); } + setInitialFocus() { + const { fileOnExtension } = this.issueReporterModel.getData(); + if (fileOnExtension) { + const issueTitle = document.getElementById('issue-title'); + if (issueTitle) { + issueTitle.focus(); + } + } else { + const issueType = document.getElementById('issue-type'); + if (issueType) { + issueType.focus(); + } + } + } + private applyZoom(zoomLevel: number) { webFrame.setZoomLevel(zoomLevel); browser.setZoomFactor(webFrame.getZoomFactor()); @@ -699,9 +717,13 @@ export class IssueReporter extends Disposable { private setSourceOptions(): void { const sourceSelect = this.getElementById('issue-source')! as HTMLSelectElement; - const selected = sourceSelect.selectedIndex; + const { issueType, fileOnExtension } = this.issueReporterModel.getData(); + let selected = sourceSelect.selectedIndex; + if (selected === -1 && fileOnExtension !== undefined) { + selected = fileOnExtension ? 2 : 1; + } + sourceSelect.innerHTML = ''; - const { issueType } = this.issueReporterModel.getData(); if (issueType === IssueType.FeatureRequest) { sourceSelect.append(...[ this.makeOption('', localize('selectSource', "Select source"), true), @@ -961,10 +983,15 @@ export class IssueReporter extends Disposable { return 0; }); - const makeOption = (extension: IOption) => ``; + const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData) => { + const selected = selectedExtension && extension.id === selectedExtension.id; + return ``; + }; + const extensionsSelector = this.getElementById('extension-selector'); if (extensionsSelector) { - extensionsSelector.innerHTML = '' + extensionOptions.map(makeOption).join('\n'); + const { selectedExtension } = this.issueReporterModel.getData(); + extensionsSelector.innerHTML = '' + extensionOptions.map(extension => makeOption(extension, selectedExtension)).join('\n'); this.addEventListener('extension-selector', 'change', (e: Event) => { const selectedExtensionId = (e.target).value; diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index f3e4ef527b..fa0b8c7819 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -29,8 +29,8 @@ const collapsedStateCache: Map = new Map(); let lastRequestTime: number; interface FormattedProcessItem { - cpu: string; - memory: string; + cpu: number; + memory: number; pid: string; name: string; formattedName: string; @@ -66,8 +66,8 @@ function getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, in const formattedName = isRoot ? name : `${repeat(' ', indent)} ${name}`; const memory = process.platform === 'win32' ? item.mem : (totalmem() * (item.mem / 100)); processes.push({ - cpu: item.load.toFixed(0), - memory: (memory / MB).toFixed(0), + cpu: item.load, + memory: (memory / MB), pid: item.pid.toFixed(0), name, formattedName, @@ -202,13 +202,13 @@ function renderTableSection(sectionName: string, processList: FormattedProcessIt p.pid === highestCPUProcess ? cpu.classList.add('centered', 'highest') : cpu.classList.add('centered'); - cpu.textContent = p.cpu.toString(); + cpu.textContent = p.cpu.toFixed(0); const memory = document.createElement('td'); p.pid === highestMemoryProcess ? memory.classList.add('centered', 'highest') : memory.classList.add('centered'); - memory.textContent = p.memory; + memory.textContent = p.memory.toFixed(0); const pid = document.createElement('td'); pid.classList.add('centered'); diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 01f34523a2..f00b97775d 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -1754,7 +1754,7 @@ export class EditorModeContext extends Disposable { this._hasSignatureHelpProvider.set(modes.SignatureHelpProviderRegistry.has(model)); this._hasDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.has(model) || modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.has(model)); - this._hasMultipleDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.all(model).length > 1 || modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); + this._hasMultipleDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.all(model).length + modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); this._hasMultipleDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); this._isInWalkThrough.set(model.uri.scheme === Schemas.walkThroughSnippet); }); diff --git a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts index 73cd2904ca..ee15970a60 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts @@ -368,9 +368,6 @@ export class ReferenceWidget extends PeekViewWidget { if (e.browserEvent instanceof KeyboardEvent) { // todo@joh make this a command goto = true; - } else if (e.browserEvent instanceof MouseEvent) { - aside = e.browserEvent.ctrlKey || e.browserEvent.metaKey || e.browserEvent.altKey; - goto = e.browserEvent.detail === 2; } if (aside) { onEvent(e.elements[0], 'side'); diff --git a/src/vs/editor/contrib/snippet/snippetController2.ts b/src/vs/editor/contrib/snippet/snippetController2.ts index bed0d6ad17..f85af23293 100644 --- a/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/snippetController2.ts @@ -197,7 +197,7 @@ export class SnippetController2 implements IEditorContribution { insertText: option.value, // insertText: `\${1|${after.concat(before).join(',')}|}$0`, // snippetType: 'textmate', - sortText: repeat('a', i), + sortText: repeat('a', i + 1), range: Range.fromPositions(this._editor.getPosition()!, this._editor.getPosition()!.delta(0, first.value.length)) }; })); diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index aa13657217..6b91875598 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -343,7 +343,7 @@ export class SuggestModel implements IDisposable { const position = this._editor.getPosition(); const ctx = new LineContext(model, position, this._state === State.Auto, false); this._onNewContext(ctx); - }, 25); + }, 0); } trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: Set, existingItems?: CompletionItem[]): void { diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 561422bd94..a6b3223af4 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -496,6 +496,9 @@ export class StandaloneTelemetryService implements ITelemetryService { public isOptedIn = false; + public setEnabled(value: boolean): void { + } + public publicLog(eventName: string, data?: any): Promise { return Promise.resolve(undefined); } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e9fb65166f..59f31d4893 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -171,8 +171,11 @@ declare namespace monaco { * @param skipEncoding Do not encode the result, default is `false` */ toString(skipEncoding?: boolean): string; - toJSON(): object; - static revive(data: UriComponents | any): Uri; + toJSON(): UriComponents; + static revive(data: UriComponents | Uri): Uri; + static revive(data: UriComponents | Uri | undefined): Uri | undefined; + static revive(data: UriComponents | Uri | null): Uri | null; + static revive(data: UriComponents | Uri | undefined | null): Uri | undefined | null; } export interface UriComponents { diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 4c111cd05b..c8cf252e69 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -83,8 +83,21 @@ export interface IConfigurationRegistry { } export const enum ConfigurationScope { + /** + * Application specific configuration, which can be configured only in local user settings. + */ APPLICATION = 1, + /** + * Machine specific configuration, which can be configured only in local and remote user settings. + */ + MACHINE, + /** + * Window specific configuration, which can be configured in the user or workspace settings. + */ WINDOW, + /** + * Resource specific configuration, which can be configured in the user, workspace or folder settings. + */ RESOURCE, } @@ -95,6 +108,10 @@ export interface IConfigurationPropertySchema extends IJSONSchema { tags?: string[]; } +export interface IConfigurationExtensionInfo { + id: string; +} + export interface IConfigurationNode { id?: string; order?: number; @@ -105,7 +122,7 @@ export interface IConfigurationNode { allOf?: IConfigurationNode[]; overridable?: boolean; scope?: ConfigurationScope; - contributedByExtension?: boolean; + extensionInfo?: IConfigurationExtensionInfo; } export interface IDefaultConfigurationExtension { @@ -116,6 +133,7 @@ export interface IDefaultConfigurationExtension { export const allSettings: { properties: {}, patternProperties: {} } = { properties: {}, patternProperties: {} }; export const applicationSettings: { properties: {}, patternProperties: {} } = { properties: {}, patternProperties: {} }; +export const machineSettings: { properties: {}, patternProperties: {} } = { properties: {}, patternProperties: {} }; export const windowSettings: { properties: {}, patternProperties: {} } = { properties: {}, patternProperties: {} }; export const resourceSettings: { properties: {}, patternProperties: {} } = { properties: {}, patternProperties: {} }; @@ -186,6 +204,9 @@ class ConfigurationRegistry implements IConfigurationRegistry { case ConfigurationScope.APPLICATION: delete applicationSettings.properties[key]; break; + case ConfigurationScope.MACHINE: + delete machineSettings.properties[key]; + break; case ConfigurationScope.WINDOW: delete windowSettings.properties[key]; break; @@ -334,6 +355,9 @@ class ConfigurationRegistry implements IConfigurationRegistry { case ConfigurationScope.APPLICATION: applicationSettings.properties[key] = properties[key]; break; + case ConfigurationScope.MACHINE: + machineSettings.properties[key] = properties[key]; + break; case ConfigurationScope.WINDOW: windowSettings.properties[key] = properties[key]; break; @@ -371,6 +395,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { delete allSettings.patternProperties[this.overridePropertyPattern]; delete applicationSettings.patternProperties[this.overridePropertyPattern]; + delete machineSettings.patternProperties[this.overridePropertyPattern]; delete windowSettings.patternProperties[this.overridePropertyPattern]; delete resourceSettings.patternProperties[this.overridePropertyPattern]; @@ -378,6 +403,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { allSettings.patternProperties[this.overridePropertyPattern] = patternProperties; applicationSettings.patternProperties[this.overridePropertyPattern] = patternProperties; + machineSettings.patternProperties[this.overridePropertyPattern] = patternProperties; windowSettings.patternProperties[this.overridePropertyPattern] = patternProperties; resourceSettings.patternProperties[this.overridePropertyPattern] = patternProperties; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 17d468fb39..7271fa87bc 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -18,7 +18,6 @@ export interface ParsedArgs { waitMarkerFilePath?: string; diff?: boolean; add?: boolean; - gitCredential?: string; goto?: boolean; 'new-window'?: boolean; 'unity-launch'?: boolean; // Always open a new window, except if opening the first window or opening a file or folder as part of the launch. diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 0d7230f46c..08b22bc33e 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -197,7 +197,7 @@ function wrapText(text: string, columns: number): string[] { return lines; } -export function buildHelpMessage(productName: string, executableName: string, version: string, isOptionSupported = (_: Option) => true): string { +export function buildHelpMessage(productName: string, executableName: string, version: string, isOptionSupported = (_: Option) => true, isPipeSupported = true): string { const columns = (process.stdout).isTTY && (process.stdout).columns || 80; let categories = new HelpCategories(); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/node/extensionManagementIpc.ts index 665995ea2a..7fd0581b1f 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementIpc.ts @@ -86,7 +86,7 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer get onDidUninstallExtension(): Event { return this.channel.listen('onDidUninstallExtension'); } zip(extension: ILocalExtension): Promise { - return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(result))); + return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(result))); } unzip(zipLocation: URI, type: ExtensionType): Promise { @@ -122,4 +122,4 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer getExtensionsReport(): Promise { return Promise.resolve(this.channel.call('getExtensionsReport')); } -} \ No newline at end of file +} diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 3c8f4923e8..b326f04338 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -124,11 +124,6 @@ export interface IFileService { */ resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise; - /** - * @deprecated use writeFile instead - */ - updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise; - /** * Updates the content replacing its previous value. */ @@ -148,18 +143,13 @@ export interface IFileService { */ copy(source: URI, target: URI, overwrite?: boolean): Promise; - /** - * @deprecated use createFile2 instead - */ - createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise; - /** * Creates a new file with the given path and optional contents. The returned promise * will have the stat model object as a result. * * The optional parameter content can be used as value to fill into the new file. */ - createFile2(resource: URI, bufferOrReadable?: VSBuffer | VSBufferReadable, options?: ICreateFileOptions): Promise; + createFile(resource: URI, bufferOrReadable?: VSBuffer | VSBufferReadable, options?: ICreateFileOptions): Promise; /** * Creates a new folder with the given path. The returned promise @@ -666,6 +656,7 @@ export interface ITextSnapshot { */ export function snapshotToString(snapshot: ITextSnapshot): string { const chunks: string[] = []; + let chunk: string | null; while (typeof (chunk = snapshot.read()) === 'string') { chunks.push(chunk); @@ -674,6 +665,22 @@ export function snapshotToString(snapshot: ITextSnapshot): string { return chunks.join(''); } +export function stringToSnapshot(value: string): ITextSnapshot { + let done = false; + + return { + read(): string | null { + if (!done) { + done = true; + + return value; + } + + return null; + } + }; +} + export class TextSnapshotReadable implements VSBufferReadable { private preambleHandled: boolean; @@ -703,6 +710,22 @@ export class TextSnapshotReadable implements VSBufferReadable { } } +export function toBufferOrReadable(value: string): VSBuffer; +export function toBufferOrReadable(value: ITextSnapshot): VSBufferReadable; +export function toBufferOrReadable(value: string | ITextSnapshot): VSBuffer | VSBufferReadable; +export function toBufferOrReadable(value: string | ITextSnapshot | undefined): VSBuffer | VSBufferReadable | undefined; +export function toBufferOrReadable(value: string | ITextSnapshot | undefined): VSBuffer | VSBufferReadable | undefined { + if (typeof value === 'undefined') { + return undefined; + } + + if (typeof value === 'string') { + return VSBuffer.fromString(value); + } + + return new TextSnapshotReadable(value); +} + /** * Streamable content and meta information of a file. */ @@ -1158,8 +1181,4 @@ export interface ILegacyFileService extends IDisposable { resolveContent(resource: URI, options?: IResolveContentOptions): Promise; resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise; - - updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise; - - createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise; } \ No newline at end of file diff --git a/src/vs/platform/history/electron-main/historyMainService.ts b/src/vs/platform/history/electron-main/historyMainService.ts index 6eb3cae891..5f8be81b21 100644 --- a/src/vs/platform/history/electron-main/historyMainService.ts +++ b/src/vs/platform/history/electron-main/historyMainService.ts @@ -15,7 +15,7 @@ import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { IWorkspaceIdentifier, IWorkspacesMainService, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IHistoryMainService, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile } from 'vs/platform/history/common/history'; import { ThrottledDelayer } from 'vs/base/common/async'; -import { isEqual as areResourcesEqual, dirname, originalFSPath } from 'vs/base/common/resources'; +import { isEqual as areResourcesEqual, dirname, originalFSPath, basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -29,6 +29,12 @@ export class HistoryMainService implements IHistoryMainService { private static readonly MAX_MACOS_DOCK_RECENT_FOLDERS = 10; private static readonly MAX_MACOS_DOCK_RECENT_FILES = 5; + // Exclude some very common files from the dock/taskbar + private static readonly COMMON_FILES_FILTER = [ + 'COMMIT_EDITMSG', + 'MERGE_MSG' + ]; + private static readonly recentlyOpenedStorageKey = 'openedPathsList'; _serviceBrand: any; @@ -52,17 +58,29 @@ export class HistoryMainService implements IHistoryMainService { const files: IRecentFile[] = []; for (let curr of newlyAdded) { + + // Workspace if (isRecentWorkspace(curr)) { if (!this.workspacesMainService.isUntitledWorkspace(curr.workspace) && indexOfWorkspace(workspaces, curr.workspace) === -1) { workspaces.push(curr); } - } else if (isRecentFolder(curr)) { + } + + // Folder + else if (isRecentFolder(curr)) { if (indexOfFolder(workspaces, curr.folderUri) === -1) { workspaces.push(curr); } - } else { - if (indexOfFile(files, curr.fileUri) === -1) { + } + + // File + else { + const alreadyExistsInHistory = indexOfFile(files, curr.fileUri) >= 0; + const shouldBeFiltered = curr.fileUri.scheme === Schemas.file && HistoryMainService.COMMON_FILES_FILTER.indexOf(basename(curr.fileUri)) >= 0; + + if (!alreadyExistsInHistory && !shouldBeFiltered) { files.push(curr); + // Add to recent documents (Windows only, macOS later) if (isWindows && curr.fileUri.scheme === Schemas.file) { app.addRecentDocument(curr.fileUri.fsPath); @@ -76,6 +94,7 @@ export class HistoryMainService implements IHistoryMainService { if (workspaces.length > HistoryMainService.MAX_TOTAL_RECENT_ENTRIES) { workspaces.length = HistoryMainService.MAX_TOTAL_RECENT_ENTRIES; } + if (files.length > HistoryMainService.MAX_TOTAL_RECENT_ENTRIES) { files.length = HistoryMainService.MAX_TOTAL_RECENT_ENTRIES; } @@ -143,7 +162,7 @@ export class HistoryMainService implements IHistoryMainService { // Fill in files for (let i = 0, entries = 0; i < mru.files.length && entries < HistoryMainService.MAX_MACOS_DOCK_RECENT_FILES; i++) { const loc = location(mru.files[i]); - if (loc.scheme === Schemas.file) { + if (loc.scheme === Schemas.file && HistoryMainService.COMMON_FILES_FILTER.indexOf(basename(loc)) === -1) { const filePath = originalFSPath(loc); if (await exists(filePath)) { app.addRecentDocument(filePath); @@ -162,7 +181,6 @@ export class HistoryMainService implements IHistoryMainService { } getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened { - const workspaces: Array = []; const files: IRecentFile[] = []; @@ -170,6 +188,7 @@ export class HistoryMainService implements IHistoryMainService { if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) { workspaces.push({ workspace: currentWorkspace }); } + if (currentFolder) { workspaces.push({ folderUri: currentFolder }); } @@ -183,12 +202,14 @@ export class HistoryMainService implements IHistoryMainService { } } } + this.addEntriesFromStorage(workspaces, files); return { workspaces, files }; } private addEntriesFromStorage(workspaces: Array, files: IRecentFile[]) { + // Get from storage let recents = this.getRecentlyOpenedFromStorage(); for (let recent of recents.workspaces) { @@ -199,6 +220,7 @@ export class HistoryMainService implements IHistoryMainService { workspaces.push(recent); } } + for (let recent of recents.files) { let index = indexOfFile(files, recent.fileUri); if (index >= 0) { @@ -211,11 +233,13 @@ export class HistoryMainService implements IHistoryMainService { private getRecentlyOpenedFromStorage(): IRecentlyOpened { const storedRecents = this.stateService.getItem(HistoryMainService.recentlyOpenedStorageKey); + return restoreRecentlyOpened(storedRecents); } private saveRecentlyOpened(recent: IRecentlyOpened): void { const serialized = toStoreData(recent); + this.stateService.setItem(HistoryMainService.recentlyOpenedStorageKey, serialized); } @@ -268,16 +292,17 @@ export class HistoryMainService implements IHistoryMainService { items: arrays.coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; const title = recent.label || getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome); + let description; let args; if (isSingleFolderWorkspaceIdentifier(workspace)) { - const parentFolder = dirname(workspace); - description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(parentFolder, this.environmentService)); + description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService)); args = `--folder-uri "${workspace.toString()}"`; } else { description = nls.localize('codeWorkspace', "Code Workspace"); args = `--file-uri "${workspace.configPath.toString()}"`; } + return { type: 'task', title, @@ -308,9 +333,11 @@ function location(recent: IRecent): URI { if (isRecentFolder(recent)) { return recent.folderUri; } + if (isRecentFile(recent)) { return recent.fileUri; } + return recent.workspace.configPath; } diff --git a/src/vs/platform/history/electron-main/historyStorage.ts b/src/vs/platform/history/electron-main/historyStorage.ts index 396638ce30..26ecc56674 100644 --- a/src/vs/platform/history/electron-main/historyStorage.ts +++ b/src/vs/platform/history/electron-main/historyStorage.ts @@ -55,7 +55,7 @@ export function restoreRecentlyOpened(data: RecentlyOpenedStorageData | undefine result.workspaces.push({ workspace: { id: workspace['id'], configPath: URI.file(workspace['configPath']) } }); } else if (workspace && typeof workspace['path'] === 'string' && typeof workspace['scheme'] === 'string') { // added by 1.26-insiders - result.workspaces.push({ folderUri: URI.revive(workspace) }); + result.workspaces.push({ folderUri: URI.revive(workspace) }); } } } diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index 37e81f559b..a8ec7a3099 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -56,6 +56,7 @@ export interface IssueReporterData extends WindowData { styles: IssueReporterStyles; enabledExtensions: IssueReporterExtensionData[]; issueType?: IssueType; + extensionId?: string; } export interface ISettingSearchResult { diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 15f4bf44ee..27d844d0b4 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -30,7 +30,8 @@ export const enum ProgressLocation { Scm = 3, Extensions = 5, Window = 10, - Notification = 15 + Notification = 15, + Dialog = 20 } export interface IProgressOptions { diff --git a/src/vs/platform/remote/common/remoteAgentEnvironment.ts b/src/vs/platform/remote/common/remoteAgentEnvironment.ts index f960f9b804..e493a5f0c7 100644 --- a/src/vs/platform/remote/common/remoteAgentEnvironment.ts +++ b/src/vs/platform/remote/common/remoteAgentEnvironment.ts @@ -19,7 +19,6 @@ export interface IRemoteAgentEnvironment { userHome: URI; extensions: IExtensionDescription[]; os: OperatingSystem; - syncExtensions: boolean; } export interface RemoteAgentConnectionContext { diff --git a/src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts b/src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts index 6f7d1d124f..f5354472d1 100644 --- a/src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts +++ b/src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts @@ -22,31 +22,30 @@ export interface IFileChangeDto { export class RemoteExtensionsFileSystemProvider extends Disposable implements IFileSystemProvider { - private readonly _session: string; - private readonly _channel: IChannel; + private readonly session: string = generateUuid(); private readonly _onDidChange = this._register(new Emitter()); readonly onDidChangeFile: Event = this._onDidChange.event; - public capabilities: FileSystemProviderCapabilities; private readonly _onDidChangeCapabilities = this._register(new Emitter()); readonly onDidChangeCapabilities: Event = this._onDidChangeCapabilities.event; - constructor(channel: IChannel, environment: Promise) { + private _capabilities: FileSystemProviderCapabilities; + get capabilities(): FileSystemProviderCapabilities { return this._capabilities; } + + constructor(private readonly channel: IChannel, environment: Promise) { super(); - this._session = generateUuid(); - this._channel = channel; this.setCaseSensitive(true); environment.then(remoteAgentEnvironment => this.setCaseSensitive(!!(remoteAgentEnvironment && remoteAgentEnvironment.os === OperatingSystem.Linux))); - this._channel.listen('filechange', [this._session])((events) => { - this._onDidChange.fire(events.map(RemoteExtensionsFileSystemProvider._createFileChange)); - }); + this.registerListeners(); } - dispose(): void { - super.dispose(); + private registerListeners(): void { + this._register(this.channel.listen('filechange', [this.session])((events) => { + this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type }))); + })); } setCaseSensitive(isCaseSensitive: boolean) { @@ -54,58 +53,55 @@ export class RemoteExtensionsFileSystemProvider extends Disposable implements IF FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileFolderCopy ); + if (isCaseSensitive) { capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; } - this.capabilities = capabilities; + + this._capabilities = capabilities; this._onDidChangeCapabilities.fire(undefined); } - watch(resource: URI, opts: IWatchOptions): IDisposable { - const req = Math.random(); - this._channel.call('watch', [this._session, req, resource, opts]); - return toDisposable(() => { - this._channel.call('unwatch', [this._session, req]); - }); - } - - private static _createFileChange(dto: IFileChangeDto): IFileChange { - return { resource: URI.revive(dto.resource), type: dto.type }; - } - // --- forwarding calls stat(resource: URI): Promise { - return this._channel.call('stat', [resource]); + return this.channel.call('stat', [resource]); } async readFile(resource: URI): Promise { - const buff = await this._channel.call('readFile', [resource]); + const buff = await this.channel.call('readFile', [resource]); + return buff.buffer; } writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { - const contents = VSBuffer.wrap(content); - return this._channel.call('writeFile', [resource, contents, opts]); + return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]); } delete(resource: URI, opts: FileDeleteOptions): Promise { - return this._channel.call('delete', [resource, opts]); + return this.channel.call('delete', [resource, opts]); } mkdir(resource: URI): Promise { - return this._channel.call('mkdir', [resource]); + return this.channel.call('mkdir', [resource]); } readdir(resource: URI): Promise<[string, FileType][]> { - return this._channel.call('readdir', [resource]); + return this.channel.call('readdir', [resource]); } rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { - return this._channel.call('rename', [resource, target, opts]); + return this.channel.call('rename', [resource, target, opts]); } copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { - return this._channel.call('copy', [resource, target, opts]); + return this.channel.call('copy', [resource, target, opts]); + } + + watch(resource: URI, opts: IWatchOptions): IDisposable { + const req = Math.random(); + this.channel.call('watch', [this.session, req, resource, opts]); + + return toDisposable(() => this.channel.call('unwatch', [this.session, req])); } } diff --git a/src/vs/platform/telemetry/browser/errorTelemetry.ts b/src/vs/platform/telemetry/browser/errorTelemetry.ts index 7d8f4691b8..272f661074 100644 --- a/src/vs/platform/telemetry/browser/errorTelemetry.ts +++ b/src/vs/platform/telemetry/browser/errorTelemetry.ts @@ -3,69 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { binarySearch } from 'vs/base/common/arrays'; +import { toDisposable } from 'vs/base/common/lifecycle'; import { globals } from 'vs/base/common/platform'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; -import * as Errors from 'vs/base/common/errors'; -import { safeStringify } from 'vs/base/common/objects'; +import BaseErrorTelemetry, { ErrorEvent } from '../common/errorTelemetry'; -/* __GDPR__FRAGMENT__ - "ErrorEvent" : { - "stack": { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" }, - "message" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" }, - "filename" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" }, - "callstack": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "msg" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "file" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "line": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "column": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "uncaught_error_name": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "uncaught_error_msg": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "count": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true } - } - */ -interface ErrorEvent { - callstack: string; - msg?: string; - file?: string; - line?: number; - column?: number; - uncaught_error_name?: string; - uncaught_error_msg?: string; - count?: number; -} - -namespace ErrorEvent { - export function compare(a: ErrorEvent, b: ErrorEvent) { - if (a.callstack < b.callstack) { - return -1; - } else if (a.callstack > b.callstack) { - return 1; - } - return 0; - } -} - -export default class ErrorTelemetry { - - public static ERROR_FLUSH_TIMEOUT: number = 5 * 1000; - - private _telemetryService: ITelemetryService; - private _flushDelay: number; - private _flushHandle: any = -1; - private _buffer: ErrorEvent[] = []; - private _disposables: IDisposable[] = []; - - constructor(telemetryService: ITelemetryService, flushDelay = ErrorTelemetry.ERROR_FLUSH_TIMEOUT) { - this._telemetryService = telemetryService; - this._flushDelay = flushDelay; - - // (1) check for unexpected but handled errors - const unbind = Errors.errorHandler.addListener((err) => this._onErrorEvent(err)); - this._disposables.push(toDisposable(unbind)); - - // (2) check for uncaught global errors +export default class ErrorTelemetry extends BaseErrorTelemetry { + protected installErrorListeners(): void { let oldOnError: Function; let that = this; if (typeof globals.onerror === 'function') { @@ -84,37 +27,7 @@ export default class ErrorTelemetry { })); } - dispose() { - clearTimeout(this._flushHandle); - this._flushBuffer(); - this._disposables = dispose(this._disposables); - } - - private _onErrorEvent(err: any): void { - - if (!err) { - return; - } - - // unwrap nested errors from loader - if (err.detail && err.detail.stack) { - err = err.detail; - } - - // work around behavior in workerServer.ts that breaks up Error.stack - let callstack = Array.isArray(err.stack) ? err.stack.join('\n') : err.stack; - let msg = err.message ? err.message : safeStringify(err); - - // errors without a stack are not useful telemetry - if (!callstack) { - return; - } - - this._enqueue({ msg, callstack }); - } - private _onUncaughtError(msg: string, file: string, line: number, column?: number, err?: any): void { - let data: ErrorEvent = { callstack: msg, msg, @@ -138,38 +51,4 @@ export default class ErrorTelemetry { this._enqueue(data); } - - private _enqueue(e: ErrorEvent): void { - - const idx = binarySearch(this._buffer, e, ErrorEvent.compare); - if (idx < 0) { - e.count = 1; - this._buffer.splice(~idx, 0, e); - } else { - if (!this._buffer[idx].count) { - this._buffer[idx].count = 0; - } - this._buffer[idx].count! += 1; - } - - if (this._flushHandle === -1) { - this._flushHandle = setTimeout(() => { - this._flushBuffer(); - this._flushHandle = -1; - }, this._flushDelay); - } - } - - private _flushBuffer(): void { - for (let error of this._buffer) { - /* __GDPR__ - "UnhandledError" : { - "${include}": [ "${ErrorEvent}" ] - } - */ - // {{SQL CARBON EDIT}} - //this._telemetryService.publicLog('UnhandledError', error, true); - } - this._buffer.length = 0; - } } diff --git a/src/vs/platform/telemetry/common/errorTelemetry.ts b/src/vs/platform/telemetry/common/errorTelemetry.ts new file mode 100644 index 0000000000..e33e300334 --- /dev/null +++ b/src/vs/platform/telemetry/common/errorTelemetry.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { binarySearch } from 'vs/base/common/arrays'; +import * as Errors from 'vs/base/common/errors'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { safeStringify } from 'vs/base/common/objects'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +/* __GDPR__FRAGMENT__ + "ErrorEvent" : { + "stack": { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" }, + "message" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" }, + "filename" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" }, + "callstack": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "msg" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "file" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "line": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "column": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "uncaught_error_name": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "uncaught_error_msg": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "count": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true } + } + */ +export interface ErrorEvent { + callstack: string; + msg?: string; + file?: string; + line?: number; + column?: number; + uncaught_error_name?: string; + uncaught_error_msg?: string; + count?: number; +} + +export namespace ErrorEvent { + export function compare(a: ErrorEvent, b: ErrorEvent) { + if (a.callstack < b.callstack) { + return -1; + } else if (a.callstack > b.callstack) { + return 1; + } + return 0; + } +} + +export default abstract class BaseErrorTelemetry { + + public static ERROR_FLUSH_TIMEOUT: number = 5 * 1000; + + private _telemetryService: ITelemetryService; + private _flushDelay: number; + private _flushHandle: any = -1; + private _buffer: ErrorEvent[] = []; + protected _disposables: IDisposable[] = []; + + constructor(telemetryService: ITelemetryService, flushDelay = BaseErrorTelemetry.ERROR_FLUSH_TIMEOUT) { + this._telemetryService = telemetryService; + this._flushDelay = flushDelay; + + // (1) check for unexpected but handled errors + const unbind = Errors.errorHandler.addListener((err) => this._onErrorEvent(err)); + this._disposables.push(toDisposable(unbind)); + + // (2) install implementation-specific error listeners + this.installErrorListeners(); + } + + dispose() { + clearTimeout(this._flushHandle); + this._flushBuffer(); + this._disposables = dispose(this._disposables); + } + + protected installErrorListeners(): void { + // to override + } + + private _onErrorEvent(err: any): void { + + if (!err) { + return; + } + + // unwrap nested errors from loader + if (err.detail && err.detail.stack) { + err = err.detail; + } + + // work around behavior in workerServer.ts that breaks up Error.stack + let callstack = Array.isArray(err.stack) ? err.stack.join('\n') : err.stack; + let msg = err.message ? err.message : safeStringify(err); + + // errors without a stack are not useful telemetry + if (!callstack) { + return; + } + + this._enqueue({ msg, callstack }); + } + + protected _enqueue(e: ErrorEvent): void { + + const idx = binarySearch(this._buffer, e, ErrorEvent.compare); + if (idx < 0) { + e.count = 1; + this._buffer.splice(~idx, 0, e); + } else { + if (!this._buffer[idx].count) { + this._buffer[idx].count = 0; + } + this._buffer[idx].count! += 1; + } + + if (this._flushHandle === -1) { + this._flushHandle = setTimeout(() => { + this._flushBuffer(); + this._flushHandle = -1; + }, this._flushDelay); + } + } + + private _flushBuffer(): void { + for (let error of this._buffer) { + /* __GDPR__ + "UnhandledError" : { + "${include}": [ "${ErrorEvent}" ] + } + */ + // {{SQL CARBON EDIT}} + // this._telemetryService.publicLog('UnhandledError', error, true); + } + this._buffer.length = 0; + } +} diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index d32d8e7b0a..10ba3821fc 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -29,6 +29,8 @@ export interface ITelemetryService { */ publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise; + setEnabled(value: boolean): void; + getTelemetryInfo(): Promise; isOptedIn: boolean; diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 388bf263e2..3b622447c0 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -31,6 +31,7 @@ export class TelemetryService implements ITelemetryService { private _commonProperties: Promise<{ [name: string]: any; }>; private _piiPaths: string[]; private _userOptIn: boolean; + private _enabled: boolean; private _disposables: IDisposable[] = []; private _cleanupPatterns: RegExp[] = []; @@ -43,6 +44,7 @@ export class TelemetryService implements ITelemetryService { this._commonProperties = config.commonProperties || Promise.resolve({}); this._piiPaths = config.piiPaths || []; this._userOptIn = true; + this._enabled = true; // static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information` this._cleanupPatterns = [/file:\/\/\/.*?\/resources\/app\//gi]; @@ -74,13 +76,17 @@ export class TelemetryService implements ITelemetryService { } } + setEnabled(value: boolean): void { + this._enabled = value; + } + private _updateUserOptIn(): void { const config = this._configurationService.getValue(TELEMETRY_SECTION_ID); this._userOptIn = config ? config.enableTelemetry : this._userOptIn; } get isOptedIn(): boolean { - return this._userOptIn; + return this._userOptIn && this._enabled; } getTelemetryInfo(): Promise { @@ -100,7 +106,7 @@ export class TelemetryService implements ITelemetryService { publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { // don't send events when the user is optout - if (!this._userOptIn) { + if (!this.isOptedIn) { return Promise.resolve(undefined); } diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index d30232b33d..c9781a9cf1 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -14,6 +14,7 @@ export const NullTelemetryService = new class implements ITelemetryService { publicLog(eventName: string, data?: ITelemetryData) { return Promise.resolve(undefined); } + setEnabled() { } isOptedIn: true; getTelemetryInfo(): Promise { return Promise.resolve({ diff --git a/src/vs/platform/telemetry/node/errorTelemetry.ts b/src/vs/platform/telemetry/node/errorTelemetry.ts new file mode 100644 index 0000000000..9cf146e5f6 --- /dev/null +++ b/src/vs/platform/telemetry/node/errorTelemetry.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from 'vs/base/common/errors'; +import BaseErrorTelemetry from '../common/errorTelemetry'; + +export default class ErrorTelemetry extends BaseErrorTelemetry { + protected installErrorListeners(): void { + // Print a console message when rejection isn't handled within N seconds. For details: + // see https://nodejs.org/api/process.html#process_event_unhandledrejection + // and https://nodejs.org/api/process.html#process_event_rejectionhandled + const unhandledPromises: Promise[] = []; + process.on('unhandledRejection', (reason: any, promise: Promise) => { + unhandledPromises.push(promise); + setTimeout(() => { + const idx = unhandledPromises.indexOf(promise); + if (idx >= 0) { + promise.catch(e => { + unhandledPromises.splice(idx, 1); + console.warn(`rejected promise not handled within 1 second: ${e}`); + if (e.stack) { + console.warn(`stack trace: ${e.stack}`); + } + onUnexpectedError(reason); + }); + } + }, 1000); + }); + + process.on('rejectionHandled', (promise: Promise) => { + const idx = unhandledPromises.indexOf(promise); + if (idx >= 0) { + unhandledPromises.splice(idx, 1); + } + }); + + // Print a console message when an exception isn't handled. + process.on('uncaughtException', (err: Error) => { + onUnexpectedError(err); + }); + } +} diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index 20219108ee..84ec1af6c2 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -16,6 +16,8 @@ import * as json from 'vs/base/common/json'; import { Schemas } from 'vs/base/common/network'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { toSlashes } from 'vs/base/common/extpath'; +import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; export const IWorkspacesMainService = createDecorator('workspacesMainService'); export const IWorkspacesService = createDecorator('workspacesService'); @@ -75,6 +77,7 @@ export interface IResolvedWorkspace extends IWorkspaceIdentifier { export interface IStoredWorkspace { folders: IStoredWorkspaceFolder[]; + remoteAuthority?: string; } export interface IWorkspaceSavedEvent { @@ -232,12 +235,15 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, // Preserve as much of the existing workspace as possible by using jsonEdit // and only changing the folders portion. - let newRawWorkspaceContents = rawWorkspaceContents; - const edits = jsonEdit.setProperty(rawWorkspaceContents, ['folders'], rewrittenFolders, { insertSpaces: false, tabSize: 4, eol: (isLinux || isMacintosh) ? '\n' : '\r\n' }); - edits.forEach(edit => { - newRawWorkspaceContents = jsonEdit.applyEdit(rawWorkspaceContents, edit); - }); - return newRawWorkspaceContents; + const formattingOptions: FormattingOptions = { insertSpaces: false, tabSize: 4, eol: (isLinux || isMacintosh) ? '\n' : '\r\n' }; + const edits = jsonEdit.setProperty(rawWorkspaceContents, ['folders'], rewrittenFolders, formattingOptions); + let newContent = jsonEdit.applyEdits(rawWorkspaceContents, edits); + + if (storedWorkspace.remoteAuthority === getRemoteAuthority(targetConfigPathURI)) { + // unsaved remote workspaces have the remoteAuthority set. Remove it when no longer nexessary. + newContent = jsonEdit.applyEdits(newContent, jsonEdit.removeProperty(newContent, ['remoteAuthority'], formattingOptions)); + } + return newContent; } function doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 03cf6111ec..d2bd7caf17 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5973,6 +5973,11 @@ declare module 'vscode' { */ export const appRoot: string; + /** + * The custom uri scheme the editor registers to in the operating system. + */ + export const uriScheme: string; + /** * Represents the preferred user-language, like `de-CH`, `fr`, or `en-US`. */ diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 57975a05be..d41ca3b532 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -616,11 +616,6 @@ declare module 'vscode' { * An [event](#Event) that fires when the log level has changed. */ export const onDidChangeLogLevel: Event; - - /** - * The custom uri scheme the editor registers to in the operating system, like 'vscode', 'vscode-insiders'. - */ - export const uriScheme: string; } //#endregion diff --git a/src/vs/workbench/api/browser/mainThreadDecorations.ts b/src/vs/workbench/api/browser/mainThreadDecorations.ts index 4b581c9d2b..d8c8d73441 100644 --- a/src/vs/workbench/api/browser/mainThreadDecorations.ts +++ b/src/vs/workbench/api/browser/mainThreadDecorations.ts @@ -112,7 +112,7 @@ export class MainThreadDecorations implements MainThreadDecorationsShape { const provider = this._provider.get(handle); if (provider) { const [emitter] = provider; - emitter.fire(resources && resources.map(URI.revive)); + emitter.fire(resources && resources.map(r => URI.revive(r))); } } diff --git a/src/vs/workbench/api/browser/mainThreadSearch.ts b/src/vs/workbench/api/browser/mainThreadSearch.ts index 5f0749a921..9e2a85bb16 100644 --- a/src/vs/workbench/api/browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/browser/mainThreadSearch.ts @@ -162,7 +162,7 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { }); } else { searchOp.addMatch({ - resource: URI.revive(result) + resource: URI.revive(result) }); } }); diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index 908d69cf1d..3634546802 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -47,7 +47,7 @@ export class OpenFolderAPICommand { arg = { forceNewWindow: arg }; } if (!uri) { - return executor.executeCommand('_files.pickFolderAndOpen', arg.forceNewWindow); + return executor.executeCommand('_files.pickFolderAndOpen', { forceNewWindow: arg.forceNewWindow }); } const options: IOpenSettings = { forceNewWindow: arg.forceNewWindow, noRecentEntry: arg.noRecentEntry }; uri = URI.revive(uri); @@ -144,6 +144,13 @@ export class RemoveFromRecentlyOpenedAPICommand { } CommandsRegistry.registerCommand(RemoveFromRecentlyOpenedAPICommand.ID, adjustHandler(RemoveFromRecentlyOpenedAPICommand.execute)); +export class OpenIssueReporter { + public static ID = 'vscode.openIssueReporter'; + public static execute(executor: ICommandsExecutor, extensionId: string): Promise { + return executor.executeCommand('workbench.action.openIssueReporter', [extensionId]); + } +} + interface RecentEntry { uri: URI; type: 'workspace' | 'folder' | 'file'; diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 36cbbc3aea..7be13e3355 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -35,16 +35,17 @@ const configurationEntrySchema: IJSONSchema = { properties: { isExecutable: { type: 'boolean', - deprecationMessage: 'This property is deprecated. Instead use `scope` property and set it to `application` value.' + deprecationMessage: 'This property is deprecated. Instead use `scope` property and set it to `machine` value.' }, scope: { type: 'string', - enum: ['application', 'window', 'resource'], + enum: ['application', 'machine', 'window', 'resource'], default: 'window', enumDescriptions: [ - nls.localize('scope.application.description', "Application specific configuration, which can be configured only in User settings."), - nls.localize('scope.window.description', "Window specific configuration, which can be configured in the User or Workspace settings."), - nls.localize('scope.resource.description', "Resource specific configuration, which can be configured in the User, Workspace or Folder settings.") + nls.localize('scope.application.description', "Application specific configuration, which can be configured only in local user settings."), + nls.localize('scope.machine.description', "Machine specific configuration, which can be configured only in local and remote user settings."), + nls.localize('scope.window.description', "Window specific configuration, which can be configured in the user or workspace settings."), + nls.localize('scope.resource.description', "Resource specific configuration, which can be configured in the user, workspace or folder settings.") ], description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `window` and `resource`.") }, @@ -161,7 +162,7 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { validateProperties(configuration, extension); configuration.id = node.id || extension.description.identifier.value; - configuration.contributedByExtension = true; + configuration.extensionInfo = { id: extension.description.identifier.value }; configuration.title = configuration.title || extension.description.displayName || extension.description.identifier.value; configurations.push(configuration); return configurations; @@ -210,6 +211,8 @@ function validateProperties(configuration: IConfigurationNode, extension: IExten if (propertyConfiguration.scope) { if (propertyConfiguration.scope.toString() === 'application') { propertyConfiguration.scope = ConfigurationScope.APPLICATION; + } else if (propertyConfiguration.scope.toString() === 'machine') { + propertyConfiguration.scope = ConfigurationScope.MACHINE; } else if (propertyConfiguration.scope.toString() === 'resource') { propertyConfiguration.scope = ConfigurationScope.RESOURCE; } else { diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 3cbc553caf..2c85b26e86 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -15,7 +15,7 @@ import * as search from 'vs/workbench/contrib/search/common/search'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { CustomCodeAction } from 'vs/workbench/api/common/extHostLanguageFeatures'; -import { ICommandsExecutor, OpenFolderAPICommand, DiffAPICommand, OpenAPICommand, RemoveFromRecentlyOpenedAPICommand, SetEditorLayoutAPICommand } from './apiCommands'; +import { ICommandsExecutor, OpenFolderAPICommand, DiffAPICommand, OpenAPICommand, RemoveFromRecentlyOpenedAPICommand, SetEditorLayoutAPICommand, OpenIssueReporter } from './apiCommands'; import { EditorGroupLayout } from 'vs/workbench/services/editor/common/editorGroupsService'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; @@ -258,6 +258,13 @@ export class ExtHostApiCommands { { name: 'layout', description: 'The editor layout to set.', constraint: (value: EditorGroupLayout) => typeof value === 'object' && Array.isArray(value.groups) } ] }); + + this._register(OpenIssueReporter.ID, adjustHandler(OpenIssueReporter.execute), { + description: 'Opens the issue reporter with the provided extension id as the selected source', + args: [ + { name: 'extensionId', description: 'extensionId to report an issue on', constraint: (value: any) => typeof value === 'string' } + ] + }); } // --- command impl diff --git a/src/vs/workbench/api/common/extHostDialogs.ts b/src/vs/workbench/api/common/extHostDialogs.ts index 7cd329ca6c..c3221b1672 100644 --- a/src/vs/workbench/api/common/extHostDialogs.ts +++ b/src/vs/workbench/api/common/extHostDialogs.ts @@ -17,7 +17,7 @@ export class ExtHostDialogs { showOpenDialog(options: vscode.OpenDialogOptions): Promise { return this._proxy.$showOpenDialog(options).then(filepaths => { - return filepaths ? filepaths.map(URI.revive) : undefined; + return filepaths ? filepaths.map(p => URI.revive(p)) : undefined; }); } diff --git a/src/vs/workbench/api/common/extHostMemento.ts b/src/vs/workbench/api/common/extHostMemento.ts new file mode 100644 index 0000000000..3921d40d63 --- /dev/null +++ b/src/vs/workbench/api/common/extHostMemento.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionMemento } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; + +export class ExtensionMemento implements IExtensionMemento { + + private readonly _id: string; + private readonly _shared: boolean; + private readonly _storage: ExtHostStorage; + + private readonly _init: Promise; + private _value: { [n: string]: any; }; + private readonly _storageListener: IDisposable; + + constructor(id: string, global: boolean, storage: ExtHostStorage) { + this._id = id; + this._shared = global; + this._storage = storage; + + this._init = this._storage.getValue(this._shared, this._id, Object.create(null)).then(value => { + this._value = value; + return this; + }); + + this._storageListener = this._storage.onDidChangeStorage(e => { + if (e.shared === this._shared && e.key === this._id) { + this._value = e.value; + } + }); + } + + get whenReady(): Promise { + return this._init; + } + + get(key: string, defaultValue: T): T { + let value = this._value[key]; + if (typeof value === 'undefined') { + value = defaultValue; + } + return value; + } + + update(key: string, value: any): Promise { + this._value[key] = value; + return this._storage + .setValue(this._shared, this._id, this._value) + .then(() => true); + } + + dispose(): void { + this._storageListener.dispose(); + } +} diff --git a/src/vs/workbench/api/common/extHostOutput.ts b/src/vs/workbench/api/common/extHostOutput.ts index 5dbf1fd509..09ae3c549f 100644 --- a/src/vs/workbench/api/common/extHostOutput.ts +++ b/src/vs/workbench/api/common/extHostOutput.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { VSBuffer } from 'vs/base/common/buffer'; export abstract class AbstractExtHostOutputChannel extends Disposable implements vscode.OutputChannel { @@ -35,7 +36,7 @@ export abstract class AbstractExtHostOutputChannel extends Disposable implements append(value: string): void { this.validate(); - this._offset += value ? Buffer.from(value).byteLength : 0; + this._offset += value ? VSBuffer.fromString(value).byteLength : 0; } update(): void { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 7380a1f850..6e52f764b5 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -478,8 +478,8 @@ export namespace WorkspaceEdit { ); } else { result.renameFile( - URI.revive((edit).oldUri), - URI.revive((edit).newUri), + URI.revive((edit).oldUri!), + URI.revive((edit).newUri!), (edit).options ); } diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index c603c6a0df..7dfdb861d9 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -420,7 +420,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac } return this._proxy.$startFileSearch(includePattern, includeFolder, excludePatternOrDisregardExcludes, maxResults, token) - .then(data => Array.isArray(data) ? data.map(URI.revive) : []); + .then(data => Array.isArray(data) ? data.map(d => URI.revive(d)) : []); } findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, callback: (result: vscode.TextSearchResult) => void, extensionId: ExtensionIdentifier, token: vscode.CancellationToken = CancellationToken.None): Promise { diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index 9bd51b0ad2..1aeafb3c8c 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -414,7 +414,7 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews function reviveWebviewOptions(options: WebviewInputOptions): WebviewInputOptions { return { ...options, - localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(URI.revive) : undefined, + localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined, }; } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 19aa221c1a..218e920af8 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -3,11 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { TernarySearchTree } from 'vs/base/common/map'; import * as path from 'vs/base/common/path'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; @@ -16,7 +14,7 @@ import { OverviewRulerLane } from 'vs/editor/common/model'; import * as languageConfiguration from 'vs/editor/common/modes/languageConfiguration'; import { score } from 'vs/editor/common/modes/languageSelector'; import * as files from 'vs/platform/files/common/files'; -import { ExtHostContext, IInitData, IMainContext, MainContext, MainThreadKeytarShape, IEnvironment, MainThreadWindowShape, MainThreadTelemetryShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, IInitData, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostApiCommands } from 'vs/workbench/api/common/extHostApiCommands'; import { ExtHostClipboard } from 'vs/workbench/api/common/extHostClipboard'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -47,7 +45,7 @@ import { LogOutputChannelFactory } from 'vs/workbench/api/node/extHostOutputServ import { ExtHostProgress } from 'vs/workbench/api/common/extHostProgress'; import { ExtHostQuickOpen } from 'vs/workbench/api/common/extHostQuickOpen'; import { ExtHostSCM } from 'vs/workbench/api/common/extHostSCM'; -import { ExtHostSearch } from 'vs/workbench/api/node/extHostSearch'; +import { ExtHostSearch, registerEHSearchProviders } from 'vs/workbench/api/node/extHostSearch'; import { ExtHostStatusBar } from 'vs/workbench/api/common/extHostStatusBar'; import { ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ExtHostTask } from 'vs/workbench/api/node/extHostTask'; @@ -60,7 +58,7 @@ import { ExtHostUrls } from 'vs/workbench/api/common/extHostUrls'; import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; import { ExtHostWindow } from 'vs/workbench/api/common/extHostWindow'; import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { throwProposedApiError, checkProposedApiEnabled, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { throwProposedApiError, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as vscode from 'vscode'; @@ -69,7 +67,7 @@ import { originalFSPath } from 'vs/base/common/resources'; import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { withNullAsUndefined } from 'vs/base/common/types'; import { values } from 'vs/base/common/collections'; -import { endsWith } from 'vs/base/common/strings'; +import { Schemas } from 'vs/base/common/network'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -93,11 +91,11 @@ export function createApiFactory( extHostConfiguration: ExtHostConfiguration, extensionService: ExtHostExtensionService, extHostLogService: ExtHostLogService, - extHostStorage: ExtHostStorage + extHostStorage: ExtHostStorage, + schemeTransformer: ISchemeTransformer | null, + outputChannelName: string ): IExtensionApiFactory { - const schemeTransformer: ISchemeTransformer | null = null; - // Addressable instances rpcProtocol.set(ExtHostContext.ExtHostLogService, extHostLogService); const extHostHeapService = rpcProtocol.set(ExtHostContext.ExtHostHeapService, new ExtHostHeapService()); @@ -131,6 +129,14 @@ export function createApiFactory( const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, new ExtHostOutputService(LogOutputChannelFactory, initData.logsLocation, rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); if (initData.remoteAuthority) { + extHostTask.registerTaskSystem(Schemas.vscodeRemote, { + scheme: Schemas.vscodeRemote, + authority: initData.remoteAuthority, + platform: process.platform + }); + + registerEHSearchProviders(extHostSearch, extHostLogService); + const cliServer = new CLIServer(extHostCommands); process.env['VSCODE_IPC_HOOK_CLI'] = cliServer.ipcHandlePath; } @@ -147,8 +153,7 @@ export function createApiFactory( const extHostLanguages = new ExtHostLanguages(rpcProtocol, extHostDocuments); // Register an output channel for exthost log - const name = localize('extensionsLog', "Extension Host"); - extHostOutputService.createOutputChannelFromLogFile(name, extHostLogService.logFile); + extHostOutputService.createOutputChannelFromLogFile(outputChannelName, extHostLogService.logFile); // Register API-ish commands ExtHostApiCommands.register(extHostCommands); @@ -881,240 +886,3 @@ class Extension implements vscode.Extension { return this._extensionService.activateByIdWithErrors(this._identifier, new ExtensionActivatedByAPI(false)).then(() => this.exports); } } - -interface LoadFunction { - (request: string, parent: { filename: string; }, isMain: any): any; -} - -interface INodeModuleFactory { - readonly nodeModuleName: string | string[]; - load(request: string, parent: { filename: string; }, isMain: any, original: LoadFunction): any; - alternaiveModuleName?(name: string): string | undefined; -} - -export class NodeModuleRequireInterceptor { - public static INSTANCE = new NodeModuleRequireInterceptor(); - - private readonly _factories: Map; - private readonly _alternatives: ((moduleName: string) => string | undefined)[]; - - constructor() { - this._factories = new Map(); - this._alternatives = []; - this._installInterceptor(this._factories, this._alternatives); - } - - private _installInterceptor(factories: Map, alternatives: ((moduleName: string) => string | undefined)[]): void { - const node_module = require.__$__nodeRequire('module'); - const original = node_module._load; - node_module._load = function load(request: string, parent: { filename: string; }, isMain: any) { - for (let alternativeModuleName of alternatives) { - let alternative = alternativeModuleName(request); - if (alternative) { - request = alternative; - break; - } - } - if (!factories.has(request)) { - return original.apply(this, arguments); - } - return factories.get(request)!.load(request, parent, isMain, original); - }; - } - - public register(interceptor: INodeModuleFactory): void { - if (Array.isArray(interceptor.nodeModuleName)) { - for (let moduleName of interceptor.nodeModuleName) { - this._factories.set(moduleName, interceptor); - } - } else { - this._factories.set(interceptor.nodeModuleName, interceptor); - } - if (typeof interceptor.alternaiveModuleName === 'function') { - this._alternatives.push((moduleName) => { - return interceptor.alternaiveModuleName!(moduleName); - }); - } - } -} - -export class VSCodeNodeModuleFactory implements INodeModuleFactory { - public readonly nodeModuleName = 'vscode'; - - private readonly _extApiImpl = new Map(); - private _defaultApiImpl: typeof vscode; - - constructor( - private readonly _apiFactory: IExtensionApiFactory, - private readonly _extensionPaths: TernarySearchTree, - private readonly _extensionRegistry: ExtensionDescriptionRegistry, - private readonly _configProvider: ExtHostConfigProvider - ) { - } - - public load(request: string, parent: { filename: string; }): any { - - // get extension id from filename and api for extension - const ext = this._extensionPaths.findSubstr(URI.file(parent.filename).fsPath); - if (ext) { - let apiImpl = this._extApiImpl.get(ExtensionIdentifier.toKey(ext.identifier)); - if (!apiImpl) { - apiImpl = this._apiFactory(ext, this._extensionRegistry, this._configProvider); - this._extApiImpl.set(ExtensionIdentifier.toKey(ext.identifier), apiImpl); - } - return apiImpl; - } - - // fall back to a default implementation - if (!this._defaultApiImpl) { - let extensionPathsPretty = ''; - this._extensionPaths.forEach((value, index) => extensionPathsPretty += `\t${index} -> ${value.identifier.value}\n`); - console.warn(`Could not identify extension for 'vscode' require call from ${parent.filename}. These are the extension path mappings: \n${extensionPathsPretty}`); - this._defaultApiImpl = this._apiFactory(nullExtensionDescription, this._extensionRegistry, this._configProvider); - } - return this._defaultApiImpl; - } -} - -interface IKeytarModule { - getPassword(service: string, account: string): Promise; - setPassword(service: string, account: string, password: string): Promise; - deletePassword(service: string, account: string): Promise; - findPassword(service: string): Promise; -} - -export class KeytarNodeModuleFactory implements INodeModuleFactory { - public readonly nodeModuleName: string = 'keytar'; - - private alternativeNames: Set | undefined; - private _impl: IKeytarModule; - - constructor(mainThreadKeytar: MainThreadKeytarShape, environment: IEnvironment) { - if (environment.appRoot) { - let appRoot = environment.appRoot.fsPath; - if (process.platform === 'win32') { - appRoot = appRoot.replace(/\\/g, '/'); - } - if (appRoot[appRoot.length - 1] === '/') { - appRoot = appRoot.substr(0, appRoot.length - 1); - } - this.alternativeNames = new Set(); - this.alternativeNames.add(`${appRoot}/node_modules.asar/keytar`); - this.alternativeNames.add(`${appRoot}/node_modules/keytar`); - } - this._impl = { - getPassword: (service: string, account: string): Promise => { - return mainThreadKeytar.$getPassword(service, account); - }, - setPassword: (service: string, account: string, password: string): Promise => { - return mainThreadKeytar.$setPassword(service, account, password); - }, - deletePassword: (service: string, account: string): Promise => { - return mainThreadKeytar.$deletePassword(service, account); - }, - findPassword: (service: string): Promise => { - return mainThreadKeytar.$findPassword(service); - } - }; - } - - public load(request: string, parent: { filename: string; }): any { - return this._impl; - } - - public alternaiveModuleName(name: string): string | undefined { - const length = name.length; - // We need at least something like: `?/keytar` which requires - // more than 7 characters. - if (length <= 7 || !this.alternativeNames) { - return undefined; - } - const sep = length - 7; - if ((name.charAt(sep) === '/' || name.charAt(sep) === '\\') && endsWith(name, 'keytar')) { - name = name.replace(/\\/g, '/'); - if (this.alternativeNames.has(name)) { - return 'keytar'; - } - } - return undefined; - } -} - -interface OpenOptions { - wait: boolean; - app: string | string[]; -} - -interface IOriginalOpen { - (target: string, options?: OpenOptions): Thenable; -} - -interface IOpenModule { - (target: string, options?: OpenOptions): Thenable; -} - -export class OpenNodeModuleFactory implements INodeModuleFactory { - - public readonly nodeModuleName: string[] = ['open', 'opn']; - - private _extensionId: string | undefined; - private _original: IOriginalOpen; - private _impl: IOpenModule; - - constructor(mainThreadWindow: MainThreadWindowShape, private _mainThreadTelemerty: MainThreadTelemetryShape, private readonly _extensionPaths: TernarySearchTree) { - this._impl = (target, options) => { - const uri: URI = URI.parse(target); - // If we have options use the original method. - if (options) { - return this.callOriginal(target, options); - } - if (uri.scheme === 'http' || uri.scheme === 'https') { - return mainThreadWindow.$openUri(uri); - } else if (uri.scheme === 'mailto') { - return mainThreadWindow.$openUri(uri); - } - return this.callOriginal(target, options); - }; - } - - public load(request: string, parent: { filename: string; }, isMain: any, original: LoadFunction): any { - // get extension id from filename and api for extension - const extension = this._extensionPaths.findSubstr(URI.file(parent.filename).fsPath); - if (extension) { - this._extensionId = extension.identifier.value; - this.sendShimmingTelemetry(); - } - - this._original = original(request, parent, isMain); - return this._impl; - } - - private callOriginal(target: string, options: OpenOptions | undefined): Thenable { - this.sendNoForwardTelemetry(); - return this._original(target, options); - } - - private sendShimmingTelemetry(): void { - if (!this._extensionId) { - return; - } - /* __GDPR__ - "shimming.open" : { - "extension": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this._mainThreadTelemerty.$publicLog('shimming.open', { extension: this._extensionId }); - } - - private sendNoForwardTelemetry(): void { - if (!this._extensionId) { - return; - } - /* __GDPR__ - "shimming.open.call.noForward" : { - "extension": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this._mainThreadTelemerty.$publicLog('shimming.open.call.noForward', { extension: this._extensionId }); - } -} diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index 78eae2800b..ed895772d7 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -26,6 +26,12 @@ export interface StatusPipeArgs { type: 'status'; } +export interface RunCommandPipeArgs { + type: 'command'; + command: string; + args: string[]; +} + export class CLIServer { private _server: http.Server; @@ -61,7 +67,7 @@ export class CLIServer { req.setEncoding('utf8'); req.on('data', (d: string) => chunks.push(d)); req.on('end', () => { - const data: OpenCommandPipeArgs | StatusPipeArgs | any = JSON.parse(chunks.join('')); + const data: OpenCommandPipeArgs | StatusPipeArgs | RunCommandPipeArgs | any = JSON.parse(chunks.join('')); switch (data.type) { case 'open': this.open(data, res); @@ -69,6 +75,10 @@ export class CLIServer { case 'status': this.getStatus(data, res); break; + case 'command': + this.runCommand(data, res) + .catch(console.error); + break; default: res.writeHead(404); res.write(`Unkown message type: ${data.type}`, err => { @@ -135,6 +145,28 @@ export class CLIServer { } } + private async runCommand(data: RunCommandPipeArgs, res: http.ServerResponse) { + try { + const { command, args } = data; + const result = await this._commands.executeCommand(command, ...args); + res.writeHead(200); + res.write(JSON.stringify(result), err => { + if (err) { + console.error(err); + } + }); + res.end(); + } catch (err) { + res.writeHead(500); + res.write(String(err), err => { + if (err) { + console.error(err); + } + }); + res.end(); + } + } + dispose(): void { this._server.close(); @@ -142,4 +174,4 @@ export class CLIServer { fs.unlinkSync(this._ipcHandlePath); } } -} +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index b2efdf0ae1..e75e90bd24 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -9,17 +9,16 @@ import * as path from 'vs/base/common/path'; import { createApiFactory, initializeExtensionApi, ISqlExtensionApiFactory } from 'sql/workbench/api/node/sqlExtHost.api.impl'; import { originalFSPath } from 'vs/base/common/resources'; import { Barrier } from 'vs/base/common/async'; -import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { dispose, toDisposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; -import * as pfs from 'vs/base/node/pfs'; import { ILogService } from 'vs/platform/log/common/log'; // {{SQL CARBON EDIT}} - Remove createApiFactory initializeExtensionApi, and IExtensionApiFactory imports -//import { createApiFactory, IExtensionApiFactory, NodeModuleRequireInterceptor, VSCodeNodeModuleFactory } from 'vs/workbench/api/node/extHost.api.impl'; -import { NodeModuleRequireInterceptor, KeytarNodeModuleFactory, OpenNodeModuleFactory } from 'vs/workbench/api/node/extHost.api.impl'; -import { ExtHostExtensionServiceShape, IEnvironment, IInitData, IMainContext, MainContext, MainThreadExtensionServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape, IStaticWorkspaceData } from 'vs/workbench/api/common/extHost.protocol'; +// import { createApiFactory, IExtensionApiFactory } from 'vs/workbench/api/node/extHost.api.impl'; +import { NodeModuleRequireInterceptor, VSCodeNodeModuleFactory, KeytarNodeModuleFactory, OpenNodeModuleFactory } from 'vs/workbench/api/node/extHostRequireInterceptor'; +import { ExtHostExtensionServiceShape, IEnvironment, IInitData, IMainContext, MainContext, MainThreadExtensionServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; -import { ActivatedExtension, EmptyExtension, ExtensionActivatedByAPI, ExtensionActivatedByEvent, ExtensionActivationReason, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionContext, IExtensionMemento, IExtensionModule, HostExtension } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { ActivatedExtension, EmptyExtension, ExtensionActivatedByAPI, ExtensionActivatedByEvent, ExtensionActivationReason, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionContext, IExtensionModule, HostExtension } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; import { ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; @@ -34,134 +33,26 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { IWorkspace } from 'vs/platform/workspace/common/workspace'; import { Schemas } from 'vs/base/common/network'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { realpath } from 'vs/base/node/extpath'; import { VSBuffer } from 'vs/base/common/buffer'; - -class ExtensionMemento implements IExtensionMemento { - - private readonly _id: string; - private readonly _shared: boolean; - private readonly _storage: ExtHostStorage; - - private readonly _init: Promise; - private _value: { [n: string]: any; }; - private readonly _storageListener: IDisposable; - - constructor(id: string, global: boolean, storage: ExtHostStorage) { - this._id = id; - this._shared = global; - this._storage = storage; - - this._init = this._storage.getValue(this._shared, this._id, Object.create(null)).then(value => { - this._value = value; - return this; - }); - - this._storageListener = this._storage.onDidChangeStorage(e => { - if (e.shared === this._shared && e.key === this._id) { - this._value = e.value; - } - }); - } - - get whenReady(): Promise { - return this._init; - } - - get(key: string, defaultValue: T): T { - let value = this._value[key]; - if (typeof value === 'undefined') { - value = defaultValue; - } - return value; - } - - update(key: string, value: any): Promise { - this._value[key] = value; - return this._storage - .setValue(this._shared, this._id, this._value) - .then(() => true); - } - - dispose(): void { - this._storageListener.dispose(); - } -} - -class ExtensionStoragePath { - - private readonly _workspace?: IStaticWorkspaceData; - private readonly _environment: IEnvironment; - - private readonly _ready: Promise; - private _value?: string; - - constructor(workspace: IStaticWorkspaceData | undefined, environment: IEnvironment) { - this._workspace = workspace; - this._environment = environment; - this._ready = this._getOrCreateWorkspaceStoragePath().then(value => this._value = value); - } - - get whenReady(): Promise { - return this._ready; - } - - workspaceValue(extension: IExtensionDescription): string | undefined { - if (this._value) { - return path.join(this._value, extension.identifier.value); - } - return undefined; - } - - globalValue(extension: IExtensionDescription): string { - return path.join(this._environment.globalStorageHome.fsPath, extension.identifier.value.toLowerCase()); - } - - private async _getOrCreateWorkspaceStoragePath(): Promise { - if (!this._workspace) { - return Promise.resolve(undefined); - } - - if (!this._environment.appSettingsHome) { - return undefined; - } - const storageName = this._workspace.id; - const storagePath = path.join(this._environment.appSettingsHome.fsPath, 'workspaceStorage', storageName); - - const exists = await pfs.dirExists(storagePath); - - if (exists) { - return storagePath; - } - - try { - await pfs.mkdirp(storagePath); - await pfs.writeFile( - path.join(storagePath, 'meta.json'), - JSON.stringify({ - id: this._workspace.id, - configuration: this._workspace.configuration && URI.revive(this._workspace.configuration).toString(), - name: this._workspace.name - }, undefined, 2) - ); - return storagePath; - - } catch (e) { - console.error(e); - return undefined; - } - } -} +import { ISchemeTransformer } from 'vs/workbench/api/common/extHostLanguageFeatures'; +import { ExtensionMemento } from 'vs/workbench/api/common/extHostMemento'; +import { ExtensionStoragePaths } from 'vs/workbench/api/node/extHostStoragePaths'; interface ITestRunner { run(testsRoot: string, clb: (error: Error, failures?: number) => void): void; } +export interface IHostUtils { + exit(code?: number): void; + exists(path: string): Promise; + realpath(path: string): Promise; +} + export class ExtHostExtensionService implements ExtHostExtensionServiceShape { private static readonly WORKSPACE_CONTAINS_TIMEOUT = 7000; - private readonly _nativeExit: (code?: number) => void; + private readonly _hostUtils: IHostUtils; private readonly _initData: IInitData; private readonly _extHostContext: IMainContext; private readonly _extHostWorkspace: ExtHostWorkspace; @@ -177,7 +68,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { private readonly _readyToRunExtensions: Barrier; private readonly _registry: ExtensionDescriptionRegistry; private readonly _storage: ExtHostStorage; - private readonly _storagePath: ExtensionStoragePath; + private readonly _storagePath: ExtensionStoragePaths; private readonly _activator: ExtensionsActivator; private _extensionPathIndex: Promise> | null; private readonly _extensionApiFactory: ISqlExtensionApiFactory; @@ -187,15 +78,17 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { private _started: boolean; constructor( - nativeExit: (code?: number) => void, + hostUtils: IHostUtils, initData: IInitData, extHostContext: IMainContext, extHostWorkspace: ExtHostWorkspace, extHostConfiguration: ExtHostConfiguration, environment: IEnvironment, - extHostLogService: ExtHostLogService + extHostLogService: ExtHostLogService, + schemeTransformer: ISchemeTransformer | null, + outputChannelName: string ) { - this._nativeExit = nativeExit; + this._hostUtils = hostUtils; this._initData = initData; this._extHostContext = extHostContext; this._extHostWorkspace = extHostWorkspace; @@ -211,7 +104,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { this._readyToRunExtensions = new Barrier(); this._registry = new ExtensionDescriptionRegistry(initData.extensions); this._storage = new ExtHostStorage(this._extHostContext); - this._storagePath = new ExtensionStoragePath(withNullAsUndefined(initData.workspace), initData.environment); + this._storagePath = new ExtensionStoragePaths(withNullAsUndefined(initData.workspace), initData.environment); const hostExtensions = new Set(); initData.hostExtensions.forEach((extensionId) => hostExtensions.add(ExtensionIdentifier.toKey(extensionId))); @@ -234,7 +127,17 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { this._extensionPathIndex = null; // initialize API first (i.e. do not release barrier until the API is initialized) - this._extensionApiFactory = createApiFactory(this._initData, this._extHostContext, this._extHostWorkspace, this._extHostConfiguration, this, this._extHostLogService, this._storage); + this._extensionApiFactory = createApiFactory( + this._initData, + this._extHostContext, + this._extHostWorkspace, + this._extHostConfiguration, + this, + this._extHostLogService, + this._storage, + schemeTransformer, + outputChannelName + ); this._resolvers = Object.create(null); @@ -337,7 +240,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { if (!ext.main) { return undefined; } - return realpath(ext.extensionLocation.fsPath).then(value => tree.set(URI.file(value).fsPath, ext)); + return this._hostUtils.realpath(ext.extensionLocation.fsPath).then(value => tree.set(URI.file(value).fsPath, ext)); }); this._extensionPathIndex = Promise.all(extensions).then(() => tree); } @@ -558,7 +461,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { // find exact path for (const { uri } of workspace.folders) { - if (await pfs.exists(path.join(URI.revive(uri).fsPath, fileName))) { + if (await this._hostUtils.exists(path.join(URI.revive(uri).fsPath, fileName))) { // the file was found return ( this._activateById(extensionId, new ExtensionActivatedByEvent(true, `workspaceContains:${fileName}`)) @@ -668,7 +571,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { return; } - this._nativeExit(code); + this._hostUtils.exit(code); }, 500); } @@ -752,12 +655,12 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { if (!extensionDescription) { return; } - const realpathValue = await realpath(extensionDescription.extensionLocation.fsPath); + const realpathValue = await this._hostUtils.realpath(extensionDescription.extensionLocation.fsPath); trie.delete(URI.file(realpathValue).fsPath); })); await Promise.all(toAdd.map(async (extensionDescription) => { - const realpathValue = await realpath(extensionDescription.extensionLocation.fsPath); + const realpathValue = await this._hostUtils.realpath(extensionDescription.extensionLocation.fsPath); trie.set(URI.file(realpathValue).fsPath, extensionDescription); })); @@ -774,7 +677,12 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { } public async $test_down(size: number): Promise { - return VSBuffer.wrap(Buffer.alloc(size, Math.random() % 256)); + let buff = VSBuffer.alloc(size); + let value = Math.random() % 256; + for (let i = 0; i < size; i++) { + buff.writeUint8(value, i); + } + return buff; } } diff --git a/src/vs/workbench/api/node/extHostRequireInterceptor.ts b/src/vs/workbench/api/node/extHostRequireInterceptor.ts new file mode 100644 index 0000000000..0093594850 --- /dev/null +++ b/src/vs/workbench/api/node/extHostRequireInterceptor.ts @@ -0,0 +1,253 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TernarySearchTree } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; +import { MainThreadKeytarShape, IEnvironment, MainThreadWindowShape, MainThreadTelemetryShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import * as vscode from 'vscode'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { endsWith } from 'vs/base/common/strings'; +import { IExtensionApiFactory } from 'vs/workbench/api/node/extHost.api.impl'; + + +interface LoadFunction { + (request: string, parent: { filename: string; }, isMain: any): any; +} + +interface INodeModuleFactory { + readonly nodeModuleName: string | string[]; + load(request: string, parent: { filename: string; }, isMain: any, original: LoadFunction): any; + alternaiveModuleName?(name: string): string | undefined; +} + +export class NodeModuleRequireInterceptor { + public static INSTANCE = new NodeModuleRequireInterceptor(); + + private readonly _factories: Map; + private readonly _alternatives: ((moduleName: string) => string | undefined)[]; + + constructor() { + this._factories = new Map(); + this._alternatives = []; + this._installInterceptor(this._factories, this._alternatives); + } + + private _installInterceptor(factories: Map, alternatives: ((moduleName: string) => string | undefined)[]): void { + const node_module = require.__$__nodeRequire('module'); + const original = node_module._load; + node_module._load = function load(request: string, parent: { filename: string; }, isMain: any) { + for (let alternativeModuleName of alternatives) { + let alternative = alternativeModuleName(request); + if (alternative) { + request = alternative; + break; + } + } + if (!factories.has(request)) { + return original.apply(this, arguments); + } + return factories.get(request)!.load(request, parent, isMain, original); + }; + } + + public register(interceptor: INodeModuleFactory): void { + if (Array.isArray(interceptor.nodeModuleName)) { + for (let moduleName of interceptor.nodeModuleName) { + this._factories.set(moduleName, interceptor); + } + } else { + this._factories.set(interceptor.nodeModuleName, interceptor); + } + if (typeof interceptor.alternaiveModuleName === 'function') { + this._alternatives.push((moduleName) => { + return interceptor.alternaiveModuleName!(moduleName); + }); + } + } +} + +export class VSCodeNodeModuleFactory implements INodeModuleFactory { + public readonly nodeModuleName = 'vscode'; + + private readonly _extApiImpl = new Map(); + private _defaultApiImpl: typeof vscode; + + constructor( + private readonly _apiFactory: IExtensionApiFactory, + private readonly _extensionPaths: TernarySearchTree, + private readonly _extensionRegistry: ExtensionDescriptionRegistry, + private readonly _configProvider: ExtHostConfigProvider + ) { + } + + public load(request: string, parent: { filename: string; }): any { + + // get extension id from filename and api for extension + const ext = this._extensionPaths.findSubstr(URI.file(parent.filename).fsPath); + if (ext) { + let apiImpl = this._extApiImpl.get(ExtensionIdentifier.toKey(ext.identifier)); + if (!apiImpl) { + apiImpl = this._apiFactory(ext, this._extensionRegistry, this._configProvider); + this._extApiImpl.set(ExtensionIdentifier.toKey(ext.identifier), apiImpl); + } + return apiImpl; + } + + // fall back to a default implementation + if (!this._defaultApiImpl) { + let extensionPathsPretty = ''; + this._extensionPaths.forEach((value, index) => extensionPathsPretty += `\t${index} -> ${value.identifier.value}\n`); + console.warn(`Could not identify extension for 'vscode' require call from ${parent.filename}. These are the extension path mappings: \n${extensionPathsPretty}`); + this._defaultApiImpl = this._apiFactory(nullExtensionDescription, this._extensionRegistry, this._configProvider); + } + return this._defaultApiImpl; + } +} + +interface IKeytarModule { + getPassword(service: string, account: string): Promise; + setPassword(service: string, account: string, password: string): Promise; + deletePassword(service: string, account: string): Promise; + findPassword(service: string): Promise; +} + +export class KeytarNodeModuleFactory implements INodeModuleFactory { + public readonly nodeModuleName: string = 'keytar'; + + private alternativeNames: Set | undefined; + private _impl: IKeytarModule; + + constructor(mainThreadKeytar: MainThreadKeytarShape, environment: IEnvironment) { + if (environment.appRoot) { + let appRoot = environment.appRoot.fsPath; + if (process.platform === 'win32') { + appRoot = appRoot.replace(/\\/g, '/'); + } + if (appRoot[appRoot.length - 1] === '/') { + appRoot = appRoot.substr(0, appRoot.length - 1); + } + this.alternativeNames = new Set(); + this.alternativeNames.add(`${appRoot}/node_modules.asar/keytar`); + this.alternativeNames.add(`${appRoot}/node_modules/keytar`); + } + this._impl = { + getPassword: (service: string, account: string): Promise => { + return mainThreadKeytar.$getPassword(service, account); + }, + setPassword: (service: string, account: string, password: string): Promise => { + return mainThreadKeytar.$setPassword(service, account, password); + }, + deletePassword: (service: string, account: string): Promise => { + return mainThreadKeytar.$deletePassword(service, account); + }, + findPassword: (service: string): Promise => { + return mainThreadKeytar.$findPassword(service); + } + }; + } + + public load(request: string, parent: { filename: string; }): any { + return this._impl; + } + + public alternaiveModuleName(name: string): string | undefined { + const length = name.length; + // We need at least something like: `?/keytar` which requires + // more than 7 characters. + if (length <= 7 || !this.alternativeNames) { + return undefined; + } + const sep = length - 7; + if ((name.charAt(sep) === '/' || name.charAt(sep) === '\\') && endsWith(name, 'keytar')) { + name = name.replace(/\\/g, '/'); + if (this.alternativeNames.has(name)) { + return 'keytar'; + } + } + return undefined; + } +} + +interface OpenOptions { + wait: boolean; + app: string | string[]; +} + +interface IOriginalOpen { + (target: string, options?: OpenOptions): Thenable; +} + +interface IOpenModule { + (target: string, options?: OpenOptions): Thenable; +} + +export class OpenNodeModuleFactory implements INodeModuleFactory { + + public readonly nodeModuleName: string[] = ['open', 'opn']; + + private _extensionId: string | undefined; + private _original: IOriginalOpen; + private _impl: IOpenModule; + + constructor(mainThreadWindow: MainThreadWindowShape, private _mainThreadTelemerty: MainThreadTelemetryShape, private readonly _extensionPaths: TernarySearchTree) { + this._impl = (target, options) => { + const uri: URI = URI.parse(target); + // If we have options use the original method. + if (options) { + return this.callOriginal(target, options); + } + if (uri.scheme === 'http' || uri.scheme === 'https') { + return mainThreadWindow.$openUri(uri); + } else if (uri.scheme === 'mailto') { + return mainThreadWindow.$openUri(uri); + } + return this.callOriginal(target, options); + }; + } + + public load(request: string, parent: { filename: string; }, isMain: any, original: LoadFunction): any { + // get extension id from filename and api for extension + const extension = this._extensionPaths.findSubstr(URI.file(parent.filename).fsPath); + if (extension) { + this._extensionId = extension.identifier.value; + this.sendShimmingTelemetry(); + } + + this._original = original(request, parent, isMain); + return this._impl; + } + + private callOriginal(target: string, options: OpenOptions | undefined): Thenable { + this.sendNoForwardTelemetry(); + return this._original(target, options); + } + + private sendShimmingTelemetry(): void { + if (!this._extensionId) { + return; + } + /* __GDPR__ + "shimming.open" : { + "extension": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this._mainThreadTelemerty.$publicLog('shimming.open', { extension: this._extensionId }); + } + + private sendNoForwardTelemetry(): void { + if (!this._extensionId) { + return; + } + /* __GDPR__ + "shimming.open.call.noForward" : { + "extension": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this._mainThreadTelemerty.$publicLog('shimming.open.call.noForward', { extension: this._extensionId }); + } +} diff --git a/src/vs/workbench/api/node/extHostStoragePaths.ts b/src/vs/workbench/api/node/extHostStoragePaths.ts new file mode 100644 index 0000000000..0adb497de0 --- /dev/null +++ b/src/vs/workbench/api/node/extHostStoragePaths.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import * as pfs from 'vs/base/node/pfs'; +import { IEnvironment, IStaticWorkspaceData } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; + +export class ExtensionStoragePaths { + + private readonly _workspace?: IStaticWorkspaceData; + private readonly _environment: IEnvironment; + + private readonly _ready: Promise; + private _value?: string; + + constructor(workspace: IStaticWorkspaceData | undefined, environment: IEnvironment) { + this._workspace = workspace; + this._environment = environment; + this._ready = this._getOrCreateWorkspaceStoragePath().then(value => this._value = value); + } + + get whenReady(): Promise { + return this._ready; + } + + workspaceValue(extension: IExtensionDescription): string | undefined { + if (this._value) { + return path.join(this._value, extension.identifier.value); + } + return undefined; + } + + globalValue(extension: IExtensionDescription): string { + return path.join(this._environment.globalStorageHome.fsPath, extension.identifier.value.toLowerCase()); + } + + private async _getOrCreateWorkspaceStoragePath(): Promise { + if (!this._workspace) { + return Promise.resolve(undefined); + } + + if (!this._environment.appSettingsHome) { + return undefined; + } + const storageName = this._workspace.id; + const storagePath = path.join(this._environment.appSettingsHome.fsPath, 'workspaceStorage', storageName); + + const exists = await pfs.dirExists(storagePath); + + if (exists) { + return storagePath; + } + + try { + await pfs.mkdirp(storagePath); + await pfs.writeFile( + path.join(storagePath, 'meta.json'), + JSON.stringify({ + id: this._workspace.id, + configuration: this._workspace.configuration && URI.revive(this._workspace.configuration).toString(), + name: this._workspace.name + }, undefined, 2) + ); + return storagePath; + + } catch (e) { + console.error(e); + return undefined; + } + } +} diff --git a/src/vs/workbench/browser/nodeless.main.ts b/src/vs/workbench/browser/nodeless.main.ts index f570de9f1e..8096f6eff7 100644 --- a/src/vs/workbench/browser/nodeless.main.ts +++ b/src/vs/workbench/browser/nodeless.main.ts @@ -20,6 +20,7 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService3 } from 'vs/workbench/services/files2/browser/fileService2'; +import { Schemas } from 'vs/base/common/network'; class CodeRendererMain extends Disposable { @@ -84,7 +85,7 @@ class CodeRendererMain extends Disposable { if (connection) { const channel = connection.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME); const remoteFileSystemProvider = this._register(new RemoteExtensionsFileSystemProvider(channel, remoteAgentService.getEnvironment())); - fileService.registerProvider('vscode-remote', remoteFileSystemProvider); + fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); } return { serviceCollection, logService }; diff --git a/src/vs/workbench/browser/nodeless.simpleservices.ts b/src/vs/workbench/browser/nodeless.simpleservices.ts index 3896079251..177caa92d7 100644 --- a/src/vs/workbench/browser/nodeless.simpleservices.ts +++ b/src/vs/workbench/browser/nodeless.simpleservices.ts @@ -61,7 +61,7 @@ import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export const workspaceResource = URI.from({ - scheme: 'vscode-remote', + scheme: Schemas.vscodeRemote, authority: document.location.host, path: (self).USER_HOME_DIR || '/' }); @@ -821,6 +821,9 @@ export class SimpleTelemetryService implements ITelemetryService { return Promise.resolve(undefined); } + setEnabled(value: boolean): void { + } + getTelemetryInfo(): Promise { return Promise.resolve({ instanceId: 'someValue.instanceId', diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 2f13d0be00..884cd78691 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -5,7 +5,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import * as nls from 'vs/nls'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { Action, IAction } from 'vs/base/common/actions'; import { IEditorQuickOpenEntry, IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import { StatusbarItemDescriptor, IStatusbarRegistry, Extensions as StatusExtensions } from 'vs/workbench/browser/parts/statusbar/statusbar'; @@ -147,7 +147,7 @@ class UntitledEditorInputFactory implements IEditorInputFactory { deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): UntitledEditorInput { return instantiationService.invokeFunction(accessor => { const deserialized: ISerializedUntitledEditorInput = JSON.parse(serializedEditorInput); - const resource = !!deserialized.resourceJSON ? URI.revive(deserialized.resourceJSON) : URI.parse(deserialized.resource); + const resource = !!deserialized.resourceJSON ? URI.revive(deserialized.resourceJSON) : URI.parse(deserialized.resource); const filePath = resource.scheme === Schemas.untitled ? undefined : resource.scheme === Schemas.file ? resource.fsPath : resource.path; const language = deserialized.modeId; const encoding = deserialized.encoding; diff --git a/src/vs/workbench/browser/parts/editor/editorWidgets.ts b/src/vs/workbench/browser/parts/editor/editorWidgets.ts index f5f583e2c0..17825d8555 100644 --- a/src/vs/workbench/browser/parts/editor/editorWidgets.ts +++ b/src/vs/workbench/browser/parts/editor/editorWidgets.ts @@ -163,7 +163,7 @@ export class OpenWorkspaceButtonContribution extends Disposable implements IEdit this._register(this.openWorkspaceButton.onClick(() => { const model = this.editor.getModel(); if (model) { - this.windowService.openWindow([{ fileUri: model.uri }]); + this.windowService.openWindow([{ workspaceUri: model.uri }]); } })); diff --git a/src/vs/workbench/browser/parts/editor/resourceViewer.ts b/src/vs/workbench/browser/parts/editor/resourceViewer.ts index c24363743f..71a49c90dd 100644 --- a/src/vs/workbench/browser/parts/editor/resourceViewer.ts +++ b/src/vs/workbench/browser/parts/editor/resourceViewer.ts @@ -108,7 +108,7 @@ export class ResourceViewer { } class ImageView { - private static readonly MAX_IMAGE_SIZE = BinarySize.MB; // showing images inline is memory intense, so we have a limit + private static readonly MAX_IMAGE_SIZE = BinarySize.MB * 10; // showing images inline is memory intense, so we have a limit private static readonly BASE64_MARKER = 'base64,'; static create( diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index cc32e5df65..90bb9e3693 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -266,7 +266,8 @@ export class PanelViewlet extends Viewlet { let title = Registry.as(Extensions.Viewlets).getViewlet(this.getId()).name; if (this.isSingleView()) { - title = `${title}: ${this.panelItems[0].panel.title}`; + const panelItemTitle = this.panelItems[0].panel.title; + title = panelItemTitle ? `${title}: ${panelItemTitle}` : title; } return title; diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index 3f3fcb57dd..070040bf4e 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -324,8 +324,8 @@ export class NotificationViewItem extends Disposable implements INotificationVie private static MAX_MESSAGE_LENGTH = 1000; // Example link: "Some message with [link text](http://link.href)." - // RegEx: [, anything not ], ], (, http:|https:, //, no whitespace) - private static LINK_REGEX = /\[([^\]]+)\]\((https?:\/\/[^\)\s]+)\)/gi; + // RegEx: [, anything not ], ], (, http://|https://|command:, no whitespace) + private static LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)\)/gi; private _expanded: boolean; diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 174438adf9..120e40e5d0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -250,7 +250,8 @@ export class DebugHoverWidget implements IContentWidget { } private layoutTreeAndContainer(): void { - const treeHeight = Math.min(MAX_TREE_HEIGHT, this.tree.contentHeight); + const scrollBarHeight = 8; + const treeHeight = Math.min(MAX_TREE_HEIGHT, this.tree.contentHeight + scrollBarHeight); this.treeContainer.style.height = `${treeHeight}px`; this.tree.layout(treeHeight, 324); } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 8031a80a62..51356eb37f 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -720,11 +720,25 @@ class ReplDelegate implements IListVirtualDelegate { getHeight(element: IReplElement): number { // Give approximate heights. Repl has dynamic height so the tree will measure the actual height on its own. const fontSize = this.configurationService.getValue('debug').console.fontSize; + const rowHeight = Math.ceil(1.4 * fontSize); if (element instanceof Expression && element.hasChildren) { - return Math.ceil(2 * 1.4 * fontSize); + return 2 * rowHeight; } - return Math.ceil(1.4 * fontSize); + // In order to keep scroll position we need to give a good approximation to the tree + if (element instanceof SimpleReplElement) { + // For every 150 characters increase the number of lines needed + let count = Math.ceil(element.value.length / 150); + for (let i = 0; i < element.value.length; i++) { + if (element.value[i] === '\n' || element.value[i] === '\r\n') { + count++; + } + } + + return Math.max(1, count) * rowHeight; + } + + return rowHeight; } getTemplateId(element: IReplElement): string { diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index 92c75fb6ba..0200ca7645 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -10,6 +10,7 @@ import { Expression, SimpleReplElement, RawObjectReplElement } from 'vs/workbenc import { isUndefinedOrNull, isObject } from 'vs/base/common/types'; import { basenameOrAuthority } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; +import { endsWith } from 'vs/base/common/strings'; const MAX_REPL_LENGTH = 10000; let topReplElementCounter = 0; @@ -39,8 +40,13 @@ export class ReplModel { } if (typeof data === 'string') { - const element = new SimpleReplElement(`topReplElement:${topReplElementCounter++}`, data.trimRight(), sev, source); - this.addReplElement(element); + const previousElement = this.replElements.length ? this.replElements[this.replElements.length - 1] : undefined; + if (previousElement instanceof SimpleReplElement && previousElement.severity === sev && !endsWith(previousElement.value, '\n') && !endsWith(previousElement.value, '\r\n')) { + previousElement.value += data; + } else { + const element = new SimpleReplElement(`topReplElement:${topReplElementCounter++}`, data, sev, source); + this.addReplElement(element); + } } else { // TODO@Isidor hack, we should introduce a new type which is an output that can fetch children like an expression (data).severity = sev; diff --git a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts index cd1784cfeb..faca5408e2 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts @@ -509,7 +509,7 @@ export class DebugService implements IDebugService { // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 if (isExtensionHostDebugging(session.configuration) && session.state === State.Running && session.configuration.noDebug) { - this.extensionHostDebugService.close(session.root.uri); + this.extensionHostDebugService.close(session.getId()); } this.telemetryDebugSessionStop(session, adapterExitEvent); @@ -556,8 +556,8 @@ export class DebugService implements IDebugService { return runTasks().then(taskResult => taskResult === TaskRunResult.Success ? session.restart() : undefined); } - if (isExtensionHostDebugging(session.configuration) && session.root) { - return runTasks().then(taskResult => taskResult === TaskRunResult.Success ? this.extensionHostDebugService.reload(session.root.uri) : undefined); + if (isExtensionHostDebugging(session.configuration)) { + return runTasks().then(taskResult => taskResult === TaskRunResult.Success ? this.extensionHostDebugService.reload(session.getId()) : undefined); } const shouldFocus = this.viewModel.focusedSession && session.getId() === this.viewModel.focusedSession.getId(); diff --git a/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts index 6322804447..cfb4e2b257 100644 --- a/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts @@ -423,34 +423,41 @@ suite('Debug - Model', () => { const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); const repl = new ReplModel(session); repl.appendToRepl('first line\n', severity.Error); - repl.appendToRepl('second line', severity.Error); - repl.appendToRepl('third line', severity.Warning); + repl.appendToRepl('second line ', severity.Error); + repl.appendToRepl('third line ', severity.Error); repl.appendToRepl('fourth line', severity.Error); let elements = repl.getReplElements(); - assert.equal(elements.length, 4); - assert.equal(elements[0].value, 'first line'); + assert.equal(elements.length, 2); + assert.equal(elements[0].value, 'first line\n'); assert.equal(elements[0].severity, severity.Error); - assert.equal(elements[1].value, 'second line'); + assert.equal(elements[1].value, 'second line third line fourth line'); assert.equal(elements[1].severity, severity.Error); - assert.equal(elements[2].value, 'third line'); - assert.equal(elements[2].severity, severity.Warning); - assert.equal(elements[3].value, 'fourth line'); - assert.equal(elements[3].severity, severity.Error); repl.appendToRepl('1', severity.Warning); elements = repl.getReplElements(); - assert.equal(elements.length, 5); - assert.equal(elements[4].value, '1'); - assert.equal(elements[4].severity, severity.Warning); + assert.equal(elements.length, 3); + assert.equal(elements[2].value, '1'); + assert.equal(elements[2].severity, severity.Warning); const keyValueObject = { 'key1': 2, 'key2': 'value' }; repl.appendToRepl(new RawObjectReplElement('fakeid', 'fake', keyValueObject), severity.Info); - const element = repl.getReplElements()[5]; + const element = repl.getReplElements()[3]; assert.equal(element.value, 'Object'); assert.deepEqual(element.valueObj, keyValueObject); repl.removeReplExpressions(); assert.equal(repl.getReplElements().length, 0); + + repl.appendToRepl('1\n', severity.Info); + repl.appendToRepl('2', severity.Info); + repl.appendToRepl('3\n4', severity.Info); + repl.appendToRepl('5\n', severity.Info); + repl.appendToRepl('6', severity.Info); + elements = repl.getReplElements(); + assert.equal(elements.length, 3); + assert.equal(elements[0], '1\n'); + assert.equal(elements[1], '23\n45\n'); + assert.equal(elements[2], '6'); }); }); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts index a484405094..4988e0a978 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts @@ -28,7 +28,7 @@ import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, IExtension import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/electron-browser/extensionsWidgets'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, SystemDisabledLabelAction, SystemDisabledWarningAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; +import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, DisabledLabelAction, SystemDisabledWarningAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -395,7 +395,7 @@ export class ExtensionEditor extends BaseEditor { this.instantiationService.createInstance(RemoteInstallAction), combinedInstallAction, systemDisabledWarningAction, - this.instantiationService.createInstance(SystemDisabledLabelAction, systemDisabledWarningAction), + this.instantiationService.createInstance(DisabledLabelAction, systemDisabledWarningAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, true), ]; const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); @@ -623,9 +623,9 @@ export class ExtensionEditor extends BaseEditor { renderDashboardContributions(content, manifest, layout) ]; - const isEmpty = !renders.reduce((v, r) => r || v, false); scrollableContent.scanDomNode(); + const isEmpty = !renders.some(x => x); if (isEmpty) { append(content, $('p.nocontent')).textContent = localize('noContributions', "No Contributions"); append(this.content, content); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts index 2d04688d44..f9c4ebac54 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts @@ -21,6 +21,7 @@ import { randomPort } from 'vs/base/node/ports'; import product from 'vs/platform/product/node/product'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler'; export class ExtensionHostProfileService extends Disposable implements IExtensionHostProfileService { @@ -76,7 +77,8 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio return null; } - if (!this._extensionService.canProfileExtensionHost()) { + const inspectPort = this._extensionService.getInspectPort(); + if (!inspectPort) { return this._dialogService.confirm({ type: 'info', message: nls.localize('restart1', "Profile Extensions"), @@ -92,7 +94,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio this._setState(ProfileSessionState.Starting); - return this._extensionService.startExtensionHostProfile().then((value) => { + return this._instantiationService.createInstance(ExtensionHostProfiler, inspectPort).start().then((value) => { this._profileSession = value; this._setState(ProfileSessionState.Running); }, (err) => { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index 1355ad40a0..88eb2601df 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -251,6 +251,11 @@ Registry.as(ConfigurationExtensions.Configuration) description: localize('extensionsPolicy', "Sets the security policy for downloading extensions."), scope: ConfigurationScope.APPLICATION, default: ExtensionsPolicy.allowAll + }, + 'extensions.showInstalledExtensionsByDefault': { + type: 'boolean', + description: localize('extensions.showInstalledExtensionsByDefault', "When enabled, extensions view shows installed extensions view by default."), + default: false } } }); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts index 67d803af75..5c03c9e736 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts @@ -16,7 +16,7 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; // {{SQL CARBON EDIT}} import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewlet, AutoUpdateConfigurationKey, IExtensionContainer, EXTENSIONS_CONFIG, ExtensionsPolicy, ExtensionsPolicyKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; -import { IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel, IExtensionRecommendation, IGalleryExtension, IExtensionsConfigContent, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel, IExtensionRecommendation, IGalleryExtension, IExtensionsConfigContent, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -60,6 +60,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { isUIExtension } from 'vs/workbench/services/extensions/node/extensionsUtil'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; // {{SQL CARBON EDIT}} @@ -183,7 +184,7 @@ export class InstallAction extends ExtensionAction { return; } - this.enabled = this.extensionsWorkbenchService.canInstall(this.extension) && this.extension.state === ExtensionState.Uninstalled; + this.enabled = this.extensionsWorkbenchService.canInstall(this.extension) && !this.extensionsWorkbenchService.local.some(e => areSameExtensions(e.identifier, this.extension.identifier)); this.class = this.extension.state === ExtensionState.Installing ? InstallAction.InstallingClass : InstallAction.Class; this.updateLabel(); } @@ -720,7 +721,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction { ]); groups.push([this.instantiationService.createInstance(UninstallAction)]); groups.push([this.instantiationService.createInstance(InstallAnotherVersionAction)]); - groups.push([this.instantiationService.createInstance(ExtensionInfoAction)]); + groups.push([this.instantiationService.createInstance(ExtensionInfoAction), this.instantiationService.createInstance(ExtensionSettingsAction)]); groups.forEach(group => group.forEach(extensionAction => extensionAction.extension = this.extension)); @@ -829,6 +830,27 @@ export class ExtensionInfoAction extends ExtensionAction { } } +export class ExtensionSettingsAction extends ExtensionAction { + + static readonly ID = 'extensions.extensionSettings'; + static readonly LABEL = localize('extensionSettingsAction', "Configure Extension Settings"); + + constructor( + @IPreferencesService private readonly preferencesService: IPreferencesService + ) { + super(ExtensionSettingsAction.ID, ExtensionSettingsAction.LABEL); + this.update(); + } + + update(): void { + this.enabled = !!this.extension; + } + run(): Promise { + this.preferencesService.openSettings(false, `@ext:${this.extension.identifier.id}`); + return Promise.resolve(); + } +} + export class EnableForWorkspaceAction extends ExtensionAction { static readonly ID = 'extensions.enableForWorkspace'; @@ -1195,9 +1217,10 @@ export class ReloadAction extends ExtensionAction { } const isUninstalled = this.extension.state === ExtensionState.Uninstalled; const runningExtension = this._runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value }, this.extension.identifier))[0]; + const isSameExtensionRunning = runningExtension && this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(runningExtension.extensionLocation); if (isUninstalled) { - if (runningExtension) { + if (isSameExtensionRunning) { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio @@ -1210,7 +1233,6 @@ export class ReloadAction extends ExtensionAction { const isEnabled = this.extensionEnablementService.isEnabled(this.extension.local); if (runningExtension) { // Extension is running - const isSameExtensionRunning = this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(runningExtension.extensionLocation); const isSameVersionRunning = isSameExtensionRunning && this.extension.version === runningExtension.version; if (isEnabled) { if (!isSameVersionRunning && !this.extensionService.canAddExtension(toExtensionDescription(this.extension.local))) { @@ -2484,6 +2506,7 @@ export class StatusLabelAction extends Action implements IExtensionContainer { return canAddExtension() ? this.initialStatus === ExtensionState.Installed ? localize('updated', "Updated") : localize('installed', "Installed") : null; } if (currentStatus === ExtensionState.Uninstalling && this.status === ExtensionState.Uninstalled) { + this.initialStatus = this.status; return canRemoveExtension() ? localize('uninstalled', "Uninstalled") : null; } } @@ -2533,7 +2556,7 @@ export class MaliciousStatusLabelAction extends ExtensionAction { } } -export class SystemDisabledLabelAction extends ExtensionAction { +export class DisabledLabelAction extends ExtensionAction { private static readonly Class = 'disable-status'; @@ -2542,19 +2565,26 @@ export class SystemDisabledLabelAction extends ExtensionAction { constructor( private readonly warningAction: SystemDisabledWarningAction, + @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService, ) { - super('extensions.systemDisabledLabel', warningAction.tooltip, `${SystemDisabledLabelAction.Class} hide`, false); + super('extensions.disabledLabel', warningAction.tooltip, `${DisabledLabelAction.Class} hide`, false); warningAction.onDidChange(() => this.update(), this, this.disposables); } update(): void { - this.enabled = this.warningAction.enabled; - if (this.enabled) { - this.class = SystemDisabledLabelAction.Class; + this.class = `${DisabledLabelAction.Class} hide`; + this.label = ''; + if (this.warningAction.enabled) { + this.enabled = true; + this.class = DisabledLabelAction.Class; this.label = this.warningAction.tooltip; - } else { - this.class = `${SystemDisabledLabelAction.Class} hide`; - this.label = ''; + return; + } + if (this.extension && this.extension.local && !this.extensionEnablementService.isEnabled(this.extension.local)) { + this.enabled = true; + this.class = DisabledLabelAction.Class; + this.label = localize('disabled by user', "This extension is disabled by the user."); + return; } } @@ -2599,27 +2629,42 @@ export class SystemDisabledWarningAction extends ExtensionAction { this.enabled = false; this.class = `${SystemDisabledWarningAction.Class} hide`; this.tooltip = ''; - if (this.extension && this.extension.local && this._runningExtensions) { + if (this.extension && this.extension.local && this.extension.server && this._runningExtensions && this.workbenchEnvironmentService.configuration.remoteAuthority && this.extensionManagementServerService.remoteExtensionManagementServer) { if ( - // Remote Window - this.workbenchEnvironmentService.configuration.remoteAuthority // Local Workspace Extension - && this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && !isUIExtension(this.extension.local.manifest, this.configurationService) - // Extension does not exist in remote - && !this.extensionsWorkbenchService.local.some(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.remoteExtensionManagementServer) + this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && !isUIExtension(this.extension.local.manifest, this.configurationService) ) { this.enabled = true; this.class = `${SystemDisabledWarningAction.Class}`; - const host = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.workbenchEnvironmentService.configuration.remoteAuthority) || localize('remote', "Remote"); - this.tooltip = localize('disabled workspace Extension', "This extension is disabled because it cannot run in a window connected to the remote server.", host, host); - if (this.extensionsWorkbenchService.canInstall(this.extension)) { - this.tooltip = `${this.tooltip} ${localize('Install in remote server', "Install it in '{0}' server to enable.", host)}`; + this.tooltip = localize('disabled workspace Extension', "This extension from {0} server is disabled because it cannot run in a window connected to the remote server.", this.getServerLabel(this.extensionManagementServerService.localExtensionManagementServer)); + if (!this.extensionsWorkbenchService.local.some(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.remoteExtensionManagementServer) + && this.extensionsWorkbenchService.canInstall(this.extension) + ) { + // Extension does not exist in remote + this.tooltip = `${this.tooltip} ${localize('Install in remote server', "Install it in {0} server to enable.", this.getServerLabel(this.extensionManagementServerService.remoteExtensionManagementServer))}`; } return; } + const runningExtension = this._runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value }, this.extension.identifier))[0]; + const runningExtensionServer = runningExtension ? this.extensionManagementServerService.getExtensionManagementServer(runningExtension.extensionLocation) : null; + if ( + // Not same as running extension + runningExtensionServer && this.extension.server !== runningExtensionServer + ) { + this.enabled = true; + this.class = `${SystemDisabledWarningAction.Class}`; + this.tooltip = localize('disabled because running in another server', "This extension from {0} server is disabled because another instance of same extension from {1} server is enabled.", this.getServerLabel(this.extension.server), this.getServerLabel(runningExtensionServer)); + return; + } } } + private getServerLabel(server: IExtensionManagementServer): string { + if (server === this.extensionManagementServerService.remoteExtensionManagementServer) { + return this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.workbenchEnvironmentService.configuration.remoteAuthority) || localize('remote', "Remote"); + } + return server.label; + } run(): Promise { return Promise.resolve(null); } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts index b383a6cd15..b2b9ea8241 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IExtensionService, IResponsiveStateChangeEvent, ICpuProfilerTarget, IExtensionHostProfile, ProfileSession } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, IResponsiveStateChangeEvent, IExtensionHostProfile, ProfileSession } from 'vs/workbench/services/extensions/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Disposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; @@ -21,11 +21,12 @@ import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; +import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler'; export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchContribution { - private readonly _session = new Map(); private readonly _blame = new Set(); + private _session: CancellationTokenSource | undefined; constructor( @IExtensionService private readonly _extensionService: IExtensionService, @@ -41,26 +42,29 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont } private async _onDidChangeResponsiveChange(event: IResponsiveStateChangeEvent): Promise { - const { target } = event; - if (!target.canProfileExtensionHost()) { + const port = this._extensionService.getInspectPort(); + + if (!port) { return; } - if (event.isResponsive && this._session.has(target)) { + if (event.isResponsive && this._session) { // stop profiling when responsive again - this._session.get(target)!.cancel(); + this._session.cancel(); - } else if (!event.isResponsive && !this._session.has(target)) { + } else if (!event.isResponsive && !this._session) { // start profiling if not yet profiling - const token = new CancellationTokenSource(); - this._session.set(target, token); + const cts = new CancellationTokenSource(); + this._session = cts; + let session: ProfileSession; try { - session = await target.startExtensionHostProfile(); + session = await this._instantiationService.createInstance(ExtensionHostProfiler, port).start(); + } catch (err) { - this._session.delete(target); + this._session = undefined; // fail silent as this is often // caused by another party being // connected already @@ -69,7 +73,7 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont // wait 5 seconds or until responsive again await new Promise(resolve => { - token.token.onCancellationRequested(resolve); + cts.token.onCancellationRequested(resolve); setTimeout(resolve, 5e3); }); @@ -79,7 +83,7 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont } catch (err) { onUnexpectedError(err); } finally { - this._session.delete(target); + this._session = undefined; } } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts index 505cf16538..1d7a120fa3 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts @@ -13,9 +13,9 @@ import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; +import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, DisabledLabelAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { Label, RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/electron-browser/extensionsWidgets'; +import { Label, RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, TooltipWidget } from 'vs/workbench/contrib/extensions/electron-browser/extensionsWidgets'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -88,13 +88,7 @@ export class Renderer implements IPagedRenderer { }); actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); - const widgets = [ - recommendationWidget, - badgeWidget, - this.instantiationService.createInstance(Label, version, (e: IExtension) => e.version), - this.instantiationService.createInstance(InstallCountWidget, installCount, true), - this.instantiationService.createInstance(RatingsWidget, ratings, true) - ]; + const systemDisabledWarningAction = this.instantiationService.createInstance(SystemDisabledWarningAction); const actions = [ this.instantiationService.createInstance(StatusLabelAction), this.instantiationService.createInstance(UpdateAction), @@ -102,10 +96,20 @@ export class Renderer implements IPagedRenderer { this.instantiationService.createInstance(InstallAction), this.instantiationService.createInstance(RemoteInstallAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, false), - this.instantiationService.createInstance(SystemDisabledWarningAction), + systemDisabledWarningAction, this.instantiationService.createInstance(ManageExtensionAction) ]; - const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); + const disabledLabelAction = this.instantiationService.createInstance(DisabledLabelAction, systemDisabledWarningAction); + const tooltipWidget = this.instantiationService.createInstance(TooltipWidget, root, disabledLabelAction, recommendationWidget); + const widgets = [ + recommendationWidget, + badgeWidget, + tooltipWidget, + this.instantiationService.createInstance(Label, version, (e: IExtension) => e.version), + this.instantiationService.createInstance(InstallCountWidget, installCount, true), + this.instantiationService.createInstance(RatingsWidget, ratings, true) + ]; + const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets, disabledLabelAction]); actionbar.push(actions, actionOptions); const disposables = [...actions, ...widgets, actionbar, extensionContainers]; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts index 1cff90dd73..19013a1e4d 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts @@ -53,7 +53,6 @@ import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { RemoteAuthorityContext as RemoteAuthorityContext } from 'vs/workbench/common/contextkeys'; import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; interface SearchInputEvent extends Event { @@ -64,7 +63,10 @@ interface SearchInputEvent extends Event { const NonEmptyWorkspaceContext = new RawContextKey('nonEmptyWorkspace', false); const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); -const SearchServerExtensionsContext = new RawContextKey('searchServerExtensions', false); +const SearchIntalledExtensionsContext = new RawContextKey('searchInstalledExtensions', false); +const SearchOutdatedExtensionsContext = new RawContextKey('searchOutdatedExtensions', false); +const SearchEnabledExtensionsContext = new RawContextKey('searchEnabledExtensions', false); +const SearchDisabledExtensionsContext = new RawContextKey('searchDisabledExtensions', false); const HasInstalledExtensionsContext = new RawContextKey('hasInstalledExtensions', true); const SearchBuiltInExtensionsContext = new RawContextKey('searchBuiltInExtensions', false); const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); @@ -72,10 +74,12 @@ const DefaultRecommendedExtensionsContext = new RawContextKey('defaultR const viewIdNameMappings: { [id: string]: string } = { 'extensions.listView': localize('marketPlace', "Marketplace"), 'extensions.enabledExtensionList': localize('enabledExtensions', "Enabled"), + 'extensions.enabledExtensionList2': localize('enabledExtensions', "Enabled"), 'extensions.disabledExtensionList': localize('disabledExtensions', "Disabled"), + 'extensions.disabledExtensionList2': localize('disabledExtensions', "Disabled"), // {{SQL CARBON EDIT}} // 'extensions.popularExtensionsList': localize('popularExtensions', "Popular"), - 'extensions.recommendedList': localize('recommendedExtensions', "Marketplace"), + 'extensions.recommendedList': localize('recommendedExtensions', "Recommended"), 'extensions.otherrecommendedList': localize('otherRecommendedExtensions', "Other Recommendations"), 'extensions.workspaceRecommendedList': localize('workspaceRecommendedExtensions', "Workspace Recommendations"), 'extensions.builtInExtensionsList': localize('builtInExtensions', "Features"), @@ -98,6 +102,8 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio viewDescriptors.push(this.createDefaultDisabledExtensionsListViewDescriptor()); // {{SQL CARBON EDIT}} // viewDescriptors.push(this.createDefaultPopularExtensionsListViewDescriptor()); + viewDescriptors.push(this.createEnabledExtensionsListViewDescriptor()); + viewDescriptors.push(this.createDisabledExtensionsListViewDescriptor()); viewDescriptors.push(this.createBuiltInExtensionsListViewDescriptor()); viewDescriptors.push(this.createBuiltInBasicsExtensionsListViewDescriptor()); viewDescriptors.push(this.createBuiltInThemesExtensionsListViewDescriptor()); @@ -133,7 +139,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id, name: viewIdNameMappings[id], ctorDescriptor: { ctor: EnabledExtensionsView }, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteAuthorityContext.isEqualTo('')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), ContextKeyExpr.not('config.extensions.showInstalledExtensionsByDefault')), weight: 40, canToggleVisibility: true, order: 1 @@ -148,7 +154,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id, name: viewIdNameMappings[id], ctorDescriptor: { ctor: DisabledExtensionsView }, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteAuthorityContext.isEqualTo('')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), ContextKeyExpr.not('config.extensions.showInstalledExtensionsByDefault')), weight: 10, canToggleVisibility: true, order: 3, @@ -175,15 +181,21 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio private createExtensionsViewDescriptorsForServer(server: IExtensionManagementServer): IViewDescriptor[] { return [{ id: `extensions.${server.authority}.installed`, - name: server.label, + name: localize('installed', "Installed"), ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server] }, - when: ContextKeyExpr.and(ContextKeyExpr.has('searchServerExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchInstalledExtensions')), + weight: 100 + }, { + id: `extensions.${server.authority}.outdated`, + name: localize('outdated', "Outdated"), + ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server] }, + when: ContextKeyExpr.and(ContextKeyExpr.has('searchOutdatedExtensions')), weight: 100 }, { id: `extensions.${server.authority}.default`, - name: server.label, + name: localize('installed', "Installed"), ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server] }, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteAuthorityContext.notEqualsTo('')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), ContextKeyExpr.has('config.extensions.showInstalledExtensionsByDefault')), weight: 40, order: 1 }]; @@ -235,6 +247,33 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio }; } + private createEnabledExtensionsListViewDescriptor(): IViewDescriptor { + const id = 'extensions.enabledExtensionList2'; + return { + id, + name: viewIdNameMappings[id], + ctorDescriptor: { ctor: EnabledExtensionsView }, + when: ContextKeyExpr.and(ContextKeyExpr.has('searchEnabledExtensions')), + weight: 40, + canToggleVisibility: true, + order: 1 + }; + } + + private createDisabledExtensionsListViewDescriptor(): IViewDescriptor { + const id = 'extensions.disabledExtensionList2'; + return { + id, + name: viewIdNameMappings[id], + ctorDescriptor: { ctor: DisabledExtensionsView }, + when: ContextKeyExpr.and(ContextKeyExpr.has('searchDisabledExtensions')), + weight: 10, + canToggleVisibility: true, + order: 3, + collapsed: true + }; + } + private createBuiltInExtensionsListViewDescriptor(): IViewDescriptor { const id = 'extensions.builtInExtensionsList'; return { @@ -278,7 +317,10 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private nonEmptyWorkspaceContextKey: IContextKey; private defaultViewsContextKey: IContextKey; private searchMarketplaceExtensionsContextKey: IContextKey; - private searchServerExtensionsContextKey: IContextKey; + private searchInstalledExtensionsContextKey: IContextKey; + private searchOutdatedExtensionsContextKey: IContextKey; + private searchEnabledExtensionsContextKey: IContextKey; + private searchDisabledExtensionsContextKey: IContextKey; private hasInstalledExtensionsContextKey: IContextKey; private searchBuiltInExtensionsContextKey: IContextKey; private recommendedExtensionsContextKey: IContextKey; @@ -317,7 +359,10 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.nonEmptyWorkspaceContextKey = NonEmptyWorkspaceContext.bindTo(contextKeyService); this.defaultViewsContextKey = DefaultViewsContext.bindTo(contextKeyService); this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); - this.searchServerExtensionsContextKey = SearchServerExtensionsContext.bindTo(contextKeyService); + this.searchInstalledExtensionsContextKey = SearchIntalledExtensionsContext.bindTo(contextKeyService); + this.searchOutdatedExtensionsContextKey = SearchOutdatedExtensionsContext.bindTo(contextKeyService); + this.searchEnabledExtensionsContextKey = SearchEnabledExtensionsContext.bindTo(contextKeyService); + this.searchDisabledExtensionsContextKey = SearchDisabledExtensionsContext.bindTo(contextKeyService); this.hasInstalledExtensionsContextKey = HasInstalledExtensionsContext.bindTo(contextKeyService); this.searchBuiltInExtensionsContextKey = SearchBuiltInExtensionsContext.bindTo(contextKeyService); this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService); @@ -457,7 +502,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio protected saveState(): void { const value = this.searchBox.getValue(); - if (ExtensionsListView.isInstalledExtensionsQuery(value)) { + if (ExtensionsListView.isLocalExtensionsQuery(value)) { this.searchViewletState['query.value'] = value; } else { this.searchViewletState['query.value'] = ''; @@ -468,13 +513,14 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private doSearch(): Promise { const value = this.normalizedQuery(); this.defaultViewsContextKey.set(!value); - const isServerExtensionsQuery = ExtensionsListView.isServerExtensionsQuery(value); - const isBuiltInExtensionsQuery = ExtensionsListView.isBuiltInExtensionsQuery(value); const isRecommendedExtensionsQuery = ExtensionsListView.isRecommendedExtensionsQuery(value); - this.searchServerExtensionsContextKey.set(isServerExtensionsQuery); - this.searchBuiltInExtensionsContextKey.set(isBuiltInExtensionsQuery); + this.searchInstalledExtensionsContextKey.set(ExtensionsListView.isInstalledExtensionsQuery(value)); + this.searchOutdatedExtensionsContextKey.set(ExtensionsListView.isOutdatedExtensionsQuery(value)); + this.searchEnabledExtensionsContextKey.set(ExtensionsListView.isEnabledExtensionsQuery(value)); + this.searchDisabledExtensionsContextKey.set(ExtensionsListView.isDisabledExtensionsQuery(value)); + this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); - this.searchMarketplaceExtensionsContextKey.set(!!value && !isServerExtensionsQuery && !isBuiltInExtensionsQuery && !isRecommendedExtensionsQuery); + this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery); this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY); return this.progress(Promise.all(this.panels.map(view => diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts index 1592695c31..9edc0a722e 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts @@ -9,7 +9,7 @@ import { assign } from 'vs/base/common/objects'; import { Event, Emitter } from 'vs/base/common/event'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; -import { SortBy, SortOrder, IQueryOptions, IExtensionTipsService, IExtensionRecommendation, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { SortBy, SortOrder, IQueryOptions, IExtensionTipsService, IExtensionRecommendation, IExtensionManagementServer, IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -41,7 +41,7 @@ import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IAction } from 'vs/base/common/actions'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import product from 'vs/platform/product/node/product'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; @@ -96,7 +96,8 @@ export class ExtensionsListView extends ViewletPanel { @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IExperimentService private readonly experimentService: IExperimentService, - @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService + @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService ) { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService); this.server = options.server; @@ -234,7 +235,7 @@ export class ExtensionsListView extends ViewletPanel { if (ids.length) { return this.queryByIds(ids, options, token); } - if (ExtensionsListView.isInstalledExtensionsQuery(query.value) || /@builtin/.test(query.value)) { + if (ExtensionsListView.isLocalExtensionsQuery(query.value) || /@builtin/.test(query.value)) { return this.queryLocal(query, options); } return this.queryGallery(query, options, token); @@ -329,7 +330,23 @@ export class ExtensionsListView extends ViewletPanel { && (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1) && (!categories.length || categories.some(category => (e.local && e.local.manifest.categories || []).some(c => c.toLowerCase() === category)))); - return this.getPagedModel(this.sortExtensions(result, options)); + if (options.sortBy !== undefined) { + result = this.sortExtensions(result, options); + } else { + const runningExtensions = await this.extensionService.getExtensions(); + const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(ExtensionIdentifier.toKey(e.identifier.value), e); return result; }, new Map()); + result = result.sort((e1, e2) => { + const running1 = runningExtensionsById.get(ExtensionIdentifier.toKey(e1.identifier.id)); + const isE1Running = running1 && this.extensionManagementServerService.getExtensionManagementServer(running1.extensionLocation) === e1.server; + const running2 = runningExtensionsById.get(ExtensionIdentifier.toKey(e2.identifier.id)); + const isE2Running = running2 && this.extensionManagementServerService.getExtensionManagementServer(running2.extensionLocation) === e2.server; + if ((isE1Running && isE2Running) || (!isE1Running && !isE2Running)) { + return e1.displayName.localeCompare(e2.displayName); + } + return isE1Running ? -1 : 1; + }); + } + return this.getPagedModel(result); } @@ -785,12 +802,28 @@ export class ExtensionsListView extends ViewletPanel { return /^\s*@builtin\s*$/i.test(query); } - static isInstalledExtensionsQuery(query: string): boolean { - return /@installed|@outdated|@enabled|@disabled/i.test(query); + static isLocalExtensionsQuery(query: string): boolean { + return this.isInstalledExtensionsQuery(query) + || this.isOutdatedExtensionsQuery(query) + || this.isEnabledExtensionsQuery(query) + || this.isDisabledExtensionsQuery(query) + || this.isBuiltInExtensionsQuery(query); } - static isServerExtensionsQuery(query: string): boolean { - return /@installed|@outdated/i.test(query); + static isInstalledExtensionsQuery(query: string): boolean { + return /@installed/i.test(query); + } + + static isOutdatedExtensionsQuery(query: string): boolean { + return /@outdated/i.test(query); + } + + static isEnabledExtensionsQuery(query: string): boolean { + return /@enabled/i.test(query); + } + + static isDisabledExtensionsQuery(query: string): boolean { + return /@disabled/i.test(query); } static isRecommendedExtensionsQuery(query: string): boolean { @@ -827,8 +860,12 @@ export class ExtensionsListView extends ViewletPanel { } } -function getServerLabel(server: IExtensionManagementServer, labelService: ILabelService, workbenchEnvironmentService: IWorkbenchEnvironmentService): string { - return workbenchEnvironmentService.configuration.remoteAuthority === server.authority ? labelService.getHostLabel(REMOTE_HOST_SCHEME, server.authority) || server.label : server.label; +function getViewTitleForServer(viewTitle: string, server: IExtensionManagementServer, labelService: ILabelService, workbenchEnvironmentService: IWorkbenchEnvironmentService): string { + const serverLabel = workbenchEnvironmentService.configuration.remoteAuthority === server.authority ? labelService.getHostLabel(REMOTE_HOST_SCHEME, server.authority) || server.label : server.label; + if (viewTitle && workbenchEnvironmentService.configuration.remoteAuthority) { + return `${serverLabel} - ${viewTitle}`; + } + return viewTitle ? viewTitle : serverLabel; } export class ServerExtensionsView extends ExtensionsListView { @@ -852,17 +889,19 @@ export class ServerExtensionsView extends ExtensionsListView { @IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService, @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @ILabelService labelService: ILabelService, - @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService ) { - options.title = getServerLabel(server, labelService, workbenchEnvironmentService); + const viewTitle = options.title; + options.title = getViewTitleForServer(viewTitle, server, labelService, workbenchEnvironmentService); options.server = server; - super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, modeService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService); - this.disposables.push(labelService.onDidChangeFormatters(() => this.updateTitle(getServerLabel(server, labelService, workbenchEnvironmentService)))); + super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, modeService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService); + this.disposables.push(labelService.onDidChangeFormatters(() => this.updateTitle(getViewTitleForServer(viewTitle, server, labelService, workbenchEnvironmentService)))); } async show(query: string): Promise> { query = query ? query : '@installed'; - if (!ExtensionsListView.isInstalledExtensionsQuery(query) && !ExtensionsListView.isBuiltInExtensionsQuery(query)) { + if (!ExtensionsListView.isLocalExtensionsQuery(query) && !ExtensionsListView.isBuiltInExtensionsQuery(query)) { query = query += ' @installed'; } return super.show(query.trim()); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts index 7dbd2b8cab..63d6069497 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts @@ -11,12 +11,13 @@ import * as platform from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { IExtensionManagementServerService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ILabelService } from 'vs/platform/label/common/label'; -import { extensionButtonProminentBackground, extensionButtonProminentForeground } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; +import { extensionButtonProminentBackground, extensionButtonProminentForeground, DisabledLabelAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_FOREGROUND } from 'vs/workbench/common/theme'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { Emitter, Event } from 'vs/base/common/event'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension; @@ -142,11 +143,53 @@ export class RatingsWidget extends ExtensionWidget { } } +export class TooltipWidget extends ExtensionWidget { + + constructor( + private readonly parent: HTMLElement, + private readonly extensionLabelAction: DisabledLabelAction, + private readonly recommendationWidget: RecommendationWidget + ) { + super(); + this._register(this.extensionLabelAction.onDidChange(() => this.render())); + this._register(this.recommendationWidget.onDidChangeTooltip(() => this.render())); + } + + render(): void { + this.parent.title = ''; + this.parent.removeAttribute('aria-label'); + if (this.extension) { + const title = this.getTitle(); + this.parent.title = title; + this.parent.setAttribute('aria-label', localize('extension-arialabel', "{0}. {1} Press enter for extension details.", this.extension.displayName)); + } + } + + private getTitle(): string { + if (this.extensionLabelAction.enabled) { + return this.extensionLabelAction.label; + } + return this.recommendationWidget.tooltip; + } + +} + export class RecommendationWidget extends ExtensionWidget { private element?: HTMLElement; private disposables: IDisposable[] = []; + private _tooltip: string; + get tooltip(): string { return this._tooltip; } + set tooltip(tooltip: string) { + if (this._tooltip !== tooltip) { + this._tooltip = tooltip; + this._onDidChangeTooltip.fire(); + } + } + private _onDidChangeTooltip: Emitter = this._register(new Emitter()); + readonly onDidChangeTooltip: Event = this._onDidChangeTooltip.event; + constructor( private parent: HTMLElement, @IThemeService private readonly themeService: IThemeService, @@ -159,7 +202,7 @@ export class RecommendationWidget extends ExtensionWidget { } private clear(): void { - this.parent.title = ''; + this.tooltip = ''; this.parent.setAttribute('aria-label', this.extension ? localize('viewExtensionDetailsAria', "{0}. Press enter for extension details.", this.extension.displayName) : ''); if (this.element) { this.parent.removeChild(this.element); @@ -186,8 +229,7 @@ export class RecommendationWidget extends ExtensionWidget { }; applyBookmarkStyle(this.themeService.getTheme()); this.themeService.onThemeChange(applyBookmarkStyle, this, this.disposables); - this.parent.title = extRecommendations[this.extension.identifier.id.toLowerCase()].reasonText; - this.parent.setAttribute('aria-label', localize('viewRecommendedExtensionDetailsAria', "{0}. {1} Press enter for extension details.", this.extension.displayName, extRecommendations[this.extension.identifier.id.toLowerCase()].reasonText)); + this.tooltip = extRecommendations[this.extension.identifier.id.toLowerCase()].reasonText; } } @@ -227,7 +269,7 @@ export class RemoteBadgeWidget extends ExtensionWidget { } if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) { this.element = append(this.parent, $('div.extension-remote-badge')); - append(this.element, $('span.octicon.octicon-file-symlink-directory')); + append(this.element, $('span.octicon.octicon-remote')); const applyBadgeStyle = () => { if (!this.element) { diff --git a/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts index f10d75de73..9db09c3ccc 100644 --- a/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts @@ -700,7 +700,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private fromGallery(gallery: IGalleryExtension, maliciousExtensionSet: Set): IExtension { - Promise.all([this.localExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet), this.remoteExtensions ? this.localExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false)]) + Promise.all([this.localExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet), this.remoteExtensions ? this.remoteExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false)]) .then(result => { if (result[0] || result[1]) { this.eventuallyAutoUpdateExtensions(); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index 716bbbf968..5bf3b9489b 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -12,7 +12,7 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/node/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, IExtensionEnablementService, IExtensionTipsService, ILocalExtension, IGalleryExtension, IQueryOptions, - DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, IExtensionManagementServerService, EnablementState, ExtensionRecommendationReason, SortBy + DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, IExtensionManagementServerService, EnablementState, ExtensionRecommendationReason, SortBy, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; @@ -39,6 +39,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { ExtensionManagementServerService } from 'vs/workbench/services/extensions/electron-browser/extensionManagementServerService'; suite('ExtensionsListView Tests', () => { @@ -89,11 +90,14 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); instantiationService.stub(IRemoteAgentService, RemoteAgentService); - instantiationService.stub(IExtensionManagementServerService, { - localExtensionManagementServer: { - extensionManagementService: instantiationService.get(IExtensionManagementService) + instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { + private _localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', authority: 'vscode-local' }; + constructor() { + super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService)); } - }); + get localExtensionManagementServer(): IExtensionManagementServer { return this._localExtensionManagementServer; } + set localExtensionManagementServer(server: IExtensionManagementServer) { } + }()); instantiationService.stub(IExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); @@ -151,14 +155,14 @@ suite('ExtensionsListView Tests', () => { test('Test query types', () => { assert.equal(ExtensionsListView.isBuiltInExtensionsQuery('@builtin'), true); - assert.equal(ExtensionsListView.isInstalledExtensionsQuery('@installed'), true); - assert.equal(ExtensionsListView.isInstalledExtensionsQuery('@enabled'), true); - assert.equal(ExtensionsListView.isInstalledExtensionsQuery('@disabled'), true); - assert.equal(ExtensionsListView.isInstalledExtensionsQuery('@outdated'), true); - assert.equal(ExtensionsListView.isInstalledExtensionsQuery('@installed searchText'), true); - assert.equal(ExtensionsListView.isInstalledExtensionsQuery('@enabled searchText'), true); - assert.equal(ExtensionsListView.isInstalledExtensionsQuery('@disabled searchText'), true); - assert.equal(ExtensionsListView.isInstalledExtensionsQuery('@outdated searchText'), true); + assert.equal(ExtensionsListView.isLocalExtensionsQuery('@installed'), true); + assert.equal(ExtensionsListView.isLocalExtensionsQuery('@enabled'), true); + assert.equal(ExtensionsListView.isLocalExtensionsQuery('@disabled'), true); + assert.equal(ExtensionsListView.isLocalExtensionsQuery('@outdated'), true); + assert.equal(ExtensionsListView.isLocalExtensionsQuery('@installed searchText'), true); + assert.equal(ExtensionsListView.isLocalExtensionsQuery('@enabled searchText'), true); + assert.equal(ExtensionsListView.isLocalExtensionsQuery('@disabled searchText'), true); + assert.equal(ExtensionsListView.isLocalExtensionsQuery('@outdated searchText'), true); }); test('Test empty query equates to sort by install count', () => { @@ -200,8 +204,8 @@ suite('ExtensionsListView Tests', () => { await testableView.show('@installed first').then(result => { assert.equal(result.length, 2, 'Unexpected number of results for @installed query'); - assert.equal(result.get(0).name, localDisabledTheme.manifest.name, 'Unexpected extension for @installed query with search text.'); - assert.equal(result.get(1).name, localEnabledTheme.manifest.name, 'Unexpected extension for @installed query with search text.'); + assert.equal(result.get(0).name, localEnabledTheme.manifest.name, 'Unexpected extension for @installed query with search text.'); + assert.equal(result.get(1).name, localDisabledTheme.manifest.name, 'Unexpected extension for @installed query with search text.'); }); await testableView.show('@disabled').then(result => { @@ -242,27 +246,27 @@ suite('ExtensionsListView Tests', () => { test('Test installed query with category', async () => { await testableView.show('@installed category:themes').then(result => { assert.equal(result.length, 2, 'Unexpected number of results for @installed query with category'); - assert.equal(result.get(0).name, localDisabledTheme.manifest.name, 'Unexpected extension for @installed query with category.'); - assert.equal(result.get(1).name, localEnabledTheme.manifest.name, 'Unexpected extension for @installed query with category.'); + assert.equal(result.get(0).name, localEnabledTheme.manifest.name, 'Unexpected extension for @installed query with category.'); + assert.equal(result.get(1).name, localDisabledTheme.manifest.name, 'Unexpected extension for @installed query with category.'); }); await testableView.show('@installed category:"themes"').then(result => { assert.equal(result.length, 2, 'Unexpected number of results for @installed query with quoted category'); - assert.equal(result.get(0).name, localDisabledTheme.manifest.name, 'Unexpected extension for @installed query with quoted category.'); - assert.equal(result.get(1).name, localEnabledTheme.manifest.name, 'Unexpected extension for @installed query with quoted category.'); + assert.equal(result.get(0).name, localEnabledTheme.manifest.name, 'Unexpected extension for @installed query with quoted category.'); + assert.equal(result.get(1).name, localDisabledTheme.manifest.name, 'Unexpected extension for @installed query with quoted category.'); }); await testableView.show('@installed category:"programming languages"').then(result => { assert.equal(result.length, 2, 'Unexpected number of results for @installed query with quoted category including space'); - assert.equal(result.get(0).name, localDisabledLanguage.manifest.name, 'Unexpected extension for @installed query with quoted category inlcuding space.'); - assert.equal(result.get(1).name, localEnabledLanguage.manifest.name, 'Unexpected extension for @installed query with quoted category including space.'); + assert.equal(result.get(0).name, localEnabledLanguage.manifest.name, 'Unexpected extension for @installed query with quoted category including space.'); + assert.equal(result.get(1).name, localDisabledLanguage.manifest.name, 'Unexpected extension for @installed query with quoted category inlcuding space.'); }); await testableView.show('@installed category:themes category:random').then(result => { assert.equal(result.length, 3, 'Unexpected number of results for @installed query with multiple category'); - assert.equal(result.get(0).name, localDisabledTheme.manifest.name, 'Unexpected extension for @installed query with multiple category.'); - assert.equal(result.get(1).name, localEnabledTheme.manifest.name, 'Unexpected extension for @installed query with multiple category.'); - assert.equal(result.get(2).name, localRandom.manifest.name, 'Unexpected extension for @installed query with multiple category.'); + assert.equal(result.get(0).name, localEnabledTheme.manifest.name, 'Unexpected extension for @installed query with multiple category.'); + assert.equal(result.get(1).name, localRandom.manifest.name, 'Unexpected extension for @installed query with multiple category.'); + assert.equal(result.get(2).name, localDisabledTheme.manifest.name, 'Unexpected extension for @installed query with multiple category.'); }); await testableView.show('@enabled category:themes').then(result => { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 595e79b513..19ebd1d43e 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ShowViewletAction } from 'vs/workbench/browser/viewlet'; import * as nls from 'vs/nls'; import { sep } from 'vs/base/common/path'; @@ -159,7 +159,7 @@ class FileEditorInputFactory implements IEditorInputFactory { public 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 = !!fileInput.resourceJSON ? URI.revive(fileInput.resourceJSON) : URI.parse(fileInput.resource); const encoding = fileInput.encoding; return accessor.get(IEditorService).createInput({ resource, encoding, forceFile: true }) as FileEditorInput; diff --git a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts index 40acccd48a..7ab47205c2 100644 --- a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts @@ -160,9 +160,9 @@ export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, I if (isReadonly) { if (triedToMakeWriteable) { - message = isWindows ? nls.localize('readonlySaveErrorAdmin', "Failed to save '{0}': File is write protected. Select 'Overwrite as Admin' to retry as administrator.", basename(resource)) : nls.localize('readonlySaveErrorSudo', "Failed to save '{0}': File is write protected. Select 'Overwrite as Sudo' to retry as superuser.", basename(resource)); + message = isWindows ? nls.localize('readonlySaveErrorAdmin', "Failed to save '{0}': File is read-only. Select 'Overwrite as Admin' to retry as administrator.", basename(resource)) : nls.localize('readonlySaveErrorSudo', "Failed to save '{0}': File is read-only. Select 'Overwrite as Sudo' to retry as superuser.", basename(resource)); } else { - message = nls.localize('readonlySaveError', "Failed to save '{0}': File is write protected. Select 'Overwrite' to attempt to remove protection.", basename(resource)); + message = nls.localize('readonlySaveError', "Failed to save '{0}': File is read-only. Select 'Overwrite' to attempt to make it writeable.", basename(resource)); } } else if (isPermissionDenied) { message = isWindows ? nls.localize('permissionDeniedSaveError', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", basename(resource)) : nls.localize('permissionDeniedSaveErrorSudo', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Sudo' to retry as superuser.", basename(resource)); diff --git a/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts index af3985c5c6..252def878e 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts @@ -6,9 +6,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import * as nls from 'vs/nls'; import product from 'vs/platform/product/node/product'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, ICommandAction, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { OpenIssueReporterAction, ReportPerformanceIssueUsingReporterAction, OpenProcessExplorer } from 'vs/workbench/contrib/issue/electron-browser/issueActions'; +import { ReportPerformanceIssueUsingReporterAction, OpenProcessExplorer } from 'vs/workbench/contrib/issue/electron-browser/issueActions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; import { WorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issueService'; @@ -19,8 +19,27 @@ const helpCategory = nls.localize('help', "Help"); const workbenchActionsRegistry = Registry.as(Extensions.WorkbenchActions); if (!!product.reportIssueUrl) { - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenIssueReporterAction, OpenIssueReporterAction.ID, OpenIssueReporterAction.LABEL), 'Help: Open Issue Reporter', helpCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ReportPerformanceIssueUsingReporterAction, ReportPerformanceIssueUsingReporterAction.ID, ReportPerformanceIssueUsingReporterAction.LABEL), 'Help: Report Performance Issue', helpCategory); + + const OpenIssueReporterActionId = 'workbench.action.openIssueReporter'; + const OpenIssueReporterActionLabel = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue"); + + CommandsRegistry.registerCommand(OpenIssueReporterActionId, function (accessor, args?: [string]) { + let extensionId: string | undefined; + if (args && Array.isArray(args)) { + [extensionId] = args; + } + + return accessor.get(IWorkbenchIssueService).openReporter({ extensionId }); + }); + + const command: ICommandAction = { + id: OpenIssueReporterActionId, + title: { value: OpenIssueReporterActionLabel, original: 'Help: Open Issue Reporter' }, + category: helpCategory + }; + + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command }); } const developerCategory = nls.localize('developer', "Developer"); diff --git a/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts b/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts index 1755c79eaf..92ddd2b2f1 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts @@ -8,23 +8,6 @@ import * as nls from 'vs/nls'; import { IssueType } from 'vs/platform/issue/common/issue'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; -export class OpenIssueReporterAction extends Action { - static readonly ID = 'workbench.action.openIssueReporter'; - static readonly LABEL = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue"); - - constructor( - id: string, - label: string, - @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService - ) { - super(id, label); - } - - run(): Promise { - return this.issueService.openReporter().then(() => true); - } -} - export class OpenProcessExplorer extends Action { static readonly ID = 'workbench.action.openProcessExplorer'; static readonly LABEL = nls.localize('openProcessExplorer', "Open Process Explorer"); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts index 8276b5d74d..c3f19e77a3 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts @@ -48,7 +48,7 @@ export class OpenSettings2Action extends Action { } run(event?: any): Promise { - return this.preferencesService.openSettings(false); + return this.preferencesService.openSettings(false, undefined); } } @@ -66,7 +66,7 @@ export class OpenSettingsJsonAction extends Action { } run(event?: any): Promise { - return this.preferencesService.openSettings(true); + return this.preferencesService.openSettings(true, undefined); } } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index db3c6895dc..e8fdb3edf7 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -256,8 +256,8 @@ export class PreferencesEditor extends BaseEditor { } const promise: Promise = this.input && this.input.isDirty() ? this.input.save() : Promise.resolve(true); promise.then(() => { - if (target === ConfigurationTarget.USER) { - this.preferencesService.switchSettings(ConfigurationTarget.USER, this.preferencesService.userSettingsResource, true); + if (target === ConfigurationTarget.USER_LOCAL) { + this.preferencesService.switchSettings(ConfigurationTarget.USER_LOCAL, this.preferencesService.userSettingsResource, true); } else if (target === ConfigurationTarget.WORKSPACE) { this.preferencesService.switchSettings(ConfigurationTarget.WORKSPACE, this.preferencesService.workspaceSettingsResource!, true); } else if (target instanceof URI) { @@ -507,7 +507,7 @@ class PreferencesRenderersController extends Disposable { private searchAllSettingsTargets(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number, token?: CancellationToken): Promise { const searchPs = [ this.searchSettingsTarget(query, searchProvider, ConfigurationTarget.WORKSPACE, groupId, groupLabel, groupOrder, token), - this.searchSettingsTarget(query, searchProvider, ConfigurationTarget.USER, groupId, groupLabel, groupOrder, token) + this.searchSettingsTarget(query, searchProvider, ConfigurationTarget.USER_LOCAL, groupId, groupLabel, groupOrder, token) ]; for (const folder of this.workspaceContextService.getWorkspace().folders) { @@ -541,9 +541,10 @@ class PreferencesRenderersController extends Disposable { } private async getPreferencesEditorModel(target: SettingsTarget | undefined): Promise { - const resource = target === ConfigurationTarget.USER ? this.preferencesService.userSettingsResource : - target === ConfigurationTarget.WORKSPACE ? this.preferencesService.workspaceSettingsResource : - target; + const resource = target === ConfigurationTarget.USER_LOCAL ? this.preferencesService.userSettingsResource : + target === ConfigurationTarget.USER_REMOTE ? this.preferencesService.userSettingsResource : + target === ConfigurationTarget.WORKSPACE ? this.preferencesService.workspaceSettingsResource : + target; if (!resource) { return undefined; @@ -821,7 +822,7 @@ class SideBySidePreferencesWidget extends Widget { this.editablePreferencesEditorContainer = DOM.$('.editable-preferences-editor-container'); const editablePreferencesHeaderContainer = DOM.append(this.editablePreferencesEditorContainer, DOM.$('.preferences-header-container')); - this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, editablePreferencesHeaderContainer)); + this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, editablePreferencesHeaderContainer, undefined)); this._register(this.settingsTargetsWidget.onDidTargetChange(target => this._onDidSettingsTargetChange.fire(target))); this._register(attachStylerCallback(this.themeService, { scrollbarShadow }, colors => { @@ -865,7 +866,7 @@ class SideBySidePreferencesWidget extends Widget { private getDefaultPreferencesHeaderText(target: ConfigurationTarget): string { switch (target) { - case ConfigurationTarget.USER: + case ConfigurationTarget.USER_LOCAL: return nls.localize('defaultUserSettings', "Default User Settings"); case ConfigurationTarget.WORKSPACE: return nls.localize('defaultWorkspaceSettings', "Default Workspace Settings"); @@ -942,7 +943,7 @@ class SideBySidePreferencesWidget extends Widget { private getSettingsTarget(resource: URI): SettingsTarget { if (this.preferencesService.userSettingsResource.toString() === resource.toString()) { - return ConfigurationTarget.USER; + return ConfigurationTarget.USER_LOCAL; } const workspaceSettingsResource = this.preferencesService.workspaceSettingsResource; @@ -955,7 +956,7 @@ class SideBySidePreferencesWidget extends Widget { return folder.uri; } - return ConfigurationTarget.USER; + return ConfigurationTarget.USER_LOCAL; } private disposeEditors(): void { @@ -1202,7 +1203,7 @@ class SettingsEditorContribution extends AbstractSettingsEditorContribution impl .then(settingsModel => { if (settingsModel instanceof SettingsEditorModel && this.editor.getModel()) { switch (settingsModel.configurationTarget) { - case ConfigurationTarget.USER: + case ConfigurationTarget.USER_LOCAL: return this.instantiationService.createInstance(UserSettingsRenderer, this.editor, settingsModel); case ConfigurationTarget.WORKSPACE: return this.instantiationService.createInstance(WorkspaceSettingsRenderer, this.editor, settingsModel); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 65099dc618..27ecd6c04f 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -24,11 +24,14 @@ import { ConfigurationTarget } from 'vs/platform/configuration/common/configurat import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { activeContrastBorder, badgeBackground, badgeForeground, contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { PANEL_ACTIVE_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND } from 'vs/workbench/common/theme'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; export class SettingsHeaderWidget extends Widget implements IViewZone { @@ -464,14 +467,20 @@ export class FolderSettingsActionItem extends BaseActionItem { } } -export type SettingsTarget = ConfigurationTarget.USER | ConfigurationTarget.WORKSPACE | URI; +export type SettingsTarget = ConfigurationTarget.USER_LOCAL | ConfigurationTarget.USER_REMOTE | ConfigurationTarget.WORKSPACE | URI; + +export interface ISettingsTargetsWidgetOptions { + enableRemoteSettings?: boolean; +} export class SettingsTargetsWidget extends Widget { private settingsSwitcherBar: ActionBar; - private userSettings: Action; + private userLocalSettings: Action; + private userRemoteSettings: Action; private workspaceSettings: Action; private folderSettings: FolderSettingsActionItem; + private options: ISettingsTargetsWidgetOptions; private _settingsTarget: SettingsTarget; @@ -480,10 +489,14 @@ export class SettingsTargetsWidget extends Widget { constructor( parent: HTMLElement, + options: ISettingsTargetsWidgetOptions | undefined, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILabelService private readonly labelService: ILabelService ) { super(); + this.options = options || {}; this.create(parent); this._register(this.contextService.onDidChangeWorkbenchState(() => this.onWorkbenchStateChanged())); this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.update())); @@ -498,18 +511,25 @@ export class SettingsTargetsWidget extends Widget { actionItemProvider: (action: Action) => action.id === 'folderSettings' ? this.folderSettings : undefined })); - this.userSettings = new Action('userSettings', localize('userSettings', "User Settings"), '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER)); - this.userSettings.tooltip = this.userSettings.label; + this.userLocalSettings = new Action('userSettings', localize('userSettings', "User Settings"), '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_LOCAL)); + this.userLocalSettings.tooltip = this.userLocalSettings.label; + + const remoteAuthority = this.environmentService.configuration.remoteAuthority; + const hostLabel = remoteAuthority && this.labelService.getHostLabel(REMOTE_HOST_SCHEME, remoteAuthority); + const remoteSettingsLabel = localize('userSettingsRemote', "Remote Settings") + + (hostLabel ? ` (${hostLabel})` : ''); + this.userRemoteSettings = new Action('userSettingsRemote', remoteSettingsLabel, '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_REMOTE)); + this.userRemoteSettings.tooltip = this.userRemoteSettings.label; this.workspaceSettings = new Action('workspaceSettings', localize('workspaceSettings', "Workspace Settings"), '.settings-tab', false, () => this.updateTarget(ConfigurationTarget.WORKSPACE)); this.workspaceSettings.tooltip = this.workspaceSettings.label; - const folderSettingsAction = new Action('folderSettings', localize('folderSettings', "Folder Settings"), '.settings-tab', false, (folder: IWorkspaceFolder) => this.updateTarget(folder ? folder.uri : ConfigurationTarget.USER)); + const folderSettingsAction = new Action('folderSettings', localize('folderSettings', "Folder Settings"), '.settings-tab', false, (folder: IWorkspaceFolder) => this.updateTarget(folder.uri)); this.folderSettings = this.instantiationService.createInstance(FolderSettingsActionItem, folderSettingsAction); this.update(); - this.settingsSwitcherBar.push([this.userSettings, this.workspaceSettings, folderSettingsAction]); + this.settingsSwitcherBar.push([this.userLocalSettings, this.userRemoteSettings, this.workspaceSettings, folderSettingsAction]); } get settingsTarget(): SettingsTarget { @@ -518,7 +538,8 @@ export class SettingsTargetsWidget extends Widget { set settingsTarget(settingsTarget: SettingsTarget) { this._settingsTarget = settingsTarget; - this.userSettings.checked = ConfigurationTarget.USER === this.settingsTarget; + this.userLocalSettings.checked = ConfigurationTarget.USER_LOCAL === this.settingsTarget; + this.userRemoteSettings.checked = ConfigurationTarget.USER_REMOTE === this.settingsTarget; this.workspaceSettings.checked = ConfigurationTarget.WORKSPACE === this.settingsTarget; if (this.settingsTarget instanceof URI) { this.folderSettings.getAction().checked = true; @@ -536,13 +557,13 @@ export class SettingsTargetsWidget extends Widget { } this.workspaceSettings.label = label; - } else if (settingsTarget === ConfigurationTarget.USER) { + } else if (settingsTarget === ConfigurationTarget.USER_LOCAL) { let label = localize('userSettings', "User Settings"); if (count) { label += ` (${count})`; } - this.userSettings.label = label; + this.userLocalSettings.label = label; } else if (settingsTarget instanceof URI) { this.folderSettings.setCount(settingsTarget, count); } @@ -552,21 +573,27 @@ export class SettingsTargetsWidget extends Widget { this.folderSettings.folder = null; this.update(); if (this.settingsTarget === ConfigurationTarget.WORKSPACE && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - this.updateTarget(ConfigurationTarget.USER); + this.updateTarget(ConfigurationTarget.USER_LOCAL); } } updateTarget(settingsTarget: SettingsTarget): Promise { - const isSameTarget = this.settingsTarget === settingsTarget || settingsTarget instanceof URI && this.settingsTarget instanceof URI && this.settingsTarget.toString() === settingsTarget.toString(); + const isSameTarget = this.settingsTarget === settingsTarget || + settingsTarget instanceof URI && + this.settingsTarget instanceof URI && + this.settingsTarget.toString() === settingsTarget.toString(); + if (!isSameTarget) { this.settingsTarget = settingsTarget; this._onDidTargetChange.fire(this.settingsTarget); } + return Promise.resolve(undefined); } private update(): void { DOM.toggleClass(this.settingsSwitcherBar.domNode, 'empty-workbench', this.contextService.getWorkbenchState() === WorkbenchState.EMPTY); + this.userRemoteSettings.enabled = !!(this.options.enableRemoteSettings && this.environmentService.configuration.remoteAuthority); this.workspaceSettings.enabled = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY; this.folderSettings.getAction().enabled = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && this.contextService.getWorkspace().folders.length > 0; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index a6913d2067..b75e240239 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -1174,7 +1174,7 @@ export class SettingsTreeFilter implements ITreeFilter { } // Non-user scope selected - if (element instanceof SettingsTreeSettingElement && this.viewState.settingsTarget !== ConfigurationTarget.USER) { + if (element instanceof SettingsTreeSettingElement && this.viewState.settingsTarget !== ConfigurationTarget.USER_LOCAL) { if (!element.matchesScope(this.viewState.settingsTarget)) { return false; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 312fbed6b9..baf5dc9651 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as arrays from 'vs/base/common/arrays'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { isArray, withUndefinedAsNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -19,6 +20,7 @@ export const ONLINE_SERVICES_SETTING_TAG = 'usesOnlineServices'; export interface ISettingsEditorViewState { settingsTarget: SettingsTarget; tagFilters?: Set; + extensionFilters?: Set; filterToCategory?: SettingsTreeGroupElement; } @@ -228,6 +230,24 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { return true; } + + matchesAnyExtension(extensionFilters?: Set): boolean { + if (!extensionFilters || !extensionFilters.size) { + return true; + } + + if (!this.setting.extensionInfo) { + return false; + } + + for (let extensionId of extensionFilters) { + if (extensionId.toLowerCase() === this.setting.extensionInfo.id.toLowerCase()) { + return true; + } + } + + return false; + } } export class SettingsTreeModel { @@ -337,9 +357,10 @@ interface IInspectResult { function inspectSetting(key: string, target: SettingsTarget, configurationService: IConfigurationService): IInspectResult { const inspectOverrides = URI.isUri(target) ? { resource: target } : undefined; const inspected = configurationService.inspect(key, inspectOverrides); - const targetSelector = target === ConfigurationTarget.USER ? 'user' : - target === ConfigurationTarget.WORKSPACE ? 'workspace' : - 'workspaceFolder'; + const targetSelector = target === ConfigurationTarget.USER_LOCAL ? 'userLocal' : + target === ConfigurationTarget.USER_REMOTE ? 'userRemote' : + target === ConfigurationTarget.WORKSPACE ? 'workspace' : + 'workspaceFolder'; const isConfigured = typeof inspected[targetSelector] !== 'undefined'; return { isConfigured, inspected, targetSelector }; @@ -494,7 +515,7 @@ export class SearchResultModel extends SettingsTreeModel { // Save time, filter children in the search model instead of relying on the tree filter, which still requires heights to be calculated. this.root.children = this.root.children - .filter(child => child instanceof SettingsTreeSettingElement && child.matchesAllTags(this._viewState.tagFilters) && child.matchesScope(this._viewState.settingsTarget)); + .filter(child => child instanceof SettingsTreeSettingElement && child.matchesAllTags(this._viewState.tagFilters) && child.matchesScope(this._viewState.settingsTarget) && child.matchesAnyExtension(this._viewState.extensionFilters)); if (this.newExtensionSearchResults && this.newExtensionSearchResults.filterMatches.length) { const newExtElement = new SettingsTreeNewExtensionsElement(); @@ -527,11 +548,14 @@ export class SearchResultModel extends SettingsTreeModel { export interface IParsedQuery { tags: string[]; query: string; + extensionFilters: string[]; } const tagRegex = /(^|\s)@tag:("([^"]*)"|[^"]\S*)/g; +const extensionRegex = /(^|\s)@ext:("([^"]*)"|[^"]\S*)?/g; export function parseQuery(query: string): IParsedQuery { const tags: string[] = []; + let extensions: string[] = []; query = query.replace(tagRegex, (_, __, quotedTag, tag) => { tags.push(tag || quotedTag); return ''; @@ -542,10 +566,19 @@ export function parseQuery(query: string): IParsedQuery { return ''; }); + query = query.replace(extensionRegex, (_, __, quotedExtensionId, extensionId) => { + let extensionIdQuery: string = extensionId || quotedExtensionId; + if (extensionIdQuery) { + extensions.push(...extensionIdQuery.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s))); + } + return ''; + }); + query = query.trim(); return { tags, + extensionFilters: extensions, query }; } diff --git a/src/vs/workbench/contrib/preferences/browser/tocTree.ts b/src/vs/workbench/contrib/preferences/browser/tocTree.ts index 29d35a96a7..000197065a 100644 --- a/src/vs/workbench/contrib/preferences/browser/tocTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/tocTree.ts @@ -81,7 +81,7 @@ export class TOCTreeModel { } // Check everything that the SettingsFilter checks except whether it's filtered by a category - return child.matchesScope(this._viewState.settingsTarget) && child.matchesAllTags(this._viewState.tagFilters); + return child.matchesScope(this._viewState.settingsTarget) && child.matchesAllTags(this._viewState.tagFilters) && child.matchesAnyExtension(this._viewState.extensionFilters); }).length; } } diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index 5df7443e4f..b6e8d2a2e4 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -108,5 +108,6 @@ export const KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS = 'keybindings.editor.show export const DEFAULT_SETTINGS_EDITOR_SETTING = 'workbench.settings.openDefaultSettings'; export const MODIFIED_SETTING_TAG = 'modified'; +export const EXTENSION_SETTING_TAG = 'ext:'; export const SETTINGS_COMMAND_OPEN_SETTINGS = 'workbench.action.openSettings'; diff --git a/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts index 6dfab4223f..27a011eda5 100644 --- a/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts @@ -217,8 +217,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingWeight.WorkbenchContrib, when: null, primary: KeyMod.CtrlCmd | KeyCode.US_COMMA, - handler: (accessor, args: any) => { - accessor.get(IPreferencesService).openSettings(); + handler: (accessor, args: string | undefined) => { + accessor.get(IPreferencesService).openSettings(undefined, typeof args === 'string' ? args : undefined); } }); diff --git a/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts index 44168d3924..725fc42b19 100644 --- a/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts @@ -11,6 +11,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import * as collections from 'vs/base/common/collections'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; import { Iterator } from 'vs/base/common/iterator'; +import * as strings from 'vs/base/common/strings'; import { isArray, withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/settingsEditor2'; @@ -35,7 +36,7 @@ import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickE import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { settingsTextInputBorder } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, IPreferencesSearchService, ISearchProvider, MODIFIED_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, IPreferencesSearchService, ISearchProvider, MODIFIED_SETTING_TAG, EXTENSION_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IPreferencesService, ISearchResult, ISettingsEditorModel, ISettingsEditorOptions, SettingsEditorOptions, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; @@ -69,7 +70,7 @@ export class SettingsEditor2 extends BaseEditor { private static SETTING_UPDATE_SLOW_DEBOUNCE: number = 1000; private static readonly SUGGESTIONS: string[] = [ - `@${MODIFIED_SETTING_TAG}`, '@tag:usesOnlineServices' + `@${MODIFIED_SETTING_TAG}`, '@tag:usesOnlineServices', `@${EXTENSION_SETTING_TAG}` ]; private static shouldSettingUpdateFast(type: SettingValueType | SettingValueType[]): boolean { @@ -149,7 +150,7 @@ export class SettingsEditor2 extends BaseEditor { this.delayedFilterLogging = new Delayer(1000); this.localSearchDelayer = new Delayer(300); this.remoteSearchThrottle = new ThrottledDelayer(200); - this.viewState = { settingsTarget: ConfigurationTarget.USER }; + this.viewState = { settingsTarget: ConfigurationTarget.USER_LOCAL }; this.settingFastUpdateDelayer = new Delayer(SettingsEditor2.SETTING_UPDATE_FAST_DEBOUNCE); this.settingSlowUpdateDelayer = new Delayer(SettingsEditor2.SETTING_UPDATE_SLOW_DEBOUNCE); @@ -212,10 +213,10 @@ export class SettingsEditor2 extends BaseEditor { if (!options) { if (!this.viewState.settingsTarget) { // Persist? - options = SettingsEditorOptions.create({ target: ConfigurationTarget.USER }); + options = SettingsEditorOptions.create({ target: ConfigurationTarget.USER_LOCAL }); } } else if (!options.target) { - options.target = ConfigurationTarget.USER; + options.target = ConfigurationTarget.USER_LOCAL; } this._setOptions(options); @@ -371,7 +372,7 @@ export class SettingsEditor2 extends BaseEditor { this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, searchContainer, { triggerCharacters: ['@'], provideResults: (query: string) => { - return SettingsEditor2.SUGGESTIONS.filter(tag => query.indexOf(tag) === -1).map(tag => tag + ' '); + return SettingsEditor2.SUGGESTIONS.filter(tag => query.indexOf(tag) === -1).map(tag => strings.endsWith(tag, ':') ? tag : tag + ' '); } }, searchBoxLabel, 'settingseditor:searchinput' + SettingsEditor2.NUM_INSTANCES++, { placeholderText: searchBoxLabel, @@ -406,8 +407,8 @@ export class SettingsEditor2 extends BaseEditor { const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls')); const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container')); - this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer)); - this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER; + this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer, { enableRemoteSettings: true })); + this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER_LOCAL; this.settingsTargetsWidget.onDidTargetChange(target => this.onDidSettingsTargetChange(target)); } @@ -458,8 +459,10 @@ export class SettingsEditor2 extends BaseEditor { const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget; const options: ISettingsEditorOptions = { query }; - if (currentSettingsTarget === ConfigurationTarget.USER) { + if (currentSettingsTarget === ConfigurationTarget.USER_LOCAL) { return this.preferencesService.openGlobalSettings(true, options); + } else if (currentSettingsTarget === ConfigurationTarget.USER_REMOTE) { + return this.preferencesService.openRemoteSettings(); } else if (currentSettingsTarget === ConfigurationTarget.WORKSPACE) { return this.preferencesService.openWorkspaceSettings(true, options); } else { @@ -615,8 +618,10 @@ export class SettingsEditor2 extends BaseEditor { this._register(this.settingRenderers.onDidClickOverrideElement((element: ISettingOverrideClickEvent) => { if (ConfigurationTargetToString(ConfigurationTarget.WORKSPACE) === element.scope.toUpperCase()) { this.settingsTargetsWidget.updateTarget(ConfigurationTarget.WORKSPACE); - } else if (ConfigurationTargetToString(ConfigurationTarget.USER) === element.scope.toUpperCase()) { - this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER); + } else if (ConfigurationTargetToString(ConfigurationTarget.USER_LOCAL) === element.scope.toUpperCase()) { + this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_LOCAL); + } else if (ConfigurationTargetToString(ConfigurationTarget.USER_REMOTE) === element.scope.toUpperCase()) { + this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_REMOTE); } this.searchWidget.setValue(element.targetKey); @@ -792,9 +797,10 @@ export class SettingsEditor2 extends BaseEditor { } } - const reportedTarget = props.settingsTarget === ConfigurationTarget.USER ? 'user' : - props.settingsTarget === ConfigurationTarget.WORKSPACE ? 'workspace' : - 'folder'; + const reportedTarget = props.settingsTarget === ConfigurationTarget.USER_LOCAL ? 'user' : + props.settingsTarget === ConfigurationTarget.USER_REMOTE ? 'user_remote' : + props.settingsTarget === ConfigurationTarget.WORKSPACE ? 'workspace' : + 'folder'; const data = { key: props.key, @@ -1035,17 +1041,19 @@ export class SettingsEditor2 extends BaseEditor { private triggerSearch(query: string): Promise { this.viewState.tagFilters = new Set(); + this.viewState.extensionFilters = new Set(); if (query) { const parsedQuery = parseQuery(query); query = parsedQuery.query; parsedQuery.tags.forEach(tag => this.viewState.tagFilters!.add(tag)); + parsedQuery.extensionFilters.forEach(extensionId => this.viewState.extensionFilters!.add(extensionId)); } if (query && query !== '@') { query = this.parseSettingFromJSON(query) || query; return this.triggerFilterPreferences(query); } else { - if (this.viewState.tagFilters && this.viewState.tagFilters.size) { + if ((this.viewState.tagFilters && this.viewState.tagFilters.size) || (this.viewState.extensionFilters && this.viewState.extensionFilters.size)) { this.searchResultModel = this.createFilterModel(); } else { this.searchResultModel = null; diff --git a/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts b/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts index 4fd119f459..3f395789bb 100644 --- a/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts +++ b/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts @@ -116,6 +116,7 @@ suite('SettingsTree', () => { '', { tags: [], + extensionFilters: [], query: '' }); @@ -123,6 +124,7 @@ suite('SettingsTree', () => { '@modified', { tags: ['modified'], + extensionFilters: [], query: '' }); @@ -130,6 +132,7 @@ suite('SettingsTree', () => { '@tag:foo', { tags: ['foo'], + extensionFilters: [], query: '' }); @@ -137,6 +140,7 @@ suite('SettingsTree', () => { '@modified foo', { tags: ['modified'], + extensionFilters: [], query: 'foo' }); @@ -144,6 +148,7 @@ suite('SettingsTree', () => { '@tag:foo @modified', { tags: ['foo', 'modified'], + extensionFilters: [], query: '' }); @@ -151,6 +156,7 @@ suite('SettingsTree', () => { '@tag:foo @modified my query', { tags: ['foo', 'modified'], + extensionFilters: [], query: 'my query' }); @@ -158,6 +164,7 @@ suite('SettingsTree', () => { 'test @modified query', { tags: ['modified'], + extensionFilters: [], query: 'test query' }); @@ -165,6 +172,7 @@ suite('SettingsTree', () => { 'test @modified', { tags: ['modified'], + extensionFilters: [], query: 'test' }); @@ -172,7 +180,24 @@ suite('SettingsTree', () => { 'query has @ for some reason', { tags: [], + extensionFilters: [], query: 'query has @ for some reason' }); + + testParseQuery( + '@ext:github.vscode-pull-request-github', + { + tags: [], + extensionFilters: ['github.vscode-pull-request-github'], + query: '' + }); + + testParseQuery( + '@ext:github.vscode-pull-request-github,vscode.git', + { + tags: [], + extensionFilters: ['github.vscode-pull-request-github', 'vscode.git'], + query: '' + }); }); }); \ No newline at end of file diff --git a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts index 8ab4b4b628..b894e27e7a 100644 --- a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts +++ b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts @@ -85,7 +85,8 @@ class PartsSplash { colorInfo, layoutInfo, baseTheme - }) + }), + { encoding: 'utf8', overwriteEncoding: true } ); if (baseTheme !== this._lastBaseTheme || colorInfo.editorBackground !== this._lastBackground) { diff --git a/src/vs/workbench/contrib/stats/node/workspaceStats.ts b/src/vs/workbench/contrib/stats/node/workspaceStats.ts index 43f48abed9..05715aae64 100644 --- a/src/vs/workbench/contrib/stats/node/workspaceStats.ts +++ b/src/vs/workbench/contrib/stats/node/workspaceStats.ts @@ -533,7 +533,7 @@ export class WorkspaceStats implements IWorkbenchContribution { const workspace = this.contextService.getWorkspace(); // Handle top-level workspace files for local single folder workspace - if (state === WorkbenchState.FOLDER && workspace.folders[0].uri.scheme === Schemas.file) { + if (state === WorkbenchState.FOLDER) { const workspaceFiles = rootFiles.filter(hasWorkspaceFileExtension); if (workspaceFiles.length > 0) { this.doHandleWorkspaceFiles(workspace.folders[0].uri, workspaceFiles); diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 277775e502..cd2bea3e86 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -184,6 +184,9 @@ module.exports = function createWebviewManager(host) { let isHandlingScroll = false; const handleInnerScroll = (event) => { + if (!event.target || !event.target.body) { + return; + } if (isHandlingScroll) { return; } @@ -205,6 +208,10 @@ module.exports = function createWebviewManager(host) { }; document.addEventListener('DOMContentLoaded', () => { + if (!document.body) { + return; + } + host.onMessage('styles', (_event, variables, activeTheme) => { initData.styles = variables; initData.activeTheme = activeTheme; @@ -214,7 +221,9 @@ module.exports = function createWebviewManager(host) { return; } - applyStyles(target.contentDocument, target.contentDocument.body); + if (target.contentDocument) { + applyStyles(target.contentDocument, target.contentDocument.body); + } }); // propagate focus @@ -346,15 +355,15 @@ module.exports = function createWebviewManager(host) { return false; }; - let onLoad = (contentDocument, contentWindow) => { - if (contentDocument.body) { + const onLoad = (contentDocument, contentWindow) => { + if (contentDocument && contentDocument.body) { // Workaround for https://github.com/Microsoft/vscode/issues/12865 // check new scrollY and reset if neccessary setInitialScrollPosition(contentDocument.body, contentWindow); } const newFrame = getPendingFrame(); - if (newFrame && newFrame.contentDocument === contentDocument) { + if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) { const oldActiveFrame = getActiveFrame(); if (oldActiveFrame) { document.body.removeChild(oldActiveFrame); diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts index e76ea4b799..ea1b40b6da 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts @@ -46,7 +46,7 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { title: input.getName(), options: input.options, extensionLocation: input.extension ? input.extension.location : undefined, - extensionId: input.extension ? input.extension.id.value : undefined, + extensionId: input.extension && input.extension.id ? input.extension.id.value : undefined, state: input.state, iconPath: input.iconPath ? { light: input.iconPath.light, dark: input.iconPath.dark, } : undefined, group: input.group diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index ff69daf130..ac55545d8e 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -526,7 +526,7 @@ export class ElectronWindow extends Disposable { const resource = URI.revive(p.fileUri); let input: IResourceInput | IUntitledResourceInput; if (isNew) { - input = { filePath: resource.fsPath, options: { pinned: true } }; + input = { filePath: resource!.fsPath, options: { pinned: true } }; } else { input = { resource, options: { pinned: true } }; } diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 10bf4765f5..b2f8bdce92 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -29,7 +29,6 @@ export class RemoteUserConfiguration extends Disposable { private readonly _cachedConfiguration: CachedUserConfiguration; private readonly _configurationFileService: IConfigurationFileService; private _userConfiguration: UserConfiguration | CachedUserConfiguration; - private _userConfigurationDisposable: IDisposable = Disposable.None; private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); public readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; @@ -43,13 +42,14 @@ export class RemoteUserConfiguration extends Disposable { super(); this._configurationFileService = configurationFileService; this._userConfiguration = this._cachedConfiguration = new CachedUserConfiguration(remoteAuthority, configurationCache); - remoteAgentService.getEnvironment().then(environment => { + remoteAgentService.getEnvironment().then(async environment => { if (environment) { + const userConfiguration = this._register(new UserConfiguration(environment.settingsPath, MACHINE_SCOPES, this._configurationFileService)); + this._register(userConfiguration.onDidChangeConfiguration(configurationModel => this.onDidUserConfigurationChange(configurationModel))); + const configurationModel = await userConfiguration.initialize(); this._userConfiguration.dispose(); - this._userConfigurationDisposable.dispose(); - this._userConfiguration = this._register(new UserConfiguration(environment.settingsPath, MACHINE_SCOPES, this._configurationFileService)); - this._userConfigurationDisposable = this._register(this._userConfiguration.onDidChangeConfiguration(configurationModel => this.onDidUserConfigurationChange(configurationModel))); - this._userConfiguration.initialize().then(configurationModel => this.onDidUserConfigurationChange(configurationModel)); + this._userConfiguration = userConfiguration; + this.onDidUserConfigurationChange(configurationModel); } }); } diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 9b89d24745..093d35f32c 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -18,10 +18,10 @@ import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides import { Configuration, WorkspaceConfigurationChangeEvent, AllKeysConfigurationChangeEvent } from 'vs/workbench/services/configuration/common/configurationModels'; import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, IConfigurationFileService, machineSettingsSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, isSingleFolderWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, IEmptyWorkspaceInitializationPayload, useSlashForPath, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ConfigurationEditingService } from 'vs/workbench/services/configuration/common/configurationEditingService'; +import { ConfigurationEditingService, EditableConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditingService'; import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, UserConfiguration } from 'vs/workbench/services/configuration/browser/configuration'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; @@ -269,6 +269,8 @@ export class WorkspaceService extends Disposable implements IConfigurationServic inspect(key: string, overrides?: IConfigurationOverrides): { default: T, user: T, + userLocal?: T, + userRemote?: T, workspace?: T, workspaceFolder?: T, memory?: T, @@ -419,8 +421,15 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } private reloadUserConfiguration(key?: string): Promise<{ local: ConfigurationModel, remote: ConfigurationModel }> { - return Promise.all([this.localUserConfiguration ? this.localUserConfiguration.reload() : Promise.resolve(new ConfigurationModel()), this.remoteUserConfiguration ? this.remoteUserConfiguration.reload() : Promise.resolve(new ConfigurationModel())]) - .then(([local, remote]) => ({ local, remote })); + return Promise.all([this.reloadLocalUserConfiguration(), this.reloadRemoeUserConfiguration()]).then(([local, remote]) => ({ local, remote })); + } + + private reloadLocalUserConfiguration(key?: string): Promise { + return this.localUserConfiguration ? this.localUserConfiguration.reload() : Promise.resolve(new ConfigurationModel()); + } + + private reloadRemoeUserConfiguration(key?: string): Promise { + return this.remoteUserConfiguration ? this.remoteUserConfiguration.reload() : Promise.resolve(new ConfigurationModel()); } private reloadWorkspaceConfiguration(key?: string): Promise { @@ -503,16 +512,18 @@ export class WorkspaceService extends Disposable implements IConfigurationServic }; const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; - const unsupportedApplicationSettings = convertToNotSuggestedProperties(applicationSettings.properties, localize('unsupportedApplicationSetting', "This setting can be applied only in User Settings")); - const workspaceSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; + const unsupportedApplicationSettings = convertToNotSuggestedProperties(applicationSettings.properties, localize('unsupportedApplicationSetting', "This setting can be applied only in application user Settings")); + const unsupportedMachineSettings = convertToNotSuggestedProperties(machineSettings.properties, localize('unsupportedMachineSetting', "This setting can be applied only in user Settings")); + const machineSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; + const workspaceSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...unsupportedMachineSettings, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; jsonRegistry.registerSchema(defaultSettingsSchemaId, allSettingsSchema); jsonRegistry.registerSchema(userSettingsSchemaId, allSettingsSchema); - jsonRegistry.registerSchema(machineSettingsSchemaId, workspaceSettingsSchema); + jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); if (WorkbenchState.WORKSPACE === this.getWorkbenchState()) { const unsupportedWindowSettings = convertToNotSuggestedProperties(windowSettings.properties, localize('unsupportedWindowSetting', "This setting cannot be applied now. It will be applied when you open this folder directly.")); - const folderSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...unsupportedWindowSettings, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; + const folderSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...unsupportedMachineSettings, ...unsupportedWindowSettings, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); jsonRegistry.registerSchema(folderSettingsSchemaId, folderSettingsSchema); } else { @@ -613,20 +624,25 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return Promise.resolve(undefined); } - return this.configurationEditingService.writeConfiguration(target, { key, value }, { scopes: overrides, donotNotifyError }) + const editableConfigurationTarget = this.toEditableConfigurationTarget(target, key); + if (!editableConfigurationTarget) { + return Promise.reject(new Error('Invalid configuration target')); + } + + if (editableConfigurationTarget === EditableConfigurationTarget.USER_REMOTE && !this.remoteUserConfiguration) { + return Promise.reject(new Error('Invalid configuration target')); + } + + return this.configurationEditingService.writeConfiguration(editableConfigurationTarget, { key, value }, { scopes: overrides, donotNotifyError }) .then(() => { - switch (target) { - case ConfigurationTarget.USER: - return this.reloadUserConfiguration() - .then(({ local, remote }) => { - this.onLocalUserConfigurationChanged(local); - if (this.remoteUserConfiguration) { - this.onRemoteUserConfigurationChanged(remote); - } - }); - case ConfigurationTarget.WORKSPACE: + switch (editableConfigurationTarget) { + case EditableConfigurationTarget.USER_LOCAL: + return this.reloadLocalUserConfiguration().then(local => this.onLocalUserConfigurationChanged(local)); + case EditableConfigurationTarget.USER_REMOTE: + return this.reloadRemoeUserConfiguration().then(remote => this.onRemoteUserConfigurationChanged(remote)); + case EditableConfigurationTarget.WORKSPACE: return this.reloadWorkspaceConfiguration(); - case ConfigurationTarget.WORKSPACE_FOLDER: + case EditableConfigurationTarget.WORKSPACE_FOLDER: const workspaceFolder = overrides && overrides.resource ? this.workspace.getFolder(overrides.resource) : null; if (workspaceFolder) { return this.reloadWorkspaceFolderConfiguration(workspaceFolder, key); @@ -681,4 +697,26 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } return {}; } + + private toEditableConfigurationTarget(target: ConfigurationTarget, key: string): EditableConfigurationTarget | null { + if (target === ConfigurationTarget.USER) { + if (this.inspect(key).userRemote !== undefined) { + return EditableConfigurationTarget.USER_REMOTE; + } + return EditableConfigurationTarget.USER_LOCAL; + } + if (target === ConfigurationTarget.USER_LOCAL) { + return EditableConfigurationTarget.USER_LOCAL; + } + if (target === ConfigurationTarget.USER_REMOTE) { + return EditableConfigurationTarget.USER_REMOTE; + } + if (target === ConfigurationTarget.WORKSPACE) { + return EditableConfigurationTarget.WORKSPACE; + } + if (target === ConfigurationTarget.WORKSPACE_FOLDER) { + return EditableConfigurationTarget.WORKSPACE_FOLDER; + } + return null; + } } \ No newline at end of file diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 444c20cbea..41af549c18 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -20,7 +20,7 @@ export const workspaceSettingsSchemaId = 'vscode://schemas/settings/workspace'; export const folderSettingsSchemaId = 'vscode://schemas/settings/folder'; export const launchSchemaId = 'vscode://schemas/launch'; -export const MACHINE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; +export const MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; export const WORKSPACE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; export const FOLDER_SCOPES = [ConfigurationScope.RESOURCE]; diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 1c7846da15..73725d8b0e 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -18,7 +18,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IConfigurationService, IConfigurationOverrides, keyFromOverrideIdentifier, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, IConfigurationOverrides, keyFromOverrideIdentifier } from 'vs/platform/configuration/common/configuration'; import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration'; import { IFileService } from 'vs/platform/files/common/files'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; @@ -42,6 +42,11 @@ export const enum ConfigurationEditingErrorCode { */ ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, + /** + * Error when trying to write a machne setting into workspace settings. + */ + ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE, + /** * Error when trying to write an invalid folder configuration key to folder settings. */ @@ -104,8 +109,15 @@ export interface IConfigurationEditingOptions { scopes?: IConfigurationOverrides; } +export const enum EditableConfigurationTarget { + USER_LOCAL = 1, + USER_REMOTE, + WORKSPACE, + WORKSPACE_FOLDER +} + interface IConfigurationEditOperation extends IConfigurationValue { - target: ConfigurationTarget; + target: EditableConfigurationTarget; jsonPath: json.JSONPath; resource?: URI; workspaceStandAloneConfigurationKey?: string; @@ -143,7 +155,7 @@ export class ConfigurationEditingService { }); } - writeConfiguration(target: ConfigurationTarget, value: IConfigurationValue, options: IConfigurationEditingOptions = {}): Promise { + writeConfiguration(target: EditableConfigurationTarget, value: IConfigurationValue, options: IConfigurationEditingOptions = {}): Promise { const operation = this.getConfigurationEditOperation(target, value, options.scopes || {}); return Promise.resolve(this.queue.queue(() => this.doWriteConfiguration(operation, options) // queue up writes to prevent race conditions .then(() => null, @@ -251,13 +263,16 @@ export class ConfigurationEditingService { private openSettings(operation: IConfigurationEditOperation): void { switch (operation.target) { - case ConfigurationTarget.USER: + case EditableConfigurationTarget.USER_LOCAL: this.preferencesService.openGlobalSettings(true); break; - case ConfigurationTarget.WORKSPACE: + case EditableConfigurationTarget.USER_REMOTE: + this.preferencesService.openRemoteSettings(); + break; + case EditableConfigurationTarget.WORKSPACE: this.preferencesService.openWorkspaceSettings(true); break; - case ConfigurationTarget.WORKSPACE_FOLDER: + case EditableConfigurationTarget.WORKSPACE_FOLDER: if (operation.resource) { const workspaceFolder = this.contextService.getWorkspaceFolder(operation.resource); if (workspaceFolder) { @@ -272,18 +287,19 @@ export class ConfigurationEditingService { this.editorService.openEditor({ resource }); } - private reject(code: ConfigurationEditingErrorCode, target: ConfigurationTarget, operation: IConfigurationEditOperation): Promise { + private reject(code: ConfigurationEditingErrorCode, target: EditableConfigurationTarget, operation: IConfigurationEditOperation): Promise { const message = this.toErrorMessage(code, target, operation); return Promise.reject(new ConfigurationEditingError(message, code)); } - private toErrorMessage(error: ConfigurationEditingErrorCode, target: ConfigurationTarget, operation: IConfigurationEditOperation): string { + private toErrorMessage(error: ConfigurationEditingErrorCode, target: EditableConfigurationTarget, operation: IConfigurationEditOperation): string { switch (error) { // API constraints case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return nls.localize('errorUnknownKey', "Unable to write to {0} because {1} is not a registered configuration.", this.stringifyTarget(target), operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION: return nls.localize('errorInvalidWorkspaceConfigurationApplication', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); + case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE: return nls.localize('errorInvalidWorkspaceConfigurationMachine', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION: return nls.localize('errorInvalidFolderConfiguration', "Unable to write to Folder Settings because {0} does not support the folder resource scope.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET: return nls.localize('errorInvalidUserTarget', "Unable to write to User Settings because {0} does not support for global scope.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET: return nls.localize('errorInvalidWorkspaceTarget', "Unable to write to Workspace Settings because {0} does not support for workspace scope in a multi folder workspace.", operation.key); @@ -299,11 +315,13 @@ export class ConfigurationEditingService { return nls.localize('errorInvalidLaunchConfiguration', "Unable to write into the launch configuration file. Please open it to correct errors/warnings in it and try again."); } switch (target) { - case ConfigurationTarget.USER: + case EditableConfigurationTarget.USER_LOCAL: return nls.localize('errorInvalidConfiguration', "Unable to write into user settings. Please open the user settings to correct errors/warnings in it and try again."); - case ConfigurationTarget.WORKSPACE: + case EditableConfigurationTarget.USER_REMOTE: + return nls.localize('errorInvalidRemoteConfiguration', "Unable to write into remote user settings. Please open the remote user settings to correct errors/warnings in it and try again."); + case EditableConfigurationTarget.WORKSPACE: return nls.localize('errorInvalidConfigurationWorkspace', "Unable to write into workspace settings. Please open the workspace settings to correct errors/warnings in the file and try again."); - case ConfigurationTarget.WORKSPACE_FOLDER: + case EditableConfigurationTarget.WORKSPACE_FOLDER: let workspaceFolderName: string = '<>'; if (operation.resource) { const folder = this.contextService.getWorkspaceFolder(operation.resource); @@ -323,11 +341,13 @@ export class ConfigurationEditingService { return nls.localize('errorLaunchConfigurationFileDirty', "Unable to write into launch configuration file because the file is dirty. Please save it first and then try again."); } switch (target) { - case ConfigurationTarget.USER: + case EditableConfigurationTarget.USER_LOCAL: return nls.localize('errorConfigurationFileDirty', "Unable to write into user settings because the file is dirty. Please save the user settings file first and then try again."); - case ConfigurationTarget.WORKSPACE: + case EditableConfigurationTarget.USER_REMOTE: + return nls.localize('errorRemoteConfigurationFileDirty', "Unable to write into remote user settings because the file is dirty. Please save the remote user settings file first and then try again."); + case EditableConfigurationTarget.WORKSPACE: return nls.localize('errorConfigurationFileDirtyWorkspace', "Unable to write into workspace settings because the file is dirty. Please save the workspace settings file first and then try again."); - case ConfigurationTarget.WORKSPACE_FOLDER: + case EditableConfigurationTarget.WORKSPACE_FOLDER: let workspaceFolderName: string = '<>'; if (operation.resource) { const folder = this.contextService.getWorkspaceFolder(operation.resource); @@ -342,13 +362,15 @@ export class ConfigurationEditingService { } } - private stringifyTarget(target: ConfigurationTarget): string { + private stringifyTarget(target: EditableConfigurationTarget): string { switch (target) { - case ConfigurationTarget.USER: + case EditableConfigurationTarget.USER_LOCAL: return nls.localize('userTarget', "User Settings"); - case ConfigurationTarget.WORKSPACE: + case EditableConfigurationTarget.USER_REMOTE: + return nls.localize('remoteUserTarget', "Remote User Settings"); + case EditableConfigurationTarget.WORKSPACE: return nls.localize('workspaceTarget', "Workspace Settings"); - case ConfigurationTarget.WORKSPACE_FOLDER: + case EditableConfigurationTarget.WORKSPACE_FOLDER: return nls.localize('folderTarget', "Folder Settings"); } return ''; @@ -375,7 +397,7 @@ export class ConfigurationEditingService { private async resolveModelReference(resource: URI): Promise> { const exists = await this.fileService.exists(resource); if (!exists) { - await this.textFileService.write(resource, '{}'); + await this.textFileService.write(resource, '{}', { encoding: 'utf8' }); } return this.textModelResolverService.createModelReference(resource); } @@ -391,7 +413,7 @@ export class ConfigurationEditingService { return parseErrors.length > 0; } - private resolveAndValidate(target: ConfigurationTarget, operation: IConfigurationEditOperation, checkDirty: boolean, overrides: IConfigurationOverrides): Promise> { + private resolveAndValidate(target: EditableConfigurationTarget, operation: IConfigurationEditOperation, checkDirty: boolean, overrides: IConfigurationOverrides): Promise> { // Any key must be a known setting from the registry (unless this is a standalone config) if (!operation.workspaceStandAloneConfigurationKey) { @@ -403,31 +425,34 @@ export class ConfigurationEditingService { if (operation.workspaceStandAloneConfigurationKey) { // Global tasks and launches are not supported - if (target === ConfigurationTarget.USER) { + if (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE) { return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET, target, operation); } // Workspace tasks are not supported - if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && operation.target === ConfigurationTarget.WORKSPACE) { + if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && operation.target === EditableConfigurationTarget.WORKSPACE) { return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET, target, operation); } } // Target cannot be workspace or folder if no workspace opened - if ((target === ConfigurationTarget.WORKSPACE || target === ConfigurationTarget.WORKSPACE_FOLDER) && this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + if ((target === EditableConfigurationTarget.WORKSPACE || target === EditableConfigurationTarget.WORKSPACE_FOLDER) && this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { return this.reject(ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED, target, operation); } - if (target === ConfigurationTarget.WORKSPACE) { + if (target === EditableConfigurationTarget.WORKSPACE) { if (!operation.workspaceStandAloneConfigurationKey) { const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); if (configurationProperties[operation.key].scope === ConfigurationScope.APPLICATION) { return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, target, operation); } + if (configurationProperties[operation.key].scope === ConfigurationScope.MACHINE) { + return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE, target, operation); + } } } - if (target === ConfigurationTarget.WORKSPACE_FOLDER) { + if (target === EditableConfigurationTarget.WORKSPACE_FOLDER) { if (!operation.resource) { return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, target, operation); } @@ -460,7 +485,7 @@ export class ConfigurationEditingService { }); } - private getConfigurationEditOperation(target: ConfigurationTarget, config: IConfigurationValue, overrides: IConfigurationOverrides): IConfigurationEditOperation { + private getConfigurationEditOperation(target: EditableConfigurationTarget, config: IConfigurationValue, overrides: IConfigurationOverrides): IConfigurationEditOperation { // Check for standalone workspace configurations if (config.key) { @@ -485,7 +510,7 @@ export class ConfigurationEditingService { let key = config.key; let jsonPath = overrides.overrideIdentifier ? [keyFromOverrideIdentifier(overrides.overrideIdentifier), key] : [key]; - if (target === ConfigurationTarget.USER) { + if (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE) { return { key, jsonPath, value: config.value, resource: withNullAsUndefined(this.getConfigurationFileResource(target, config, '', null)), target }; } @@ -501,26 +526,19 @@ export class ConfigurationEditingService { return !!(workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath); } - private getConfigurationFileResource(target: ConfigurationTarget, config: IConfigurationValue, relativePath: string, resource: URI | null | undefined): URI | null { - if (target === ConfigurationTarget.USER_LOCAL) { + private getConfigurationFileResource(target: EditableConfigurationTarget, config: IConfigurationValue, relativePath: string, resource: URI | null | undefined): URI | null { + if (target === EditableConfigurationTarget.USER_LOCAL) { return URI.file(this.environmentService.appSettingsPath); } - if (target === ConfigurationTarget.USER_REMOTE) { + if (target === EditableConfigurationTarget.USER_REMOTE) { return this.remoteSettingsResource; } - if (target === ConfigurationTarget.USER) { - if (this.configurationService.inspect(config.key).userRemote !== undefined) { - return this.remoteSettingsResource; - } - return URI.file(this.environmentService.appSettingsPath); - } - const workbenchState = this.contextService.getWorkbenchState(); if (workbenchState !== WorkbenchState.EMPTY) { const workspace = this.contextService.getWorkspace(); - if (target === ConfigurationTarget.WORKSPACE) { + if (target === EditableConfigurationTarget.WORKSPACE) { if (workbenchState === WorkbenchState.WORKSPACE) { return withUndefinedAsNull(workspace.configuration); } @@ -529,7 +547,7 @@ export class ConfigurationEditingService { } } - if (target === ConfigurationTarget.WORKSPACE_FOLDER) { + if (target === EditableConfigurationTarget.WORKSPACE_FOLDER) { if (resource) { const folder = this.contextService.getWorkspaceFolder(resource); if (folder) { diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index cf35ba7c66..fc07e7a648 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -97,6 +97,8 @@ export class Configuration extends BaseConfiguration { inspect(key: string, overrides: IConfigurationOverrides = {}): { default: C, user: C, + userLocal?: C, + userRemote?: C, workspace?: C, workspaceFolder?: C memory?: C diff --git a/src/vs/workbench/services/configuration/common/jsonEditingService.ts b/src/vs/workbench/services/configuration/common/jsonEditingService.ts index 29b46137a5..c897ba7570 100644 --- a/src/vs/workbench/services/configuration/common/jsonEditingService.ts +++ b/src/vs/workbench/services/configuration/common/jsonEditingService.ts @@ -86,7 +86,7 @@ export class JSONEditingService implements IJSONEditingService { private async resolveModelReference(resource: URI): Promise> { const exists = await this.fileService.exists(resource); if (!exists) { - await this.textFileService.write(resource, '{}'); + await this.textFileService.write(resource, '{}', { encoding: 'utf8' }); } return this.textModelResolverService.createModelReference(resource); } diff --git a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index 7c7a5faecf..8392728979 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts @@ -35,6 +35,11 @@ suite('FolderSettingsModelParser', () => { 'type': 'string', 'default': 'isSet', scope: ConfigurationScope.APPLICATION + }, + 'FolderSettingsModelParser.machine': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.MACHINE } } }); @@ -43,7 +48,7 @@ suite('FolderSettingsModelParser', () => { test('parse all folder settings', () => { const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]); - testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' })); + testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'application', 'FolderSettingsModelParser.machine': 'executable' })); assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'window': 'window', 'resource': 'resource' } }); }); @@ -51,7 +56,7 @@ suite('FolderSettingsModelParser', () => { test('parse resource folder settings', () => { const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE]); - testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' })); + testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'application', 'FolderSettingsModelParser.machine': 'executable' })); assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource' } }); }); @@ -59,12 +64,12 @@ suite('FolderSettingsModelParser', () => { test('parse overridable resource settings', () => { const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE]); - testObject.parseContent(JSON.stringify({ '[json]': { 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' } })); + testObject.parseContent(JSON.stringify({ '[json]': { 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'application', 'FolderSettingsModelParser.machine': 'executable' } })); assert.deepEqual(testObject.configurationModel.overrides, [{ 'contents': { 'FolderSettingsModelParser': { 'resource': 'resource' } }, 'identifiers': ['json'] }]); }); - test('reprocess folder settings excludes application setting', () => { + test('reprocess folder settings excludes application and machine setting', () => { const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]); testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.anotherApplicationSetting': 'executable' })); @@ -80,6 +85,11 @@ suite('FolderSettingsModelParser', () => { 'type': 'string', 'default': 'isSet', scope: ConfigurationScope.APPLICATION + }, + 'FolderSettingsModelParser.anotherMachineSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.MACHINE } } }); diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts index 15027e035e..2108a1beed 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts @@ -19,9 +19,9 @@ import * as uuid from 'vs/base/common/uuid'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; -import { ConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/common/configurationEditingService'; +import { ConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode, EditableConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditingService'; import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -153,41 +153,41 @@ suite('ConfigurationEditingService', () => { } test('errors cases - invalid key', () => { - return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'unknown.key', value: 'value' }) + return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'unknown.key', value: 'value' }) .then(() => assert.fail('Should fail with ERROR_UNKNOWN_KEY'), (error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY)); }); test('errors cases - invalid target', () => { - return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'tasks.something', value: 'value' }) + return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'tasks.something', value: 'value' }) .then(() => assert.fail('Should fail with ERROR_INVALID_TARGET'), (error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET)); }); test('errors cases - no workspace', () => { return setUpServices(true) - .then(() => testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'configurationEditing.service.testSetting', value: 'value' })) + .then(() => testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'configurationEditing.service.testSetting', value: 'value' })) .then(() => assert.fail('Should fail with ERROR_NO_WORKSPACE_OPENED'), (error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED)); }); test('errors cases - invalid configuration', () => { fs.writeFileSync(globalSettingsFile, ',,,,,,,,,,,,,,'); - return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }) + return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }) .then(() => assert.fail('Should fail with ERROR_INVALID_CONFIGURATION'), (error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION)); }); test('errors cases - dirty', () => { instantiationService.stub(ITextFileService, 'isDirty', true); - return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }) + return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }) .then(() => assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'), (error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY)); }); test('dirty error is not thrown if not asked to save', () => { instantiationService.stub(ITextFileService, 'isDirty', true); - return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotSave: true }) + return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotSave: true }) .then(() => null, error => assert.fail('Should not fail.')); }); @@ -195,7 +195,7 @@ suite('ConfigurationEditingService', () => { instantiationService.stub(ITextFileService, 'isDirty', true); const target = sinon.stub(); instantiationService.stub(INotificationService, { prompt: target, _serviceBrand: null, notify: null!, error: null!, info: null!, warn: null! }); - return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotNotifyError: true }) + return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotNotifyError: true }) .then(() => assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'), (error: ConfigurationEditingError) => { assert.equal(false, target.calledOnce); @@ -204,7 +204,7 @@ suite('ConfigurationEditingService', () => { }); test('write one setting - empty file', () => { - return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }) + return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }) .then(() => { const contents = fs.readFileSync(globalSettingsFile).toString('utf8'); const parsed = json.parse(contents); @@ -214,7 +214,7 @@ suite('ConfigurationEditingService', () => { test('write one setting - existing file', () => { fs.writeFileSync(globalSettingsFile, '{ "my.super.setting": "my.super.value" }'); - return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }) + return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }) .then(() => { const contents = fs.readFileSync(globalSettingsFile).toString('utf8'); const parsed = json.parse(contents); @@ -225,7 +225,7 @@ suite('ConfigurationEditingService', () => { test('remove an existing setting - existing file', () => { fs.writeFileSync(globalSettingsFile, '{ "my.super.setting": "my.super.value", "configurationEditing.service.testSetting": "value" }'); - return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: undefined }) + return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: undefined }) .then(() => { const contents = fs.readFileSync(globalSettingsFile).toString('utf8'); const parsed = json.parse(contents); @@ -236,7 +236,7 @@ suite('ConfigurationEditingService', () => { test('remove non existing setting - existing file', () => { fs.writeFileSync(globalSettingsFile, '{ "my.super.setting": "my.super.value" }'); - return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: undefined }) + return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: undefined }) .then(() => { const contents = fs.readFileSync(globalSettingsFile).toString('utf8'); const parsed = json.parse(contents); @@ -246,7 +246,7 @@ suite('ConfigurationEditingService', () => { }); test('write workspace standalone setting - empty file', () => { - return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks.service.testSetting', value: 'value' }) + return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks.service.testSetting', value: 'value' }) .then(() => { const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']); const contents = fs.readFileSync(target).toString('utf8'); @@ -258,7 +258,7 @@ suite('ConfigurationEditingService', () => { test('write workspace standalone setting - existing file', () => { const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['launch']); fs.writeFileSync(target, '{ "my.super.setting": "my.super.value" }'); - return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'launch.service.testSetting', value: 'value' }) + return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'launch.service.testSetting', value: 'value' }) .then(() => { const contents = fs.readFileSync(target).toString('utf8'); const parsed = json.parse(contents); @@ -268,7 +268,7 @@ suite('ConfigurationEditingService', () => { }); test('write workspace standalone setting - empty file - full JSON', () => { - return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) + return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) .then(() => { const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']); const contents = fs.readFileSync(target).toString('utf8'); @@ -282,7 +282,7 @@ suite('ConfigurationEditingService', () => { test('write workspace standalone setting - existing file - full JSON', () => { const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']); fs.writeFileSync(target, '{ "my.super.setting": "my.super.value" }'); - return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) + return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) .then(() => { const contents = fs.readFileSync(target).toString('utf8'); const parsed = json.parse(contents); @@ -295,7 +295,7 @@ suite('ConfigurationEditingService', () => { test('write workspace standalone setting - existing file with JSON errors - full JSON', () => { const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']); fs.writeFileSync(target, '{ "my.super.setting": '); // invalid JSON - return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) + return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) .then(() => { const contents = fs.readFileSync(target).toString('utf8'); const parsed = json.parse(contents); @@ -318,7 +318,7 @@ suite('ConfigurationEditingService', () => { } ] }`); - return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask1' }] } }) + return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask1' }] } }) .then(() => { const actual = fs.readFileSync(target).toString('utf8'); const expected = JSON.stringify({ 'version': '1.0.0', tasks: [{ 'taskName': 'myTask1' }] }, null, '\t'); diff --git a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts index 4298b53bb5..7de4708647 100644 --- a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts @@ -48,10 +48,10 @@ export class RemoteFileDialog { private scheme: string = REMOTE_HOST_SCHEME; private shouldOverwriteFile: boolean = false; private contextKey: IContextKey; - private userEnteredPathSegment: string; private autoCompletePathSegment: string; private activeItem: FileQuickPickItem; + private userHome: URI; constructor( @IFileService private readonly fileService: IFileService, @@ -73,6 +73,7 @@ export class RemoteFileDialog { public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { this.scheme = this.getScheme(options.defaultUri, options.availableFileSystems); + this.userHome = await this.getUserHome(); const newOptions = await this.getOptions(options); if (!newOptions) { return Promise.resolve(undefined); @@ -90,6 +91,7 @@ export class RemoteFileDialog { public async showSaveDialog(options: ISaveDialogOptions): Promise { this.scheme = this.getScheme(options.defaultUri, options.availableFileSystems); + this.userHome = await this.getUserHome(); this.requiresTrailing = true; const newOptions = await this.getOptions(options, true); if (!newOptions) { @@ -107,19 +109,11 @@ export class RemoteFileDialog { }); } - private async getOptions(options: ISaveDialogOptions | IOpenDialogOptions, isSave: boolean = false): Promise { + private getOptions(options: ISaveDialogOptions | IOpenDialogOptions, isSave: boolean = false): IOpenDialogOptions | undefined { let defaultUri = options.defaultUri; const filename = (defaultUri && isSave && (resources.dirname(defaultUri).path === '/')) ? resources.basename(defaultUri) : undefined; if (!defaultUri || filename) { - if (this.scheme !== Schemas.file) { - const env = await this.remoteAgentService.getEnvironment(); - if (env) { - defaultUri = env.userHome; - } - } - if (!defaultUri) { - defaultUri = URI.from({ scheme: this.scheme, path: this.environmentService.userHome }); - } + defaultUri = this.userHome; if (filename) { defaultUri = resources.joinPath(defaultUri, filename); } @@ -149,6 +143,16 @@ export class RemoteFileDialog { return undefined; } + private async getUserHome(): Promise { + if (this.scheme !== Schemas.file) { + const env = await this.remoteAgentService.getEnvironment(); + if (env) { + return env.userHome; + } + } + return URI.from({ scheme: this.scheme, path: this.environmentService.userHome }); + } + private async pickResource(isSave: boolean = false): Promise { this.allowFolderSelection = !!this.options.canSelectFolders; this.allowFileSelection = !!this.options.canSelectFiles; @@ -357,7 +361,10 @@ export class RemoteFileDialog { } private async tryUpdateItems(value: string, valueUri: URI): Promise { - if (this.endsWithSlash(value) || (!resources.isEqual(this.currentFolder, resources.dirname(valueUri), true) && resources.isEqualOrParent(this.currentFolder, resources.dirname(valueUri), true))) { + if (value[value.length - 1] === '~') { + await this.updateItems(this.userHome); + return true; + } else if (this.endsWithSlash(value) || (!resources.isEqual(this.currentFolder, resources.dirname(valueUri), true) && resources.isEqualOrParent(this.currentFolder, resources.dirname(valueUri), true))) { let stat: IFileStat | undefined; try { stat = await this.fileService.resolve(valueUri); @@ -415,6 +422,12 @@ export class RemoteFileDialog { } private setAutoComplete(startingValue: string, startingBasename: string, quickPickItem: FileQuickPickItem, force: boolean = false): boolean { + if (this.filePickBox.busy) { + // We're in the middle of something else. Doing an auto complete now can result jumbled or incorrect autocompletes. + this.userEnteredPathSegment = startingBasename; + this.autoCompletePathSegment = ''; + return false; + } const itemBasename = (quickPickItem.label === '..') ? quickPickItem.label : resources.basename(quickPickItem.uri); // Either force the autocomplete, or the old value should be one smaller than the new value and match the new value. if (!force && (itemBasename.length >= startingBasename.length) && equalsIgnoreCase(itemBasename.substr(0, startingBasename.length), startingBasename)) { @@ -559,18 +572,18 @@ export class RemoteFileDialog { } private async updateItems(newFolder: URI, trailing?: string) { + this.filePickBox.busy = true; this.userEnteredPathSegment = trailing ? trailing : ''; this.autoCompletePathSegment = ''; - this.filePickBox.valueSelection = [0, this.filePickBox.value.length]; const newValue = trailing ? this.pathFromUri(resources.joinPath(newFolder, trailing)) : this.pathFromUri(newFolder, true); this.currentFolder = this.remoteUriFrom(this.pathFromUri(newFolder, true)); - this.insertText(newValue, newValue); - this.filePickBox.busy = true; return this.createItems(this.currentFolder).then(items => { this.filePickBox.items = items; if (this.allowFolderSelection) { this.filePickBox.activeItems = []; } + this.filePickBox.valueSelection = [0, this.filePickBox.value.length]; + this.insertText(newValue, newValue); this.filePickBox.busy = false; }); } diff --git a/src/vs/workbench/services/extensions/common/extensionHostDebug.ts b/src/vs/workbench/services/extensions/common/extensionHostDebug.ts index de19f9a2d9..08d14adc77 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostDebug.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostDebug.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { IRemoteConsoleLog } from 'vs/base/common/console'; @@ -26,14 +25,22 @@ export interface ITerminateSessionEvent { subId?: string; } +export interface IReloadSessionEvent { + sessionId: string; +} + +export interface ICloseSessionEvent { + sessionId: string; +} + export interface IExtensionHostDebugService { _serviceBrand: any; - reload(resource: URI): void; - onReload: Event; + reload(sessionId: string): void; + onReload: Event; - close(resource: URI): void; - onClose: Event; + close(sessionId: string): void; + onClose: Event; attachSession(sessionId: string, port: number, subId?: string): void; onAttachSession: Event; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts b/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts similarity index 96% rename from src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts rename to src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts index 61301d81e9..ffba240e50 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts @@ -12,9 +12,6 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ExtHostCustomersRegistry } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ProfileSession } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionHostStarter } from 'vs/workbench/services/extensions/electron-browser/extensionHost'; -import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; @@ -28,6 +25,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IUntitledResourceInput } 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'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -169,10 +167,6 @@ export class ExtensionHostProcessManager extends Disposable { return ExtensionHostProcessManager._convert(SIZE, sw.elapsed()); } - public canProfileExtensionHost(): boolean { - return this._extensionHostProcessWorker && Boolean(this._extensionHostProcessWorker.getInspectPort()); - } - private _createExtensionHostCustomers(protocol: IMessagePassingProtocol): ExtHostExtensionServiceShape { let logger: IRPCProtocolLogger | null = null; @@ -236,16 +230,6 @@ export class ExtensionHostProcessManager extends Disposable { }); } - public startExtensionHostProfile(): Promise { - if (this._extensionHostProcessWorker) { - let port = this._extensionHostProcessWorker.getInspectPort(); - if (port) { - return this._instantiationService.createInstance(ExtensionHostProfiler, port).start(); - } - } - throw new Error('Extension host not running or no inspect port available'); - } - public getInspectPort(): number { if (this._extensionHostProcessWorker) { let port = this._extensionHostProcessWorker.getInspectPort(); @@ -256,6 +240,10 @@ export class ExtensionHostProcessManager extends Disposable { return 0; } + public canProfileExtensionHost(): boolean { + return this._extensionHostProcessWorker && Boolean(this._extensionHostProcessWorker.getInspectPort()); + } + public async resolveAuthority(remoteAuthority: string): Promise { const authorityPlusIndex = remoteAuthority.indexOf('+'); if (authorityPlusIndex === -1) { diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 22b27c5a21..a234d6c846 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -10,6 +10,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; export const nullExtensionDescription = Object.freeze({ identifier: new ExtensionIdentifier('nullExtensionDescription'), @@ -82,6 +83,14 @@ export interface IExtensionHostProfile { getAggregatedTimes(): Map; } +export interface IExtensionHostStarter { + readonly onCrashed: Event<[number, string | null]>; + start(): Promise | null; + getInspectPort(): number | undefined; + dispose(): void; +} + + /** * Extension id or one of the four known program states. */ @@ -116,11 +125,10 @@ export interface IWillActivateEvent { } export interface IResponsiveStateChangeEvent { - target: ICpuProfilerTarget; isResponsive: boolean; } -export interface IExtensionService extends ICpuProfilerTarget { +export interface IExtensionService { _serviceBrand: any; /** @@ -200,7 +208,8 @@ export interface IExtensionService extends ICpuProfilerTarget { getExtensionsStatus(): { [id: string]: IExtensionsStatus }; /** - * Return the inspect port or 0. + * Return the inspect port or `0`, the latter means inspection + * is not possible. */ getInspectPort(): number; @@ -227,19 +236,6 @@ export interface IExtensionService extends ICpuProfilerTarget { _onExtensionHostExit(code: number): void; } -export interface ICpuProfilerTarget { - - /** - * Check if the extension host can be profiled. - */ - canProfileExtensionHost(): boolean; - - /** - * Begin an extension host process profile session. - */ - startExtensionHostProfile(): Promise; -} - export interface ProfileSession { stop(): Promise; } @@ -277,9 +273,7 @@ export class NullExtensionService implements IExtensionService { getExtension() { return Promise.resolve(undefined); } readExtensionPointContributions(_extPoint: IExtensionPoint): Promise[]> { return Promise.resolve(Object.create(null)); } getExtensionsStatus(): { [id: string]: IExtensionsStatus; } { return Object.create(null); } - canProfileExtensionHost(): boolean { return false; } getInspectPort(): number { return 0; } - startExtensionHostProfile(): Promise { return Promise.resolve(Object.create(null)); } restartExtensionHost(): void { } startExtensionHost(): void { } stopExtensionHost(): void { } diff --git a/src/vs/workbench/services/extensions/common/rpcProtocol.ts b/src/vs/workbench/services/extensions/common/rpcProtocol.ts index 5ff035cab0..405d8c22ac 100644 --- a/src/vs/workbench/services/extensions/common/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/common/rpcProtocol.ts @@ -258,11 +258,6 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { this._receiveReply(msgLength, req, value); break; } - case MessageType.ReplyOKBuffer: { - let value = MessageIO.deserializeReplyOKBuffer(buff); - this._receiveReply(msgLength, req, value); - break; - } case MessageType.ReplyOKVSBuffer: { let value = MessageIO.deserializeReplyOKVSBuffer(buff); this._receiveReply(msgLength, req, value); @@ -517,21 +512,11 @@ class MessageBuffer { return str; } - public static sizeBuffer(buff: VSBuffer): number { - return 4 /* buffer length */ + buff.byteLength /* actual buffer */; - } - public writeBuffer(buff: VSBuffer): void { this._buff.writeUint32BE(buff.byteLength, this._offset); this._offset += 4; this._buff.set(buff, this._offset); this._offset += buff.byteLength; } - public readBuffer(): Buffer { - const buffLength = this._buff.readUint32BE(this._offset); this._offset += 4; - const buff = this._buff.slice(this._offset, this._offset + buffLength); this._offset += buffLength; - return buff.buffer; - } - public static sizeVSBuffer(buff: VSBuffer): number { return 4 /* buffer length */ + buff.byteLength /* actual buffer */; } @@ -556,8 +541,6 @@ class MessageBuffer { size += 1; // arg type if (elType === ArgType.String) { size += this.sizeLongString(el); - } else if (elType === ArgType.Buffer) { - size += this.sizeBuffer(el); } else { size += this.sizeVSBuffer(el); } @@ -573,9 +556,6 @@ class MessageBuffer { if (elType === ArgType.String) { this.writeUInt8(ArgType.String); this.writeLongString(el); - } else if (elType === ArgType.Buffer) { - this.writeUInt8(ArgType.Buffer); - this.writeVSBuffer(el); } else { this.writeUInt8(ArgType.VSBuffer); this.writeVSBuffer(el); @@ -583,18 +563,15 @@ class MessageBuffer { } } - public readMixedArray(): Array { + public readMixedArray(): Array { const arrLen = this._buff.readUint8(this._offset); this._offset += 1; - let arr: Array = new Array(arrLen); + let arr: Array = new Array(arrLen); for (let i = 0; i < arrLen; i++) { const argType = this.readUInt8(); switch (argType) { case ArgType.String: arr[i] = this.readLongString(); break; - case ArgType.Buffer: - arr[i] = this.readBuffer(); - break; case ArgType.VSBuffer: arr[i] = this.readVSBuffer(); break; @@ -608,9 +585,6 @@ class MessageIO { private static _arrayContainsBuffer(arr: any[]): boolean { for (let i = 0, len = arr.length; i < len; i++) { - if (Buffer.isBuffer(arr[i])) { - return true; - } if (arr[i] instanceof VSBuffer) { return true; } @@ -624,10 +598,7 @@ class MessageIO { let massagedArgsType: ArgType[] = []; for (let i = 0, len = args.length; i < len; i++) { const arg = args[i]; - if (Buffer.isBuffer(arg)) { - massagedArgs[i] = VSBuffer.wrap(arg); - massagedArgsType[i] = ArgType.Buffer; - } else if (arg instanceof VSBuffer) { + if (arg instanceof VSBuffer) { massagedArgs[i] = arg; massagedArgsType[i] = ArgType.VSBuffer; } else { @@ -714,9 +685,6 @@ class MessageIO { if (typeof res === 'undefined') { return this._serializeReplyOKEmpty(req); } - if (Buffer.isBuffer(res)) { - return this._serializeReplyOKBuffer(req, res); - } if (res instanceof VSBuffer) { return this._serializeReplyOKVSBuffer(req, res); } @@ -727,17 +695,6 @@ class MessageIO { return MessageBuffer.alloc(MessageType.ReplyOKEmpty, req, 0).buffer; } - private static _serializeReplyOKBuffer(req: number, res: Buffer): VSBuffer { - const buff = VSBuffer.wrap(res); - - let len = 0; - len += MessageBuffer.sizeBuffer(buff); - - let result = MessageBuffer.alloc(MessageType.ReplyOKBuffer, req, len); - result.writeBuffer(buff); - return result.buffer; - } - private static _serializeReplyOKVSBuffer(req: number, res: VSBuffer): VSBuffer { let len = 0; len += MessageBuffer.sizeVSBuffer(res); @@ -747,10 +704,6 @@ class MessageIO { return result.buffer; } - public static deserializeReplyOKBuffer(buff: MessageBuffer): Buffer { - return buff.readBuffer(); - } - public static deserializeReplyOKVSBuffer(buff: MessageBuffer): VSBuffer { return buff.readVSBuffer(); } @@ -807,15 +760,13 @@ const enum MessageType { Acknowledged = 5, Cancel = 6, ReplyOKEmpty = 7, - ReplyOKBuffer = 8, - ReplyOKVSBuffer = 9, - ReplyOKJSON = 10, - ReplyErrError = 11, - ReplyErrEmpty = 12, + ReplyOKVSBuffer = 8, + ReplyOKJSON = 9, + ReplyErrError = 10, + ReplyErrEmpty = 11, } const enum ArgType { String = 1, - Buffer = 2, - VSBuffer = 3 + VSBuffer = 2 } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index ff1c3153a2..5a8c43eb24 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -14,7 +14,6 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; -import { isEqual } from 'vs/base/common/resources'; import pkg from 'vs/platform/product/node/package'; import { URI } from 'vs/base/common/uri'; import { IRemoteConsoleLog, log, parse } from 'vs/base/common/console'; @@ -38,13 +37,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { parseExtensionDevOptions } from '../common/extensionDevOptions'; import { VSBuffer } from 'vs/base/common/buffer'; import { IExtensionHostDebugService } from 'vs/workbench/services/extensions/common/extensionHostDebug'; - -export interface IExtensionHostStarter { - readonly onCrashed: Event<[number, string | null]>; - start(): Promise | null; - getInspectPort(): number | undefined; - dispose(): void; -} +import { IExtensionHostStarter } from 'vs/workbench/services/extensions/common/extensions'; export class ExtensionHostProcessWorker implements IExtensionHostStarter { @@ -102,13 +95,13 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { this._toDispose.push(this._onCrashed); this._toDispose.push(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); this._toDispose.push(this._lifecycleService.onShutdown(reason => this.terminate())); - this._toDispose.push(this._extensionHostDebugService.onClose(resource => { - if (this._isExtensionDevHost && this.matchesExtDevLocations(resource)) { + this._toDispose.push(this._extensionHostDebugService.onClose(event => { + if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) { this._windowService.closeWindow(); } })); - this._toDispose.push(this._extensionHostDebugService.onReload(resource => { - if (this._isExtensionDevHost && this.matchesExtDevLocations(resource)) { + this._toDispose.push(this._extensionHostDebugService.onReload(event => { + if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) { this._windowService.reloadWindow(); } })); @@ -120,16 +113,6 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { })); } - // returns true if the given resource url matches one of the extension development paths passed to VS Code - private matchesExtDevLocations(resource: URI): boolean { - - const extDevLocs = this._environmentService.extensionDevelopmentLocationURI; - if (extDevLocs) { - return extDevLocs.some(extDevLoc => isEqual(extDevLoc, resource)); - } - return false; - } - public dispose(): void { this.terminate(); } @@ -241,8 +224,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { if (!this._environmentService.isBuilt && !this._environmentService.configuration.remoteAuthority || this._isExtensionDevHost) { startupTimeoutHandle = setTimeout(() => { const msg = this._isExtensionDevDebugBrk - ? nls.localize('extensionHostProcess.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.") - : nls.localize('extensionHostProcess.startupFail', "Extension host did not start in 10 seconds, that might be a problem."); + ? nls.localize('extensionHost.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.") + : nls.localize('extensionHost.startupFail', "Extension host did not start in 10 seconds, that might be a problem."); this._notificationService.prompt(Severity.Warning, msg, [{ @@ -457,7 +440,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { this._lastExtensionHostError = errorMessage; - this._notificationService.error(nls.localize('extensionHostProcess.error', "Error from the extension host: {0}", errorMessage)); + this._notificationService.error(nls.localize('extensionHost.error', "Error from the extension host: {0}", errorMessage)); } private _onExtHostProcessExit(code: number, signal: string): void { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts index a5e7a2dffd..aa4a35f54d 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts @@ -6,23 +6,20 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IExtensionHostDebugService, IAttachSessionEvent, ITerminateSessionEvent, ILogToSessionEvent } from 'vs/workbench/services/extensions/common/extensionHostDebug'; -import { URI } from 'vs/base/common/uri'; +import { IExtensionHostDebugService, IAttachSessionEvent, ITerminateSessionEvent, ILogToSessionEvent, IReloadSessionEvent, ICloseSessionEvent } from 'vs/workbench/services/extensions/common/extensionHostDebug'; import { IRemoteConsoleLog } from 'vs/base/common/console'; import { ipcRenderer as ipc } from 'electron'; -interface IReloadBroadcast { +interface IReloadBroadcast extends IReloadSessionEvent { type: 'vscode:extensionReload'; - resource: string; } interface IAttachSessionBroadcast extends IAttachSessionEvent { type: 'vscode:extensionAttach'; } -interface ICloseBroadcast { +interface ICloseBroadcast extends ICloseSessionEvent { type: 'vscode:extensionCloseExtensionHost'; - resource: string; } interface ILogToSessionBroadcast extends ILogToSessionEvent { @@ -39,8 +36,8 @@ class ExtensionHostDebugService implements IExtensionHostDebugService { _serviceBrand: any; private windowId: number; - private readonly _onReload = new Emitter(); - private readonly _onClose = new Emitter(); + private readonly _onReload = new Emitter(); + private readonly _onClose = new Emitter(); private readonly _onAttachSession = new Emitter(); private readonly _onLogToSession = new Emitter(); private readonly _onTerminateSession = new Emitter(); @@ -53,10 +50,10 @@ class ExtensionHostDebugService implements IExtensionHostDebugService { ipc.on(CHANNEL, (_: unknown, broadcast: IReloadBroadcast | ICloseBroadcast | IAttachSessionBroadcast | ILogToSessionBroadcast | ITerminateSessionBroadcast) => { switch (broadcast.type) { case 'vscode:extensionReload': - this._onReload.fire(URI.parse(broadcast.resource)); + this._onReload.fire(broadcast); break; case 'vscode:extensionCloseExtensionHost': - this._onClose.fire(URI.parse(broadcast.resource)); + this._onClose.fire(broadcast); break; case 'vscode:extensionAttach': this._onAttachSession.fire(broadcast); @@ -71,25 +68,25 @@ class ExtensionHostDebugService implements IExtensionHostDebugService { }); } - reload(resource: URI): void { + reload(sessionId: string): void { ipc.send(CHANNEL, this.windowId, { type: 'vscode:extensionReload', - resource: resource.toString() + sessionId }); } - get onReload(): Event { + get onReload(): Event { return this._onReload.event; } - close(resource: URI): void { + close(sessionId: string): void { ipc.send(CHANNEL, this.windowId, { type: 'vscode:extensionCloseExtensionHost', - resource: resource.toString() + sessionId }); } - get onClose(): Event { + get onClose(): Event { return this._onClose.event; } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 92c4c05010..aacfcc5574 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -23,13 +23,13 @@ import product from 'vs/platform/product/node/product'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; -import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, ProfileSession, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser, schema } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; import { CachedExtensionScanner, Logger } from 'vs/workbench/services/extensions/electron-browser/cachedExtensionScanner'; -import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/electron-browser/extensionHostProcessManager'; +import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/common/extensionHostProcessManager'; import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Schemas } from 'vs/base/common/network'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -442,7 +442,7 @@ export class ExtensionService extends Disposable implements IExtensionService { const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, autoStart, extensions, this._extensionHostLogsLocation); const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, extHostProcessWorker, null, initialActivationEvents); extHostProcessManager.onDidCrash(([code, signal]) => this._onExtensionHostCrashed(code, signal)); - extHostProcessManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ target: extHostProcessManager, isResponsive: responsiveState === ResponsiveState.Responsive }); }); + extHostProcessManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ isResponsive: responsiveState === ResponsiveState.Responsive }); }); this._extensionHostProcessManagers.push(extHostProcessManager); } @@ -453,7 +453,7 @@ export class ExtensionService extends Disposable implements IExtensionService { if (code === 55) { this._notificationService.prompt( Severity.Error, - nls.localize('extensionHostProcess.versionMismatchCrash', "Extension host cannot start: version mismatch."), + nls.localize('extensionService.versionMismatchCrash', "Extension host cannot start: version mismatch."), [{ label: nls.localize('relaunch', "Relaunch VS Code"), run: () => { @@ -467,9 +467,9 @@ export class ExtensionService extends Disposable implements IExtensionService { return; } - let message = nls.localize('extensionHostProcess.crash', "Extension host terminated unexpectedly."); + let message = nls.localize('extensionService.crash', "Extension host terminated unexpectedly."); if (code === 87) { - message = nls.localize('extensionHostProcess.unresponsiveCrash', "Extension host terminated because it was not responsive."); + message = nls.localize('extensionService.unresponsiveCrash', "Extension host terminated because it was not responsive."); } this._notificationService.prompt(Severity.Error, message, @@ -569,26 +569,6 @@ export class ExtensionService extends Disposable implements IExtensionService { return result; } - public canProfileExtensionHost(): boolean { - for (let i = 0, len = this._extensionHostProcessManagers.length; i < len; i++) { - const extHostProcessManager = this._extensionHostProcessManagers[i]; - if (extHostProcessManager.canProfileExtensionHost()) { - return true; - } - } - return false; - } - - public startExtensionHostProfile(): Promise { - for (let i = 0, len = this._extensionHostProcessManagers.length; i < len; i++) { - const extHostProcessManager = this._extensionHostProcessManagers[i]; - if (extHostProcessManager.canProfileExtensionHost()) { - return extHostProcessManager.startExtensionHostProfile(); - } - } - throw new Error('Extension host not running or no inspect port available'); - } - public getInspectPort(): number { if (this._extensionHostProcessManagers.length > 0) { return this._extensionHostProcessManagers[0].getInspectPort(); diff --git a/src/vs/workbench/services/extensions/node/extensionHostMain.ts b/src/vs/workbench/services/extensions/node/extensionHostMain.ts index 18775d01b5..60e47348ba 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostMain.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostMain.ts @@ -12,13 +12,14 @@ import { IURITransformer } from 'vs/base/common/uriIpc'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { IInitData, MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; -import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; +import { ExtHostExtensionService, IHostUtils } from 'vs/workbench/api/node/extHostExtensionService'; import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { RPCProtocol } from 'vs/workbench/services/extensions/common/rpcProtocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { withNullAsUndefined } from 'vs/base/common/types'; import { ILogService } from 'vs/platform/log/common/log'; +import { ISchemeTransformer } from 'vs/workbench/api/common/extHostLanguageFeatures'; // we don't (yet) throw when extensions parse // uris that have no scheme @@ -39,17 +40,25 @@ export interface ILogServiceFn { export class ExtensionHostMain { private _isTerminating: boolean; - private readonly _exitFn: IExitFn; + private readonly _hostUtils: IHostUtils; private readonly _extensionService: ExtHostExtensionService; private readonly _extHostLogService: ExtHostLogService; private disposables: IDisposable[] = []; private _searchRequestIdProvider: Counter; - constructor(protocol: IMessagePassingProtocol, initData: IInitData, exitFn: IExitFn, consolePatchFn: IConsolePatchFn, logServiceFn: ILogServiceFn) { + constructor( + protocol: IMessagePassingProtocol, + initData: IInitData, + hostUtils: IHostUtils, + consolePatchFn: IConsolePatchFn, + logServiceFn: ILogServiceFn, + uriTransformer: IURITransformer | null, + schemeTransformer: ISchemeTransformer | null, + outputChannelName: string, + ) { this._isTerminating = false; - this._exitFn = exitFn; - const uriTransformer: IURITransformer | null = null; + this._hostUtils = hostUtils; const rpcProtocol = new RPCProtocol(protocol, null, uriTransformer); // ensure URIs are transformed and revived @@ -69,7 +78,17 @@ export class ExtensionHostMain { this._extHostLogService.trace('initData', initData); const extHostConfiguraiton = new ExtHostConfiguration(rpcProtocol.getProxy(MainContext.MainThreadConfiguration), extHostWorkspace); - this._extensionService = new ExtHostExtensionService(exitFn, initData, rpcProtocol, extHostWorkspace, extHostConfiguraiton, initData.environment, this._extHostLogService); + this._extensionService = new ExtHostExtensionService( + hostUtils, + initData, + rpcProtocol, + extHostWorkspace, + extHostConfiguraiton, + initData.environment, + this._extHostLogService, + schemeTransformer, + outputChannelName + ); // error forwarding and stack trace scanning Error.stackTraceLimit = 100; // increase number of stack frames (from 10, https://github.com/v8/v8/wiki/Stack-Trace-API) @@ -122,7 +141,7 @@ export class ExtensionHostMain { // Give extensions 1 second to wrap up any async dispose, then exit in at most 4 seconds setTimeout(() => { - Promise.race([timeout(4000), extensionsDeactivated]).finally(() => this._exitFn()); + Promise.race([timeout(4000), extensionsDeactivated]).finally(() => this._hostUtils.exit()); }, 1000); } diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcess.ts b/src/vs/workbench/services/extensions/node/extensionHostProcess.ts index 6009d6f9db..f8f0ec2bc9 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcess.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcess.ts @@ -3,287 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nativeWatchdog from 'native-watchdog'; -import * as net from 'net'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { Event } from 'vs/base/common/event'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; -import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import product from 'vs/platform/product/node/product'; -import { IInitData, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; -import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { ExtensionHostMain, IExitFn, ILogServiceFn } from 'vs/workbench/services/extensions/node/extensionHostMain'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; -import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions'; +import * as nls from 'vs/nls'; +import { startExtensionHostProcess } from 'vs/workbench/services/extensions/node/extensionHostProcessSetup'; -// With Electron 2.x and node.js 8.x the "natives" module -// can cause a native crash (see https://github.com/nodejs/node/issues/19891 and -// https://github.com/electron/electron/issues/10905). To prevent this from -// happening we essentially blocklist this module from getting loaded in any -// extension by patching the node require() function. -(function () { - const Module = require.__$__nodeRequire('module') as any; - const originalLoad = Module._load; - - Module._load = function (request: string) { - if (request === 'natives') { - throw new Error('Either the extension or a NPM dependency is using the "natives" node module which is unsupported as it can cause a crash of the extension host. Click [here](https://go.microsoft.com/fwlink/?linkid=871887) to find out more'); - } - - return originalLoad.apply(this, arguments); - }; -})(); - -// custom process.exit logic... -const nativeExit: IExitFn = process.exit.bind(process); -function patchProcess(allowExit: boolean) { - process.exit = function (code?: number) { - if (allowExit) { - nativeExit(code); - } else { - const err = new Error('An extension called process.exit() and this was prevented.'); - console.warn(err.stack); - } - } as (code?: number) => never; - - process.crash = function () { - const err = new Error('An extension called process.crash() and this was prevented.'); - console.warn(err.stack); - }; -} - -// use IPC messages to forward console-calls -function patchPatchedConsole(mainThreadConsole: MainThreadConsoleShape): void { - // The console is already patched to use `process.send()` - const nativeProcessSend = process.send!; - process.send = (...args: any[]) => { - if (args.length === 0 || !args[0] || args[0].type !== '__$console') { - return nativeProcessSend.apply(process, args); - } - - mainThreadConsole.$logExtensionHostMessage(args[0]); - }; -} - -const createLogService: ILogServiceFn = initData => createSpdLogService(ExtensionHostLogFileName, initData.logLevel, initData.logsLocation.fsPath); - -interface IRendererConnection { - protocol: IMessagePassingProtocol; - initData: IInitData; -} - -// This calls exit directly in case the initialization is not finished and we need to exit -// Otherwise, if initialization completed we go to extensionHostMain.terminate() -let onTerminate = function () { - nativeExit(); -}; - -function _createExtHostProtocol(): Promise { - if (process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET) { - - return new Promise((resolve, reject) => { - - let protocol: PersistentProtocol | null = null; - - let timer = setTimeout(() => { - reject(new Error('VSCODE_EXTHOST_IPC_SOCKET timeout')); - }, 60000); - - let disconnectWaitTimer: NodeJS.Timeout | null = null; - - process.on('message', (msg: IExtHostSocketMessage, handle: net.Socket) => { - if (msg && msg.type === 'VSCODE_EXTHOST_IPC_SOCKET') { - const initialDataChunk = VSBuffer.wrap(Buffer.from(msg.initialDataChunk, 'base64')); - if (protocol) { - // reconnection case - if (disconnectWaitTimer) { - clearTimeout(disconnectWaitTimer); - disconnectWaitTimer = null; - } - protocol.beginAcceptReconnection(new NodeSocket(handle), initialDataChunk); - protocol.endAcceptReconnection(); - } else { - clearTimeout(timer); - protocol = new PersistentProtocol(new NodeSocket(handle), initialDataChunk); - protocol.onClose(() => onTerminate()); - resolve(protocol); - - protocol.onSocketClose(() => { - // The socket has closed, let's give the renderer a certain amount of time to reconnect - disconnectWaitTimer = setTimeout(() => { - disconnectWaitTimer = null; - onTerminate(); - }, ProtocolConstants.ReconnectionGraceTime); - }); - } - } - }); - - // Now that we have managed to install a message listener, ask the other side to send us the socket - const req: IExtHostReadyMessage = { type: 'VSCODE_EXTHOST_IPC_READY' }; - if (process.send) { - process.send(req); - } - }); - - } else { - - const pipeName = process.env.VSCODE_IPC_HOOK_EXTHOST!; - - return new Promise((resolve, reject) => { - - const socket = net.createConnection(pipeName, () => { - socket.removeListener('error', reject); - resolve(new PersistentProtocol(new NodeSocket(socket))); - }); - socket.once('error', reject); - - }); - } -} - -async function createExtHostProtocol(): Promise { - - const protocol = await _createExtHostProtocol(); - - return new class implements IMessagePassingProtocol { - - private _terminating = false; - - readonly onMessage: Event = Event.filter(protocol.onMessage, msg => { - if (!isMessageOfType(msg, MessageType.Terminate)) { - return true; - } - this._terminating = true; - onTerminate(); - return false; - }); - - send(msg: any): void { - if (!this._terminating) { - protocol.send(msg); - } - } - }; -} - -function connectToRenderer(protocol: IMessagePassingProtocol): Promise { - return new Promise((c, e) => { - - // Listen init data message - const first = protocol.onMessage(raw => { - first.dispose(); - - const initData = JSON.parse(raw.toString()); - - const rendererCommit = initData.commit; - const myCommit = product.commit; - - if (rendererCommit && myCommit) { - // Running in the built version where commits are defined - if (rendererCommit !== myCommit) { - nativeExit(55); - } - } - - // Print a console message when rejection isn't handled within N seconds. For details: - // see https://nodejs.org/api/process.html#process_event_unhandledrejection - // and https://nodejs.org/api/process.html#process_event_rejectionhandled - const unhandledPromises: Promise[] = []; - process.on('unhandledRejection', (reason: any, promise: Promise) => { - unhandledPromises.push(promise); - setTimeout(() => { - const idx = unhandledPromises.indexOf(promise); - if (idx >= 0) { - promise.catch(e => { - unhandledPromises.splice(idx, 1); - console.warn(`rejected promise not handled within 1 second: ${e}`); - if (e.stack) { - console.warn(`stack trace: ${e.stack}`); - } - onUnexpectedError(reason); - }); - } - }, 1000); - }); - - process.on('rejectionHandled', (promise: Promise) => { - const idx = unhandledPromises.indexOf(promise); - if (idx >= 0) { - unhandledPromises.splice(idx, 1); - } - }); - - // Print a console message when an exception isn't handled. - process.on('uncaughtException', function (err: Error) { - onUnexpectedError(err); - }); - - // {{SQL CARBON EDIT}} - process.on('SIGPIPE', () => { - onUnexpectedError(new Error('Unexpected SIGPIPE')); - }); - // {{SQL CARBON EDIT}} - End - - // Kill oneself if one's parent dies. Much drama. - setInterval(function () { - try { - process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore. - } catch (e) { - onTerminate(); - } - }, 1000); - - // In certain cases, the event loop can become busy and never yield - // e.g. while-true or process.nextTick endless loops - // So also use the native node module to do it from a separate thread - let watchdog: typeof nativeWatchdog; - try { - watchdog = require.__$__nodeRequire('native-watchdog'); - watchdog.start(initData.parentPid); - } catch (err) { - // no problem... - onUnexpectedError(err); - } - - // Tell the outside that we are initialized - protocol.send(createMessageOfType(MessageType.Initialized)); - - c({ protocol, initData }); - }); - - // Tell the outside that we are ready to receive messages - protocol.send(createMessageOfType(MessageType.Ready)); - }); -} - -patchExecArgv(); - -createExtHostProtocol().then(protocol => { - // connect to main side - return connectToRenderer(protocol); -}).then(renderer => { - const { initData } = renderer; - // setup things - patchProcess(!!initData.environment.extensionTestsLocationURI); // to support other test frameworks like Jasmin that use process.exit (https://github.com/Microsoft/vscode/issues/37708) - - const extensionHostMain = new ExtensionHostMain(renderer.protocol, initData, nativeExit, patchPatchedConsole, createLogService); - - // rewrite onTerminate-function to be a proper shutdown - onTerminate = () => extensionHostMain.terminate(); -}).catch(err => console.error(err)); - -function patchExecArgv() { - // when encountering the prevent-inspect flag we delete this - // and the prior flag - if (process.env.VSCODE_PREVENT_FOREIGN_INSPECT) { - for (let i = 0; i < process.execArgv.length; i++) { - if (process.execArgv[i].match(/--inspect-brk=\d+|--inspect=\d+/)) { - process.execArgv.splice(i, 1); - break; - } - } - } -} +startExtensionHostProcess( + _ => null, + _ => null, + _ => nls.localize('extension host Log', "Extension Host") +).catch((err) => console.log(err)); diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts new file mode 100644 index 0000000000..cd9d907c66 --- /dev/null +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -0,0 +1,308 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nativeWatchdog from 'native-watchdog'; +import * as net from 'net'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Event } from 'vs/base/common/event'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import product from 'vs/platform/product/node/product'; +import { IInitData, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; +import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { ExtensionHostMain, IExitFn, ILogServiceFn } from 'vs/workbench/services/extensions/node/extensionHostMain'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; +import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions'; +import { ISchemeTransformer } from 'vs/workbench/api/common/extHostLanguageFeatures'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { exists } from 'vs/base/node/pfs'; +import { realpath } from 'vs/base/node/extpath'; +import { IHostUtils } from 'vs/workbench/api/node/extHostExtensionService'; + +// With Electron 2.x and node.js 8.x the "natives" module +// can cause a native crash (see https://github.com/nodejs/node/issues/19891 and +// https://github.com/electron/electron/issues/10905). To prevent this from +// happening we essentially blocklist this module from getting loaded in any +// extension by patching the node require() function. +(function () { + const Module = require.__$__nodeRequire('module') as any; + const originalLoad = Module._load; + + Module._load = function (request: string) { + if (request === 'natives') { + throw new Error('Either the extension or a NPM dependency is using the "natives" node module which is unsupported as it can cause a crash of the extension host. Click [here](https://go.microsoft.com/fwlink/?linkid=871887) to find out more'); + } + + return originalLoad.apply(this, arguments); + }; +})(); + +// custom process.exit logic... +const nativeExit: IExitFn = process.exit.bind(process); +function patchProcess(allowExit: boolean) { + process.exit = function (code?: number) { + if (allowExit) { + nativeExit(code); + } else { + const err = new Error('An extension called process.exit() and this was prevented.'); + console.warn(err.stack); + } + } as (code?: number) => never; + + process.crash = function () { + const err = new Error('An extension called process.crash() and this was prevented.'); + console.warn(err.stack); + }; +} + +// use IPC messages to forward console-calls +function patchPatchedConsole(mainThreadConsole: MainThreadConsoleShape): void { + // The console is already patched to use `process.send()` + const nativeProcessSend = process.send!; + process.send = (...args: any[]) => { + if (args.length === 0 || !args[0] || args[0].type !== '__$console') { + return nativeProcessSend.apply(process, args); + } + + mainThreadConsole.$logExtensionHostMessage(args[0]); + }; +} + +const createLogService: ILogServiceFn = initData => createSpdLogService(ExtensionHostLogFileName, initData.logLevel, initData.logsLocation.fsPath); + +interface IRendererConnection { + protocol: IMessagePassingProtocol; + initData: IInitData; +} + +// This calls exit directly in case the initialization is not finished and we need to exit +// Otherwise, if initialization completed we go to extensionHostMain.terminate() +let onTerminate = function () { + nativeExit(); +}; + +function _createExtHostProtocol(): Promise { + if (process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET) { + + return new Promise((resolve, reject) => { + + let protocol: PersistentProtocol | null = null; + + let timer = setTimeout(() => { + reject(new Error('VSCODE_EXTHOST_IPC_SOCKET timeout')); + }, 60000); + + let disconnectWaitTimer: NodeJS.Timeout | null = null; + + process.on('message', (msg: IExtHostSocketMessage, handle: net.Socket) => { + if (msg && msg.type === 'VSCODE_EXTHOST_IPC_SOCKET') { + const initialDataChunk = VSBuffer.wrap(Buffer.from(msg.initialDataChunk, 'base64')); + if (protocol) { + // reconnection case + if (disconnectWaitTimer) { + clearTimeout(disconnectWaitTimer); + disconnectWaitTimer = null; + } + protocol.beginAcceptReconnection(new NodeSocket(handle), initialDataChunk); + protocol.endAcceptReconnection(); + } else { + clearTimeout(timer); + protocol = new PersistentProtocol(new NodeSocket(handle), initialDataChunk); + protocol.onClose(() => onTerminate()); + resolve(protocol); + + protocol.onSocketClose(() => { + // The socket has closed, let's give the renderer a certain amount of time to reconnect + disconnectWaitTimer = setTimeout(() => { + disconnectWaitTimer = null; + onTerminate(); + }, ProtocolConstants.ReconnectionGraceTime); + }); + } + } + }); + + // Now that we have managed to install a message listener, ask the other side to send us the socket + const req: IExtHostReadyMessage = { type: 'VSCODE_EXTHOST_IPC_READY' }; + if (process.send) { + process.send(req); + } + }); + + } else { + + const pipeName = process.env.VSCODE_IPC_HOOK_EXTHOST!; + + return new Promise((resolve, reject) => { + + const socket = net.createConnection(pipeName, () => { + socket.removeListener('error', reject); + resolve(new PersistentProtocol(new NodeSocket(socket))); + }); + socket.once('error', reject); + + }); + } +} + +async function createExtHostProtocol(): Promise { + + const protocol = await _createExtHostProtocol(); + + return new class implements IMessagePassingProtocol { + + private _terminating = false; + + readonly onMessage: Event = Event.filter(protocol.onMessage, msg => { + if (!isMessageOfType(msg, MessageType.Terminate)) { + return true; + } + this._terminating = true; + onTerminate(); + return false; + }); + + send(msg: any): void { + if (!this._terminating) { + protocol.send(msg); + } + } + }; +} + +function connectToRenderer(protocol: IMessagePassingProtocol): Promise { + return new Promise((c, e) => { + + // Listen init data message + const first = protocol.onMessage(raw => { + first.dispose(); + + const initData = JSON.parse(raw.toString()); + + const rendererCommit = initData.commit; + const myCommit = product.commit; + + if (rendererCommit && myCommit) { + // Running in the built version where commits are defined + if (rendererCommit !== myCommit) { + nativeExit(55); + } + } + + // Print a console message when rejection isn't handled within N seconds. For details: + // see https://nodejs.org/api/process.html#process_event_unhandledrejection + // and https://nodejs.org/api/process.html#process_event_rejectionhandled + const unhandledPromises: Promise[] = []; + process.on('unhandledRejection', (reason: any, promise: Promise) => { + unhandledPromises.push(promise); + setTimeout(() => { + const idx = unhandledPromises.indexOf(promise); + if (idx >= 0) { + promise.catch(e => { + unhandledPromises.splice(idx, 1); + console.warn(`rejected promise not handled within 1 second: ${e}`); + if (e.stack) { + console.warn(`stack trace: ${e.stack}`); + } + onUnexpectedError(reason); + }); + } + }, 1000); + }); + + process.on('rejectionHandled', (promise: Promise) => { + const idx = unhandledPromises.indexOf(promise); + if (idx >= 0) { + unhandledPromises.splice(idx, 1); + } + }); + + // Print a console message when an exception isn't handled. + process.on('uncaughtException', function (err: Error) { + onUnexpectedError(err); + }); + + // Kill oneself if one's parent dies. Much drama. + setInterval(function () { + try { + process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore. + } catch (e) { + onTerminate(); + } + }, 1000); + + // In certain cases, the event loop can become busy and never yield + // e.g. while-true or process.nextTick endless loops + // So also use the native node module to do it from a separate thread + let watchdog: typeof nativeWatchdog; + try { + watchdog = require.__$__nodeRequire('native-watchdog'); + watchdog.start(initData.parentPid); + } catch (err) { + // no problem... + onUnexpectedError(err); + } + + // Tell the outside that we are initialized + protocol.send(createMessageOfType(MessageType.Initialized)); + + c({ protocol, initData }); + }); + + // Tell the outside that we are ready to receive messages + protocol.send(createMessageOfType(MessageType.Ready)); + }); +} + +// patchExecArgv: +(function () { + // when encountering the prevent-inspect flag we delete this + // and the prior flag + if (process.env.VSCODE_PREVENT_FOREIGN_INSPECT) { + for (let i = 0; i < process.execArgv.length; i++) { + if (process.execArgv[i].match(/--inspect-brk=\d+|--inspect=\d+/)) { + process.execArgv.splice(i, 1); + break; + } + } + } +})(); + +export async function startExtensionHostProcess( + uriTransformerFn: (initData: IInitData) => IURITransformer | null, + schemeTransformerFn: (initData: IInitData) => ISchemeTransformer | null, + outputChannelNameFn: (initData: IInitData) => string, +): Promise { + + const protocol = await createExtHostProtocol(); + const renderer = await connectToRenderer(protocol); + const { initData } = renderer; + // setup things + patchProcess(!!initData.environment.extensionTestsLocationURI); // to support other test frameworks like Jasmin that use process.exit (https://github.com/Microsoft/vscode/issues/37708) + + // host abstraction + const hostUtils = new class NodeHost implements IHostUtils { + exit(code: number) { nativeExit(code); } + exists(path: string) { return exists(path); } + realpath(path: string) { return realpath(path); } + }; + + + const extensionHostMain = new ExtensionHostMain( + renderer.protocol, + initData, + hostUtils, + patchPatchedConsole, + createLogService, + uriTransformerFn(initData), + schemeTransformerFn(initData), + outputChannelNameFn(initData) + ); + + // rewrite onTerminate-function to be a proper shutdown + onTerminate = () => extensionHostMain.terminate(); +} diff --git a/src/vs/workbench/services/extensions/node/multiExtensionManagement.ts b/src/vs/workbench/services/extensions/node/multiExtensionManagement.ts index 9a114e1155..dd505d4bee 100644 --- a/src/vs/workbench/services/extensions/node/multiExtensionManagement.ts +++ b/src/vs/workbench/services/extensions/node/multiExtensionManagement.ts @@ -19,7 +19,6 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { localize } from 'vs/nls'; import { isUIExtension } from 'vs/workbench/services/extensions/node/extensionsUtil'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; export class MultiExtensionManagementService extends Disposable implements IExtensionManagementService { @@ -35,8 +34,7 @@ export class MultiExtensionManagementService extends Disposable implements IExte constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.servers = this.extensionManagementServerService.remoteExtensionManagementServer ? [this.extensionManagementServerService.localExtensionManagementServer, this.extensionManagementServerService.remoteExtensionManagementServer] : [this.extensionManagementServerService.localExtensionManagementServer]; @@ -58,8 +56,7 @@ export class MultiExtensionManagementService extends Disposable implements IExte if (!server) { return Promise.reject(`Invalid location ${extension.location.toString()}`); } - const syncExtensions = await this.hasToSyncExtensions(); - if (syncExtensions || isLanguagePackExtension(extension.manifest)) { + if (isLanguagePackExtension(extension.manifest)) { return this.uninstallEverywhere(extension, force); } return this.uninstallInServer(extension, server, force); @@ -134,9 +131,8 @@ export class MultiExtensionManagementService extends Disposable implements IExte async install(vsix: URI): Promise { if (this.extensionManagementServerService.remoteExtensionManagementServer) { - const syncExtensions = await this.hasToSyncExtensions(); const manifest = await getManifest(vsix.fsPath); - if (syncExtensions || isLanguagePackExtension(manifest)) { + if (isLanguagePackExtension(manifest)) { // Install on both servers const [extensionIdentifier] = await Promise.all(this.servers.map(server => server.extensionManagementService.install(vsix))); return extensionIdentifier; @@ -156,9 +152,9 @@ export class MultiExtensionManagementService extends Disposable implements IExte async installFromGallery(gallery: IGalleryExtension): Promise { if (this.extensionManagementServerService.remoteExtensionManagementServer) { - const [manifest, syncExtensions] = await Promise.all([this.extensionGalleryService.getManifest(gallery, CancellationToken.None), this.hasToSyncExtensions()]); + const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); if (manifest) { - if (syncExtensions || isLanguagePackExtension(manifest)) { + if (isLanguagePackExtension(manifest)) { // Install on both servers return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(() => undefined); } @@ -199,17 +195,6 @@ export class MultiExtensionManagementService extends Disposable implements IExte private getServer(extension: ILocalExtension): IExtensionManagementServer | null { return this.extensionManagementServerService.getExtensionManagementServer(extension.location); } - - private async hasToSyncExtensions(): Promise { - if (!this.extensionManagementServerService.remoteExtensionManagementServer) { - return false; - } - const remoteEnv = await this.remoteAgentService.getEnvironment(); - if (!remoteEnv) { - return false; - } - return remoteEnv.syncExtensions; - } } registerSingleton(IExtensionManagementService, MultiExtensionManagementService); diff --git a/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts b/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts index a7cf7ac082..1bab0adf75 100644 --- a/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts +++ b/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts @@ -70,15 +70,15 @@ suite('RPCProtocol', () => { }); test('passing buffer as argument', function (done) { - delegate = (a1: Buffer, a2: number) => { - assert.ok(Buffer.isBuffer(a1)); - return a1[a2]; + delegate = (a1: VSBuffer, a2: number) => { + assert.ok(a1 instanceof VSBuffer); + return a1.buffer[a2]; }; - let b = Buffer.allocUnsafe(4); - b[0] = 1; - b[1] = 2; - b[2] = 3; - b[3] = 4; + let b = VSBuffer.alloc(4); + b.buffer[0] = 1; + b.buffer[1] = 2; + b.buffer[2] = 3; + b.buffer[3] = 4; bProxy.$m(b, 2).then((res: number) => { assert.equal(res, 3); done(null); @@ -87,19 +87,19 @@ suite('RPCProtocol', () => { test('returning a buffer', function (done) { delegate = (a1: number, a2: number) => { - let b = Buffer.allocUnsafe(4); - b[0] = 1; - b[1] = 2; - b[2] = 3; - b[3] = 4; + let b = VSBuffer.alloc(4); + b.buffer[0] = 1; + b.buffer[1] = 2; + b.buffer[2] = 3; + b.buffer[3] = 4; return b; }; - bProxy.$m(4, 1).then((res: Buffer) => { - assert.ok(Buffer.isBuffer(res)); - assert.equal(res[0], 1); - assert.equal(res[1], 2); - assert.equal(res[2], 3); - assert.equal(res[3], 4); + bProxy.$m(4, 1).then((res: VSBuffer) => { + assert.ok(res instanceof VSBuffer); + assert.equal(res.buffer[0], 1); + assert.equal(res.buffer[1], 2); + assert.equal(res.buffer[2], 3); + assert.equal(res.buffer[3], 4); done(null); }, done); }); diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index b2b133d77d..5d7d815aaf 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -5,28 +5,21 @@ import * as paths from 'vs/base/common/path'; import * as fs from 'fs'; -import * as os from 'os'; import * as assert from 'assert'; -import { FileOperation, FileOperationEvent, IContent, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IWriteTextFileOptions, ICreateFileOptions, IContentData, ITextSnapshot, ILegacyFileService, IFileStatWithMetadata, IFileService, IFileSystemProvider, etag } from 'vs/platform/files/common/files'; +import { FileOperationEvent, IContent, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IContentData, ILegacyFileService, IFileService, IFileSystemProvider } from 'vs/platform/files/common/files'; import { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/platform/files/node/fileConstants'; -import * as objects from 'vs/base/common/objects'; -import { timeout } from 'vs/base/common/async'; import { URI as uri } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; -import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import * as pfs from 'vs/base/node/pfs'; -import { detectEncodingFromBuffer, decodeStream, detectEncodingByBOM, UTF8 } from 'vs/base/node/encoding'; +import { detectEncodingFromBuffer, decodeStream } from 'vs/base/node/encoding'; import { Event, Emitter } from 'vs/base/common/event'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Schemas } from 'vs/base/common/network'; import { onUnexpectedError } from 'vs/base/common/errors'; -import product from 'vs/platform/product/node/product'; import { IEncodingOverride, ResourceEncodings } from 'vs/workbench/services/files/node/encoding'; -import { createReadableOfSnapshot } from 'vs/workbench/services/files/node/streams'; import { withUndefinedAsNull } from 'vs/base/common/types'; export interface IFileServiceTestOptions { @@ -380,290 +373,8 @@ export class LegacyFileService extends Disposable implements ILegacyFileService //#endregion - //#region File Writing - - updateContent(resource: uri, value: string | ITextSnapshot, options: IWriteTextFileOptions = Object.create(null)): Promise { - if (options.writeElevated) { - return this.doUpdateContentElevated(resource, value, options); - } - - return this.doUpdateContent(resource, value, options); - } - - private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IWriteTextFileOptions = Object.create(null)): Promise { - const absolutePath = this.toAbsolutePath(resource); - - // 1.) check file for writing - return this.checkFileBeforeWriting(absolutePath, options).then(exists => { - let createParentsPromise: Promise; - if (exists) { - createParentsPromise = Promise.resolve(); - } else { - createParentsPromise = pfs.mkdirp(paths.dirname(absolutePath)); - } - - // 2.) create parents as needed - return createParentsPromise.then(() => { - const { encoding, hasBOM } = this._encoding.getWriteEncoding(resource, options.encoding); - let addBomPromise: Promise = Promise.resolve(false); - - // Some encodings come with a BOM automatically - if (hasBOM) { - addBomPromise = Promise.resolve(hasBOM); - } - - // Existing UTF-8 file: check for options regarding BOM - else if (exists && encoding === UTF8) { - if (options.overwriteEncoding) { - addBomPromise = Promise.resolve(false); // if we are to overwrite the encoding, we do not preserve it if found - } else { - addBomPromise = detectEncodingByBOM(absolutePath).then(enc => enc === UTF8); // otherwise preserve it if found - } - } - - // 3.) check to add UTF BOM - return addBomPromise.then(addBom => { - - // 4.) set contents and resolve - if (!exists || !isWindows) { - return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encoding); - } - - // On Windows and if the file exists, we use a different strategy of saving the file - // by first truncating the file and then writing with r+ mode. This helps to save hidden files on Windows - // (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams - // (see https://github.com/Microsoft/vscode/issues/6363) - else { - - // 4.) truncate - return pfs.truncate(absolutePath, 0).then(() => { - - // 5.) set contents (with r+ mode) and resolve - return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encoding, { flag: 'r+' }).then(undefined, error => { - if (this.environmentService.verbose) { - console.error(`Truncate succeeded, but save failed (${error}), retrying after 100ms`); - } - - // We heard from one user that fs.truncate() succeeds, but the save fails (https://github.com/Microsoft/vscode/issues/61310) - // In that case, the file is now entirely empty and the contents are gone. This can happen if an external file watcher is - // installed that reacts on the truncate and keeps the file busy right after. Our workaround is to retry to save after a - // short timeout, assuming that the file is free to write then. - return timeout(100).then(() => this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encoding, { flag: 'r+' })); - }); - }, error => { - if (this.environmentService.verbose) { - console.error(`Truncate failed (${error}), falling back to normal save`); - } - - // we heard from users that fs.truncate() fails (https://github.com/Microsoft/vscode/issues/59561) - // in that case we simply save the file without truncating first (same as macOS and Linux) - return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encoding); - }); - } - }); - }); - }).then(undefined, error => { - if (error.code === 'EACCES' || error.code === 'EPERM') { - return Promise.reject(new FileOperationError( - nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)), - FileOperationResult.FILE_PERMISSION_DENIED, - options - )); - } - - return Promise.reject(error); - }); - } - - private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): Promise { - - // Configure encoding related options as needed - const writeFileOptions: pfs.IWriteFileOptions = options ? options : Object.create(null); - if (addBOM || encodingToWrite !== UTF8) { - writeFileOptions.encoding = { - charset: encodingToWrite, - addBOM - }; - } - - let writeFilePromise: Promise; - if (typeof value === 'string') { - writeFilePromise = pfs.writeFile(absolutePath, value, writeFileOptions); - } else { - writeFilePromise = pfs.writeFile(absolutePath, createReadableOfSnapshot(value), writeFileOptions); - } - - // set contents - return writeFilePromise.then(() => { - - // resolve - return this.fileService.resolve(resource); - }); - } - - private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IWriteTextFileOptions = Object.create(null)): Promise { - const absolutePath = this.toAbsolutePath(resource); - - // 1.) check file for writing - return this.checkFileBeforeWriting(absolutePath, options, options.overwriteReadonly /* ignore readonly if we overwrite readonly, this is handled via sudo later */).then(exists => { - const writeOptions: IWriteTextFileOptions = objects.assign(Object.create(null), options); - writeOptions.writeElevated = false; - writeOptions.encoding = this._encoding.getWriteEncoding(resource, options.encoding).encoding; - - // 2.) write to a temporary file to be able to copy over later - const tmpPath = paths.join(os.tmpdir(), `code-elevated-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 6)}`); - return this.updateContent(uri.file(tmpPath), value, writeOptions).then(() => { - - // 3.) invoke our CLI as super user - return import('sudo-prompt').then(sudoPrompt => { - return new Promise((resolve, reject) => { - const promptOptions = { - name: this.environmentService.appNameLong.replace('-', ''), - icns: (isMacintosh && this.environmentService.isBuilt) ? paths.join(paths.dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : undefined - }; - - const sudoCommand: string[] = [`"${this.environmentService.cliPath}"`]; - if (options.overwriteReadonly) { - sudoCommand.push('--file-chmod'); - } - sudoCommand.push('--file-write', `"${tmpPath}"`, `"${absolutePath}"`); - - sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => { - if (error || stderr) { - reject(error || stderr); - } else { - resolve(undefined); - } - }); - }); - }).then(() => { - - // 3.) delete temp file - return pfs.rimraf(tmpPath, pfs.RimRafMode.MOVE).then(() => { - - // 4.) resolve again - return this.fileService.resolve(resource); - }); - }); - }); - }).then(undefined, error => { - if (this.environmentService.verbose) { - onUnexpectedError(`Unable to write to file '${resource.toString(true)}' as elevated user (${error})`); - } - - if (!FileOperationError.isFileOperationError(error)) { - error = new FileOperationError( - nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)), - FileOperationResult.FILE_PERMISSION_DENIED, - options - ); - } - - return Promise.reject(error); - }); - } - - //#endregion - - //#region Create File - - createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): Promise { - const absolutePath = this.toAbsolutePath(resource); - - let checkFilePromise: Promise; - if (options.overwrite) { - checkFilePromise = Promise.resolve(false); - } else { - checkFilePromise = pfs.exists(absolutePath); - } - - // Check file exists - return checkFilePromise.then(exists => { - if (exists && !options.overwrite) { - return Promise.reject(new FileOperationError( - nls.localize('fileExists', "File to create already exists ({0})", resource.toString(true)), - FileOperationResult.FILE_MODIFIED_SINCE, - options - )); - } - - // Create file - return this.updateContent(resource, content).then(result => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result)); - - return result; - }); - }); - } - - //#endregion - //#region Helpers - private checkFileBeforeWriting(absolutePath: string, options: IWriteTextFileOptions = Object.create(null), ignoreReadonly?: boolean): Promise { - return pfs.exists(absolutePath).then(exists => { - if (exists) { - return pfs.stat(absolutePath).then(stat => { - if (stat.isDirectory()) { - return Promise.reject(new Error('Expected file is actually a directory')); - } - - // Dirty write prevention: if the file on disk has been changed and does not match our expected - // mtime and etag, we bail out to prevent dirty writing. - // - // First, we check for a mtime that is in the future before we do more checks. The assumption is - // that only the mtime is an indicator for a file that has changd on disk. - // - // Second, if the mtime has advanced, we compare the size of the file on disk with our previous - // one using the etag() function. Relying only on the mtime check has prooven to produce false - // positives due to file system weirdness (especially around remote file systems). As such, the - // check for size is a weaker check because it can return a false negative if the file has changed - // but to the same length. This is a compromise we take to avoid having to produce checksums of - // the file content for comparison which would be much slower to compute. - if (typeof options.mtime === 'number' && typeof options.etag === 'string' && options.mtime < stat.mtime.getTime() && options.etag !== etag(stat.size, options.mtime)) { - return Promise.reject(new FileOperationError(nls.localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options)); - } - - // Throw if file is readonly and we are not instructed to overwrite - if (!ignoreReadonly && !(stat.mode & 128) /* readonly */) { - if (!options.overwriteReadonly) { - return this.readOnlyError(options); - } - - // Try to change mode to writeable - let mode = stat.mode; - mode = mode | 128; - return pfs.chmod(absolutePath, mode).then(() => { - - // Make sure to check the mode again, it could have failed - return pfs.stat(absolutePath).then(stat => { - if (!(stat.mode & 128) /* readonly */) { - return this.readOnlyError(options); - } - - return exists; - }); - }); - } - - return exists; - }); - } - - return exists; - }); - } - - private readOnlyError(options: IWriteTextFileOptions): Promise { - return Promise.reject(new FileOperationError( - nls.localize('fileReadOnlyError', "File is Read Only"), - FileOperationResult.FILE_READ_ONLY, - options - )); - } - private toAbsolutePath(arg1: uri | IFileStat): string { let resource: uri; if (arg1 instanceof uri) { diff --git a/src/vs/workbench/services/files/node/remoteFileService.ts b/src/vs/workbench/services/files/node/remoteFileService.ts index 8d1e3f9061..a9cf41ae10 100644 --- a/src/vs/workbench/services/files/node/remoteFileService.ts +++ b/src/vs/workbench/services/files/node/remoteFileService.ts @@ -7,28 +7,16 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { IDecodeStreamOptions, toDecodeStream, encodeStream } from 'vs/base/node/encoding'; +import { IDecodeStreamOptions, toDecodeStream } from 'vs/base/node/encoding'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileSystemProvider, IResolveContentOptions, IStreamContent, ITextSnapshot, IWriteTextFileOptions, ILegacyFileService, IFileService, toFileOperationResult, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { FileOperationError, FileOperationResult, IContent, IFileSystemProvider, IResolveContentOptions, IStreamContent, ILegacyFileService, IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; -import { createReadableOfProvider, createReadableOfSnapshot, createWritableOfProvider } from 'vs/workbench/services/files/node/streams'; +import { createReadableOfProvider } from 'vs/workbench/services/files/node/streams'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -class StringSnapshot implements ITextSnapshot { - private _value: string | null; - constructor(value: string) { - this._value = value; - } - read(): string | null { - let ret = this._value; - this._value = null; - return ret; - } -} - export class LegacyRemoteFileService extends LegacyFileService { private readonly _provider: Map; @@ -163,63 +151,6 @@ export class LegacyRemoteFileService extends LegacyFileService { // --- saving - private static _throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider { - if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { - throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED); - } - return provider; - } - - createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise { - if (resource.scheme === Schemas.file) { - return super.createFile(resource, content, options); - } else { - - return this._withProvider(resource).then(LegacyRemoteFileService._throwIfFileSystemIsReadonly).then(provider => { - - return this.fileService.createFolder(resources.dirname(resource)).then(() => { - const { encoding } = this.encoding.getWriteEncoding(resource); - return this._writeFile(provider, resource, new StringSnapshot(content || ''), encoding, { create: true, overwrite: Boolean(options && options.overwrite) }); - }); - - }).then(fileStat => { - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); - return fileStat; - }, err => { - const message = localize('err.create', "Failed to create file {0}", resource.toString(false)); - const result = toFileOperationResult(err); - throw new FileOperationError(message, result, options); - }); - } - } - - updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { - if (resource.scheme === Schemas.file) { - return super.updateContent(resource, value, options); - } else { - return this._withProvider(resource).then(LegacyRemoteFileService._throwIfFileSystemIsReadonly).then(provider => { - return this.fileService.createFolder(resources.dirname(resource)).then(() => { - const snapshot = typeof value === 'string' ? new StringSnapshot(value) : value; - return this._writeFile(provider, resource, snapshot, options && options.encoding, { create: true, overwrite: true }); - }); - }); - } - } - - private _writeFile(provider: IFileSystemProvider, resource: URI, snapshot: ITextSnapshot, preferredEncoding: string | undefined = undefined, options: FileWriteOptions): Promise { - const readable = createReadableOfSnapshot(snapshot); - const { encoding, hasBOM } = this.encoding.getWriteEncoding(resource, preferredEncoding); - const encoder = encodeStream(encoding, { addBOM: hasBOM }); - const target = createWritableOfProvider(provider, resource, options); - return new Promise((resolve, reject) => { - readable.pipe(encoder).pipe(target); - target.once('error', err => reject(err)); - target.once('finish', (_: unknown) => resolve(undefined)); - }).then(_ => { - return this.fileService.resolve(resource, { resolveMetadata: true }) as Promise; - }); - } - private static _asContent(content: IStreamContent): Promise { return new Promise((resolve, reject) => { let result: IContent = { diff --git a/src/vs/workbench/services/files/node/streams.ts b/src/vs/workbench/services/files/node/streams.ts index ebce14cc72..fba583452c 100644 --- a/src/vs/workbench/services/files/node/streams.ts +++ b/src/vs/workbench/services/files/node/streams.ts @@ -3,72 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Readable, Writable, WritableOptions } from 'stream'; -import { UTF8 } from 'vs/base/node/encoding'; +import { Readable } from 'stream'; import { URI } from 'vs/base/common/uri'; -import { IFileSystemProvider, ITextSnapshot, FileSystemProviderCapabilities, FileWriteOptions } from 'vs/platform/files/common/files'; +import { IFileSystemProvider, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { illegalArgument } from 'vs/base/common/errors'; -export function createWritableOfProvider(provider: IFileSystemProvider, resource: URI, opts: FileWriteOptions): Writable { - if (provider.capabilities & FileSystemProviderCapabilities.FileOpenReadWriteClose) { - return createWritable(provider, resource, opts); - } else if (provider.capabilities & FileSystemProviderCapabilities.FileReadWrite) { - return createSimpleWritable(provider, resource, opts); - } else { - throw illegalArgument(); - } -} - -function createSimpleWritable(provider: IFileSystemProvider, resource: URI, opts: FileWriteOptions): Writable { - return new class extends Writable { - _chunks: Buffer[] = []; - constructor(opts?: WritableOptions) { - super(opts); - } - _write(chunk: Buffer, encoding: string, callback: Function) { - this._chunks.push(chunk); - callback(null); - } - end() { - // todo@joh - end might have another chunk... - provider.writeFile!(resource, Buffer.concat(this._chunks), opts).then(_ => { - super.end(); - }, err => { - this.emit('error', err); - }); - } - }; -} - -function createWritable(provider: IFileSystemProvider, resource: URI, opts: FileWriteOptions): Writable { - return new class extends Writable { - _fd: number; - _pos: number = 0; - constructor(opts?: WritableOptions) { - super(opts); - } - async _write(chunk: Buffer, encoding: string, callback: Function) { - try { - if (typeof this._fd !== 'number') { - this._fd = await provider.open!(resource, { create: true }); - } - let bytesWritten = await provider.write!(this._fd, this._pos, chunk, 0, chunk.length); - this._pos += bytesWritten; - callback(); - } catch (err) { - callback(err); - } - } - _final(callback: (err?: any) => any) { - if (typeof this._fd !== 'number') { - provider.open!(resource, { create: true }).then(fd => provider.close!(fd)).finally(callback); - } else { - provider.close!(this._fd).finally(callback); - } - } - }; -} - export function createReadableOfProvider(provider: IFileSystemProvider, resource: URI, position: number): Readable { if (provider.capabilities & FileSystemProviderCapabilities.FileOpenReadWriteClose) { return createReadable(provider, resource, position); @@ -135,29 +74,4 @@ function createSimpleReadable(provider: IFileSystemProvider, resource: URI, posi }); } }; -} - -export function createReadableOfSnapshot(snapshot: ITextSnapshot): Readable { - return new Readable({ - read: function () { - try { - let chunk: string | null = null; - let canPush = true; - - // Push all chunks as long as we can push and as long as - // the underlying snapshot returns strings to us - while (canPush && typeof (chunk = snapshot.read()) === 'string') { - canPush = this.push(chunk); - } - - // Signal EOS by pushing NULL - if (typeof chunk !== 'string') { - this.push(null); - } - } catch (error) { - this.emit('error', error); - } - }, - encoding: UTF8 // very important, so that strings are passed around and not buffers! - }); -} +} \ No newline at end of file diff --git a/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts index 411454d985..2ae47b8804 100644 --- a/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; +// import * as fs from 'fs'; import * as path from 'vs/base/common/path'; import * as os from 'os'; import * as assert from 'assert'; @@ -17,7 +17,6 @@ import { TestEnvironmentService, TestContextService, TestTextResourceConfigurati import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { TextModel } from 'vs/editor/common/model/textModel'; import { IEncodingOverride } from 'vs/workbench/services/files/node/encoding'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; @@ -53,153 +52,6 @@ suite('LegacyFileService', () => { return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); - test('updateContent - use encoding (UTF 16 BE)', function () { - const resource = uri.file(path.join(testDir, 'small.txt')); - const encoding = 'utf16be'; - - return service.resolveContent(resource).then(c => { - c.encoding = encoding; - - return service.updateContent(c.resource, c.value, { encoding: encoding }).then(c => { - return encodingLib.detectEncodingByBOM(c.resource.fsPath).then((enc) => { - assert.equal(enc, encodingLib.UTF16be); - - return service.resolveContent(resource).then(c => { - assert.equal(c.encoding, encoding); - }); - }); - }); - }); - }); - - test('updateContent - use encoding (UTF 16 BE, ITextSnapShot)', function () { - const resource = uri.file(path.join(testDir, 'small.txt')); - const encoding = 'utf16be'; - - return service.resolveContent(resource).then(c => { - c.encoding = encoding; - - const model = TextModel.createFromString(c.value); - - return service.updateContent(c.resource, model.createSnapshot(), { encoding: encoding }).then(c => { - return encodingLib.detectEncodingByBOM(c.resource.fsPath).then((enc) => { - assert.equal(enc, encodingLib.UTF16be); - - return service.resolveContent(resource).then(c => { - assert.equal(c.encoding, encoding); - - model.dispose(); - }); - }); - }); - }); - }); - - test('updateContent - encoding preserved (UTF 16 LE)', function () { - const encoding = 'utf16le'; - const resource = uri.file(path.join(testDir, 'some_utf16le.css')); - - return service.resolveContent(resource).then(c => { - assert.equal(c.encoding, encoding); - - c.value = 'Some updates'; - - return service.updateContent(c.resource, c.value, { encoding: encoding }).then(c => { - return encodingLib.detectEncodingByBOM(c.resource.fsPath).then((enc) => { - assert.equal(enc, encodingLib.UTF16le); - - return service.resolveContent(resource).then(c => { - assert.equal(c.encoding, encoding); - }); - }); - }); - }); - }); - - test('updateContent - encoding preserved (UTF 16 LE, ITextSnapShot)', function () { - const encoding = 'utf16le'; - const resource = uri.file(path.join(testDir, 'some_utf16le.css')); - - return service.resolveContent(resource).then(c => { - assert.equal(c.encoding, encoding); - - const model = TextModel.createFromString('Some updates'); - - return service.updateContent(c.resource, model.createSnapshot(), { encoding: encoding }).then(c => { - return encodingLib.detectEncodingByBOM(c.resource.fsPath).then((enc) => { - assert.equal(enc, encodingLib.UTF16le); - - return service.resolveContent(resource).then(c => { - assert.equal(c.encoding, encoding); - - model.dispose(); - }); - }); - }); - }); - }); - - test('updateContent - UTF 8 BOMs', function () { - - // setup - const _id = uuid.generateUuid(); - const _testDir = path.join(parentDir, _id); - const _sourceDir = getPathFromAmdModule(require, './fixtures/service'); - const resource = uri.file(path.join(testDir, 'index.html')); - - const fileService = new FileService2(new NullLogService()); - fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); - - const _service = new LegacyFileService( - fileService, - new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))), - TestEnvironmentService, - new TestTextResourceConfigurationService() - ); - - return pfs.copy(_sourceDir, _testDir).then(() => { - return pfs.readFile(resource.fsPath).then(data => { - assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); - - const model = TextModel.createFromString('Hello Bom'); - - // Update content: UTF_8 => UTF_8_BOM - return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8_with_bom }).then(() => { - return pfs.readFile(resource.fsPath).then(data => { - assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8); - - // Update content: PRESERVE BOM when using UTF-8 - model.setValue('Please stay Bom'); - return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).then(() => { - return pfs.readFile(resource.fsPath).then(data => { - assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8); - - // Update content: REMOVE BOM - model.setValue('Go away Bom'); - return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8, overwriteEncoding: true }).then(() => { - return pfs.readFile(resource.fsPath).then(data => { - assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); - - // Update content: BOM comes not back - model.setValue('Do not come back Bom'); - return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).then(() => { - return pfs.readFile(resource.fsPath).then(data => { - assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); - - model.dispose(); - _service.dispose(); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - test('resolveContent - large file', function () { const resource = uri.file(path.join(testDir, 'lorem.txt')); @@ -263,17 +115,17 @@ suite('LegacyFileService', () => { }); }); - test('resolveContent - FILE_MODIFIED_SINCE', function () { - const resource = uri.file(path.join(testDir, 'index.html')); + // test('resolveContent - FILE_MODIFIED_SINCE', function () { + // const resource = uri.file(path.join(testDir, 'index.html')); - return service.resolveContent(resource).then(c => { - fs.writeFileSync(resource.fsPath, 'Updates Incoming!'); + // return service.resolveContent(resource).then(c => { + // fs.writeFileSync(resource.fsPath, 'Updates Incoming!'); - return service.updateContent(resource, c.value, { etag: c.etag, mtime: c.mtime - 1000 }).then(undefined, (e: FileOperationError) => { - assert.equal(e.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); - }); - }); - }); + // return service.updateContent(resource, c.value, { etag: c.etag, mtime: c.mtime - 1000 }).then(undefined, (e: FileOperationError) => { + // assert.equal(e.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); + // }); + // }); + // }); test('resolveContent - encoding picked up', function () { const resource = uri.file(path.join(testDir, 'index.html')); diff --git a/src/vs/workbench/services/files2/common/fileService2.ts b/src/vs/workbench/services/files2/common/fileService2.ts index 453b9162b2..591d011f32 100644 --- a/src/vs/workbench/services/files2/common/fileService2.ts +++ b/src/vs/workbench/services/files2/common/fileService2.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable, toDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IWriteTextFileOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, ILegacyFileService, IWriteFileOptions } from 'vs/platform/files/common/files'; +import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, ILegacyFileService, IWriteFileOptions } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; @@ -15,6 +15,7 @@ import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays'; import { getBaseLabel } from 'vs/base/common/labels'; import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable } from 'vs/base/common/buffer'; +import { Queue } from 'vs/base/common/async'; export class FileService2 extends Disposable implements IFileService { @@ -295,7 +296,7 @@ export class FileService2 extends Disposable implements IFileService { return this._legacy.encoding; } - async createFile2(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise { + async createFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise { // validate overwrite const overwrite = !!(options && options.overwrite); @@ -316,36 +317,12 @@ export class FileService2 extends Disposable implements IFileService { const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource)); // validate write - const exists = await this.exists(resource); - if (exists) { - const stat = await provider.stat(resource); - - // file cannot be directory - if ((stat.type & FileType.Directory) !== 0) { - throw new Error(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", resource.toString())); - } - - // Dirty write prevention: if the file on disk has been changed and does not match our expected - // mtime and etag, we bail out to prevent dirty writing. - // - // First, we check for a mtime that is in the future before we do more checks. The assumption is - // that only the mtime is an indicator for a file that has changd on disk. - // - // Second, if the mtime has advanced, we compare the size of the file on disk with our previous - // one using the etag() function. Relying only on the mtime check has prooven to produce false - // positives due to file system weirdness (especially around remote file systems). As such, the - // check for size is a weaker check because it can return a false negative if the file has changed - // but to the same length. This is a compromise we take to avoid having to produce checksums of - // the file content for comparison which would be much slower to compute. - if (options && typeof options.mtime === 'number' && typeof options.etag === 'string' && options.mtime < stat.mtime && options.etag !== etag(stat.size, options.mtime)) { - throw new FileOperationError(localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options); - } - } + const stat = await this.validateWriteFile(provider, resource, options); try { // mkdir recursively as needed - if (!exists) { + if (!stat) { await this.mkdirp(provider, dirname(resource)); } @@ -370,8 +347,36 @@ export class FileService2 extends Disposable implements IFileService { return this.resolve(resource, { resolveMetadata: true }); } - createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise { - return this.joinOnLegacy.then(legacy => legacy.createFile(resource, content, options)); + private async validateWriteFile(provider: IFileSystemProvider, resource: URI, options?: IWriteFileOptions): Promise { + let stat: IStat | undefined = undefined; + try { + stat = await provider.stat(resource); + } catch (error) { + return undefined; // file might not exist + } + + // file cannot be directory + if ((stat.type & FileType.Directory) !== 0) { + throw new Error(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", resource.toString())); + } + + // Dirty write prevention: if the file on disk has been changed and does not match our expected + // mtime and etag, we bail out to prevent dirty writing. + // + // First, we check for a mtime that is in the future before we do more checks. The assumption is + // that only the mtime is an indicator for a file that has changd on disk. + // + // Second, if the mtime has advanced, we compare the size of the file on disk with our previous + // one using the etag() function. Relying only on the mtime check has prooven to produce false + // positives due to file system weirdness (especially around remote file systems). As such, the + // check for size is a weaker check because it can return a false negative if the file has changed + // but to the same length. This is a compromise we take to avoid having to produce checksums of + // the file content for comparison which would be much slower to compute. + if (options && typeof options.mtime === 'number' && typeof options.etag === 'string' && options.mtime < stat.mtime && options.etag !== etag(stat.size, options.mtime)) { + throw new FileOperationError(localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options); + } + + return stat; } resolveContent(resource: URI, options?: IResolveContentOptions): Promise { @@ -382,10 +387,6 @@ export class FileService2 extends Disposable implements IFileService { return this.joinOnLegacy.then(legacy => legacy.resolveStreamContent(resource, options)); } - updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { - return this.joinOnLegacy.then(legacy => legacy.updateContent(resource, value, options)); - } - //#endregion //#region Move/Copy/Delete/Create Folder @@ -672,12 +673,10 @@ export class FileService2 extends Disposable implements IFileService { } private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string { - const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); - return [ - isPathCaseSensitive ? resource.toString() : resource.toString().toLowerCase(), // lowercase path is the provider is case insensitive - String(options.recursive), // use recursive: true | false as part of the key - options.excludes.join() // use excludes as part of the key + this.toMapKey(provider, resource), // lowercase path if the provider is case insensitive + String(options.recursive), // use recursive: true | false as part of the key + options.excludes.join() // use excludes as part of the key ].join(); } @@ -692,7 +691,39 @@ export class FileService2 extends Disposable implements IFileService { //#region Helpers + private writeQueues: Map> = new Map(); + + private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue { + // ensure to never write to the same resource without finishing + // the one write. this ensures a write finishes consistently + // (even with error) before another write is done. + const queueKey = this.toMapKey(provider, resource); + let writeQueue = this.writeQueues.get(queueKey); + if (!writeQueue) { + writeQueue = new Queue(); + this.writeQueues.set(queueKey, writeQueue); + + const onFinish = Event.once(writeQueue.onFinished); + onFinish(() => { + this.writeQueues.delete(queueKey); + dispose(writeQueue); + }); + } + + return writeQueue; + } + + private toMapKey(provider: IFileSystemProvider, resource: URI): string { + const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + + return isPathCaseSensitive ? resource.toString() : resource.toString().toLowerCase(); + } + private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readable: VSBufferReadable): Promise { + return this.ensureWriteQueue(provider, resource).queue(() => this.doWriteBufferedQueued(provider, resource, readable)); + } + + private async doWriteBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readable: VSBufferReadable): Promise { // open handle const handle = await provider.open(resource, { create: true }); @@ -723,6 +754,10 @@ export class FileService2 extends Disposable implements IFileService { } private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable): Promise { + return this.ensureWriteQueue(provider, resource).queue(() => this.doWriteUnbufferedQueued(provider, resource, bufferOrReadable)); + } + + private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable): Promise { let buffer: VSBuffer; if (bufferOrReadable instanceof VSBuffer) { buffer = bufferOrReadable; @@ -734,6 +769,9 @@ export class FileService2 extends Disposable implements IFileService { } private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { + return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target)); + } + private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { let sourceHandle: number | undefined = undefined; let targetHandle: number | undefined = undefined; @@ -776,10 +814,18 @@ export class FileService2 extends Disposable implements IFileService { } private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise { + return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target)); + } + + private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise { return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true }); } private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { + return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target)); + } + + private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { // Open handle const targetHandle = await targetProvider.open(target, { create: true }); diff --git a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts index eab107bdaa..deac33c0bb 100644 --- a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts @@ -3,14 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mkdir, open, close, read, write } from 'fs'; +import { mkdir, open, close, read, write, fdatasync } from 'fs'; import { promisify } from 'util'; import { IDisposable, Disposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { isLinux, isWindows } from 'vs/base/common/platform'; -import { statLink, readdir, unlink, move, copy, readFile, writeFile, fileExists, truncate, rimraf, RimRafMode } from 'vs/base/node/pfs'; +import { statLink, readdir, unlink, move, copy, readFile, writeFile, truncate, rimraf, RimRafMode, exists } from 'vs/base/node/pfs'; import { normalize, basename, dirname } from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/extpath'; @@ -116,14 +116,14 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro const filePath = this.toFilePath(resource); // Validate target - const exists = await fileExists(filePath); - if (exists && !opts.overwrite) { + const fileExists = await exists(filePath); + if (fileExists && !opts.overwrite) { throw createFileSystemProviderError(new Error(localize('fileExists', "File already exists")), FileSystemProviderErrorCode.FileExists); - } else if (!exists && !opts.create) { + } else if (!fileExists && !opts.create) { throw createFileSystemProviderError(new Error(localize('fileNotExists', "File does not exist")), FileSystemProviderErrorCode.FileNotFound); } - if (exists && isWindows) { + if (fileExists && isWindows) { try { // On Windows and if the file exists, we use a different strategy of saving the file // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows @@ -154,16 +154,36 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro } } + private writeHandles: Set = new Set(); + private canFlush: boolean = true; + async open(resource: URI, opts: FileOpenOptions): Promise { try { const filePath = this.toFilePath(resource); - let flags: string; + let flags: string | undefined = undefined; if (opts.create) { - // we take this as a hint that the file is opened for writing + if (isWindows && await exists(filePath)) { + try { + // On Windows and if the file exists, we use a different strategy of saving the file + // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows + // (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams + // (see https://github.com/Microsoft/vscode/issues/6363) + await truncate(filePath, 0); + + // After a successful truncate() the flag can be set to 'r+' which will not truncate. + flags = 'r+'; + } catch (error) { + this.logService.trace(error); + } + } + + // we take opts.create as a hint that the file is opened for writing // as such we use 'w' to truncate an existing or create the // file otherwise. we do not allow reading. - flags = 'w'; + if (!flags) { + flags = 'w'; + } } else { // otherwise we assume the file is opened for reading // as such we use 'r' to neither truncate, nor create @@ -171,7 +191,14 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro flags = 'r'; } - return await promisify(open)(filePath, flags); + const handle = await promisify(open)(filePath, flags); + + // remember that this handle was used for writing + if (opts.create) { + this.writeHandles.add(handle); + } + + return handle; } catch (error) { throw this.toFileSystemProviderError(error); } @@ -179,6 +206,19 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro async close(fd: number): Promise { try { + // if a handle is closed that was used for writing, ensure + // to flush the contents to disk if possible. + if (this.writeHandles.delete(fd) && this.canFlush) { + try { + await promisify(fdatasync)(fd); + } catch (error) { + // In some exotic setups it is well possible that node fails to sync + // In that case we disable flushing and log the error to our logger + this.canFlush = false; + this.logService.error(error); + } + } + return await promisify(close)(fd); } catch (error) { throw this.toFileSystemProviderError(error); @@ -199,6 +239,13 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro } async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + // we know at this point that the file to write to is truncated and thus empty + // if the write now fails, the file remains empty. as such we really try hard + // to ensure the write succeeds by retrying up to three times. + return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */); + } + + private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { try { const result = await promisify(write)(fd, data, offset, length, pos); if (typeof result === 'number') { @@ -295,7 +342,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro const isCaseChange = isPathCaseSensitive ? false : isEqual(fromFilePath, toFilePath, true /* ignore case */); // handle existing target (unless this is a case change) - if (!isCaseChange && await fileExists(toFilePath)) { + if (!isCaseChange && await exists(toFilePath)) { if (!overwrite) { throw createFileSystemProviderError(new Error('File at target already exists'), FileSystemProviderErrorCode.FileExists); } @@ -440,7 +487,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro code = FileSystemProviderErrorCode.FileExists; break; case 'EPERM': - case 'EACCESS': + case 'EACCES': code = FileSystemProviderErrorCode.NoPermissions; break; default: diff --git a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts index 04945d097a..d8969e3473 100644 --- a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts +++ b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts @@ -800,13 +800,13 @@ suite('Disk File Service', () => { } }); - test('createFile2', async () => { + test('createFile', async () => { let event: FileOperationEvent; disposables.push(service.onAfterOperation(e => event = e)); const contents = 'Hello World'; const resource = URI.file(join(testDir, 'test.txt')); - const fileStat = await service.createFile2(resource, VSBuffer.fromString(contents)); + const fileStat = await service.createFile(resource, VSBuffer.fromString(contents)); assert.equal(fileStat.name, 'test.txt'); assert.equal(existsSync(fileStat.resource.fsPath), true); assert.equal(readFileSync(fileStat.resource.fsPath), contents); @@ -817,21 +817,21 @@ suite('Disk File Service', () => { assert.equal(event!.target!.resource.fsPath, resource.fsPath); }); - test('createFile2 (does not overwrite by default)', async () => { + test('createFile (does not overwrite by default)', async () => { const contents = 'Hello World'; const resource = URI.file(join(testDir, 'test.txt')); writeFileSync(resource.fsPath, ''); // create file try { - await service.createFile2(resource, VSBuffer.fromString(contents)); + await service.createFile(resource, VSBuffer.fromString(contents)); } catch (error) { assert.ok(error); } }); - test('createFile2 (allows to overwrite existing)', async () => { + test('createFile (allows to overwrite existing)', async () => { let event: FileOperationEvent; disposables.push(service.onAfterOperation(e => event = e)); @@ -840,7 +840,7 @@ suite('Disk File Service', () => { writeFileSync(resource.fsPath, ''); // create file - const fileStat = await service.createFile2(resource, VSBuffer.fromString(contents), { overwrite: true }); + const fileStat = await service.createFile(resource, VSBuffer.fromString(contents), { overwrite: true }); assert.equal(fileStat.name, 'test.txt'); assert.equal(existsSync(fileStat.resource.fsPath), true); assert.equal(readFileSync(fileStat.resource.fsPath), contents); @@ -875,6 +875,21 @@ suite('Disk File Service', () => { assert.equal(readFileSync(resource.fsPath), newContent); }); + test('writeFile (large file) - multiple parallel writes queue up', async () => { + const resource = URI.file(join(testDir, 'lorem.txt')); + + const content = readFileSync(resource.fsPath); + const newContent = content.toString() + content.toString(); + + await Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => { + const fileStat = await service.writeFile(resource, VSBuffer.fromString(offset + newContent)); + assert.equal(fileStat.name, 'lorem.txt'); + })); + + const fileContent = readFileSync(resource.fsPath).toString(); + assert.ok(['0', '00', '000', '0000', '00000'].some(offset => fileContent === offset + newContent)); + }); + test('writeFile (readable)', async () => { const resource = URI.file(join(testDir, 'small.txt')); diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 1af731d9ee..aec7bbf220 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { onUnexpectedError } from 'vs/base/common/errors'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { IEditor } from 'vs/editor/common/editorCommon'; import { ITextEditorOptions, IResourceInput, ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IEditorInput, IEditor as IBaseEditor, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, Extensions as EditorInputExtensions, IFileInputFactory, IEditorIdentifier } from 'vs/workbench/common/editor'; @@ -888,7 +888,7 @@ export class HistoryService extends Disposable implements IHistoryService { // File resource: via URI.revive() if (serializedEditorHistoryEntry.resourceJSON) { - return { resource: URI.revive(serializedEditorHistoryEntry.resourceJSON) }; + return { resource: URI.revive(serializedEditorHistoryEntry.resourceJSON) }; } // Editor input: via factory @@ -974,4 +974,4 @@ export class HistoryService extends Disposable implements IHistoryService { } } -registerSingleton(IHistoryService, HistoryService); \ No newline at end of file +registerSingleton(IHistoryService, HistoryService); diff --git a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts index fe8af9dc58..21b3e6af8d 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts @@ -211,7 +211,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding return this.fileService.exists(this.resource) .then(exists => { const EOL = this.configurationService.getValue('files', { overrideIdentifier: 'json' })['eol']; - const result: Promise = exists ? Promise.resolve(null) : this.textFileService.write(this.resource, this.getEmptyContent(EOL)); + const result: Promise = exists ? Promise.resolve(null) : this.textFileService.write(this.resource, this.getEmptyContent(EOL), { encoding: 'utf8' }); return result.then(() => this.textModelResolverService.createModelReference(this.resource)); }); } diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 5cf957b514..10863c947f 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -183,24 +183,24 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.editorService.openEditor({ resource: this.userSettingsResource }); } - openSettings(jsonEditor?: boolean): Promise { + openSettings(jsonEditor: boolean | undefined, query: string | undefined): Promise { jsonEditor = typeof jsonEditor === 'undefined' ? this.configurationService.getValue('workbench.settings.editor') === 'json' : jsonEditor; if (!jsonEditor) { - return this.openSettings2(); + return this.openSettings2({ query: query }); } const editorInput = this.getActiveSettingsEditorInput() || this.lastOpenedSettingsInput; const resource = editorInput ? editorInput.master.getResource()! : this.userSettingsResource; const target = this.getConfigurationTargetFromSettingsResource(resource); - return this.openOrSwitchSettings(target, resource); + return this.openOrSwitchSettings(target, resource, { query: query }); } - private openSettings2(): Promise { + private openSettings2(options?: ISettingsEditorOptions): Promise { const input = this.settingsEditor2Input; - return this.editorGroupService.activeGroup.openEditor(input) + return this.editorGroupService.activeGroup.openEditor(input, options) .then(() => this.editorGroupService.activeGroup.activeControl!); } @@ -215,10 +215,10 @@ export class PreferencesService extends Disposable implements IPreferencesServic } async openRemoteSettings(): Promise { - const environemnt = await this.remoteAgentService.getEnvironment(); - if (environemnt) { - await this.createIfNotExists(environemnt.settingsPath, emptyEditableSettingsContent); - return this.editorService.openEditor({ resource: environemnt.settingsPath, options: { pinned: true, revealIfOpened: true } }); + const environment = await this.remoteAgentService.getEnvironment(); + if (environment) { + await this.createIfNotExists(environment.settingsPath, emptyEditableSettingsContent); + return this.editorService.openEditor({ resource: environment.settingsPath, options: { pinned: true, revealIfOpened: true } }); } return null; } @@ -520,6 +520,9 @@ export class PreferencesService extends Disposable implements IPreferencesServic private getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): URI | null { switch (configurationTarget) { case ConfigurationTarget.USER: + case ConfigurationTarget.USER_LOCAL: + return URI.file(this.environmentService.appSettingsPath); + case ConfigurationTarget.USER_REMOTE: return URI.file(this.environmentService.appSettingsPath); case ConfigurationTarget.WORKSPACE: if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 12c2759cdc..9fa18f6029 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -10,7 +10,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope, IConfigurationExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -65,6 +65,7 @@ export interface ISetting { enumDescriptions?: string[]; enumDescriptionsAreMarkdown?: boolean; tags?: string[]; + extensionInfo?: IConfigurationExtensionInfo; validator?: (value: any) => string | null; } @@ -199,7 +200,7 @@ export interface IPreferencesService { createSettings2EditorModel(): Settings2EditorModel; // TODO openRawDefaultSettings(): Promise; - openSettings(jsonEditor?: boolean): Promise; + openSettings(jsonEditor: boolean | undefined, query: string | undefined): Promise; openGlobalSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; openRemoteSettings(): Promise; openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; @@ -213,7 +214,7 @@ export interface IPreferencesService { export function getSettingsTargetName(target: ConfigurationTarget, resource: URI, workspaceContextService: IWorkspaceContextService): string { switch (target) { - case ConfigurationTarget.USER: + case ConfigurationTarget.USER_LOCAL: return localize('userSettingsTarget', "User Settings"); case ConfigurationTarget.WORKSPACE: return localize('workspaceSettingsTarget', "Workspace Settings"); diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index d5bc290f27..51d96a9f7c 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -17,7 +17,7 @@ import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/mod import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, OVERRIDE_PROPERTY_PATTERN, IConfigurationExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorModel } from 'vs/workbench/common/editor'; @@ -565,7 +565,7 @@ export class DefaultSettings extends Disposable { if (!settingsGroup) { settingsGroup = find(result, g => g.title === title); if (!settingsGroup) { - settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: title || '', titleRange: nullRange, range: nullRange, contributedByExtension: !!config.contributedByExtension }; + settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: title || '', titleRange: nullRange, range: nullRange, contributedByExtension: !!config.extensionInfo }; result.push(settingsGroup); } } else { @@ -574,11 +574,11 @@ export class DefaultSettings extends Disposable { } if (config.properties) { if (!settingsGroup) { - settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: config.id || '', titleRange: nullRange, range: nullRange, contributedByExtension: !!config.contributedByExtension }; + settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: config.id || '', titleRange: nullRange, range: nullRange, contributedByExtension: !!config.extensionInfo }; result.push(settingsGroup); } const configurationSettings: ISetting[] = []; - for (const setting of [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties)]) { + for (const setting of [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties, config.extensionInfo)]) { if (!seenSettings[setting.key]) { configurationSettings.push(setting); seenSettings[setting.key] = true; @@ -605,7 +605,7 @@ export class DefaultSettings extends Disposable { return result; } - private parseSettings(settingsObject: { [path: string]: IConfigurationPropertySchema; }): ISetting[] { + private parseSettings(settingsObject: { [path: string]: IConfigurationPropertySchema; }, extensionInfo?: IConfigurationExtensionInfo): ISetting[] { const result: ISetting[] = []; for (const key in settingsObject) { const prop = settingsObject[key]; @@ -629,6 +629,7 @@ export class DefaultSettings extends Disposable { enumDescriptions: prop.enumDescriptions || prop.markdownEnumDescriptions, enumDescriptionsAreMarkdown: !prop.enumDescriptions, tags: prop.tags, + extensionInfo: extensionInfo, deprecationMessage: prop.deprecationMessage, validator: createValidator(prop) }); diff --git a/src/vs/workbench/services/progress/browser/progressService2.ts b/src/vs/workbench/services/progress/browser/progressService2.ts index 6a138b54be..925f36b1c3 100644 --- a/src/vs/workbench/services/progress/browser/progressService2.ts +++ b/src/vs/workbench/services/progress/browser/progressService2.ts @@ -16,6 +16,10 @@ import { INotificationService, Severity, INotificationHandle, INotificationActio import { Action } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { Dialog } from 'vs/base/browser/ui/dialog/dialog'; +import { attachDialogStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; export class ProgressService2 implements IProgressService2 { @@ -29,6 +33,8 @@ export class ProgressService2 implements IProgressService2 { @IViewletService private readonly _viewletService: IViewletService, @INotificationService private readonly _notificationService: INotificationService, @IStatusbarService private readonly _statusbarService: IStatusbarService, + @ILayoutService private readonly _layoutService: ILayoutService, + @IThemeService private readonly _themeService: IThemeService ) { } withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: () => void): Promise { @@ -53,6 +59,8 @@ export class ProgressService2 implements IProgressService2 { return this._withViewletProgress('workbench.view.scm', task); case ProgressLocation.Extensions: return this._withViewletProgress('workbench.view.extensions', task); + case ProgressLocation.Dialog: + return this._withDialogProgress(options, task, onDidCancel); default: return Promise.reject(new Error(`Bad progress location: ${location}`)); } @@ -265,6 +273,54 @@ export class ProgressService2 implements IProgressService2 { promise.then(onDone, onDone); return promise; } + + private _withDialogProgress

, R = unknown>(options: IProgressOptions, task: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: () => void): P { + const disposables: IDisposable[] = []; + + let dialog: Dialog; + + const createDialog = (message: string) => { + dialog = new Dialog( + this._layoutService.container, + message, + [options.cancellable ? localize('cancel', "Cancel") : localize('dismiss', "Dismiss")], + { type: 'pending' } + ); + + disposables.push(dialog); + disposables.push(attachDialogStyler(dialog, this._themeService)); + + dialog.show().then(() => { + if (options.cancellable && typeof onDidCancel === 'function') { + onDidCancel(); + } + + dispose(dialog); + }); + + return dialog; + }; + + const updateDialog = (message?: string) => { + if (message && !dialog) { + dialog = createDialog(message); + } else if (message) { + dialog.updateMessage(message); + } + }; + + const p = task({ + report: progress => { + updateDialog(progress.message); + } + }); + + p.finally(() => { + dispose(disposables); + }); + + return p; + } } registerSingleton(IProgressService2, ProgressService2, true); diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 19b6202970..2c74e2260b 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -56,6 +56,16 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I return Promise.resolve(undefined); } + + disableTelemetry(): Promise { + const connection = this.getConnection(); + if (connection) { + const client = new RemoteExtensionEnvironmentChannelClient(connection.getChannel('remoteextensionsenvironment')); + return client.disableTelemetry(); + } + + return Promise.resolve(undefined); + } } export class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection { @@ -125,7 +135,7 @@ class RemoteConnectionFailureNotificationContribution implements IWorkbenchContr ) { // Let's cover the case where connecting to fetch the remote extension info fails remoteAgentService.getEnvironment(true) - .then(undefined, err => notificationService.error(nls.localize('connectionError', "Failed to connect to the remote extension host agent (Error: {0})", err ? err.message : ''))); + .then(undefined, err => notificationService.error(nls.localize('connectionError', "Failed to connect to the remote extension host server (Error: {0})", err ? err.message : ''))); } } diff --git a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts index ae0fe9086c..9938857805 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts @@ -28,7 +28,6 @@ export interface IRemoteAgentEnvironmentDTO { userHome: UriComponents; extensions: IExtensionDescription[]; os: platform.OperatingSystem; - syncExtensions: boolean; } export class RemoteExtensionEnvironmentChannelClient { @@ -54,8 +53,7 @@ export class RemoteExtensionEnvironmentChannelClient { globalStorageHome: URI.revive(data.globalStorageHome), userHome: URI.revive(data.userHome), extensions: data.extensions.map(ext => { (ext).extensionLocation = URI.revive(ext.extensionLocation); return ext; }), - os: data.os, - syncExtensions: data.syncExtensions + os: data.os }; }); } @@ -63,4 +61,8 @@ export class RemoteExtensionEnvironmentChannelClient { getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise { return this.channel.call('getDiagnosticInfo', options); } + + disableTelemetry(): Promise { + return this.channel.call('disableTelemetry'); + } } diff --git a/src/vs/workbench/services/remote/common/remoteAgentService.ts b/src/vs/workbench/services/remote/common/remoteAgentService.ts index 49681d4417..b76336ee5f 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentService.ts @@ -19,6 +19,7 @@ export interface IRemoteAgentService { getConnection(): IRemoteAgentConnection | null; getEnvironment(bail?: boolean): Promise; getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise; + disableTelemetry(): Promise; } export interface IRemoteAgentConnection { diff --git a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts index 1961049b6a..6e25f6ffd3 100644 --- a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts @@ -47,6 +47,10 @@ export class TelemetryService extends Disposable implements ITelemetryService { } } + setEnabled(value: boolean): void { + return this.impl.setEnabled(value); + } + get isOptedIn(): boolean { return this.impl.isOptedIn; } diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index f24936760b..6a7123748f 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -15,7 +15,7 @@ import { IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, I import { ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor'; import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration, ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration, ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata, toBufferOrReadable, ICreateFileOptions } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -69,7 +69,7 @@ export class TextFileService extends Disposable implements ITextFileService { @IFileService protected readonly fileService: IFileService, @IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService, @ILifecycleService private readonly lifecycleService: ILifecycleService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService protected instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, @IModeService private readonly modeService: IModeService, @IModelService private readonly modelService: IModelService, @@ -382,15 +382,14 @@ export class TextFileService extends Disposable implements ITextFileService { }; } - async create(resource: URI, contents?: string, options?: { overwrite?: boolean }): Promise { - const existingModel = this.models.get(resource); - - const stat = await this.fileService.createFile(resource, contents, options); + async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise { + const stat = await this.doCreate(resource, value, options); // If we had an existing model for the given resource, load // it again to make sure it is up to date with the contents // we just wrote into the underlying resource by calling // revert() + const existingModel = this.models.get(resource); if (existingModel && !existingModel.isDisposed()) { await existingModel.revert(); } @@ -398,10 +397,12 @@ export class TextFileService extends Disposable implements ITextFileService { return stat; } - async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { - const stat = await this.fileService.updateContent(resource, value, options); + protected doCreate(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise { + return this.fileService.createFile(resource, toBufferOrReadable(value), options); + } - return stat; + async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { + return this.fileService.writeFile(resource, toBufferOrReadable(value), options); } async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise { diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index e50956d535..09ade6d8f6 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -345,7 +345,7 @@ export interface ITextFileService extends IDisposable { * Create a file. If the file exists it will be overwritten with the contents if * the options enable to overwrite. */ - create(resource: URI, contents?: string, options?: { overwrite?: boolean }): Promise; + create(resource: URI, contents?: string | ITextSnapshot, options?: { overwrite?: boolean }): Promise; /** * Resolve the contents of a file identified by the resource. diff --git a/src/vs/workbench/services/textfile/node/textFileService.ts b/src/vs/workbench/services/textfile/node/textFileService.ts index f66f82dcd0..87c164018a 100644 --- a/src/vs/workbench/services/textfile/node/textFileService.ts +++ b/src/vs/workbench/services/textfile/node/textFileService.ts @@ -4,19 +4,53 @@ *--------------------------------------------------------------------------------------------*/ import { tmpdir } from 'os'; +import { localize } from 'vs/nls'; import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { URI } from 'vs/base/common/uri'; -import { ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata, IResourceEncoding, IResolveContentOptions, stringToSnapshot, ICreateFileOptions, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { Schemas } from 'vs/base/common/network'; import { exists, stat, chmod, rimraf } from 'vs/base/node/pfs'; import { join, dirname } from 'vs/base/common/path'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, isLinux } from 'vs/base/common/platform'; import product from 'vs/platform/product/node/product'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, IDetectedEncodingResult, detectEncodingByBOM, encodeStream, UTF8_BOM, UTF16be_BOM, UTF16le_BOM } from 'vs/base/node/encoding'; +import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; +import { joinPath, extname, isEqualOrParent } from 'vs/base/common/resources'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { VSBufferReadable, VSBuffer } from 'vs/base/common/buffer'; +import { Readable } from 'stream'; +import { isUndefinedOrNull } from 'vs/base/common/types'; export class NodeTextFileService extends TextFileService { + private _encoding: EncodingOracle; + protected get encoding(): EncodingOracle { + if (!this._encoding) { + this._encoding = this._register(this.instantiationService.createInstance(EncodingOracle)); + } + + return this._encoding; + } + + protected async doCreate(resource: URI, value?: string, options?: ICreateFileOptions): Promise { + + // check for encoding + const { encoding, addBOM } = await this.encoding.getWriteEncoding(resource); + + // return to parent when encoding is standard + if (encoding === UTF8 && !addBOM) { + return super.doCreate(resource, value, options); + } + + // otherwise create with encoding + return this.fileService.createFile(resource, this.getEncodedReadable(value || '', encoding, addBOM), options); + } + async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { // check for overwriteReadonly property (only supported for local file://) @@ -36,14 +70,137 @@ export class NodeTextFileService extends TextFileService { return this.writeElevated(resource, value, options); } - return super.write(resource, value, options); + try { + + // check for encoding + const { encoding, addBOM } = await this.encoding.getWriteEncoding(resource, options); + + // return to parent when encoding is standard + if (encoding === UTF8 && !addBOM) { + return await super.write(resource, value, options); + } + + // otherwise save with encoding + else { + return await this.fileService.writeFile(resource, this.getEncodedReadable(value, encoding, addBOM), options); + } + } catch (error) { + + // In case of permission denied, we need to check for readonly + if ((error).fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED) { + let isReadonly = false; + try { + const fileStat = await stat(resource.fsPath); + if (!(fileStat.mode & 128)) { + isReadonly = true; + } + } catch (error) { + // ignore - rethrow original error + } + + if (isReadonly) { + throw new FileOperationError(localize('fileReadOnlyError', "File is Read Only"), FileOperationResult.FILE_READ_ONLY, options); + } + } + + throw error; + } + } + + private getEncodedReadable(value: string | ITextSnapshot, encoding: string, addBOM: boolean): VSBufferReadable { + const readable = this.toNodeReadable(value); + const encoder = encodeStream(encoding, { addBOM }); + + const encodedReadable = readable.pipe(encoder); + + return this.toBufferReadable(encodedReadable, encoding, addBOM); + } + + private toNodeReadable(value: string | ITextSnapshot): Readable { + let snapshot: ITextSnapshot; + if (typeof value === 'string') { + snapshot = stringToSnapshot(value); + } else { + snapshot = value; + } + + return new Readable({ + read: function () { + try { + let chunk: string | null = null; + let canPush = true; + + // Push all chunks as long as we can push and as long as + // the underlying snapshot returns strings to us + while (canPush && typeof (chunk = snapshot.read()) === 'string') { + canPush = this.push(chunk); + } + + // Signal EOS by pushing NULL + if (typeof chunk !== 'string') { + this.push(null); + } + } catch (error) { + this.emit('error', error); + } + }, + encoding: UTF8 // very important, so that strings are passed around and not buffers! + }); + } + + private toBufferReadable(stream: NodeJS.ReadWriteStream, encoding: string, addBOM: boolean): VSBufferReadable { + let bytesRead = 0; + let done = false; + + return { + read(): VSBuffer | null { + if (done) { + return null; + } + + const res = stream.read(); + if (isUndefinedOrNull(res)) { + done = true; + + // If we are instructed to add a BOM but we detect that no + // bytes have been read, we must ensure to return the BOM + // ourselves so that we comply with the contract. + if (bytesRead === 0 && addBOM) { + switch (encoding) { + case UTF8: + case UTF8_with_bom: + return VSBuffer.wrap(Buffer.from(UTF8_BOM)); + case UTF16be: + return VSBuffer.wrap(Buffer.from(UTF16be_BOM)); + case UTF16le: + return VSBuffer.wrap(Buffer.from(UTF16le_BOM)); + } + } + + return null; + } + + // Handle String + if (typeof res === 'string') { + bytesRead += res.length; + return VSBuffer.fromString(res); + } + + // Handle Buffer + else { + bytesRead += res.byteLength; + return VSBuffer.wrap(res); + } + } + }; } private async writeElevated(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { // write into a tmp file first const tmpPath = join(tmpdir(), `code-elevated-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 6)}`); - await this.write(URI.file(tmpPath), value, { encoding: this.fileService.encoding.getWriteEncoding(resource, options ? options.encoding : undefined).encoding }); + const { encoding, addBOM } = await this.encoding.getWriteEncoding(resource, options); + await this.write(URI.file(tmpPath), value, { encoding: encoding === UTF8 && addBOM ? UTF8_with_bom : encoding }); // sudo prompt copy await this.sudoPromptCopy(tmpPath, resource.fsPath, options); @@ -83,4 +240,143 @@ export class NodeTextFileService extends TextFileService { } } +export interface IEncodingOverride { + parent?: URI; + extension?: string; + encoding: string; +} + +export class EncodingOracle extends Disposable { + protected encodingOverrides: IEncodingOverride[]; + + constructor( + @ITextResourceConfigurationService private textResourceConfigurationService: ITextResourceConfigurationService, + @IEnvironmentService private environmentService: IEnvironmentService, + @IWorkspaceContextService private contextService: IWorkspaceContextService + ) { + super(); + + this.encodingOverrides = this.getDefaultEncodingOverrides(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // Workspace Folder Change + this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.encodingOverrides = this.getDefaultEncodingOverrides())); + } + + private getDefaultEncodingOverrides(): IEncodingOverride[] { + const defaultEncodingOverrides: IEncodingOverride[] = []; + + // Global settings + defaultEncodingOverrides.push({ parent: URI.file(this.environmentService.appSettingsHome), encoding: UTF8 }); + + // Workspace files + defaultEncodingOverrides.push({ extension: WORKSPACE_EXTENSION, encoding: UTF8 }); + + // Folder Settings + this.contextService.getWorkspace().folders.forEach(folder => { + defaultEncodingOverrides.push({ parent: joinPath(folder.uri, '.vscode'), encoding: UTF8 }); + }); + + return defaultEncodingOverrides; + } + + async getWriteEncoding(resource: URI, options?: IWriteTextFileOptions): Promise<{ encoding: string, addBOM: boolean }> { + const { encoding, hasBOM } = this.doGetWriteEncoding(resource, options ? options.encoding : undefined); + + // Some encodings come with a BOM automatically + if (hasBOM) { + return { encoding, addBOM: true }; + } + + // Ensure that we preserve an existing BOM if found for UTF8 + // unless we are instructed to overwrite the encoding + const overwriteEncoding = options && options.overwriteEncoding; + if (!overwriteEncoding && encoding === UTF8 && resource.scheme === Schemas.file && await detectEncodingByBOM(resource.fsPath) === UTF8) { + return { encoding, addBOM: true }; + } + + return { encoding, addBOM: false }; + } + + private doGetWriteEncoding(resource: URI, preferredEncoding?: string): IResourceEncoding { + const resourceEncoding = this.getEncodingForResource(resource, preferredEncoding); + + return { + encoding: resourceEncoding, + hasBOM: resourceEncoding === UTF16be || resourceEncoding === UTF16le || resourceEncoding === UTF8_with_bom // enforce BOM for certain encodings + }; + } + + getReadEncoding(resource: URI, options: IResolveContentOptions | undefined, detected: IDetectedEncodingResult): string { + let preferredEncoding: string | undefined; + + // Encoding passed in as option + if (options && options.encoding) { + if (detected.encoding === UTF8 && options.encoding === UTF8) { + preferredEncoding = UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8 + } else { + preferredEncoding = options.encoding; // give passed in encoding highest priority + } + } + + // Encoding detected + else if (detected.encoding) { + if (detected.encoding === UTF8) { + preferredEncoding = UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM + } else { + preferredEncoding = detected.encoding; + } + } + + // Encoding configured + else if (this.textResourceConfigurationService.getValue(resource, 'files.encoding') === UTF8_with_bom) { + preferredEncoding = UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then + } + + return this.getEncodingForResource(resource, preferredEncoding); + } + + private getEncodingForResource(resource: URI, preferredEncoding?: string): string { + let fileEncoding: string; + + const override = this.getEncodingOverride(resource); + if (override) { + fileEncoding = override; // encoding override always wins + } else if (preferredEncoding) { + fileEncoding = preferredEncoding; // preferred encoding comes second + } else { + fileEncoding = this.textResourceConfigurationService.getValue(resource, 'files.encoding'); // and last we check for settings + } + + if (!fileEncoding || !encodingExists(fileEncoding)) { + fileEncoding = UTF8; // the default is UTF 8 + } + + return fileEncoding; + } + + private getEncodingOverride(resource: URI): string | undefined { + if (this.encodingOverrides && this.encodingOverrides.length) { + for (const override of this.encodingOverrides) { + + // check if the resource is child of encoding override path + if (override.parent && isEqualOrParent(resource, override.parent, !isLinux /* ignorecase */)) { + return override.encoding; + } + + // check if the resource extension is equal to encoding override + if (override.extension && extname(resource) === `.${override.extension}`) { + return override.encoding; + } + } + } + + return undefined; + } +} + registerSingleton(ITextFileService, NodeTextFileService); \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/test/fixtures/index.html b/src/vs/workbench/services/textfile/test/fixtures/index.html new file mode 100644 index 0000000000..bccd24d927 --- /dev/null +++ b/src/vs/workbench/services/textfile/test/fixtures/index.html @@ -0,0 +1,121 @@ + + + + + Strada + + + + + + + + +

TypeScript

+
+ + +
+ + +
+ +
Press 'run' to execute code...
+
...write your results into #results...
+
+ + + diff --git a/src/vs/workbench/services/textfile/test/fixtures/lorem.txt b/src/vs/workbench/services/textfile/test/fixtures/lorem.txt new file mode 100644 index 0000000000..9d348ac090 --- /dev/null +++ b/src/vs/workbench/services/textfile/test/fixtures/lorem.txt @@ -0,0 +1,283 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur vulputate, ipsum quis interdum fermentum, lorem sem fermentum eros, vitae auctor neque lacus in nisi. Suspendisse potenti. Maecenas et scelerisque elit, in tincidunt quam. Sed eu tincidunt quam. Nullam justo ex, imperdiet a imperdiet et, fermentum sit amet eros. Aenean quis tempus sem. Pellentesque accumsan magna mi, ut mollis velit sagittis id. Etiam quis ipsum orci. Fusce purus ante, accumsan a lobortis at, venenatis eu nisl. Praesent ornare sed ante placerat accumsan. Suspendisse tempus dignissim fermentum. Nunc a leo ac lacus sodales iaculis eu vitae mi. In feugiat ante at massa finibus cursus. Suspendisse posuere fringilla ornare. Mauris elementum ac quam id convallis. Vestibulum non elit quis urna volutpat aliquam a eu lacus. + +Aliquam vestibulum imperdiet neque, suscipit aliquam elit ultrices bibendum. Suspendisse ultrices pulvinar cursus. Morbi risus nisi, cursus consequat rutrum vitae, molestie sed dui. Fusce posuere, augue quis dignissim aliquam, nisi ipsum porttitor ante, quis fringilla nisl turpis ac nisi. Nulla varius enim eget lorem vehicula gravida. Donec finibus malesuada leo nec semper. Proin ac enim eros. Vivamus non tincidunt nisi, vel tristique lorem. + +Nunc consequat ex id eros dignissim, id rutrum risus laoreet. Sed euismod non erat eu ultricies. Etiam vehicula gravida lacus ut porta. Vestibulum eu eros quis nunc aliquet luctus. Cras quis semper ligula. Nullam gravida vehicula quam sed porta. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In porta cursus vulputate. Quisque porta a nisi eget cursus. Aliquam risus leo, luctus ac magna in, efficitur cursus magna. In condimentum non mi id semper. Donec interdum ante eget commodo maximus. + +Vivamus sit amet vestibulum lectus. Fusce tincidunt mi sapien, dictum sollicitudin diam vulputate in. Integer fringilla consequat mollis. Cras aliquet consequat felis eget feugiat. Nunc tempor cursus arcu, vitae ornare nunc varius et. Vestibulum et tortor vel ante viverra porttitor. Nam at tortor ullamcorper, facilisis augue quis, tristique erat. Aenean ut euismod nibh. Quisque eu tincidunt est, nec euismod eros. + +Proin vehicula nibh non viverra egestas. Phasellus sem dolor, ultricies ac sagittis tristique, lacinia a purus. Vestibulum in ante eros. Pellentesque lacus nulla, tristique vitae interdum vel, malesuada ac diam. Aenean bibendum posuere turpis in accumsan. Ut est nulla, ullamcorper quis turpis at, viverra sagittis mauris. Sed in interdum purus. Praesent scelerisque nibh eget sem euismod, ut imperdiet mi venenatis. Vivamus pulvinar orci sed dapibus auctor. Nulla facilisi. Vestibulum tincidunt erat nec porttitor egestas. Mauris quis risus ante. Nulla facilisi. + +Aliquam ullamcorper ornare lobortis. Phasellus quis sem et ipsum mollis malesuada sed in ex. Ut aliquam ex eget metus finibus maximus. Proin suscipit mauris eu nibh lacinia, quis feugiat dui dapibus. Nam sed libero est. Aenean vulputate orci sit amet diam faucibus, eu sagittis sapien volutpat. Nam imperdiet felis turpis, at pretium odio pulvinar in. Sed vestibulum id eros nec ultricies. Sed quis aliquam tortor, vitae ullamcorper tellus. Donec egestas laoreet eros, id suscipit est rutrum nec. Sed auctor nulla eget metus aliquam, ut condimentum enim elementum. + +Aliquam suscipit non turpis sit amet bibendum. Fusce velit ligula, euismod et maximus at, luctus sed neque. Quisque pretium, nisl at ullamcorper finibus, lectus leo mattis sapien, vel euismod mauris diam ullamcorper ex. Nulla ut risus finibus, lacinia ligula at, auctor erat. Mauris consectetur sagittis ligula vel dapibus. Nullam libero libero, lobortis aliquam libero vel, venenatis ultricies leo. Duis porttitor, nibh congue fermentum posuere, erat libero pulvinar tortor, a pellentesque nunc ipsum vel sem. Nullam volutpat, eros sit amet facilisis consectetur, ipsum est vehicula massa, non vestibulum neque elit in mauris. Nunc hendrerit ipsum non enim bibendum, vitae rhoncus mi egestas. Etiam ullamcorper massa vel nisl sagittis, nec bibendum arcu malesuada. Aenean aliquet turpis justo, a consectetur arcu mollis convallis. Etiam tellus ipsum, ultricies vitae lorem et, ornare facilisis orci. Praesent fringilla justo urna, vel mollis neque pulvinar vestibulum. + +Donec non iaculis erat. Aliquam et mi sed nunc pulvinar ultricies in ut ipsum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Praesent feugiat lacus ac dignissim semper. Phasellus vitae quam nisi. Morbi vel diam ultricies risus lobortis ornare. Fusce maximus et ligula quis iaculis. Sed congue ex eget felis convallis, sit amet hendrerit elit tempor. Donec vehicula blandit ante eget commodo. Vestibulum eleifend diam at feugiat euismod. Etiam magna tellus, dignissim eget fermentum vel, vestibulum vitae mauris. Nam accumsan et erat id sagittis. Donec lacinia, odio ut ornare ultricies, dolor velit accumsan tortor, non finibus erat tellus quis ligula. Nunc quis metus in leo volutpat ornare vulputate eu nisl. + +Donec quis viverra ex. Nullam id feugiat mauris, eu fringilla nulla. Vestibulum id maximus elit. Cras elementum elit sed felis lobortis, eget sagittis nisi hendrerit. Vivamus vitae elit neque. Donec vulputate lacus ut libero ultrices accumsan. Vivamus accumsan nulla orci, in dignissim est laoreet sagittis. Proin at commodo velit. Curabitur in velit felis. Aliquam erat volutpat. Sed consequat, nulla et cursus sodales, nisi lacus mattis risus, quis eleifend erat ex nec turpis. Sed suscipit ultrices lorem in hendrerit. + +Morbi vitae lacus nec libero ornare tempus eu et diam. Suspendisse magna ipsum, fermentum vel odio quis, molestie aliquam urna. Fusce mollis turpis a eros accumsan porttitor. Pellentesque rhoncus dolor sit amet magna rutrum, et dapibus justo tempor. Sed purus nisi, maximus vitae fringilla eu, molestie nec urna. Fusce malesuada finibus pretium. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec sed aliquet eros. Pellentesque luctus diam ante, eget euismod nisl aliquet eu. Sed accumsan elit purus, tempor varius ligula tempus nec. Curabitur ornare leo suscipit suscipit fermentum. Morbi eget nulla est. Maecenas faucibus interdum tristique. + +Etiam ut elit eros. Nulla pharetra suscipit molestie. Nulla facilisis bibendum nisl non molestie. Curabitur turpis lectus, facilisis vel diam non, vulputate ultrices mauris. Aenean placerat aliquam convallis. Suspendisse sed scelerisque tellus. Vivamus lacinia neque eget risus cursus suscipit. Proin consequat dolor vel neque tempor, eu aliquam sem scelerisque. Duis non eros a purus malesuada pharetra non et nulla. Suspendisse potenti. Mauris libero eros, finibus vel nulla id, sagittis dapibus ante. Proin iaculis sed nunc et cursus. + +Quisque accumsan lorem sit amet lorem aliquet euismod. Curabitur fermentum rutrum posuere. Etiam ultricies, sem id pellentesque suscipit, urna magna lacinia eros, quis efficitur risus nisl at lacus. Nulla quis lacus tortor. Mauris placerat ex in dolor tincidunt, vel aliquet nisi pretium. Cras iaculis risus vitae pellentesque aliquet. Quisque a enim imperdiet, ullamcorper arcu vitae, rutrum risus. Nullam consectetur libero at felis fringilla, nec congue nibh dignissim. Nam et lobortis felis, eu pellentesque ligula. Aenean facilisis, ligula non imperdiet maximus, massa orci gravida sapien, at sagittis lacus nisl in lacus. Nulla quis mauris luctus, scelerisque felis consequat, tempus risus. Fusce auctor nisl non nulla luctus molestie. Maecenas sapien nisl, auctor non dolor et, iaculis scelerisque lorem. Suspendisse egestas enim aliquet, accumsan mauris nec, posuere quam. Nulla iaculis dui dui, sit amet vestibulum erat ultricies ac. + +Cras eget dolor erat. Proin at nisl ut leo consectetur ultricies vel ut arcu. Nulla in felis malesuada, ullamcorper tortor et, convallis massa. Nunc urna justo, ornare in nibh vitae, hendrerit condimentum libero. Etiam vitae libero in purus venenatis fringilla. Nullam velit nulla, consequat ut turpis non, egestas hendrerit nibh. Duis tortor turpis, interdum non ante ac, cursus accumsan lectus. Cras pharetra bibendum augue quis dictum. Sed euismod vestibulum justo. Proin porta lobortis purus. Duis venenatis diam tortor, sit amet condimentum eros rhoncus a. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nunc at magna nec diam lobortis efficitur sit amet ut lacus. Nulla quis orci tortor. Pellentesque tempus velit a odio finibus porta. + +Proin feugiat mauris a tellus scelerisque convallis. Maecenas libero magna, blandit nec ultrices id, congue vel mi. Aliquam lacinia, quam vel condimentum convallis, tortor turpis aliquam odio, sed blandit libero lacus et eros. In eleifend iaculis magna ac finibus. Praesent auctor facilisis tellus in congue. Sed molestie lobortis dictum. Nam quis dignissim augue, vel euismod lorem. Curabitur posuere dapibus luctus. Donec ultricies dictum lectus, quis blandit arcu commodo ac. Aenean tincidunt ligula in nunc imperdiet dignissim. Curabitur egestas sollicitudin sapien ut semper. Aenean nec dignissim lacus. + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec aliquam dictum vehicula. Donec tortor est, volutpat non nisi nec, varius gravida ex. Nunc vel tristique nunc, vitae mattis nisi. Nunc nec luctus ex, vitae tincidunt lectus. In hac habitasse platea dictumst. Curabitur lobortis ex eget tincidunt tempor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Ut a vehicula mi. + +Fusce eu libero finibus, interdum nulla a, placerat neque. Cras bibendum tempor libero nec feugiat. Cras ut sodales eros. Proin viverra, massa sit amet viverra egestas, neque nisl porta ex, sit amet hendrerit libero ligula vel urna. Mauris suscipit lacus id justo rhoncus suscipit. Etiam vel libero tellus. Maecenas non diam molestie, condimentum tellus a, bibendum enim. Mauris aliquet imperdiet tellus, eget sagittis dolor. Sed blandit in neque et luctus. Cras elementum sagittis nunc, vel mollis lorem euismod et. Donec posuere at lacus eget suscipit. + +Nulla nunc mi, pretium non massa vel, tempor semper magna. Nunc a leo pulvinar, tincidunt nunc at, dignissim mi. Aliquam erat volutpat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Ut viverra nulla a nisl finibus, at hendrerit ligula ullamcorper. Donec a lorem semper, tempor magna et, lobortis libero. Mauris id sapien leo. Donec dignissim, quam vitae porttitor dignissim, quam justo mattis dui, vel consequat odio elit quis orci. Etiam nec pretium neque, sit amet pretium orci. Duis ac tortor venenatis, feugiat purus non, feugiat nunc. Proin scelerisque nisl in turpis aliquam vulputate. + +Praesent sed est semper, fringilla lorem vitae, tincidunt nibh. Cras eros metus, auctor at mauris sit amet, sodales semper orci. Nunc a ornare ex. Curabitur bibendum arcu congue urna vulputate egestas. Vestibulum finibus id risus et accumsan. Aenean ut volutpat tellus. Aenean tincidunt malesuada urna sit amet vestibulum. Mauris vel tellus dictum, varius lacus quis, dictum arcu. + +Aenean quis metus eu erat feugiat cursus vel at ligula. Proin dapibus sodales urna, id euismod lectus tempus id. Pellentesque ex ligula, convallis et erat vel, vulputate condimentum nisl. Pellentesque pharetra nulla quis massa eleifend hendrerit. Praesent sed massa ipsum. Maecenas vehicula dolor massa, id sodales urna faucibus et. Mauris ac quam non massa tincidunt feugiat et at lacus. Fusce libero massa, vulputate vel scelerisque non, mollis in leo. Ut sit amet ultricies odio. Suspendisse in sapien viverra, facilisis purus ut, pretium libero. + +Vivamus tristique pharetra molestie. Nam a volutpat purus. Praesent consequat gravida nisi, ac blandit nisi suscipit ut. Quisque posuere, ligula a ultrices laoreet, ligula nunc vulputate libero, ut rutrum erat odio tincidunt justo. Sed vitae leo at leo fringilla bibendum. Vestibulum ut augue nec dolor auctor accumsan. Praesent laoreet id eros pulvinar commodo. Suspendisse potenti. Ut pharetra, mauris vitae blandit fringilla, odio ante tincidunt lorem, sit amet tempor metus diam ut turpis. + +Praesent quis egestas arcu. Nullam at porta arcu. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi vulputate ligula malesuada ligula luctus, vulputate tempus erat bibendum. Nunc ullamcorper non lectus at euismod. Etiam nibh felis, tincidunt a metus vel, pellentesque rhoncus neque. Etiam at diam in erat luctus interdum. Nunc vel ipsum pulvinar, sollicitudin lacus ac, tempus urna. Etiam vel lacinia sapien. Pellentesque sagittis velit vel mi efficitur iaculis. Integer euismod sit amet urna in sagittis. Cras eleifend ut nibh in facilisis. Donec et lacus vitae nunc placerat sodales. Nulla sed hendrerit ligula, at dapibus sapien. + +Praesent at iaculis ex. Curabitur est purus, cursus a faucibus quis, dictum id velit. Donec dignissim fringilla viverra. Nunc mauris felis, laoreet sit amet sagittis at, vestibulum in libero. Maecenas quis orci turpis. Quisque ut nibh vitae magna mollis consequat id at mauris. Aliquam eu odio eget nulla bibendum sodales. Quisque vel orci eleifend nisi pretium lacinia. Suspendisse eget risus eget mi volutpat molestie eget quis lacus. Duis nisi libero, tincidunt nec nulla id, faucibus cursus felis. + +Donec tempor eget risus pellentesque molestie. Phasellus porta neque vel arcu egestas, nec blandit velit fringilla. Nullam porta faucibus justo vitae laoreet. Pellentesque viverra id nunc eu varius. Nulla pulvinar lobortis iaculis. Etiam vestibulum odio nec velit tristique, a tristique nisi mattis. In sed fringilla orci, vitae efficitur odio. Quisque dui odio, ornare eget velit at, lacinia consequat libero. Quisque lectus nulla, aliquet eu leo in, porta rutrum diam. Donec nec mattis neque. Nam rutrum, odio ac eleifend bibendum, dolor arcu rutrum neque, eget porta elit tellus a lacus. Sed massa metus, sollicitudin et sapien eu, finibus tempus orci. Proin et sapien sit amet erat molestie interdum. In quis rutrum velit, faucibus ultrices tellus. + +Sed sagittis sed justo eget tincidunt. Maecenas ut leo sagittis, feugiat magna et, viverra velit. Maecenas ex arcu, feugiat at consequat vitae, auctor eu massa. Integer egestas, enim vitae maximus convallis, est lectus pretium mauris, ac posuere lectus nisl quis quam. Aliquam tempus laoreet mi, vitae dapibus dolor varius dapibus. Suspendisse potenti. Donec sit amet purus nec libero dapibus tristique. Pellentesque viverra bibendum ligula. Donec sed felis et ex lobortis laoreet. Phasellus a fringilla libero, vitae malesuada nulla. Pellentesque blandit mattis lacus, et blandit tortor laoreet consequat. Suspendisse libero nunc, viverra sed fermentum in, accumsan egestas arcu. Proin in placerat elit. Sed interdum imperdiet malesuada. Suspendisse aliquet quis mauris eget sollicitudin. + +Vivamus accumsan tellus non erat volutpat, quis dictum dolor feugiat. Praesent rutrum nunc ac est mollis cursus. Fusce semper volutpat dui ut egestas. Curabitur sit amet posuere massa. Cras tincidunt nulla et mi mollis imperdiet. Suspendisse scelerisque ex id sodales vulputate. In nunc augue, pharetra in placerat eu, mattis id tellus. Vivamus cursus efficitur vehicula. Nulla aliquet vehicula aliquet. + +Sed cursus tellus sed porta pulvinar. Sed vitae nisi neque. Nullam aliquet, lorem et efficitur scelerisque, arcu diam aliquam felis, sed pulvinar lorem odio et turpis. Praesent convallis pulvinar turpis eu iaculis. Aliquam nec gravida mi. Curabitur eu nibh tempor, blandit justo in, ultrices felis. Fusce placerat metus non mi sagittis rutrum. Morbi sed dui fringilla, sagittis mauris eget, imperdiet nunc. Phasellus hendrerit sem elit, id hendrerit libero auctor sit amet. Integer sodales elit sit amet consequat cursus. + +Nam semper est eget nunc mollis, in pellentesque lectus fringilla. In finibus vel diam id semper. Nunc mattis quis erat eu consectetur. In hac habitasse platea dictumst. Nullam et ipsum vestibulum ex pulvinar ultricies sit amet id velit. Aenean suscipit mi tortor, a lobortis magna viverra non. Nulla condimentum aliquet ante et ullamcorper. Pellentesque porttitor arcu a posuere tempus. Aenean lacus quam, imperdiet eu justo vitae, pretium efficitur ex. Duis id purus id magna rhoncus ultrices id eu risus. Nunc dignissim et libero id dictum. + +Quisque a tincidunt neque. Phasellus commodo mi sit amet tempor fringilla. Ut rhoncus, neque non porttitor elementum, libero nulla egestas augue, sed fringilla sapien felis ac velit. Phasellus viverra rhoncus mollis. Nam ullamcorper leo vel erat laoreet luctus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus semper a metus a cursus. Nulla sed orci egestas, efficitur purus ac, malesuada tellus. Aenean rutrum velit at tellus fermentum mollis. Aliquam eleifend euismod metus. + +In hac habitasse platea dictumst. Vestibulum volutpat neque vitae porttitor laoreet. Nam at tellus consequat, sodales quam in, pulvinar arcu. Maecenas varius convallis diam, ac lobortis tellus pellentesque quis. Maecenas eget augue massa. Nullam volutpat nibh ac justo rhoncus, ut iaculis tellus rutrum. Fusce efficitur efficitur libero quis condimentum. Curabitur congue neque non tincidunt tristique. Fusce eget tempor ex, at pellentesque odio. Praesent luctus dictum vestibulum. Etiam non orci nunc. Vivamus vitae laoreet purus, a lobortis velit. Curabitur tincidunt purus ac lectus elementum pellentesque. Quisque sed tincidunt est. + +Sed vel ultrices massa, vitae ultricies justo. Cras finibus mauris nec lacus tempus dignissim. Cras faucibus maximus velit, eget faucibus orci luctus vehicula. Nulla massa nunc, porta ac consequat eget, rhoncus non tellus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce sed maximus metus, vel imperdiet ipsum. Ut scelerisque lectus at blandit porttitor. Ut vulputate nunc pharetra, aliquet sapien ac, sollicitudin sapien. Aenean eget ante lorem. Nam accumsan venenatis tellus id dignissim. + +Curabitur fringilla, magna non maximus dapibus, nulla sapien vestibulum lectus, sit amet semper dolor neque vitae nisl. Nunc ultrices vehicula augue sed iaculis. Maecenas nec diam mollis, suscipit orci et, vestibulum ante. Pellentesque eu nisl tortor. Nunc eleifend, lacus quis volutpat volutpat, nisi mi molestie sem, quis mollis ipsum libero a tellus. Ut viverra dolor mattis convallis interdum. Sed tempus nisl at nunc scelerisque aliquet. Quisque tempor tempor lorem id feugiat. Nullam blandit lectus velit, vitae porta lacus tincidunt a. Vivamus sit amet arcu ultrices, tincidunt mi quis, viverra quam. Aenean fringilla libero elementum lorem semper, quis pulvinar eros gravida. Nullam sodales blandit mauris, sed fermentum velit fermentum sit amet. Donec malesuada mauris in augue sodales vulputate. Vestibulum gravida turpis id elit rhoncus dignissim. Integer non congue lorem, eu viverra orci. + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec at dolor magna. Aliquam consectetur erat augue, id iaculis velit pharetra ac. Integer rutrum venenatis dignissim. Integer non sodales elit. Curabitur ut magna ut nibh feugiat aliquam ac ut risus. Morbi nibh quam, aliquam id placerat nec, vestibulum eget velit. Suspendisse at dignissim quam. Vivamus aliquet sem sed nisl volutpat, ut cursus orci ultrices. Aliquam ultrices lacinia enim, vitae aliquet neque. + +Quisque scelerisque finibus diam in mattis. Cras cursus auctor velit. Aliquam sem leo, fermentum et maximus et, molestie a libero. Aenean justo elit, rutrum a ornare id, egestas eget enim. Aenean auctor tristique erat. Curabitur condimentum libero lacus, nec consequat orci vestibulum sed. Fusce elit ligula, blandit vitae sapien vitae, dictum ultrices risus. Nam laoreet suscipit sapien, at interdum velit faucibus sit amet. Duis quis metus egestas lectus elementum posuere non nec libero. Aliquam a dolor bibendum, facilisis nunc a, maximus diam. Vestibulum suscipit tristique magna, non dignissim turpis sodales sed. Nunc ornare, velit ac facilisis fringilla, dolor mi consectetur lorem, vitae finibus erat justo suscipit urna. Maecenas sit amet eros erat. Nunc non arcu ornare, suscipit lorem eget, sodales mauris. Aliquam tincidunt, quam nec mollis lacinia, nisi orci fermentum libero, consequat eleifend lectus quam et sapien. Vestibulum a quam urna. + +Cras arcu leo, euismod ac ullamcorper at, faucibus sed massa. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus porttitor velit in enim interdum, non commodo metus ornare. Morbi vel lorem quis nisl luctus tristique quis vitae nisl. Suspendisse condimentum tortor enim, nec eleifend ipsum euismod et. Sed gravida quam ut tristique lacinia. Mauris eu interdum ipsum, ac ultrices odio. Nullam auctor tellus a risus porttitor vehicula. Nulla blandit euismod dictum. In pharetra, enim iaculis pulvinar interdum, dui nunc placerat nunc, sit amet pretium lectus nulla vitae quam. Phasellus quis enim sollicitudin, varius nulla id, ornare purus. Donec quam lacus, vestibulum quis nunc ac, mollis dictum nisi. Cras ut mollis elit. Maecenas ultrices ligula at risus faucibus scelerisque. Etiam vitae porttitor purus. Curabitur blandit lectus urna, ut hendrerit tortor feugiat ut. + +Phasellus fringilla, sapien pellentesque commodo pharetra, ante libero aliquam tellus, ut consectetur augue libero a sapien. Maecenas blandit luctus nisl eget aliquet. Maecenas vitae porta dolor, faucibus laoreet sapien. Suspendisse lobortis, ipsum sed vehicula aliquam, elit purus scelerisque dui, rutrum consectetur diam odio et lorem. In nec lacinia metus. Donec viverra libero est, vel bibendum erat condimentum quis. Donec feugiat purus leo. In laoreet vitae felis a porttitor. Mauris ullamcorper, lacus id condimentum suscipit, neque magna pellentesque arcu, eget cursus neque tellus id metus. Curabitur volutpat ac orci vel ultricies. + +Sed ut finibus erat. Sed diam purus, varius non tincidunt quis, ultrices sit amet ipsum. Donec et egestas nulla. Suspendisse placerat nisi at dui laoreet iaculis. Aliquam aliquet leo at augue faucibus molestie. Nullam lacus augue, hendrerit sed nisi eu, faucibus porta est. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam ut leo aliquet sem fermentum rutrum quis ac justo. Integer placerat aliquam nisl ut sagittis. Proin erat orci, lobortis et sem eget, eleifend fringilla augue. Mauris varius laoreet arcu, sed tincidunt felis. Pellentesque venenatis lorem odio, id pulvinar velit molestie feugiat. Donec mattis lacus sed eleifend pulvinar. + +Sed condimentum ex in tincidunt hendrerit. Etiam eget risus lacinia, euismod nibh eu, pellentesque quam. Proin elit eros, convallis id mauris ac, bibendum ultrices lectus. Morbi venenatis, purus id fermentum consequat, nunc libero tincidunt ligula, non dictum ligula orci nec quam. Nulla nec ultrices lorem. Aenean maximus augue vel dictum pharetra. Etiam turpis urna, pellentesque quis malesuada eu, molestie faucibus felis. + +Vestibulum pharetra augue ut quam blandit congue in nec risus. Proin eu nibh eu dui eleifend porta vitae id lectus. Proin lacus nibh, lobortis sed ligula vitae, interdum lobortis erat. Suspendisse potenti. In sollicitudin quis sapien ut aliquet. Mauris ac nulla arcu. Fusce tristique justo quis lectus mollis, eu volutpat lectus finibus. Vivamus venenatis facilisis ex ut vestibulum. + +Etiam varius lobortis purus, in hendrerit elit tristique at. In tempus, augue vestibulum fermentum gravida, ligula tellus vulputate arcu, eu molestie ex sapien at purus. Vestibulum nec egestas metus. Duis pulvinar quam nec consequat interdum. Aenean non dapibus lacus. Aliquam sit amet aliquet nulla. Sed venenatis volutpat purus nec convallis. Phasellus aliquet semper sodales. Cras risus sapien, condimentum auctor urna a, pulvinar ornare nisl. Sed tincidunt felis elit, ut elementum est bibendum ac. Morbi interdum justo vel dui faucibus condimentum. + +Sed convallis eu sem at tincidunt. Nullam at auctor est, et ullamcorper ipsum. Pellentesque eget ante ante. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer euismod, sapien sed dapibus ornare, nibh enim maximus lacus, lacinia placerat urna quam quis felis. Morbi accumsan id nisl ut condimentum. Donec bibendum nisi est, sed volutpat lorem rhoncus in. Vestibulum ac lacinia nunc, eget volutpat magna. Integer aliquam pharetra ipsum, id placerat nunc volutpat quis. Etiam urna diam, rhoncus sit amet varius vel, euismod vel sem. Nullam vel molestie urna. Vivamus ornare erat at venenatis euismod. Suspendisse potenti. Fusce diam justo, tincidunt vel sem at, commodo faucibus nisl. Duis gravida efficitur diam, vel sagittis erat pulvinar ut. + +Quisque vel pharetra felis. Duis efficitur tortor dolor, vitae porttitor erat fermentum sed. Sed eu mi purus. Etiam dignissim tortor eu tempus molestie. Aenean pretium erat enim, in hendrerit ante hendrerit at. Sed ut risus vel nunc venenatis ultricies quis in lacus. Pellentesque vitae purus euismod, placerat risus non, ullamcorper augue. Quisque varius quam ligula, nec aliquet ex faucibus vitae. Quisque rhoncus sit amet leo tincidunt mattis. Cras id mauris eget purus pretium gravida sit amet eu augue. Aliquam dapibus odio augue, id lacinia velit pulvinar eu. + +Mauris fringilla, tellus nec pharetra iaculis, neque nisi ultrices massa, et tincidunt sem dui sed mi. Curabitur erat lorem, venenatis quis tempus lacinia, tempus sit amet nunc. Aliquam at neque ac metus commodo dictum quis vitae justo. Phasellus eget lacus tempus, blandit lorem vel, rutrum est. Aenean pharetra sem ut augue lobortis dignissim. Sed rhoncus at nulla id ultrices. Cras id condimentum felis. In suscipit luctus vulputate. Donec tincidunt lacus nec enim tincidunt sollicitudin ut quis enim. Nam at libero urna. Praesent sit amet massa vitae massa ullamcorper vehicula. + +Nullam bibendum augue ut turpis condimentum bibendum. Proin sit amet urna hendrerit, sodales tortor a, lobortis lectus. Integer sagittis velit turpis, et tincidunt nisi commodo eget. Duis tincidunt elit finibus accumsan cursus. Aenean dignissim scelerisque felis vel lacinia. Nunc lacinia maximus luctus. In hac habitasse platea dictumst. Vestibulum eget urna et enim tempor tempor. Nam feugiat, felis vel vestibulum tempus, orci justo viverra diam, id dapibus lorem justo in ligula. + +Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In ac pellentesque sem. Vestibulum lacinia magna dui, eu lacinia augue placerat et. Maecenas pulvinar congue est. Pellentesque commodo dui non pulvinar scelerisque. Etiam interdum est posuere sem bibendum, ac commodo magna dictum. Cras ipsum turpis, rhoncus nec posuere vitae, laoreet a arcu. Integer ac massa sit amet enim placerat lacinia sed ultrices arcu. Suspendisse sem nibh, luctus sit amet volutpat in, pellentesque eu metus. Ut gravida neque eget mi accumsan tempus. Nam sit amet aliquet nibh. + +Pellentesque a purus cursus nulla hendrerit congue quis et odio. Aenean hendrerit, leo ullamcorper sagittis hendrerit, erat dui molestie quam, sed condimentum lacus risus sed tellus. Morbi a dapibus lectus, ut feugiat ex. Phasellus pretium quam et sapien mollis, vel iaculis dui dignissim. Sed ullamcorper est turpis, a viverra lorem consectetur in. Aenean aliquet nibh non cursus rutrum. Suspendisse at tristique urna, id lobortis urna. In hac habitasse platea dictumst. Phasellus libero velit, rutrum sed tellus nec, dapibus tincidunt ligula. Quisque vel dui venenatis, consequat nisl ut, lacinia ipsum. Phasellus vitae magna pellentesque, lobortis est id, faucibus quam. Nam eleifend faucibus dui vel pellentesque. + +Etiam ut est non lacus tincidunt interdum. Maecenas sed massa urna. Quisque ut nibh tortor. Pellentesque felis ipsum, tempor finibus ipsum et, euismod pretium metus. Donec sit amet est ipsum. Quisque rhoncus justo non finibus elementum. Nulla nec lectus ac tortor placerat fringilla. Phasellus ac ultrices nunc, eu efficitur nisl. Nulla rhoncus nunc vitae ante dictum tincidunt. Nunc ultrices, massa sit amet malesuada dignissim, lectus lacus consequat sapien, non eleifend metus sem in eros. Phasellus mauris ante, dictum sit amet suscipit ac, rhoncus eget nisi. Phasellus at orci mollis, imperdiet neque eget, faucibus nulla. In at purus massa. Pellentesque quis rutrum lectus. + +Integer eu faucibus turpis, sit amet mollis massa. Vestibulum id nulla commodo, rutrum ipsum sed, semper ante. Phasellus condimentum orci nec nibh convallis, ac maximus orci ullamcorper. Maecenas vitae sollicitudin mi. Integer et finibus lectus, et condimentum ligula. Donec elementum tristique quam vitae dapibus. Morbi euismod ipsum in tristique ullamcorper. + +Duis fermentum non enim eu auctor. Quisque lacinia nibh vehicula nibh posuere, eu volutpat turpis facilisis. Ut ac faucibus nulla. Sed eleifend quis ex et pellentesque. Vestibulum sollicitudin in libero id fringilla. Phasellus dignissim purus consequat, condimentum dui sit amet, condimentum ante. Pellentesque ac consectetur massa, quis sagittis est. Nulla maximus tristique risus accumsan convallis. Curabitur imperdiet ac lacus a ultrices. Nulla facilisi. Sed quis quam quis lectus placerat lobortis vel sed turpis. In mollis dui id neque iaculis, ut aliquet tellus malesuada. Proin at luctus odio, vel blandit sapien. Praesent dignissim tortor vehicula libero fringilla, nec ultrices erat suscipit. Maecenas scelerisque purus in dapibus fermentum. + +Curabitur magna odio, mattis in tortor ut, porttitor congue est. Vestibulum mollis lacinia elementum. Fusce maximus erat vitae nunc rutrum lobortis. Integer ligula eros, auctor vel elit non, posuere luctus lacus. Maecenas quis auctor massa. Ut ipsum lacus, efficitur posuere euismod et, hendrerit efficitur est. Phasellus fringilla, quam id tincidunt pretium, nunc dui sollicitudin orci, eu dignissim nisi metus ut magna. Integer lobortis interdum dolor, non bibendum purus posuere et. Donec non lectus aliquet, pretium dolor eu, cursus massa. Sed ut dui sapien. In sed vestibulum massa. Pellentesque blandit, dui non sodales vehicula, orci metus mollis nunc, non pharetra ex tellus ac est. Mauris sagittis metus et fermentum pretium. Nulla facilisi. Quisque quis ante ut nulla placerat mattis ut quis nisi. + +Sed quis nulla ligula. Quisque dignissim ligula urna, sed aliquam purus semper at. Suspendisse potenti. Nunc massa lectus, pharetra vehicula arcu bibendum, imperdiet sodales ipsum. Nam ac sapien diam. Mauris iaculis fringilla mattis. Pellentesque tempus eros sit amet justo volutpat mollis. Phasellus ac turpis ipsum. Morbi vel ante elit. Aenean posuere quam consequat velit varius suscipit. Donec tempor quam ut nibh cursus efficitur. + +Morbi molestie dolor nec sem egestas suscipit. Etiam placerat pharetra lectus, et ullamcorper risus tristique in. Sed faucibus ullamcorper lectus eget fringilla. Maecenas malesuada hendrerit congue. Sed eget neque a erat placerat tincidunt. Aliquam vitae dignissim turpis. Fusce at placerat magna, a laoreet lectus. Maecenas a purus nec diam gravida fringilla. Nam malesuada euismod ante non vehicula. In faucibus bibendum leo, faucibus posuere nisl pretium quis. Fusce finibus bibendum finibus. Vestibulum eu justo maximus, hendrerit diam nec, dignissim sapien. Aenean dolor lacus, malesuada quis vestibulum ac, venenatis ac ipsum. Cras a est id nunc finibus facilisis. Cras lacinia neque et interdum vehicula. Suspendisse vulputate tellus elit, eget tempor dui finibus vel. + +Cras sed pretium odio. Proin hendrerit elementum felis in tincidunt. Nam sed turpis vel justo molestie accumsan condimentum eu nunc. Praesent lobortis euismod rhoncus. Nulla vitae euismod nibh, quis mattis mi. Fusce ultrices placerat porttitor. Duis sem ipsum, pellentesque sit amet odio a, molestie vulputate mauris. + +Duis blandit mollis ligula, sit amet mattis ligula finibus sit amet. Nunc a leo molestie, placerat diam et, vestibulum leo. Suspendisse facilisis neque purus, nec pellentesque ligula fermentum nec. Aenean malesuada mauris lorem, eu blandit arcu pulvinar quis. Duis laoreet urna lacus, non maximus arcu rutrum ultricies. Nulla augue dolor, suscipit eu mollis eu, aliquam condimentum diam. Ut semper orci luctus, pharetra turpis at, euismod mi. Nulla leo diam, finibus sit amet purus sed, maximus dictum lorem. Integer eu mi id turpis laoreet rhoncus. + +Integer a mauris tincidunt, finibus orci ut, pretium mauris. Nulla molestie nunc mi, id finibus lorem elementum sed. Proin quis laoreet ante. Integer nulla augue, commodo id molestie quis, rutrum ut turpis. Suspendisse et tortor turpis. Sed ut pharetra massa. Pellentesque elementum blandit sem, ut elementum tellus egestas a. Fusce eu purus nibh. + +Cras dignissim ligula scelerisque magna faucibus ullamcorper. Proin at condimentum risus, auctor malesuada quam. Nullam interdum interdum egestas. Nulla aliquam nisi vitae felis mollis dictum. Suspendisse dapibus consectetur tortor. Ut ut nisi non sem bibendum tincidunt. Vivamus suscipit leo quis gravida dignissim. + +Aliquam interdum, leo id vehicula mollis, eros eros rhoncus diam, non mollis ligula mi eu mauris. Sed ultrices vel velit sollicitudin tincidunt. Nunc auctor metus at ligula gravida elementum. Praesent interdum eu elit et mollis. Duis egestas quam sit amet velit dignissim consequat. Aliquam ac turpis nec nunc convallis sagittis. Fusce blandit, erat ac fringilla consectetur, dolor eros sodales leo, vel aliquet risus nisl et diam. Aliquam luctus felis vitae est eleifend euismod facilisis et lacus. Sed leo tellus, auctor eu arcu in, volutpat sagittis nisl. Pellentesque nisl ligula, placerat vel ullamcorper at, vulputate ac odio. Morbi ac faucibus orci, et tempus nulla. Proin rhoncus rutrum dolor, in venenatis mauris. Suspendisse a fermentum augue, non semper mi. Nunc eget pretium neque. Phasellus augue erat, feugiat ac aliquam congue, rutrum non sapien. Pellentesque ac diam gravida, consectetur felis at, ornare neque. + +Nullam interdum mattis sapien quis porttitor. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus aliquet rutrum ipsum id euismod. Maecenas consectetur massa et mi porta viverra. Nunc quam nibh, dignissim vitae maximus et, ullamcorper nec lorem. Nunc vitae justo dapibus, luctus lacus vitae, pretium elit. Maecenas et efficitur leo. Curabitur mauris lectus, placerat quis vehicula vitae, auctor ut urna. Quisque rhoncus pharetra luctus. In hac habitasse platea dictumst. Integer sit amet metus nec eros malesuada aliquam. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi hendrerit mi ac leo aliquam, sit amet ultricies libero commodo. Mauris dapibus purus metus, sit amet viverra nibh imperdiet et. Nullam porta nulla tellus, quis vehicula diam imperdiet non. Vivamus enim massa, bibendum in fermentum in, ultrices at ex. + +Suspendisse fermentum id nibh eget accumsan. Duis dapibus bibendum erat ut sollicitudin. Aliquam nec felis risus. Pellentesque rhoncus ligula id sem maximus mollis sed nec massa. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ipsum ipsum, sodales sed enim id, convallis faucibus eros. Donec ultricies dictum tincidunt. Cras vitae nibh arcu. Pellentesque cursus, sapien nec consequat fermentum, ipsum ante suscipit dui, imperdiet hendrerit est nisl eu massa. Quisque vitae sem ligula. Aenean iaculis metus ut mauris interdum laoreet. Vivamus sed gravida dolor. + +Morbi nulla metus, porttitor sed eros sit amet, efficitur efficitur est. In vel nisl urna. Ut aliquet tellus at congue convallis. Phasellus imperdiet lobortis sollicitudin. Integer sodales, sem eu ultricies pharetra, erat erat porttitor odio, eget dapibus libero ipsum eget velit. Phasellus gravida nulla nisl, eu pharetra mi auctor vel. Sed blandit pharetra velit, ut egestas libero placerat non. Aliquam a interdum quam. Proin at tortor nec dui sollicitudin tempus sed vestibulum elit. Nunc non sollicitudin velit. + +Aenean consequat diam velit, sed rutrum tortor faucibus dictum. Quisque at semper augue. Duis ut est eget mi ornare bibendum id et ligula. Phasellus consequat tortor non leo pulvinar posuere. Proin vestibulum eleifend felis, in hendrerit tortor sollicitudin eu. Phasellus hendrerit, lacus vel laoreet interdum, dui tortor consequat justo, commodo ultricies arcu felis vitae enim. Vivamus eu sapien at leo suscipit rutrum eu at justo. Aenean et dolor a libero ullamcorper posuere. Integer laoreet placerat nisi in vulputate. Mauris laoreet eget risus sed cursus. Donec scelerisque neque a libero eleifend hendrerit. Nulla varius condimentum nunc sit amet fermentum. Aliquam lorem ex, varius nec mollis ut, ultrices in neque. Morbi sit amet porta leo. Integer iaculis fermentum lacus in vestibulum. + +Ut gravida, tellus ut maximus ultrices, erat est venenatis nisl, vitae pretium massa ex ac magna. Sed non purus eget ligula aliquet volutpat non quis arcu. Nam aliquam tincidunt risus, sit amet fringilla sapien vulputate ut. Mauris luctus suscipit pellentesque. Nunc porttitor dapibus ex quis tempus. Ut ullamcorper metus a eros vulputate, vitae viverra lectus convallis. Mauris semper imperdiet augue quis tincidunt. Integer porta pretium magna, sed cursus sem scelerisque sollicitudin. Nam efficitur, nibh pretium eleifend vestibulum, purus diam posuere sem, in egestas mauris augue sit amet urna. + +Vestibulum tincidunt euismod massa in congue. Duis interdum metus non laoreet fringilla. Donec at ligula congue, tincidunt nunc non, scelerisque nunc. Donec bibendum magna non est scelerisque feugiat at nec neque. Ut orci tortor, tempus eget massa non, dignissim faucibus dolor. Nam odio risus, accumsan pretium neque eget, accumsan dignissim dui. In ut neque auctor, scelerisque tellus sed, ullamcorper nisi. Suspendisse varius cursus quam at hendrerit. Vivamus elit libero, sagittis vitae sem ac, vulputate iaculis ligula. + +Sed lobortis laoreet purus sit amet rutrum. Pellentesque feugiat non leo vel lacinia. Quisque feugiat nisl a orci bibendum vestibulum. In et sollicitudin urna. Morbi a arcu ac metus faucibus tempus. Nam eu imperdiet sapien, suscipit mattis tortor. Aenean blandit ipsum nisi, a eleifend ligula euismod at. Integer tincidunt pharetra felis, mollis placerat mauris hendrerit at. Curabitur convallis, est sit amet luctus volutpat, massa lacus cursus augue, sed eleifend magna quam et risus. Aliquam lobortis tincidunt metus vitae porttitor. Suspendisse potenti. Aenean ullamcorper, neque id commodo luctus, nulla nunc lobortis quam, id dapibus neque dui nec mauris. Etiam quis lorem quis elit commodo ornare. Ut pharetra purus ultricies enim ultrices efficitur. Proin vehicula tincidunt molestie. Mauris et placerat sem. + +Aliquam erat volutpat. Suspendisse velit turpis, posuere ac lacus eu, lacinia laoreet velit. Sed interdum felis neque, id blandit sem malesuada sit amet. Ut sagittis justo erat, efficitur semper orci tempor sed. Donec enim massa, posuere varius lectus egestas, pellentesque posuere mi. Cras tincidunt ut libero sed mattis. Suspendisse quis magna et tellus posuere interdum vel at purus. Pellentesque fringilla tristique neque, id aliquet tellus ultricies non. Duis ut tellus vel odio lobortis vulputate. + +Integer at magna ac erat convallis vestibulum. Sed lobortis porttitor mauris. Fusce varius lorem et volutpat pulvinar. Aenean ac vulputate lectus, vitae consequat velit. Suspendisse ex dui, varius ut risus ut, dictum scelerisque sem. Vivamus urna orci, volutpat ut convallis ac, venenatis vitae urna. In hac habitasse platea dictumst. Etiam eu purus arcu. Aenean vulputate leo urna, vel tristique dui sagittis euismod. Suspendisse non tellus efficitur ante rhoncus volutpat at et sapien. + +Sed dapibus accumsan porttitor. Phasellus facilisis lectus finibus ligula dignissim, id pulvinar lectus feugiat. Nullam egestas commodo nisi posuere aliquet. Morbi sit amet tortor sagittis, rutrum dui nec, dapibus sapien. Sed posuere tortor tortor, interdum auctor magna varius vitae. Vestibulum id sagittis augue. Curabitur fermentum arcu sem, eu condimentum quam rutrum non. Phasellus rutrum nibh quis lectus rhoncus pretium. Curabitur dictum interdum elit. Vestibulum maximus sodales imperdiet. Mauris auctor nec purus sed venenatis. In in urna purus. + +Duis placerat molestie suscipit. Morbi a elit id purus efficitur consequat. Nunc ac commodo turpis. Etiam sit amet lacus a ipsum tempus venenatis sed vel nibh. Duis elementum aliquam mi sed tristique. Morbi ligula tortor, semper ac est vel, lobortis maximus erat. Curabitur ipsum felis, laoreet vel condimentum eget, ullamcorper sit amet mauris. Nulla facilisi. Nam at purus sed mi egestas placerat vitae vel magna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Suspendisse at dignissim diam. Phasellus consectetur eget neque vel viverra. Donec sollicitudin mattis dolor vel malesuada. Vivamus vehicula leo neque, vitae fermentum leo posuere et. Praesent dui est, finibus sit amet tristique quis, pharetra vel nibh. + +Duis nulla leo, accumsan eu odio eget, sagittis semper orci. Quisque ullamcorper ligula quam, commodo porttitor mauris ullamcorper eu. Cras varius sagittis felis in aliquam. Duis sodales risus ac justo vehicula, nec mattis diam lacinia. Cras eget lectus ipsum. Ut commodo, enim vitae malesuada hendrerit, ex dolor egestas lectus, sit amet hendrerit metus diam nec est. Vestibulum tortor metus, lobortis sit amet ante eget, tempor molestie lacus. In molestie et urna et semper. Mauris mollis, sem non hendrerit condimentum, sapien nisi cursus est, non suscipit quam justo non metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam enim est, porta ac feugiat vitae, rutrum in lorem. Duis vehicula tortor ut posuere maximus. + +Nullam vestibulum non tellus sed commodo. Quisque mattis elit sit amet sapien sollicitudin, ut condimentum nisl congue. Aenean sagittis massa vel elit faucibus fermentum. Donec tincidunt nisi nec nisl sodales pellentesque. Mauris congue congue ligula ut suscipit. Vivamus velit tortor, tempor et gravida eget, fermentum sit amet ante. Nullam fringilla, lorem at ultrices cursus, urna neque ornare dolor, eu lacinia orci enim sed nibh. Ut a ullamcorper lectus, id mattis purus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean maximus sollicitudin posuere. Nunc at augue lacus. Aenean efficitur leo sit amet lacinia efficitur. + +Quisque venenatis quam mi, in pharetra odio vulputate eu. In vel nisl pulvinar, pulvinar ligula ut, sodales risus. Sed efficitur lectus at vestibulum tincidunt. Vestibulum eu ullamcorper elit. Fusce vestibulum magna enim, et tempor lacus posuere vitae. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer leo elit, luctus nec mattis sit amet, sollicitudin in turpis. + +Proin convallis venenatis leo, vitae tristique erat iaculis nec. Nulla facilisi. Duis porttitor, sapien et bibendum vulputate, sem libero sodales lacus, non malesuada felis erat ut libero. Nam non felis semper, finibus est a, mattis mauris. Praesent nec eros quam. Nulla hendrerit, augue consectetur eleifend ultricies, purus mi condimentum nulla, eget dapibus est nunc sed libero. Nullam elementum dui erat, vitae luctus libero sollicitudin et. Nulla odio magna, placerat in augue eu, dapibus imperdiet odio. Suspendisse imperdiet metus sit amet rhoncus dapibus. Cras at enim et urna vehicula cursus eu a mauris. Integer magna ante, eleifend ac placerat vitae, porta at nisi. Cras eget malesuada orci. Curabitur nunc est, vulputate id viverra et, dignissim sed odio. Curabitur non mattis sem. Sed bibendum, turpis vitae vehicula faucibus, nunc quam ultricies lectus, vitae viverra felis turpis at libero. + +Nullam ut egestas ligula. Proin hendrerit justo a lectus commodo venenatis. Nulla facilisi. Ut cursus lorem quis est bibendum condimentum. Aenean in tristique odio. Fusce tempor hendrerit ipsum. Curabitur mollis felis justo, quis dapibus erat auctor vel. Sed augue lectus, finibus ut urna quis, ullamcorper vestibulum dui. Etiam molestie aliquam tempor. Integer mattis sollicitudin erat, et tristique elit varius vel. Mauris a ex justo. + +Nam eros est, imperdiet non volutpat rutrum, pellentesque accumsan ligula. Duis sit amet turpis metus. Aenean in rhoncus metus, ac fringilla ex. Suspendisse condimentum egestas purus, ut pharetra odio vulputate vel. Duis tincidunt massa a placerat ultrices. Mauris ultricies nibh sit amet condimentum malesuada. Duis tincidunt id ipsum sed congue. + +Praesent eu ex augue. Nullam in porta ligula. In tincidunt accumsan arcu, in pellentesque magna tristique in. Mauris eleifend libero ac nisl viverra faucibus. Nam sollicitudin dolor in commodo hendrerit. Cras at orci metus. Ut quis laoreet orci. Vivamus ultrices leo pellentesque tempor aliquet. Maecenas ut eros vitae purus placerat vestibulum. Etiam vitae gravida dolor, quis rhoncus diam. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. + +Suspendisse fringilla lacinia sagittis. Integer tincidunt consectetur tristique. Morbi non orci convallis, congue sapien quis, vulputate nunc. Donec a libero vel magna elementum facilisis non quis mi. Mauris posuere tellus non ipsum ultrices elementum. Vivamus massa velit, facilisis quis placerat aliquet, aliquet nec leo. Praesent a maximus sem. Sed neque elit, feugiat vel quam non, molestie sagittis nunc. Etiam luctus nunc ac mauris scelerisque, nec rhoncus lacus convallis. Nunc pharetra, nunc ac pulvinar aliquam, ex ipsum euismod augue, nec porttitor lacus turpis vitae neque. Fusce bibendum odio id tortor faucibus pellentesque. Sed ac porta nibh, eu gravida erat. + +Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aliquam quis ullamcorper felis. Nulla mattis sagittis ante ac tincidunt. Integer ac felis efficitur, viverra libero et, facilisis ligula. Suspendisse a metus a massa rhoncus posuere. Phasellus suscipit ligula ut lacus facilisis, ac pellentesque ex tempor. Quisque consectetur massa mi, ac molestie libero dictum quis. Proin porttitor ligula quis erat tincidunt venenatis. Proin congue nunc sed elit gravida, nec consectetur lectus sodales. Etiam tincidunt convallis ipsum at vestibulum. Quisque maximus enim et mauris porttitor, et molestie magna tristique. Morbi vitae metus elit. Maecenas sed volutpat turpis. Aliquam vitae dolor vestibulum, elementum purus eget, dapibus nibh. Nullam egestas dui ac rutrum semper. + +Etiam hendrerit est metus, et condimentum metus aliquam ac. Pellentesque id neque id ipsum rhoncus vulputate. Aliquam erat nisl, posuere sit amet ligula ac, fermentum blandit felis. Vivamus fermentum mi risus, non lacinia purus viverra id. Aenean ac sapien consequat, finibus mauris nec, porta sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed quis consectetur ex, dignissim bibendum nulla. Phasellus ac libero at quam vehicula euismod non eu leo. Phasellus a sapien augue. + +Maecenas ligula dui, bibendum vitae mauris et, auctor laoreet felis. Duis non libero a mi semper mattis. Quisque consequat luctus massa, quis tristique eros auctor feugiat. Maecenas sodales euismod neque vitae facilisis. Nullam laoreet imperdiet velit at pellentesque. Etiam massa odio, facilisis a consequat vitae, placerat vel magna. Nunc sagittis eros nec urna fringilla, pulvinar vestibulum nibh scelerisque. Sed magna metus, cursus eu consequat et, pharetra a est. Suspendisse elementum neque a dui malesuada lacinia. Donec sed ipsum volutpat, cursus urna id, ullamcorper arcu. Maecenas laoreet nisl eget velit egestas sollicitudin. Etiam nisl turpis, mollis id dignissim vitae, tristique vehicula ante. Maecenas eget placerat est, at rutrum augue. Vivamus faucibus lacinia ullamcorper. Sed pulvinar urna sodales ante sodales, at gravida leo dictum. + +Morbi maximus, quam a lobortis bibendum, enim felis varius elit, ac vehicula elit nisl ut lacus. Quisque ut arcu augue. Praesent id turpis quam. Sed sed arcu eros. Maecenas at cursus lorem, ac eleifend nisi. Fusce mattis felis at commodo pharetra. Praesent ac commodo ipsum. Quisque finibus et eros vitae tincidunt. In hac habitasse platea dictumst. Praesent purus ipsum, luctus lobortis ornare quis, auctor eget justo. Nam vel enim sollicitudin, faucibus tortor eu, sagittis eros. Ut nec consectetur erat. Donec ultricies malesuada ligula, a hendrerit sapien volutpat in. Maecenas sed enim vitae sapien pulvinar faucibus. + +Proin semper nunc nibh, non consequat neque ullamcorper vel. Maecenas lobortis sagittis blandit. Aenean et arcu ultricies turpis malesuada malesuada. Ut quam ex, laoreet ut blandit cursus, feugiat vitae dolor. Etiam ex lacus, scelerisque vel erat vel, efficitur tincidunt magna. Morbi tristique lacinia dolor, in egestas magna ultrices vitae. Integer ultrices leo ac tempus venenatis. Praesent ac porta tortor. Vivamus ornare blandit tristique. Nulla rutrum finibus pellentesque. In non dui elementum, fermentum ipsum vel, varius magna. Pellentesque euismod tortor risus, ac pellentesque nisl faucibus eget. + +Vivamus eu enim purus. Cras ultrices rutrum egestas. Sed mollis erat nibh, at posuere nisl luctus nec. Nunc vulputate, sapien id auctor molestie, nisi diam tristique ante, non convallis tellus nibh at orci. Morbi a posuere purus, in ullamcorper ligula. Etiam elementum sit amet dui imperdiet iaculis. Proin vitae tincidunt ipsum, sit amet placerat lectus. Curabitur commodo sapien quam, et accumsan lectus fringilla non. Nullam eget accumsan enim, ac pharetra mauris. Sed quis tristique velit, vitae commodo nisi. Duis turpis dui, maximus ut risus at, finibus consequat nunc. Maecenas sed est accumsan, aliquet diam in, facilisis risus. Curabitur vehicula rutrum auctor. Nam iaculis risus pulvinar maximus viverra. Nulla vel augue et ex sagittis blandit. + +Ut sem nulla, porta ac ante ac, posuere laoreet eros. Donec sodales posuere justo a auctor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras mollis at orci hendrerit porta. Nullam sodales tortor tortor, non lacinia diam finibus id. Duis libero orci, suscipit ac odio et, dictum consequat ipsum. Pellentesque eu ligula sagittis, volutpat eros at, lacinia lorem. Cras euismod tellus in iaculis tempor. Quisque accumsan, magna a congue venenatis, ante ipsum aliquam lectus, at egestas enim nunc at justo. Quisque sem purus, viverra ut tristique ut, maximus id enim. Etiam quis placerat sem. In sollicitudin, lacus eu rutrum mollis, nulla eros luctus elit, vel dapibus urna purus nec urna. Phasellus egestas massa quam, ac molestie erat hendrerit a. Praesent ultrices neque ut turpis molestie auctor. Etiam molestie placerat purus, et euismod erat aliquam in. Morbi id suscipit justo. + +Proin est ante, consequat at varius a, mattis quis felis. Sed accumsan nibh sit amet ipsum elementum posuere. Vestibulum bibendum id diam sit amet gravida. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi nec dolor vel ipsum dignissim hendrerit vel non ipsum. Praesent facilisis orci quis elit auctor lobortis. Phasellus cursus risus lectus, vel lobortis libero dapibus in. Quisque tristique tempus leo a pulvinar. Pellentesque a magna tincidunt, pellentesque massa nec, laoreet orci. Morbi congue ornare dolor quis commodo. Phasellus massa nisi, tincidunt at eros dictum, hendrerit lobortis urna. Maecenas porta, magna id mattis molestie, nibh tellus lobortis sem, eget tincidunt ipsum quam eu turpis. + +Ut gravida orci risus, vel rutrum mauris vehicula id. Etiam bibendum, neque a placerat condimentum, ex orci imperdiet lectus, quis dapibus arcu lacus eget lectus. Sed consequat non mi sit amet venenatis. Fusce vestibulum erat libero, eget hendrerit risus vulputate sollicitudin. Integer sed eleifend felis. Donec commodo, sem eu mattis placerat, urna odio aliquam tellus, et laoreet justo tellus eget erat. Fusce sed suscipit tortor. Nam hendrerit nibh ac nunc auctor lacinia. Pellentesque placerat condimentum ipsum, eget semper tortor hendrerit vel. Nullam non urna eu lacus pellentesque congue ut id eros. + +Nunc finibus leo in rhoncus tristique. Sed eu ipsum nec nisl egestas faucibus eget a felis. Pellentesque vitae nisi in nulla accumsan fermentum. Sed venenatis feugiat eleifend. Fusce porttitor varius placerat. Aliquam aliquet lacus sit amet mattis mollis. Sed vel nulla quis dolor suscipit vehicula ac viverra lorem. Duis viverra ipsum eget nulla ullamcorper fermentum. Mauris tincidunt arcu quis quam fringilla ornare. Donec et iaculis tortor. Nam ultricies libero vel ipsum aliquet efficitur. Morbi eget dolor aliquam, tempus sapien eget, viverra ante. Donec varius mollis ex, sed efficitur purus euismod interdum. Quisque vel sapien non neque tincidunt semper. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. + +Suspendisse sit amet purus leo. Fusce lectus lorem, aliquam ac nulla eget, imperdiet ornare eros. Nullam sem augue, varius in nisi non, sollicitudin pellentesque ante. Etiam eu odio condimentum, tempor libero et, egestas arcu. Cras pellentesque eleifend aliquet. Pellentesque non blandit ligula. Ut congue viverra rhoncus. Phasellus mattis mi ac eros placerat, eu feugiat tellus ultrices. Aenean mollis laoreet libero eu imperdiet. Cras sed pulvinar mi, ac vehicula ligula. Vestibulum sit amet ex massa. In a egestas eros. + +Mauris pretium ipsum risus, venenatis cursus ante imperdiet id. Praesent eu turpis nec risus feugiat maximus ullamcorper ac lectus. Integer placerat at mi vel dapibus. Vestibulum fermentum turpis sit amet turpis viverra, id aliquet diam suscipit. Nam nec ex sed ante ullamcorper pharetra quis sit amet risus. Sed ac faucibus velit, id feugiat nibh. Nullam eget ipsum ex. Vivamus tincidunt non nunc non faucibus. Quisque bibendum viverra facilisis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at nisi hendrerit quam suscipit egestas. Curabitur laoreet maximus ultricies. Duis ut tellus ac augue molestie dictum. + +Suspendisse rhoncus iaculis erat, ut ullamcorper est tristique eget. Donec auctor nec risus at gravida. Vivamus volutpat vulputate tellus, vel ultricies eros suscipit eget. Ut pulvinar id mi eu tempus. Morbi malesuada augue in dui varius, nec blandit neque vehicula. Donec ornare nec nisl in mollis. Morbi enim nisi, rhoncus nec est id, dapibus tempus urna. Ut id elit a felis vestibulum consectetur. Duis lectus quam, pharetra sit amet diam sed, posuere vestibulum erat. Fusce vitae maximus massa. Nullam id metus tempus, iaculis risus eu, lobortis urna. Quisque in congue urna. Pellentesque placerat neque in augue dapibus, non varius ex malesuada. Curabitur ut eleifend libero. Fusce vitae ligula luctus, fermentum enim vitae, ultrices erat. + +Sed viverra augue turpis, scelerisque egestas sapien mattis eu. Duis laoreet magna at ex pharetra dapibus. Praesent eget odio vel quam venenatis dictum. Nulla in sollicitudin dolor. Mauris lobortis nec eros vel rhoncus. Vestibulum porta viverra venenatis. Curabitur vel scelerisque quam, a egestas velit. Praesent volutpat tincidunt magna at laoreet. + +Cras nec lorem odio. Pellentesque quis dui urna. Praesent at tellus ac lectus scelerisque placerat nec eu risus. Vestibulum sit amet mattis ligula. Vivamus sed nisi at leo elementum accumsan at sit amet arcu. Aenean mattis tellus nec leo gravida, eget hendrerit nisl faucibus. Mauris pellentesque luctus condimentum. Maecenas pretium sapien nunc, eget commodo dolor maximus id. Mauris vestibulum accumsan massa a dictum. Phasellus interdum quam ligula, ut maximus diam blandit aliquam. Nunc vitae ex eu erat condimentum consectetur. Maecenas interdum condimentum volutpat. + +Donec et enim a libero rutrum laoreet. Praesent a condimentum sem, at tincidunt quam. In vel molestie risus. Sed urna dui, molestie vitae mollis laoreet, tempor quis lectus. Praesent vitae auctor est, et aliquet nunc. Curabitur vulputate blandit nulla, at gravida metus. Maecenas gravida dui eu iaculis tristique. Pellentesque posuere turpis nec auctor eleifend. Suspendisse bibendum diam eu tellus lobortis, et laoreet quam congue. In hac habitasse platea dictumst. Morbi dictum neque velit, eget rutrum eros ultrices sit amet. + +Phasellus fermentum risus pharetra consectetur bibendum. Donec magna tortor, lacinia vitae nibh quis, aliquet pretium lorem. Donec turpis nisi, pretium eu enim volutpat, mattis malesuada augue. Nullam vel tellus iaculis, sollicitudin elit eget, tincidunt lacus. Fusce elementum elementum felis et iaculis. Suspendisse porta eros nec neque malesuada, in malesuada ante sollicitudin. Vivamus bibendum viverra molestie. + +Integer feugiat, erat nec convallis aliquam, velit felis congue erat, molestie eleifend tellus erat in tellus. Nunc et justo purus. Donec egestas fermentum dui non feugiat. Quisque in sapien sagittis, gravida quam id, iaculis lectus. Cras sagittis rhoncus bibendum. Fusce quis metus in velit scelerisque tincidunt at non ipsum. Vivamus efficitur ante eu odio vulputate, vitae ultricies risus vehicula. Proin eget odio eu sem tincidunt feugiat vel id lorem. + +Vestibulum sit amet nulla dignissim, euismod mi in, fermentum tortor. Donec ut aliquet libero, lacinia accumsan velit. Donec et nulla quam. Nullam laoreet odio nec nunc imperdiet, a congue eros venenatis. Quisque nec tellus sit amet neque interdum posuere. Duis quis mi gravida, tincidunt diam convallis, ultricies augue. Mauris consequat risus non porttitor congue. Ut in ligula consequat, viverra nunc a, eleifend enim. Duis ligula urna, imperdiet nec facilisis et, ornare eu ex. Proin lobortis lectus a lobortis porttitor. Nulla leo metus, egestas eu libero sed, pretium faucibus felis. Vestibulum non sem tortor. Nam cursus est leo. Vivamus luctus enim odio, non interdum sem dapibus a. Aenean accumsan consequat lectus in imperdiet. + +Donec vehicula laoreet ipsum in posuere. Quisque vel quam imperdiet, sollicitudin nisi quis, suscipit velit. Morbi id sodales mauris. Curabitur tellus arcu, feugiat sed dui sit amet, sodales sagittis libero. Aenean vel suscipit metus, non placerat leo. Vestibulum quis nulla elit. Proin scelerisque non ante ut commodo. Interdum et malesuada fames ac ante ipsum primis in faucibus. + +Sed non urna dolor. Suspendisse convallis mi porta pulvinar ultrices. Suspendisse quam ipsum, hendrerit non scelerisque molestie, interdum dictum nunc. Morbi condimentum condimentum turpis eu luctus. Pellentesque sagittis sollicitudin odio, sed ultricies felis ornare sit amet. Sed ultrices ex leo, a tincidunt nisl gravida sed. Nullam ornare accumsan porta. Praesent consectetur id est nec sollicitudin. + +In hac habitasse platea dictumst. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed sed ultrices nibh. Duis accumsan suscipit eros, a dictum odio tempus sit amet. Aenean imperdiet erat ac lacus finibus, scelerisque cursus massa imperdiet. Mauris molestie risus ut lacinia posuere. Nulla et sodales purus. Maecenas orci erat, placerat in tristique quis, placerat in mi. + +Donec sollicitudin pellentesque odio in feugiat. Morbi eu dolor ut mauris congue sollicitudin. Aliquam erat volutpat. Nulla id varius dui. Curabitur finibus urna ante, consectetur interdum nisi volutpat a. Quisque quis mi tristique, consequat tellus eget, rutrum sapien. Vivamus vitae tellus vulputate, rutrum ex eu, vulputate sem. Suspendisse viverra lorem tellus, vel interdum orci gravida quis. Ut laoreet arcu at mi ullamcorper finibus. Duis porta sagittis vestibulum. Sed commodo nisl vitae urna sollicitudin, nec lacinia est sodales. Curabitur imperdiet sodales dui sed iaculis. Sed ac tellus maximus, eleifend quam sit amet, feugiat elit. Aenean viverra, dui at mattis varius, est odio vestibulum sapien, sit amet mollis libero massa nec velit. Etiam quis sodales justo. + +Ut ultricies, sem eget sodales feugiat, nunc arcu congue elit, ac tempor justo massa nec purus. Maecenas enim nunc, pharetra eget dictum sit amet, tempus pellentesque velit. Suspendisse venenatis ligula in nulla mattis, et imperdiet ex tincidunt. Etiam vulputate, tellus et ultrices suscipit, enim velit laoreet massa, vitae congue odio enim ac urna. Morbi quam lorem, iaculis ac varius sagittis, euismod quis dolor. In ut dui eu purus feugiat consectetur. Vestibulum cursus velit quis lacus pellentesque iaculis. Cras in risus sed mauris porta rutrum. Nulla facilisi. Nullam eu bibendum est, non pellentesque lectus. Sed imperdiet feugiat lorem, quis convallis ante auctor in. Maecenas justo magna, scelerisque sit amet tellus eget, varius elementum risus. Duis placerat et quam sed varius. + +Duis nec nibh vitae nibh dignissim mollis quis sed felis. Curabitur vitae quam placerat, venenatis purus ut, euismod nisl. Curabitur porttitor nibh eu pulvinar ullamcorper. Suspendisse posuere nec ipsum ac dapibus. Cras convallis consectetur urna. Phasellus a nibh in dolor lacinia posuere id eget augue. In eu pharetra lorem, vitae cursus lacus. Aliquam tincidunt nibh lectus. Aenean facilisis ultricies posuere. Sed ut placerat orci. Curabitur scelerisque gravida blandit. Maecenas placerat ligula eget suscipit fringilla. Mauris a tortor justo. Aliquam hendrerit semper mollis. Phasellus et tincidunt libero. Etiam vel quam libero. + +Quisque aliquet tempor ex. Ut ante sem, vehicula at enim vel, gravida porta elit. Etiam vitae lacus a neque lobortis consectetur. Mauris sed interdum odio. Mauris elementum ex blandit tempor cursus. Integer in enim in leo viverra elementum. Fusce consectetur metus et sem rutrum, mattis euismod diam semper. Nunc sed ipsum vel urna consequat vehicula. Donec cursus pretium lorem, vestibulum pretium felis commodo sit amet. Nam blandit felis enim, eget gravida ex faucibus a. In nec neque massa. Etiam laoreet posuere ipsum. Praesent volutpat nunc dolor, ac vulputate magna facilisis non. Aenean congue turpis vel lectus sollicitudin tristique. Sed nec consequat purus, non vehicula quam. Etiam ultricies, est ac dictum tincidunt, turpis turpis pretium massa, a vulputate libero justo at nibh. + +Aliquam erat volutpat. Cras ultrices augue ac sollicitudin lobortis. Curabitur et aliquet purus. Duis feugiat semper facilisis. Phasellus lobortis cursus velit, a sollicitudin tortor. Nam feugiat sapien non dapibus condimentum. Morbi at mi bibendum, commodo quam at, laoreet enim. Integer eu ultrices enim. Sed vestibulum eu urna ut dictum. Curabitur at mattis leo, sed cursus massa. Aliquam porttitor, felis quis fermentum porttitor, justo velit feugiat nulla, eget condimentum sem dui ut sapien. + +In fringilla elit eu orci aliquam consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Ut eget fringilla tellus. Curabitur fermentum, mi et condimentum suscipit, elit neque bibendum dui, et hendrerit nunc metus id ipsum. Morbi placerat mi in hendrerit congue. Ut feugiat mauris eget scelerisque viverra. Vivamus sit amet erat dictum, sagittis lectus nec, pulvinar lorem. Sed non enim ac dui sollicitudin aliquet. Quisque ut lacus dolor. Fusce hendrerit malesuada euismod. Nulla faucibus vel mauris eu mollis. Mauris est diam, fringilla ac arcu feugiat, efficitur volutpat turpis. Aliquam venenatis cursus massa sed porttitor. Ut ac finibus enim, in tincidunt sapien. + +Nunc faucibus semper turpis a lacinia. Phasellus gravida, libero vel pulvinar ornare, ex sem tincidunt lectus, sit amet convallis augue risus at tortor. Quisque sit amet ipsum id nulla posuere vestibulum. Pellentesque scelerisque mauris vel leo viverra sodales. Nulla viverra aliquam ex, ut rutrum enim fermentum venenatis. Aenean eget dapibus ex, eget faucibus metus. Vestibulum volutpat leo in diam semper, eget porta magna suscipit. Sed sit amet nulla blandit, aliquam dolor ac, gravida velit. Sed vel velit viverra, maximus est id, convallis justo. + +Curabitur nulla ante, vulputate at libero vel, ullamcorper rutrum nibh. Pellentesque porttitor eu mauris id mattis. Duis vulputate augue elit, eget interdum justo pretium vel. Maecenas eu vulputate arcu, eget posuere purus. Suspendisse viverra a velit dictum eleifend. Suspendisse vitae dapibus diam. Donec vehicula justo in ante interdum, eu luctus diam placerat. Vivamus convallis ipsum eu orci suscipit, sed fermentum enim euismod. Maecenas faucibus elit vitae ex ornare tristique. Donec vestibulum nec elit sit amet porttitor. Aenean tempor lectus eget tortor hendrerit luctus. Nullam interdum vitae lectus vel feugiat. Cras in risus non magna consectetur lobortis. Sed faucibus enim quis gravida convallis. + +Phasellus eget massa sit amet libero ultrices suscipit. Vivamus at risus sapien. Nam mollis nunc eget velit dictum maximus. Sed pellentesque, nunc ac fringilla lacinia, quam enim mattis ex, sed euismod tortor metus eu neque. Ut mattis nisl ut lectus rhoncus, sodales bibendum eros porta. Nulla porttitor enim nec diam sagittis, eget porta velit efficitur. Vestibulum ultricies eros neque. Phasellus rutrum suscipit enim, in interdum ante gravida vitae. Sed in sagittis diam, non commodo velit. + +Morbi hendrerit odio orci, nec tincidunt odio rhoncus nec. Mauris neque velit, vehicula a lorem at, suscipit tristique dui. Sed finibus, nisl in mattis convallis, turpis neque sodales lacus, eu porta enim magna non diam. Nam commodo sodales risus consectetur malesuada. In eget elementum justo. Phasellus sit amet massa imperdiet, dapibus nunc sit amet, suscipit orci. Fusce condimentum laoreet feugiat. Ut ut viverra ante. Praesent bibendum interdum commodo. Nulla mollis nisi a est ornare volutpat. Sed at ligula eu nisi dapibus tempus. Proin cursus vestibulum justo, nec efficitur justo dignissim vel. Nunc quis maximus eros. + +Cras viverra, diam a tristique mattis, libero felis vulputate tellus, a ornare felis leo a dui. Nulla ante nulla, finibus ut tellus ut, blandit pharetra nibh. Proin eleifend fermentum ex, eget auctor libero vulputate in. Nullam ultricies, mauris placerat pretium placerat, leo urna lobortis leo, vel placerat arcu libero sed mauris. Aliquam mauris ligula, ornare at urna at, eleifend gravida ligula. Vestibulum consectetur ut nulla non scelerisque. Donec ornare, sem nec elementum aliquam, urna nulla bibendum metus, eu euismod dui ligula ac est. Fusce laoreet erat eu ex lobortis, quis bibendum ligula interdum. Sed vel mi erat. Vivamus id lacus ac enim mattis tempor. Nunc ultricies pellentesque enim sed euismod. Fusce tincidunt convallis elit quis aliquam. Mauris nulla ipsum, sollicitudin quis diam ac, feugiat volutpat tellus. In nibh nibh, vulputate quis tincidunt quis, pulvinar eget magna. Pellentesque quis finibus dolor. Suspendisse viverra vitae lectus non eleifend. + +Nunc ut orci et sapien maximus semper. Nulla dignissim sem urna, ac varius lectus ultricies id. Quisque aliquet pulvinar pretium. In ultricies molestie tellus vehicula porta. Nam enim lorem, aliquam eget ex et, hendrerit volutpat quam. Maecenas diam lacus, pellentesque eget tempus ac, pharetra eu elit. Donec vel eros a sem facilisis vulputate. Nullam ac nisi vulputate, laoreet nisl ac, eleifend sem. Nullam mi massa, rhoncus sed pharetra interdum, tincidunt eget nunc. Aliquam viverra mattis posuere. Mauris et dui sed nisl sollicitudin fermentum quis ut arcu. Nam placerat eget orci at tincidunt. Curabitur vel turpis metus. Phasellus nibh nulla, fermentum scelerisque sem vel, gravida tincidunt velit. Pellentesque vel quam tempor, finibus massa pellentesque, condimentum dui. + +Donec at mattis neque. Etiam velit diam, consequat auctor mauris id, hendrerit faucibus metus. Maecenas ullamcorper eros a est sodales, ac consectetur odio scelerisque. Donec leo metus, imperdiet at pellentesque vel, feugiat id erat. Suspendisse at magna enim. Vestibulum placerat sodales lorem id sollicitudin. Aenean at euismod ligula, eget mollis diam. Phasellus pulvinar, orci nec pretium condimentum, est erat facilisis purus, quis feugiat augue elit aliquam nulla. Aenean vitae tortor id risus congue tincidunt. Sed dolor enim, mattis a ullamcorper id, volutpat ac leo. + +Proin vehicula feugiat augue, id feugiat quam sodales quis. Donec et ultricies massa, a lacinia nulla. Duis aliquam augue ornare euismod viverra. Ut lectus risus, rutrum sit amet efficitur a, luctus nec nisl. Cras volutpat ullamcorper congue. Sed vitae odio metus. Phasellus aliquet euismod varius. + +Nullam sem ex, malesuada ut magna ut, pretium mollis arcu. Nam porttitor eros cursus mi lacinia faucibus. Suspendisse aliquet eleifend iaculis. Maecenas sit amet viverra tortor. Nunc a mollis risus. Etiam tempus dolor in tortor malesuada mattis. Ut tincidunt venenatis est sit amet dignissim. Vestibulum massa enim, tristique sed scelerisque eu, fringilla ac velit. Donec efficitur quis urna sit amet malesuada. Vestibulum consequat ac ligula in dapibus. Maecenas massa massa, molestie non posuere nec, elementum ut magna. In nisi erat, mollis non venenatis eu, faucibus in justo. Morbi gravida non ex non egestas. Pellentesque finibus laoreet diam, eu commodo augue congue vitae. + +Aenean sem mi, ullamcorper dapibus lobortis vitae, interdum tincidunt tortor. Vivamus eget vulputate libero. Ut bibendum posuere lectus, vel tincidunt tortor aliquet at. Phasellus malesuada orci et bibendum accumsan. Aliquam quis libero vel leo mollis porta. Sed sagittis leo ac lacus dictum, ac malesuada elit finibus. Suspendisse pharetra luctus commodo. Vivamus ultricies a odio non interdum. Vivamus scelerisque tincidunt turpis quis tempor. Pellentesque tortor ligula, varius non nunc eu, blandit sollicitudin neque. Nunc imperdiet, diam et tristique luctus, ipsum ex condimentum nunc, sit amet aliquam justo velit sed libero. Duis vel suscipit ligula. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed tincidunt neque vel massa ultricies, id dictum leo consequat. Curabitur lobortis ultricies tellus, eget mattis nisl aliquam sit amet. + +Proin at suscipit justo. Vivamus ut vestibulum nisl. Pellentesque enim odio, pharetra non magna sed, efficitur auctor magna. Praesent tincidunt ante quis ante hendrerit viverra. Pellentesque vel ipsum id magna vulputate efficitur. Sed nec neque accumsan, pulvinar sapien quis, euismod mauris. Donec condimentum laoreet sapien quis gravida. Quisque sed mattis purus. Vestibulum placerat vel neque maximus scelerisque. + +Vestibulum mattis quam quis efficitur elementum. Duis dictum dolor ac scelerisque commodo. Fusce sollicitudin nisi sit amet dictum placerat. Suspendisse euismod pharetra eleifend. In eros nisl, porttitor sed mauris at, consectetur aliquet mauris. Donec euismod viverra neque sed fermentum. Phasellus libero magna, accumsan ut ultricies vitae, dignissim eget metus. Donec tellus turpis, interdum eget maximus nec, hendrerit eget massa. Curabitur auctor ligula in iaculis auctor. In ultrices quam suscipit cursus finibus. Aenean id mi at dolor interdum iaculis vitae ut lorem. Nullam sed nibh fringilla, lacinia odio nec, placerat erat. In dui libero, viverra ac viverra ac, pellentesque sit amet turpis. + +Nulla in enim ex. Sed feugiat est et consectetur venenatis. Cras varius facilisis dui vel convallis. Vestibulum et elit eget tellus feugiat pellentesque. In ut ante eu purus aliquet posuere. Nulla nec ornare sem, sed luctus lorem. Nam varius iaculis odio, eget faucibus nisl ullamcorper in. Sed eget cursus felis, nec efficitur nisi. + +Vivamus commodo et sem quis pulvinar. Pellentesque libero ante, venenatis vitae ligula sit amet, ornare sollicitudin nulla. Mauris eget tellus hendrerit, pulvinar metus quis, tempor nisi. Proin magna ex, laoreet sed tortor quis, varius fermentum enim. Integer eu dolor dictum, vulputate tortor et, aliquet ligula. Vestibulum vitae justo id mauris luctus sollicitudin. Suspendisse eget auctor neque, sodales egestas lorem. Vestibulum lacinia egestas metus vitae euismod. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus ex tellus, volutpat nec pulvinar sit amet, condimentum vitae dui. Curabitur vel felis sodales, lacinia nunc iaculis, ullamcorper augue. Pellentesque consequat dolor quis eros efficitur malesuada. Nulla ut malesuada lectus. + +Morbi et tristique ante. Aliquam erat volutpat. Vivamus vitae dui nec turpis pellentesque fermentum. Quisque eget velit massa. Pellentesque tristique aliquam nisl, eu sollicitudin justo venenatis sed. Duis eleifend sem eros, ut aliquam libero porttitor id. Sed non nunc consequat, rhoncus diam eu, commodo erat. Praesent fermentum in lectus id blandit. Donec quis ipsum at justo volutpat finibus. Nulla blandit justo nulla, at mollis lacus consequat eget. Aenean sollicitudin quis eros ut ullamcorper. + +Pellentesque venenatis nulla ut mi aliquet feugiat. Cras semper vel magna nec pharetra. Integer mattis felis et sapien commodo imperdiet. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis quis luctus felis. Vestibulum justo nibh, aliquam non lectus vitae, molestie placerat justo. Donec lorem nibh, gravida sit amet hendrerit ac, maximus id ipsum. Nunc ac libero sodales risus eleifend sagittis. Phasellus est massa, lobortis elementum ex sed, scelerisque consectetur neque. Nunc faucibus neque id lorem malesuada, eget convallis ex mattis. + +Sed turpis tortor, fermentum non turpis id, posuere varius nibh. Donec iaculis lorem dui. Etiam eros ante, sodales eget venenatis at, consectetur eget risus. Curabitur non aliquam ante, a pretium justo. Maecenas tempor nisl tortor, vitae dictum nisi ultrices eu. Duis eget dui ultrices, porttitor lacus sed, lobortis purus. Quisque mattis elit nec neque sagittis, sed commodo leo blandit. Mauris sodales interdum eleifend. Vestibulum condimentum consectetur augue, id luctus diam convallis et. + +Nunc suscipit risus in justo accumsan, a placerat magna tincidunt. Proin a nisl ipsum. Sed libero dui, tristique in augue quis, auctor tristique risus. Sed porttitor ex augue, eu porta augue molestie a. Duis rhoncus purus libero, eu tempus turpis condimentum at. Sed mollis nisi id lectus placerat tincidunt. Maecenas non scelerisque elit, quis rutrum orci. Donec in tellus pharetra urna ornare lobortis. Phasellus id risus at nisi varius rutrum eu ut turpis. + +Duis dictum justo quis nisl porta, eget tincidunt magna suscipit. Sed velit massa, ullamcorper eu sodales ac, pretium a massa. Duis et rutrum tortor. Nulla accumsan hendrerit sapien, cursus volutpat eros egestas eget. Donec sollicitudin at ante quis sollicitudin. Aenean blandit feugiat diam, id feugiat eros faucibus eget. Donec viverra dolor vel justo scelerisque dignissim. Nulla semper sem nunc, rhoncus semper tellus ultricies sed. Duis in ornare diam. Donec vehicula feugiat varius. Maecenas ut suscipit est. Vivamus sem sem, finibus at dolor sit amet, euismod dapibus ligula. Vestibulum fringilla odio dapibus, congue massa eget, congue sem. Donec feugiat magna eget tortor lacinia scelerisque non et ipsum. + +Suspendisse potenti. Nunc convallis sollicitudin ex eget venenatis. Sed iaculis nibh ex, vel ornare ligula congue dignissim. Quisque sollicitudin dolor ac dui vestibulum, sit amet molestie nisi aliquet. Donec at risus felis. Aenean sollicitudin metus a feugiat porta. Aenean a tortor ut dolor cursus sagittis. Vivamus consectetur porttitor nunc in facilisis. Proin sit amet mi vel lectus consectetur ultrices. + +Sed cursus lectus vitae nunc tristique, nec commodo turpis dapibus. Pellentesque luctus ex id facilisis ornare. Morbi quis placerat dolor. Donec in lectus in arcu mattis porttitor ac sit amet metus. Cras congue mauris non risus sodales, vitae feugiat ipsum bibendum. Nulla venenatis urna sed libero elementum, a cursus lorem commodo. Mauris faucibus lobortis eros nec commodo. + +Nullam suscipit ligula ullamcorper lorem commodo blandit. Nulla porta nibh quis pulvinar placerat. Vivamus eu arcu justo. Vestibulum imperdiet est ut fermentum porttitor. Pellentesque consectetur libero in sapien efficitur scelerisque. Curabitur ac erat sit amet odio aliquet dignissim. Pellentesque mi sem, rhoncus et luctus at, porttitor rutrum lectus. Vestibulum sollicitudin sollicitudin suscipit. Aenean efficitur dolor non ultrices imperdiet. Donec vel sem ex. + +Sed convallis mauris aliquam rutrum cursus. Ut tempor porttitor sodales. Etiam eu risus ac augue gravida egestas et eu dolor. Proin id magna ex. Suspendisse quis lectus quis lorem ultricies tempus. Donec porttitor velit vitae tincidunt faucibus. Aliquam vitae semper nisi. Morbi ultrices, leo non pretium dapibus, dui libero pellentesque ex, vel placerat enim ante vitae dui. Nunc varius, sem sit amet sagittis lobortis, lectus odio scelerisque mauris, ut vestibulum orci magna quis neque. Sed id congue justo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris congue nisi est, malesuada mollis elit tincidunt sed. Curabitur sed ex sit amet felis tristique elementum vitae vel nibh. + +Etiam mollis pretium lobortis. Mauris augue lacus, efficitur at lacus sed, mollis tincidunt lectus. Aliquam erat volutpat. Donec at euismod elit, et mattis felis. Sed id lobortis urna. Morbi imperdiet vestibulum leo, sed maximus leo blandit eu. Aliquam semper lorem neque, nec euismod turpis mattis mollis. Quisque lobortis urna ultrices odio pretium, ac venenatis orci faucibus. Suspendisse bibendum odio ligula, sed lobortis massa pharetra nec. Donec turpis justo, iaculis at dictum ac, finibus eu libero. Maecenas quis porttitor mi, sit amet aliquet neque. + +Vivamus auctor vulputate ante, at egestas lorem. Donec eu risus in nulla mollis ultricies at et urna. Duis accumsan porta egestas. Ut vel euismod augue. Fusce convallis nulla ante, nec fringilla velit aliquet at. Nam malesuada dapibus ligula, a aliquam nibh scelerisque ac. Praesent malesuada neque et pellentesque interdum. Curabitur volutpat at turpis vitae tristique. Vivamus porttitor semper congue. Quisque suscipit lacus mi, rhoncus ultrices tortor auctor quis. Maecenas neque neque, molestie ac facilisis eget, luctus ac lorem. In ut odio ut lacus suscipit pulvinar vitae sed elit. Nulla imperdiet, sem quis euismod sagittis, dui erat luctus dolor, faucibus faucibus erat sem eget nunc. Nam accumsan placerat malesuada. Maecenas convallis finibus pulvinar. + +Cras at placerat tortor. Morbi facilisis auctor felis sit amet molestie. Donec sodales sed lorem vitae suscipit. Etiam fermentum pharetra ipsum, nec luctus orci gravida eu. Pellentesque gravida, est non condimentum tempus, mauris ligula molestie est, in congue dolor nisl vel sapien. Duis congue tempor augue, id rutrum eros porta dapibus. Etiam rutrum eget est eget vestibulum. Aenean mollis arcu vel consequat varius. Praesent at condimentum felis. Duis nec interdum nisl. Donec commodo lorem sed sapien scelerisque malesuada non eu urna. In blandit non ipsum at porta. Nam lobortis leo vitae dui auctor, non feugiat quam bibendum. Donec auctor lectus sagittis laoreet maximus. Maecenas rhoncus laoreet porttitor. Vestibulum porttitor augue ut lectus hendrerit, eget posuere mi gravida. + +Sed mattis ex in erat pulvinar, eu imperdiet magna dapibus. Etiam nisi nibh, tempus non tellus sit amet, mattis tempor odio. Quisque nec lorem feugiat, lobortis odio et, commodo nunc. Maecenas semper purus nisi, nec vehicula nibh eleifend vitae. Nulla fermentum a lectus at maximus. Phasellus finibus metus non euismod ultrices. Etiam a pulvinar ante. Quisque convallis nec metus sit amet facilisis. Praesent laoreet massa et sollicitudin laoreet. Vestibulum in mauris aliquet, convallis mi ut, elementum purus. Nulla purus nulla, sodales at hendrerit quis, tempus sed lectus. + +Nam ut laoreet neque, ut maximus nibh. Maecenas quis justo pellentesque, sollicitudin elit at, venenatis velit. Aenean nunc velit, vehicula scelerisque odio at, consectetur laoreet purus. Duis dui purus, malesuada quis ipsum sit amet, tempor interdum libero. Curabitur porta scelerisque sapien, vitae cursus diam condimentum eu. Phasellus sed orci quam. Nullam vitae dui quis purus tincidunt vestibulum. Curabitur quis nulla porta, cursus arcu non, auctor enim. Etiam sollicitudin ex id sem vehicula mollis. Morbi viverra laoreet tincidunt. Praesent ut semper dui. Nam sit amet pretium neque. Mauris vitae luctus diam, in lacinia purus. Maecenas ut placerat justo, ut porta felis. Integer eu mauris ante. + +Aenean porttitor tellus diam, tempor consequat metus efficitur id. Suspendisse ut felis at erat tempor dictum at nec sapien. Sed vestibulum interdum felis, ac mattis mauris porta in. Nunc et condimentum massa. Sed cursus dictum justo et luctus. Integer convallis enim nisl, a rutrum lectus ultricies in. Donec dapibus lacus at nulla dapibus, id sollicitudin velit hendrerit. Fusce a magna at orci mollis rutrum ac a dolor. Aliquam erat volutpat. Morbi varius porta nunc, sit amet sodales ex hendrerit commodo. Donec tincidunt tortor sapien, vitae egestas sapien vehicula eget. + +Suspendisse potenti. Donec pulvinar felis nec leo malesuada interdum. Integer posuere placerat maximus. Donec nibh ipsum, tincidunt vitae luctus vitae, bibendum at leo. Sed cursus nisl ut ex faucibus aliquet sed nec eros. Curabitur molestie posuere felis. Integer faucibus velit eget consequat iaculis. Mauris sed vulputate odio. Phasellus maximus, elit a pharetra egestas, lorem magna semper tellus, vestibulum semper diam felis at sapien. Suspendisse facilisis, nisl sit amet euismod vehicula, libero nulla vehicula dolor, quis fermentum nibh elit sit amet diam. + +Morbi lorem enim, euismod eu varius ut, scelerisque quis odio. Nam tempus vitae eros id molestie. Nunc pretium in nulla eget accumsan. Quisque mattis est ut semper aliquet. Maecenas eget diam elementum, fermentum ipsum a, euismod sapien. Duis quam ligula, cursus et velit nec, ullamcorper tincidunt magna. Donec vulputate nisl est, et ullamcorper urna tempor sit amet. + +Proin lacinia dui non turpis congue pretium. Morbi posuere metus vel purus imperdiet interdum. Morbi venenatis vel eros non ultricies. Nulla vel semper elit. Ut quis purus tincidunt, auctor justo ut, faucibus turpis. Proin quis mattis erat, at faucibus ligula. Mauris in mauris enim. Donec facilisis enim at est feugiat hendrerit. Nam vel nisi lorem. Fusce ultricies convallis diam, in feugiat tortor luctus quis. Donec tempor, leo vitae volutpat aliquam, magna elit feugiat leo, quis placerat sapien felis eget arcu. Donec ornare fermentum eleifend. Integer a est orci. + +Proin rhoncus egestas leo. Nulla ultricies porta elit quis ornare. Nunc fermentum interdum vehicula. In in ligula lorem. Donec nec arcu sit amet orci lobortis iaculis. Mauris at mollis erat, sit amet mollis tortor. Mauris laoreet justo ullamcorper porttitor auctor. Aenean sit amet aliquam lectus, id fermentum eros. Praesent urna sem, vehicula ac fermentum id, dapibus ut purus. Vestibulum vitae tempus nunc. Donec at nunc ornare metus volutpat porta at eget magna. Donec varius aliquet metus, eu lobortis risus aliquam sed. Ut dapibus fermentum velit, ac tincidunt libero faucibus at. + +In in purus auctor, feugiat massa quis, facilisis nisi. Donec dolor purus, gravida eget dolor ac, porttitor imperdiet urna. Donec faucibus placerat erat, a sagittis ante finibus ac. Sed venenatis dignissim elit, in iaculis felis posuere faucibus. Praesent sed viverra dolor. Mauris sed nulla consectetur nunc laoreet molestie in ut metus. Proin ac ex sit amet magna vulputate hendrerit ac condimentum urna. Proin ligula metus, gravida et sollicitudin facilisis, iaculis ut odio. Cras tincidunt urna et augue varius, ut facilisis urna consequat. Aenean vehicula finibus quam. Ut iaculis eu diam ac mollis. Nam mi lorem, tristique eget varius at, sodales at urna. + +Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin vitae dictum erat, et auctor ipsum. Nullam nunc nunc, sollicitudin quis magna a, vestibulum fermentum mauris. Praesent at erat dolor. Proin laoreet tristique nulla vel efficitur. Nam sed ultrices nibh, id rutrum nunc. Curabitur eleifend a erat sit amet sollicitudin. Nullam metus quam, laoreet vitae dapibus id, placerat sed leo. Aliquam erat volutpat. Donec turpis nisl, cursus eu ex sit amet, lacinia pellentesque nisl. Sed id ipsum massa. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec interdum scelerisque lorem eu mattis. + +Vivamus ac tristique massa, nec facilisis nisl. Nam ipsum neque, tincidunt vel urna in, cursus imperdiet enim. Nam pellentesque egestas tempus. Morbi facilisis imperdiet libero vitae fringilla. Nam lacinia ligula at sapien facilisis malesuada. Nullam accumsan pulvinar sem, et cursus libero porta sit amet. Curabitur vulputate erat elit, ut pulvinar erat maximus vel. + +Cras aliquet metus ut purus sagittis, vel venenatis ante consectetur. Pellentesque nulla lacus, viverra viverra mattis non, placerat vitae nibh. Donec enim turpis, accumsan sit amet tincidunt eu, imperdiet non metus. Morbi ipsum eros, tincidunt vel est ac, tristique porttitor nibh. Praesent ut ullamcorper mauris. Sed laoreet sit amet diam congue venenatis. Integer porta purus nec orci sagittis posuere. + +Donec vehicula mauris eget lacus mollis venenatis et sed nibh. Nam sodales ligula ipsum, scelerisque lacinia ligula sagittis in. Nam sit amet ipsum at erat malesuada congue. Aenean ut sollicitudin sapien. Etiam at tempor odio. Mauris vitae purus ut magna suscipit consequat. Vivamus quis sapien neque. Nulla vulputate sem sit amet massa pellentesque, eleifend tristique ligula egestas. Suspendisse tincidunt gravida mi, in pulvinar lectus egestas non. Aenean imperdiet ex sit amet nunc sollicitudin porta. Integer justo odio, ultricies at interdum in, rhoncus vitae sem. Sed porttitor arcu quis purus aliquet hendrerit. Praesent tempor tortor at dolor dictum pulvinar. Nulla aliquet nunc non ligula scelerisque accumsan. Donec nulla justo, congue vitae massa in, faucibus hendrerit magna. Donec non egestas purus. + +Vivamus iaculis, lacus efficitur faucibus porta, dui nulla facilisis ligula, ut sodales odio nunc id sapien. Cras viverra auctor ipsum, dapibus mattis neque dictum sed. Sed convallis fermentum molestie. Nulla facilisi turpis duis. \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/test/fixtures/small.txt b/src/vs/workbench/services/textfile/test/fixtures/small.txt new file mode 100644 index 0000000000..da2e8042fb --- /dev/null +++ b/src/vs/workbench/services/textfile/test/fixtures/small.txt @@ -0,0 +1 @@ +Small File \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/test/fixtures/small_umlaut.txt b/src/vs/workbench/services/textfile/test/fixtures/small_umlaut.txt new file mode 100644 index 0000000000..a01c1626b3 --- /dev/null +++ b/src/vs/workbench/services/textfile/test/fixtures/small_umlaut.txt @@ -0,0 +1 @@ +Small File with Ümlaut \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/test/fixtures/some_big5.txt b/src/vs/workbench/services/textfile/test/fixtures/some_big5.txt new file mode 100644 index 0000000000..b9e2570fef --- /dev/null +++ b/src/vs/workbench/services/textfile/test/fixtures/some_big5.txt @@ -0,0 +1 @@ +abc \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/test/fixtures/some_cp1252.txt b/src/vs/workbench/services/textfile/test/fixtures/some_cp1252.txt new file mode 100644 index 0000000000..2ea52dc709 --- /dev/null +++ b/src/vs/workbench/services/textfile/test/fixtures/some_cp1252.txt @@ -0,0 +1,3 @@ +ObjectCount = LoadObjects("ffentlicher Ordner"); + +Private = "Persnliche Information" diff --git a/src/vs/workbench/services/textfile/test/fixtures/some_cyrillic.txt b/src/vs/workbench/services/textfile/test/fixtures/some_cyrillic.txt new file mode 100644 index 0000000000..f8ee306671 --- /dev/null +++ b/src/vs/workbench/services/textfile/test/fixtures/some_cyrillic.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/test/fixtures/some_gbk.txt b/src/vs/workbench/services/textfile/test/fixtures/some_gbk.txt new file mode 100644 index 0000000000..eab73d1951 --- /dev/null +++ b/src/vs/workbench/services/textfile/test/fixtures/some_gbk.txt @@ -0,0 +1 @@ +йabc \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/test/fixtures/some_shiftjs.txt b/src/vs/workbench/services/textfile/test/fixtures/some_shiftjs.txt new file mode 100644 index 0000000000..efa955b3ec --- /dev/null +++ b/src/vs/workbench/services/textfile/test/fixtures/some_shiftjs.txt @@ -0,0 +1 @@ +abc \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/test/fixtures/some_utf16le.css b/src/vs/workbench/services/textfile/test/fixtures/some_utf16le.css new file mode 100644 index 0000000000..aea04aa2cd Binary files /dev/null and b/src/vs/workbench/services/textfile/test/fixtures/some_utf16le.css differ diff --git a/src/vs/workbench/services/textfile/test/fixtures/some_utf8_bom.txt b/src/vs/workbench/services/textfile/test/fixtures/some_utf8_bom.txt new file mode 100644 index 0000000000..36cdec0c88 --- /dev/null +++ b/src/vs/workbench/services/textfile/test/fixtures/some_utf8_bom.txt @@ -0,0 +1 @@ +This is some UTF 8 with BOM file. \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts index 8798b36e43..70ac33bbf6 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts @@ -44,21 +44,21 @@ suite('Files - TextFileEditorModel', () => { accessor.fileService.setContent(content); }); - test('Save', function () { + test('Save', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); - return model.load().then(() => { - model.textEditorModel!.setValue('bar'); - assert.ok(getLastModifiedTime(model) <= Date.now()); + await model.load(); - return model.save().then(() => { - assert.ok(model.getLastSaveAttemptTime() <= Date.now()); - assert.ok(!model.isDirty()); + model.textEditorModel!.setValue('bar'); + assert.ok(getLastModifiedTime(model) <= Date.now()); - model.dispose(); - assert.ok(!accessor.modelService.getModel(model.getResource())); - }); - }); + await model.save(); + + assert.ok(model.getLastSaveAttemptTime() <= Date.now()); + assert.ok(!model.isDirty()); + + model.dispose(); + assert.ok(!accessor.modelService.getModel(model.getResource())); }); test('setEncoding - encode', function () { @@ -74,29 +74,26 @@ suite('Files - TextFileEditorModel', () => { model.dispose(); }); - test('setEncoding - decode', function () { + test('setEncoding - decode', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); model.setEncoding('utf16', EncodingMode.Decode); - return timeout(0).then(() => { // due to model updating async - assert.ok(model.isResolved()); // model got loaded due to decoding - - model.dispose(); - }); + await timeout(0); + assert.ok(model.isResolved()); // model got loaded due to decoding + model.dispose(); }); - test('disposes when underlying model is destroyed', function () { + test('disposes when underlying model is destroyed', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); - return model.load().then(() => { - model.textEditorModel!.dispose(); + await model.load(); - assert.ok(model.isDisposed()); - }); + model.textEditorModel!.dispose(); + assert.ok(model.isDisposed()); }); - test('Load does not trigger save', function () { + test('Load does not trigger save', async function () { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8'); assert.ok(model.hasState(ModelState.SAVED)); @@ -104,32 +101,26 @@ suite('Files - TextFileEditorModel', () => { assert.ok(e !== StateChange.DIRTY && e !== StateChange.SAVED); }); - return model.load().then(() => { - assert.ok(model.isResolved()); - - model.dispose(); - - assert.ok(!accessor.modelService.getModel(model.getResource())); - }); + await model.load(); + assert.ok(model.isResolved()); + model.dispose(); + assert.ok(!accessor.modelService.getModel(model.getResource())); }); - test('Load returns dirty model as long as model is dirty', function () { + test('Load returns dirty model as long as model is dirty', async function () { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.ok(model.isDirty()); + assert.ok(model.hasState(ModelState.DIRTY)); - assert.ok(model.isDirty()); - assert.ok(model.hasState(ModelState.DIRTY)); - return model.load().then(() => { - assert.ok(model.isDirty()); - - model.dispose(); - }); - }); + await model.load(); + assert.ok(model.isDirty()); + model.dispose(); }); - test('Revert', function () { + test('Revert', async function () { let eventCounter = 0; const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); @@ -140,22 +131,18 @@ suite('Files - TextFileEditorModel', () => { } }); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.ok(model.isDirty()); - assert.ok(model.isDirty()); - - return model.revert().then(() => { - assert.ok(!model.isDirty()); - assert.equal(model.textEditorModel!.getValue(), 'Hello Html'); - assert.equal(eventCounter, 1); - - model.dispose(); - }); - }); + await model.revert(); + assert.ok(!model.isDirty()); + assert.equal(model.textEditorModel!.getValue(), 'Hello Html'); + assert.equal(eventCounter, 1); + model.dispose(); }); - test('Revert (soft)', function () { + test('Revert (soft)', async function () { let eventCounter = 0; const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); @@ -166,99 +153,88 @@ suite('Files - TextFileEditorModel', () => { } }); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.ok(model.isDirty()); - assert.ok(model.isDirty()); - - return model.revert(true /* soft revert */).then(() => { - assert.ok(!model.isDirty()); - assert.equal(model.textEditorModel!.getValue(), 'foo'); - assert.equal(eventCounter, 1); - - model.dispose(); - }); - }); + await model.revert(true /* soft revert */); + assert.ok(!model.isDirty()); + assert.equal(model.textEditorModel!.getValue(), 'foo'); + assert.equal(eventCounter, 1); + model.dispose(); }); - test('Load and undo turns model dirty', function () { + test('Load and undo turns model dirty', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); - return model.load().then(() => { - accessor.fileService.setContent('Hello Change'); - return model.load().then(() => { - model.textEditorModel!.undo(); + await model.load(); + accessor.fileService.setContent('Hello Change'); - assert.ok(model.isDirty()); - }); - }); + await model.load(); + model.textEditorModel!.undo(); + assert.ok(model.isDirty()); }); - test('File not modified error is handled gracefully', function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); + test('File not modified error is handled gracefully', async function () { + let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); - return model.load().then(() => { - const mtime = getLastModifiedTime(model); - accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_MODIFIED_SINCE)); + await model.load(); - return model.load().then((model: TextFileEditorModel) => { - assert.ok(model); - assert.equal(getLastModifiedTime(model), mtime); - model.dispose(); - }); - }); + const mtime = getLastModifiedTime(model); + accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_MODIFIED_SINCE)); + + model = await model.load() as TextFileEditorModel; + + assert.ok(model); + assert.equal(getLastModifiedTime(model), mtime); + model.dispose(); }); - test('Load error is handled gracefully if model already exists', function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); + test('Load error is handled gracefully if model already exists', async function () { + let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); - return model.load().then(() => { - accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_FOUND)); + await model.load(); + accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_FOUND)); - return model.load().then((model: TextFileEditorModel) => { - assert.ok(model); - model.dispose(); - }); - }); + model = await model.load() as TextFileEditorModel; + assert.ok(model); + model.dispose(); }); - test('save() and isDirty() - proper with check for mtimes', function () { + test('save() and isDirty() - proper with check for mtimes', async function () { const input1 = createFileInput(instantiationService, toResource.call(this, '/path/index_async2.txt')); const input2 = createFileInput(instantiationService, toResource.call(this, '/path/index_async.txt')); - return input1.resolve().then((model1: TextFileEditorModel) => { - return input2.resolve().then((model2: TextFileEditorModel) => { - model1.textEditorModel!.setValue('foo'); + const model1 = await input1.resolve() as TextFileEditorModel; + const model2 = await input2.resolve() as TextFileEditorModel; - const m1Mtime = model1.getStat().mtime; - const m2Mtime = model2.getStat().mtime; - assert.ok(m1Mtime > 0); - assert.ok(m2Mtime > 0); + model1.textEditorModel!.setValue('foo'); - assert.ok(accessor.textFileService.isDirty()); - assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt'))); - assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); + const m1Mtime = model1.getStat().mtime; + const m2Mtime = model2.getStat().mtime; + assert.ok(m1Mtime > 0); + assert.ok(m2Mtime > 0); - model2.textEditorModel!.setValue('foo'); - assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); + assert.ok(accessor.textFileService.isDirty()); + assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt'))); + assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); - return timeout(10).then(() => { - accessor.textFileService.saveAll().then(() => { - assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); - assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt'))); - assert.ok(model1.getStat().mtime > m1Mtime); - assert.ok(model2.getStat().mtime > m2Mtime); - assert.ok(model1.getLastSaveAttemptTime() > m1Mtime); - assert.ok(model2.getLastSaveAttemptTime() > m2Mtime); + model2.textEditorModel!.setValue('foo'); + assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); - model1.dispose(); - model2.dispose(); - }); - }); - }); - }); + await timeout(10); + await accessor.textFileService.saveAll(); + assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); + assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt'))); + assert.ok(model1.getStat().mtime > m1Mtime); + assert.ok(model2.getStat().mtime > m2Mtime); + assert.ok(model1.getLastSaveAttemptTime() > m1Mtime); + assert.ok(model2.getLastSaveAttemptTime() > m2Mtime); + + model1.dispose(); + model2.dispose(); }); - test('Save Participant', function () { + test('Save Participant', async function () { let eventCounter = 0; const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); @@ -280,18 +256,15 @@ suite('Files - TextFileEditorModel', () => { } }); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); - return model.save().then(() => { - model.dispose(); - - assert.equal(eventCounter, 2); - }); - }); + await model.save(); + model.dispose(); + assert.equal(eventCounter, 2); }); - test('Save Participant, async participant', function () { + test('Save Participant, async participant', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); @@ -301,18 +274,16 @@ suite('Files - TextFileEditorModel', () => { } }); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); - const now = Date.now(); - return model.save().then(() => { - assert.ok(Date.now() - now >= 10); - model.dispose(); - }); - }); + const now = Date.now(); + await model.save(); + assert.ok(Date.now() - now >= 10); + model.dispose(); }); - test('Save Participant, bad participant', function () { + test('Save Participant, bad participant', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8'); TextFileEditorModel.setSaveParticipant({ @@ -321,15 +292,14 @@ suite('Files - TextFileEditorModel', () => { } }); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); - return model.save().then(() => { - model.dispose(); - }); - }); + await model.load(); + model.textEditorModel!.setValue('foo'); + + await model.save(); + model.dispose(); }); - test('SaveSequentializer - pending basics', function () { + test('SaveSequentializer - pending basics', async function () { const sequentializer = new SaveSequentializer(); assert.ok(!sequentializer.hasPendingSave()); @@ -337,27 +307,25 @@ suite('Files - TextFileEditorModel', () => { assert.ok(!sequentializer.pendingSave); // pending removes itself after done - return sequentializer.setPending(1, Promise.resolve()).then(() => { - assert.ok(!sequentializer.hasPendingSave()); - assert.ok(!sequentializer.hasPendingSave(1)); - assert.ok(!sequentializer.pendingSave); + await sequentializer.setPending(1, Promise.resolve()); + assert.ok(!sequentializer.hasPendingSave()); + assert.ok(!sequentializer.hasPendingSave(1)); + assert.ok(!sequentializer.pendingSave); - // pending removes itself after done (use timeout) - sequentializer.setPending(2, timeout(1)); - assert.ok(sequentializer.hasPendingSave()); - assert.ok(sequentializer.hasPendingSave(2)); - assert.ok(!sequentializer.hasPendingSave(1)); - assert.ok(sequentializer.pendingSave); + // pending removes itself after done (use timeout) + sequentializer.setPending(2, timeout(1)); + assert.ok(sequentializer.hasPendingSave()); + assert.ok(sequentializer.hasPendingSave(2)); + assert.ok(!sequentializer.hasPendingSave(1)); + assert.ok(sequentializer.pendingSave); - return timeout(2).then(() => { - assert.ok(!sequentializer.hasPendingSave()); - assert.ok(!sequentializer.hasPendingSave(2)); - assert.ok(!sequentializer.pendingSave); - }); - }); + await timeout(2); + assert.ok(!sequentializer.hasPendingSave()); + assert.ok(!sequentializer.hasPendingSave(2)); + assert.ok(!sequentializer.pendingSave); }); - test('SaveSequentializer - pending and next (finishes instantly)', function () { + test('SaveSequentializer - pending and next (finishes instantly)', async function () { const sequentializer = new SaveSequentializer(); let pendingDone = false; @@ -367,13 +335,12 @@ suite('Files - TextFileEditorModel', () => { let nextDone = false; const res = sequentializer.setNext(() => Promise.resolve(null).then(() => { nextDone = true; return; })); - return res.then(() => { - assert.ok(pendingDone); - assert.ok(nextDone); - }); + await res; + assert.ok(pendingDone); + assert.ok(nextDone); }); - test('SaveSequentializer - pending and next (finishes after timeout)', function () { + test('SaveSequentializer - pending and next (finishes after timeout)', async function () { const sequentializer = new SaveSequentializer(); let pendingDone = false; @@ -383,13 +350,12 @@ suite('Files - TextFileEditorModel', () => { let nextDone = false; const res = sequentializer.setNext(() => timeout(1).then(() => { nextDone = true; return; })); - return res.then(() => { - assert.ok(pendingDone); - assert.ok(nextDone); - }); + await res; + assert.ok(pendingDone); + assert.ok(nextDone); }); - test('SaveSequentializer - pending and multiple next (last one wins)', function () { + test('SaveSequentializer - pending and multiple next (last one wins)', async function () { const sequentializer = new SaveSequentializer(); let pendingDone = false; @@ -405,11 +371,10 @@ suite('Files - TextFileEditorModel', () => { let thirdDone = false; let thirdRes = sequentializer.setNext(() => timeout(4).then(() => { thirdDone = true; return; })); - return Promise.all([firstRes, secondRes, thirdRes]).then(() => { - assert.ok(pendingDone); - assert.ok(!firstDone); - assert.ok(!secondDone); - assert.ok(thirdDone); - }); + await Promise.all([firstRes, secondRes, thirdRes]); + assert.ok(pendingDone); + assert.ok(!firstDone); + assert.ok(!secondDone); + assert.ok(thirdDone); }); }); diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts index a8fa8ec7f0..c8081048b0 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts @@ -94,29 +94,24 @@ suite('Files - TextFileEditorModelManager', () => { model3.dispose(); }); - test('loadOrCreate', () => { + test('loadOrCreate', async () => { const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); const resource = URI.file('/test.html'); const encoding = 'utf8'; - return manager.loadOrCreate(resource, { encoding }).then(model => { - assert.ok(model); - assert.equal(model.getEncoding(), encoding); - assert.equal(manager.get(resource), model); + const model = await manager.loadOrCreate(resource, { encoding }); + assert.ok(model); + assert.equal(model.getEncoding(), encoding); + assert.equal(manager.get(resource), model); - return manager.loadOrCreate(resource, { encoding }).then(model2 => { - assert.equal(model2, model); + const model2 = await manager.loadOrCreate(resource, { encoding }); + assert.equal(model2, model); + model.dispose(); - model.dispose(); - - return manager.loadOrCreate(resource, { encoding }).then(model3 => { - assert.notEqual(model3, model2); - assert.equal(manager.get(resource), model3); - - model3.dispose(); - }); - }); - }); + const model3 = await manager.loadOrCreate(resource, { encoding }); + assert.notEqual(model3, model2); + assert.equal(manager.get(resource), model3); + model3.dispose(); }); test('removed from cache when model disposed', function () { @@ -139,7 +134,7 @@ suite('Files - TextFileEditorModelManager', () => { model3.dispose(); }); - test('events', function () { + test('events', async function () { TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = 0; TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 0; @@ -189,46 +184,37 @@ suite('Files - TextFileEditorModelManager', () => { disposeCounter++; }); - return manager.loadOrCreate(resource1, { encoding: 'utf8' }).then(model1 => { - accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }])); - accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }])); + const model1 = await manager.loadOrCreate(resource1, { encoding: 'utf8' }); + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }])); + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }])); - return manager.loadOrCreate(resource2, { encoding: 'utf8' }).then(model2 => { - model1.textEditorModel!.setValue('changed'); - model1.updatePreferredEncoding('utf16'); + const model2 = await manager.loadOrCreate(resource2, { encoding: 'utf8' }); + model1.textEditorModel!.setValue('changed'); + model1.updatePreferredEncoding('utf16'); - return model1.revert().then(() => { - model1.textEditorModel!.setValue('changed again'); + await model1.revert(); + model1.textEditorModel!.setValue('changed again'); - return model1.save().then(() => { - model1.dispose(); - model2.dispose(); - assert.equal(disposeCounter, 2); + await model1.save(); + model1.dispose(); + model2.dispose(); + assert.equal(disposeCounter, 2); - return model1.revert().then(() => { // should not trigger another event if disposed - assert.equal(dirtyCounter, 2); - assert.equal(revertedCounter, 1); - assert.equal(savedCounter, 1); - assert.equal(encodingCounter, 2); + await model1.revert(); + assert.equal(dirtyCounter, 2); + assert.equal(revertedCounter, 1); + assert.equal(savedCounter, 1); + assert.equal(encodingCounter, 2); - // content change event if done async - return timeout(10).then(() => { - assert.equal(contentCounter, 2); - - model1.dispose(); - model2.dispose(); - - assert.ok(!accessor.modelService.getModel(resource1)); - assert.ok(!accessor.modelService.getModel(resource2)); - }); - }); - }); - }); - }); - }); + await timeout(10); + assert.equal(contentCounter, 2); + model1.dispose(); + model2.dispose(); + assert.ok(!accessor.modelService.getModel(resource1)); + assert.ok(!accessor.modelService.getModel(resource2)); }); - test('events debounced', function () { + test('events debounced', async function () { const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); const resource1 = toResource.call(this, '/path/index.txt'); @@ -255,69 +241,53 @@ suite('Files - TextFileEditorModelManager', () => { assert.equal(e[0].resource.toString(), resource1.toString()); }); - return manager.loadOrCreate(resource1, { encoding: 'utf8' }).then(model1 => { - return manager.loadOrCreate(resource2, { encoding: 'utf8' }).then(model2 => { - model1.textEditorModel!.setValue('changed'); - model1.updatePreferredEncoding('utf16'); + const model1 = await manager.loadOrCreate(resource1, { encoding: 'utf8' }); + const model2 = await manager.loadOrCreate(resource2, { encoding: 'utf8' }); + model1.textEditorModel!.setValue('changed'); + model1.updatePreferredEncoding('utf16'); - return model1.revert().then(() => { - model1.textEditorModel!.setValue('changed again'); + await model1.revert(); + model1.textEditorModel!.setValue('changed again'); - return model1.save().then(() => { - model1.dispose(); - model2.dispose(); + await model1.save(); + model1.dispose(); + model2.dispose(); - return model1.revert().then(() => { // should not trigger another event if disposed - return timeout(20).then(() => { - assert.equal(dirtyCounter, 2); - assert.equal(revertedCounter, 1); - assert.equal(savedCounter, 1); - - model1.dispose(); - model2.dispose(); - - assert.ok(!accessor.modelService.getModel(resource1)); - assert.ok(!accessor.modelService.getModel(resource2)); - }); - }); - }); - }); - }); - }); + await model1.revert(); + await timeout(20); + assert.equal(dirtyCounter, 2); + assert.equal(revertedCounter, 1); + assert.equal(savedCounter, 1); + model1.dispose(); + model2.dispose(); + assert.ok(!accessor.modelService.getModel(resource1)); + assert.ok(!accessor.modelService.getModel(resource2)); }); - test('disposing model takes it out of the manager', function () { + test('disposing model takes it out of the manager', async function () { const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); const resource = toResource.call(this, '/path/index_something.txt'); - return manager.loadOrCreate(resource, { encoding: 'utf8' }).then(model => { - model.dispose(); - - assert.ok(!manager.get(resource)); - assert.ok(!accessor.modelService.getModel(model.getResource())); - - manager.dispose(); - }); + const model = await manager.loadOrCreate(resource, { encoding: 'utf8' }); + model.dispose(); + assert.ok(!manager.get(resource)); + assert.ok(!accessor.modelService.getModel(model.getResource())); + manager.dispose(); }); - test('dispose prevents dirty model from getting disposed', function () { + test('dispose prevents dirty model from getting disposed', async function () { const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); const resource = toResource.call(this, '/path/index_something.txt'); - return manager.loadOrCreate(resource, { encoding: 'utf8' }).then(model => { - model.textEditorModel!.setValue('make dirty'); - - manager.disposeModel(model as TextFileEditorModel); - assert.ok(!model.isDisposed()); - - model.revert(true); - - manager.disposeModel(model as TextFileEditorModel); - assert.ok(model.isDisposed()); - - manager.dispose(); - }); + const model = await manager.loadOrCreate(resource, { encoding: 'utf8' }); + model.textEditorModel!.setValue('make dirty'); + manager.disposeModel((model as TextFileEditorModel)); + assert.ok(!model.isDisposed()); + model.revert(true); + manager.disposeModel((model as TextFileEditorModel)); + assert.ok(model.isDisposed()); + manager.dispose(); }); }); \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/test/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/textFileService.io.test.ts new file mode 100644 index 0000000000..65b968a90c --- /dev/null +++ b/src/vs/workbench/services/textfile/test/textFileService.io.test.ts @@ -0,0 +1,415 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestWindowsService, TestContextService, TestFileService, TestEnvironmentService, TestTextResourceConfigurationService } from 'vs/workbench/test/workbenchTestServices'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; +import { IFileService, ITextSnapshot, snapshotToString } from 'vs/platform/files/common/files'; +import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { Schemas } from 'vs/base/common/network'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { rimraf, RimRafMode, copy, readFile, exists } from 'vs/base/node/pfs'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { tmpdir } from 'os'; +import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider'; +import { generateUuid } from 'vs/base/common/uuid'; +import { join } from 'vs/base/common/path'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { detectEncodingByBOM, UTF16be, UTF16le, UTF8_with_bom, UTF8 } from 'vs/base/node/encoding'; +import { NodeTextFileService, EncodingOracle, IEncodingOverride } from 'vs/workbench/services/textfile/node/textFileService'; +import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; +import { DefaultEndOfLine } from 'vs/editor/common/model'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { isWindows } from 'vs/base/common/platform'; + +class ServiceAccessor { + constructor( + @ILifecycleService public lifecycleService: TestLifecycleService, + @ITextFileService public textFileService: TestTextFileService, + @IUntitledEditorService public untitledEditorService: IUntitledEditorService, + @IWindowsService public windowsService: TestWindowsService, + @IWorkspaceContextService public contextService: TestContextService, + @IModelService public modelService: ModelServiceImpl, + @IFileService public fileService: TestFileService + ) { + } +} + +class TestNodeTextFileService extends NodeTextFileService { + + private _testEncoding: TestEncodingOracle; + protected get encoding(): TestEncodingOracle { + if (!this._testEncoding) { + this._testEncoding = this._register(this.instantiationService.createInstance(TestEncodingOracle)); + } + + return this._testEncoding; + } +} + +class TestEncodingOracle extends EncodingOracle { + + protected get encodingOverrides(): IEncodingOverride[] { + return [ + { extension: 'utf16le', encoding: UTF16le }, + { extension: 'utf16be', encoding: UTF16be }, + { extension: 'utf8bom', encoding: UTF8_with_bom } + ]; + } + + protected set encodingOverrides(overrides: IEncodingOverride[]) { } +} + +suite('Files - TextFileService i/o', () => { + const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'textfileservice'); + + let accessor: ServiceAccessor; + let disposables: IDisposable[] = []; + let service: ITextFileService; + let testDir: string; + + setup(async () => { + const instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(ServiceAccessor); + + const logService = new NullLogService(); + const fileService = new FileService2(logService); + + const fileProvider = new DiskFileSystemProvider(logService); + disposables.push(fileService.registerProvider(Schemas.file, fileProvider)); + disposables.push(fileProvider); + + fileService.setLegacyService(new LegacyFileService( + fileService, + accessor.contextService, + TestEnvironmentService, + new TestTextResourceConfigurationService() + )); + + const collection = new ServiceCollection(); + collection.set(IFileService, fileService); + + service = instantiationService.createChild(collection).createInstance(TestNodeTextFileService); + + const id = generateUuid(); + testDir = join(parentDir, id); + const sourceDir = getPathFromAmdModule(require, './fixtures'); + + await copy(sourceDir, testDir); + }); + + teardown(async () => { + (accessor.textFileService.models).clear(); + (accessor.textFileService.models).dispose(); + accessor.untitledEditorService.revertAll(); + + disposables = dispose(disposables); + + await rimraf(parentDir, RimRafMode.MOVE); + }); + + test('create - no encoding - content empty', async () => { + const resource = URI.file(join(testDir, 'small_new.txt')); + + await service.create(resource); + + assert.equal(await exists(resource.fsPath), true); + }); + + test('create - no encoding - content provided', async () => { + const resource = URI.file(join(testDir, 'small_new.txt')); + + await service.create(resource, 'Hello World'); + + assert.equal(await exists(resource.fsPath), true); + assert.equal((await readFile(resource.fsPath)).toString(), 'Hello World'); + }); + + test('create - UTF 16 LE - no content', async () => { + const resource = URI.file(join(testDir, 'small_new.utf16le')); + + await service.create(resource); + + assert.equal(await exists(resource.fsPath), true); + + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF16le); + }); + + test('create - UTF 16 LE - content provided', async () => { + const resource = URI.file(join(testDir, 'small_new.utf16le')); + + await service.create(resource, 'Hello World'); + + assert.equal(await exists(resource.fsPath), true); + + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF16le); + }); + + test('create - UTF 16 BE - no content', async () => { + const resource = URI.file(join(testDir, 'small_new.utf16be')); + + await service.create(resource); + + assert.equal(await exists(resource.fsPath), true); + + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF16be); + }); + + test('create - UTF 16 BE - content provided', async () => { + const resource = URI.file(join(testDir, 'small_new.utf16be')); + + await service.create(resource, 'Hello World'); + + assert.equal(await exists(resource.fsPath), true); + + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF16be); + }); + + test('create - UTF 8 BOM - no content', async () => { + const resource = URI.file(join(testDir, 'small_new.utf8bom')); + + await service.create(resource); + + assert.equal(await exists(resource.fsPath), true); + + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + }); + + test('create - UTF 8 BOM - content provided', async () => { + const resource = URI.file(join(testDir, 'small_new.utf8bom')); + + await service.create(resource, 'Hello World'); + + assert.equal(await exists(resource.fsPath), true); + + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + }); + + test('create - UTF 8 BOM - empty content - snapshot', async () => { + const resource = URI.file(join(testDir, 'small_new.utf8bom')); + + await service.create(resource, TextModel.createFromString('').createSnapshot()); + + assert.equal(await exists(resource.fsPath), true); + + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + }); + + test('create - UTF 8 BOM - content provided - snapshot', async () => { + const resource = URI.file(join(testDir, 'small_new.utf8bom')); + + await service.create(resource, TextModel.createFromString('Hello World').createSnapshot()); + + assert.equal(await exists(resource.fsPath), true); + + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + }); + + test('write - use encoding (UTF 16 BE) - small content as string', async () => { + await testEncoding(URI.file(join(testDir, 'small.txt')), UTF16be, 'Hello\nWorld', 'Hello\nWorld'); + }); + + test('write - use encoding (UTF 16 BE) - small content as snapshot', async () => { + await testEncoding(URI.file(join(testDir, 'small.txt')), UTF16be, TextModel.createFromString('Hello\nWorld').createSnapshot(), 'Hello\nWorld'); + }); + + test('write - use encoding (UTF 16 BE) - large content as string', async () => { + await testEncoding(URI.file(join(testDir, 'lorem.txt')), UTF16be, 'Hello\nWorld', 'Hello\nWorld'); + }); + + test('write - use encoding (UTF 16 BE) - large content as snapshot', async () => { + await testEncoding(URI.file(join(testDir, 'lorem.txt')), UTF16be, TextModel.createFromString('Hello\nWorld').createSnapshot(), 'Hello\nWorld'); + }); + + async function testEncoding(resource: URI, encoding: string, content: string | ITextSnapshot, expectedContent: string) { + await service.write(resource, content, { encoding }); + + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, encoding); + + const resolved = await service.resolve(resource); + assert.equal(resolved.encoding, encoding); + + assert.equal(snapshotToString(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(false)), expectedContent); + } + + test('write - use encoding (cp1252)', async () => { + await testEncodingKeepsData(URI.file(join(testDir, 'some_cp1252.txt')), 'cp1252', ['ObjectCount = LoadObjects("Öffentlicher Ordner");', '', 'Private = "Persönliche Information"', ''].join(isWindows ? '\r\n' : '\n')); + }); + + test('write - use encoding (shiftjis)', async () => { + await testEncodingKeepsData(URI.file(join(testDir, 'some_shiftjs.txt')), 'shiftjis', '中文abc'); + }); + + test('write - use encoding (gbk)', async () => { + await testEncodingKeepsData(URI.file(join(testDir, 'some_gbk.txt')), 'gbk', '中国abc'); + }); + + test('write - use encoding (cyrillic)', async () => { + await testEncodingKeepsData(URI.file(join(testDir, 'some_cyrillic.txt')), 'cp866', 'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя'); + }); + + test('write - use encoding (big5)', async () => { + await testEncodingKeepsData(URI.file(join(testDir, 'some_big5.txt')), 'cp950', '中文abc'); + }); + + async function testEncodingKeepsData(resource: URI, encoding: string, expected: string) { + let resolved = await service.resolve(resource, { encoding }); + const content = snapshotToString(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(false)); + assert.equal(content, expected); + + await service.write(resource, content, { encoding }); + + resolved = await service.resolve(resource, { encoding }); + assert.equal(snapshotToString(resolved.value.create(DefaultEndOfLine.CRLF).createSnapshot(false)), content); + + await service.write(resource, TextModel.createFromString(content).createSnapshot(), { encoding }); + + resolved = await service.resolve(resource, { encoding }); + assert.equal(snapshotToString(resolved.value.create(DefaultEndOfLine.CRLF).createSnapshot(false)), content); + } + + test('write - no encoding - content as string', async () => { + const resource = URI.file(join(testDir, 'small.txt')); + + const content = (await readFile(resource.fsPath)).toString(); + + await service.write(resource, content); + + const resolved = await service.resolve(resource); + assert.equal(snapshotToString(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(false)), content); + }); + + test('write - no encoding - content as snapshot', async () => { + const resource = URI.file(join(testDir, 'small.txt')); + + const content = (await readFile(resource.fsPath)).toString(); + + await service.write(resource, TextModel.createFromString(content).createSnapshot()); + + const resolved = await service.resolve(resource); + assert.equal(snapshotToString(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(false)), content); + }); + + test('write - encoding preserved (UTF 16 LE) - content as string', async () => { + const resource = URI.file(join(testDir, 'some_utf16le.css')); + + const resolved = await service.resolve(resource); + assert.equal(resolved.encoding, UTF16le); + + await testEncoding(URI.file(join(testDir, 'some_utf16le.css')), UTF16le, 'Hello\nWorld', 'Hello\nWorld'); + }); + + test('write - encoding preserved (UTF 16 LE) - content as snapshot', async () => { + const resource = URI.file(join(testDir, 'some_utf16le.css')); + + const resolved = await service.resolve(resource); + assert.equal(resolved.encoding, UTF16le); + + await testEncoding(URI.file(join(testDir, 'some_utf16le.css')), UTF16le, TextModel.createFromString('Hello\nWorld').createSnapshot(), 'Hello\nWorld'); + }); + + test('write - UTF8 variations - content as string', async () => { + const resource = URI.file(join(testDir, 'index.html')); + + let detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, null); + + const content = (await readFile(resource.fsPath)).toString() + 'updates'; + await service.write(resource, content, { encoding: UTF8_with_bom }); + + detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + + // ensure BOM preserved + await service.write(resource, content, { encoding: UTF8 }); + detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + + // allow to remove BOM + await service.write(resource, content, { encoding: UTF8, overwriteEncoding: true }); + detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, null); + + // BOM does not come back + await service.write(resource, content, { encoding: UTF8 }); + detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, null); + }); + + test('write - UTF8 variations - content as snapshot', async () => { + const resource = URI.file(join(testDir, 'index.html')); + + let detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, null); + + const model = TextModel.createFromString((await readFile(resource.fsPath)).toString() + 'updates'); + await service.write(resource, model.createSnapshot(), { encoding: UTF8_with_bom }); + + detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + + // ensure BOM preserved + await service.write(resource, model.createSnapshot(), { encoding: UTF8 }); + detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + + // allow to remove BOM + await service.write(resource, model.createSnapshot(), { encoding: UTF8, overwriteEncoding: true }); + detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, null); + + // BOM does not come back + await service.write(resource, model.createSnapshot(), { encoding: UTF8 }); + detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, null); + }); + + test('write - preserve UTF8 BOM - content as string', async () => { + const resource = URI.file(join(testDir, 'some_utf8_bom.txt')); + + let detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + + await service.write(resource, 'Hello World'); + detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + }); + + test('write - ensure BOM in empty file - content as string', async () => { + const resource = URI.file(join(testDir, 'small.txt')); + + await service.write(resource, '', { encoding: UTF8_with_bom }); + + let detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + }); + + test('write - ensure BOM in empty file - content as snapshot', async () => { + const resource = URI.file(join(testDir, 'small.txt')); + + await service.write(resource, TextModel.createFromString('').createSnapshot(), { encoding: UTF8_with_bom }); + + let detectedEncoding = await detectEncodingByBOM(resource.fsPath); + assert.equal(detectedEncoding, UTF8); + }); +}); diff --git a/src/vs/workbench/services/textfile/test/textFileService.test.ts b/src/vs/workbench/services/textfile/test/textFileService.test.ts index 5f429e039a..b4bdb0c4b1 100644 --- a/src/vs/workbench/services/textfile/test/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileService.test.ts @@ -15,7 +15,6 @@ import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textF import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ConfirmResult } from 'vs/workbench/common/editor'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; import { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; @@ -83,26 +82,23 @@ suite('Files - TextFileService', () => { } }); - test('confirm onWillShutdown - veto if user cancels', function () { + test('confirm onWillShutdown - veto if user cancels', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); const service = accessor.textFileService; service.setConfirmResult(ConfirmResult.CANCEL); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.equal(service.getDirty().length, 1); - assert.equal(service.getDirty().length, 1); - - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - - assert.ok(event.value); - }); + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); + assert.ok(event.value); }); - test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', function () { + test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); @@ -110,30 +106,25 @@ suite('Files - TextFileService', () => { service.setConfirmResult(ConfirmResult.DONT_SAVE); service.onFilesConfigurationChange({ files: { hotExit: 'off' } }); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.equal(service.getDirty().length, 1); + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); - assert.equal(service.getDirty().length, 1); + let veto = event.value; + if (typeof veto === 'boolean') { + assert.ok(service.cleanupBackupsBeforeShutdownCalled); + assert.ok(!veto); + return; + } - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - - const veto = event.value; - if (typeof veto === 'boolean') { - assert.ok(service.cleanupBackupsBeforeShutdownCalled); - assert.ok(!veto); - - return undefined; - } else { - return veto.then(veto => { - assert.ok(service.cleanupBackupsBeforeShutdownCalled); - assert.ok(!veto); - }); - } - }); + veto = await veto; + assert.ok(service.cleanupBackupsBeforeShutdownCalled); + assert.ok(!veto); }); - test('confirm onWillShutdown - save (hot.exit: off)', function () { + test('confirm onWillShutdown - save (hot.exit: off)', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); @@ -141,66 +132,60 @@ suite('Files - TextFileService', () => { service.setConfirmResult(ConfirmResult.SAVE); service.onFilesConfigurationChange({ files: { hotExit: 'off' } }); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.equal(service.getDirty().length, 1); + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); - assert.equal(service.getDirty().length, 1); - - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - - return (>event.value).then(veto => { - assert.ok(!veto); - assert.ok(!model.isDirty()); - }); - }); + const veto = await (>event.value); + assert.ok(!veto); + assert.ok(!model.isDirty()); }); - test('isDirty/getDirty - files and untitled', function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); - (accessor.textFileService.models).add(model.getResource(), model); - - const service = accessor.textFileService; - return model.load().then(() => { - assert.ok(!service.isDirty(model.getResource())); - model.textEditorModel!.setValue('foo'); - - assert.ok(service.isDirty(model.getResource())); - assert.equal(service.getDirty().length, 1); - assert.equal(service.getDirty([model.getResource()])[0].toString(), model.getResource().toString()); - - const untitled = accessor.untitledEditorService.createOrGet(); - return untitled.resolve().then((model: UntitledEditorModel) => { - assert.ok(!service.isDirty(untitled.getResource())); - assert.equal(service.getDirty().length, 1); - model.textEditorModel!.setValue('changed'); - - assert.ok(service.isDirty(untitled.getResource())); - assert.equal(service.getDirty().length, 2); - assert.equal(service.getDirty([untitled.getResource()])[0].toString(), untitled.getResource().toString()); - }); - }); - }); - - test('save - file', function () { + test('isDirty/getDirty - files and untitled', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); const service = accessor.textFileService; - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); - assert.ok(service.isDirty(model.getResource())); + assert.ok(!service.isDirty(model.getResource())); + model.textEditorModel!.setValue('foo'); - return service.save(model.getResource()).then(res => { - assert.ok(res); - assert.ok(!service.isDirty(model.getResource())); - }); - }); + assert.ok(service.isDirty(model.getResource())); + assert.equal(service.getDirty().length, 1); + assert.equal(service.getDirty([model.getResource()])[0].toString(), model.getResource().toString()); + + const untitled = accessor.untitledEditorService.createOrGet(); + const untitledModel = await untitled.resolve(); + + assert.ok(!service.isDirty(untitled.getResource())); + assert.equal(service.getDirty().length, 1); + untitledModel.textEditorModel!.setValue('changed'); + + assert.ok(service.isDirty(untitled.getResource())); + assert.equal(service.getDirty().length, 2); + assert.equal(service.getDirty([untitled.getResource()])[0].toString(), untitled.getResource().toString()); }); - test('save - UNC path', function () { + test('save - file', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); + (accessor.textFileService.models).add(model.getResource(), model); + + const service = accessor.textFileService; + + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.ok(service.isDirty(model.getResource())); + + const res = await service.save(model.getResource()); + assert.ok(res); + assert.ok(!service.isDirty(model.getResource())); + }); + + test('save - UNC path', async function () { const untitledUncUri = URI.from({ scheme: 'untitled', authority: 'server', path: '/share/path/file.txt' }); model = instantiationService.createInstance(TextFileEditorModel, untitledUncUri, 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); @@ -213,97 +198,82 @@ suite('Files - TextFileService', () => { sinon.stub(accessor.untitledEditorService, 'hasAssociatedFilePath', () => true); sinon.stub(accessor.modelService, 'updateModel', () => { }); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); - return accessor.textFileService.saveAll(true).then(res => { - assert.ok(loadOrCreateStub.calledOnce); - assert.equal(res.results.length, 1); - assert.ok(res.results[0].success); - - assert.equal(res.results[0].target!.scheme, Schemas.file); - assert.equal(res.results[0].target!.authority, untitledUncUri.authority); - assert.equal(res.results[0].target!.path, untitledUncUri.path); - }); - }); + const res = await accessor.textFileService.saveAll(true); + assert.ok(loadOrCreateStub.calledOnce); + assert.equal(res.results.length, 1); + assert.ok(res.results[0].success); + assert.equal(res.results[0].target!.scheme, Schemas.file); + assert.equal(res.results[0].target!.authority, untitledUncUri.authority); + assert.equal(res.results[0].target!.path, untitledUncUri.path); }); - test('saveAll - file', function () { + test('saveAll - file', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); const service = accessor.textFileService; - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.ok(service.isDirty(model.getResource())); - assert.ok(service.isDirty(model.getResource())); - - return service.saveAll([model.getResource()]).then(res => { - assert.ok(res); - assert.ok(!service.isDirty(model.getResource())); - assert.equal(res.results.length, 1); - assert.equal(res.results[0].source.toString(), model.getResource().toString()); - }); - }); + const res = await service.saveAll([model.getResource()]); + assert.ok(res); + assert.ok(!service.isDirty(model.getResource())); + assert.equal(res.results.length, 1); + assert.equal(res.results[0].source.toString(), model.getResource().toString()); }); - test('saveAs - file', function () { + test('saveAs - file', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); const service = accessor.textFileService; service.setPromptPath(model.getResource()); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.ok(service.isDirty(model.getResource())); - assert.ok(service.isDirty(model.getResource())); - - return service.saveAs(model.getResource()).then(res => { - assert.equal(res!.toString(), model.getResource().toString()); - assert.ok(!service.isDirty(model.getResource())); - }); - }); + const res = await service.saveAs(model.getResource()); + assert.equal(res!.toString(), model.getResource().toString()); + assert.ok(!service.isDirty(model.getResource())); }); - test('revert - file', function () { + test('revert - file', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); const service = accessor.textFileService; service.setPromptPath(model.getResource()); - return model.load().then(() => { - model!.textEditorModel!.setValue('foo'); + await model.load(); + model!.textEditorModel!.setValue('foo'); + assert.ok(service.isDirty(model.getResource())); - assert.ok(service.isDirty(model.getResource())); - - return service.revert(model.getResource()).then(res => { - assert.ok(res); - assert.ok(!service.isDirty(model.getResource())); - }); - }); + const res = await service.revert(model.getResource()); + assert.ok(res); + assert.ok(!service.isDirty(model.getResource())); }); - test('delete - dirty file', function () { + test('delete - dirty file', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); const service = accessor.textFileService; - return model.load().then(() => { - model!.textEditorModel!.setValue('foo'); + await model.load(); + model!.textEditorModel!.setValue('foo'); + assert.ok(service.isDirty(model.getResource())); - assert.ok(service.isDirty(model.getResource())); - - return service.delete(model.getResource()).then(() => { - assert.ok(!service.isDirty(model.getResource())); - }); - }); + await service.delete(model.getResource()); + assert.ok(!service.isDirty(model.getResource())); }); - test('move - dirty file', function () { + test('move - dirty file', async function () { let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target.txt'), 'utf8'); (accessor.textFileService.models).add(sourceModel.getResource(), sourceModel); @@ -311,18 +281,14 @@ suite('Files - TextFileService', () => { const service = accessor.textFileService; - return sourceModel.load().then(() => { - sourceModel.textEditorModel!.setValue('foo'); + await sourceModel.load(); + sourceModel.textEditorModel!.setValue('foo'); + assert.ok(service.isDirty(sourceModel.getResource())); - assert.ok(service.isDirty(sourceModel.getResource())); - - return service.move(sourceModel.getResource(), targetModel.getResource(), true).then(() => { - assert.ok(!service.isDirty(sourceModel.getResource())); - - sourceModel.dispose(); - targetModel.dispose(); - }); - }); + await service.move(sourceModel.getResource(), targetModel.getResource(), true); + assert.ok(!service.isDirty(sourceModel.getResource())); + sourceModel.dispose(); + targetModel.dispose(); }); // {{SQL CARBON EDIT}} @@ -430,7 +396,7 @@ suite('Files - TextFileService', () => { }); }); - function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: true, shouldVeto: boolean): Promise { + async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: true, shouldVeto: boolean): Promise { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); @@ -448,21 +414,16 @@ suite('Files - TextFileService', () => { // Set cancel to force a veto if hot exit does not trigger service.setConfirmResult(ConfirmResult.CANCEL); - return model.load().then(() => { - model.textEditorModel!.setValue('foo'); + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.equal(service.getDirty().length, 1); + const event = new BeforeShutdownEventImpl(); + event.reason = shutdownReason; + accessor.lifecycleService.fireWillShutdown(event); - assert.equal(service.getDirty().length, 1); - - const event = new BeforeShutdownEventImpl(); - event.reason = shutdownReason; - accessor.lifecycleService.fireWillShutdown(event); - - return (>event.value).then(veto => { - // When hot exit is set, backups should never be cleaned since the confirm result is cancel - assert.ok(!service.cleanupBackupsBeforeShutdownCalled); - assert.equal(veto, shouldVeto); - }); - }); + const veto = await (>event.value); + assert.ok(!service.cleanupBackupsBeforeShutdownCalled); // When hot exit is set, backups should never be cleaned since the confirm result is cancel + assert.equal(veto, shouldVeto); } }); // {{SQL CARBON EDIT}} diff --git a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts index f532fb714a..5ed56444d2 100644 --- a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts @@ -164,6 +164,9 @@ class TestTelemetryService implements ITelemetryService { public events: any[] = []; + public setEnabled(value: boolean): void { + } + public publicLog(eventName: string, data?: any): Promise { this.events.push({ name: eventName, data: data }); return Promise.resolve(undefined); diff --git a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts index 9c2f0f6318..be27e969ae 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -155,6 +155,9 @@ class TestTelemetryService implements ITelemetryService { return this.emitter.event; } + public setEnabled(value: boolean): void { + } + public publicLog(eventName: string, data?: any): Promise { const event = { name: eventName, data: data }; this.events.push(event); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 4f4c0bb46a..b6ee9ade98 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -26,7 +26,7 @@ import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchS import { ILifecycleService, BeforeShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; -import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, IContent, IWriteTextFileOptions, IStreamContent, ICreateFileOptions, ITextSnapshot, IResourceEncodings, IResourceEncoding, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions } from 'vs/platform/files/common/files'; +import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, IContent, IStreamContent, ICreateFileOptions, ITextSnapshot, IResourceEncodings, IResourceEncoding, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; @@ -82,7 +82,7 @@ import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService'; -import { VSBuffer } from 'vs/base/common/buffer'; +import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined); @@ -984,7 +984,7 @@ export class TestFileService implements IFileService { }); } - updateContent(resource: URI, _value: string | ITextSnapshot, _options?: IWriteTextFileOptions): Promise { + writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise { return timeout(0).then(() => ({ resource, etag: 'index.txt', @@ -996,10 +996,6 @@ export class TestFileService implements IFileService { })); } - writeFile(resource: URI, bufferOrReadable: VSBuffer, options?: IWriteFileOptions): Promise { - return this.updateContent(resource, bufferOrReadable.toString(), options); - } - move(_source: URI, _target: URI, _overwrite?: boolean): Promise { return Promise.resolve(null!); } @@ -1008,11 +1004,7 @@ export class TestFileService implements IFileService { throw new Error('not implemented'); } - createFile(_resource: URI, _content?: string, _options?: ICreateFileOptions): Promise { - throw new Error('not implemented'); - } - - createFile2(_resource: URI, _content?: VSBuffer, _options?: ICreateFileOptions): Promise { + createFile(_resource: URI, _content?: VSBuffer | VSBufferReadable, _options?: ICreateFileOptions): Promise { throw new Error('not implemented'); } diff --git a/test/smoke/README.md b/test/smoke/README.md index d918fd357a..b8b98a358d 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -14,7 +14,7 @@ cd ../.. yarn smoketest # Build -yarn smoketest --build PATH_TO_BUILD --stable-build PATH_TO_STABLE_BUILD +yarn smoketest --build PATH_TO_NEW_BUILD_PARENT_FOLDER --stable-build PATH_TO_LAST_STABLE_BUILD_PARENT_FOLDER ``` ### Run for a release @@ -24,7 +24,14 @@ You must always run the smoketest version which matches the release you are test ```bash git checkout release/1.22 yarn -yarn smoketest --build PATH_TO_RELEASE_BUILD --stable-build PATH_TO_STABLE_BUILD +``` + +In addition to the new build to be released you will need the previous stable build so that the smoketest can test the data migration. +The recommended way to make these builds available for the smoketest is by downloading their archive version (\*.zip) and extracting +them into two folders. Pass the folder paths to the smoketest as follows: + +```bash +yarn smoketest --build PATH_TO_NEW_RELEASE_PARENT_FOLDER --stable-build PATH_TO_LAST_STABLE_RELEASE_PARENT_FOLDER ``` ### Debug diff --git a/test/smoke/src/application.ts b/test/smoke/src/application.ts index e9d2018e4b..7c60cabdfb 100644 --- a/test/smoke/src/application.ts +++ b/test/smoke/src/application.ts @@ -127,6 +127,7 @@ export class Application { verbose: this.options.verbose, log: this.options.log, extraArgs, + remote: this.options.remote }); this._workbench = new Workbench(this._code, this.userDataPath); diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 458bf4c52c..b687c067ba 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -56,7 +56,8 @@ const opts = minimist(args, { 'log' ], boolean: [ - 'verbose' + 'verbose', + 'remote' ], default: { verbose: false @@ -210,7 +211,8 @@ function createOptions(): ApplicationOptions { logger: new MultiLogger(loggers), verbose: opts.verbose, log, - screenshotsPath + screenshotsPath, + remote: opts.remote }; } diff --git a/test/smoke/src/vscode/code.ts b/test/smoke/src/vscode/code.ts index 9f94c93d88..e94398beea 100644 --- a/test/smoke/src/vscode/code.ts +++ b/test/smoke/src/vscode/code.ts @@ -90,6 +90,7 @@ export interface SpawnOptions { verbose?: boolean; extraArgs?: string[]; log?: string; + remote?: boolean; } async function createDriverHandle(): Promise { @@ -120,6 +121,15 @@ export async function spawn(options: SpawnOptions): Promise { '--driver', handle ]; + if (options.remote) { + // Replace workspace path with URI + args.shift(); + args.push( + `--${options.workspacePath.endsWith('.code-workspace') ? 'file' : 'folder'}-uri`, + `vscode-remote://test+test${options.workspacePath}`, + ); + } + if (!codePath) { args.unshift(repoPath); } diff --git a/yarn.lock b/yarn.lock index 9118479b29..e9cb0e6a3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1013,6 +1013,13 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= + dependencies: + inherits "~2.0.0" + bluebird@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -2964,7 +2971,7 @@ event-emitter@~0.3.5: d "1" es5-ext "~0.10.14" -event-stream@3.3.4, event-stream@^3.3.4: +event-stream@3.3.4, event-stream@^3.3.4, event-stream@~3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= @@ -3566,6 +3573,16 @@ fsevents@^1.2.7: nan "^2.9.2" node-pre-gyp "^0.10.0" +fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + integrity sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE= + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + function-bind@^1.0.2, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -3972,6 +3989,14 @@ gulp-flatmap@^1.0.2: plugin-error "0.1.2" through2 "2.0.3" +gulp-gunzip@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/gulp-gunzip/-/gulp-gunzip-1.1.0.tgz#56f81b4ed8106ba3c905e310661ee7f11eb5ac03" + integrity sha512-3INeprGyz5fUtAs75k6wVslGuRZIjKAoQp39xA7Bz350ReqkrfYaLYqjZ67XyIfLytRXdzeX04f+DnBduYhQWw== + dependencies: + through2 "~2.0.3" + vinyl "~2.0.1" + gulp-json-editor@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/gulp-json-editor/-/gulp-json-editor-2.5.0.tgz#23aaa7d30f8425cf60cf1aefae098c257da11ada" @@ -4087,6 +4112,17 @@ gulp-uglify@^3.0.0: uglify-js "^3.0.5" vinyl-sourcemaps-apply "^0.2.0" +gulp-untar@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/gulp-untar/-/gulp-untar-0.0.7.tgz#92067d79e0fa1e92d60562a100233a44a5aa08b4" + integrity sha512-0QfbCH2a1k2qkTLWPqTX+QO4qNsHn3kC546YhAP3/n0h+nvtyGITDuDrYBMDZeW4WnFijmkOvBWa5HshTic1tw== + dependencies: + event-stream "~3.3.4" + streamifier "~0.1.1" + tar "^2.2.1" + through2 "~2.0.3" + vinyl "^1.2.0" + gulp-vinyl-zip@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.2.tgz#b79cc1a0e2c3b158ffee294590ade1e9caaf5e7b" @@ -5916,7 +5952,7 @@ mkdirp@0.5.0: dependencies: minimist "0.0.8" -mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -7906,6 +7942,13 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" +rimraf@2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + rimraf@^2.2.8: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" @@ -8573,7 +8616,7 @@ streamfilter@^1.0.5: dependencies: readable-stream "^2.0.2" -streamifier@^0.1.1: +streamifier@^0.1.1, streamifier@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/streamifier/-/streamifier-0.1.1.tgz#97e98d8fa4d105d62a2691d1dc07e820db8dfc4f" integrity sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8= @@ -8822,6 +8865,15 @@ tar-stream@^1.1.2: to-buffer "^1.1.0" xtend "^4.0.0" +tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + integrity sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE= + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + tar@^4: version "4.4.6" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" @@ -9583,7 +9635,7 @@ vinyl-sourcemaps-apply@^0.2.0, vinyl-sourcemaps-apply@^0.2.1: dependencies: source-map "^0.5.1" -vinyl@^1.0.0: +vinyl@^1.0.0, vinyl@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" integrity sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ= @@ -9797,10 +9849,10 @@ vscode-windows-registry@1.0.1: dependencies: nan "^2.12.1" -vscode-xterm@3.13.0-beta2: - version "3.13.0-beta2" - resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.13.0-beta2.tgz#4517a96ef30d8a8d6f27c60bfc374a405811ee6e" - integrity sha512-9iu3oxpFbyUzsAPYeipMSaI7I/nY4WHlpq2kJXXzibZAO9l7pVFBxFR9H0gfDBVn1+fMJsLIzRWGX9lKC83lfQ== +vscode-xterm@3.13.0-beta3: + version "3.13.0-beta3" + resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.13.0-beta3.tgz#ab642ed77df07c2adfca7b15ae39c18328db3fc6" + integrity sha512-XvgD/P6CCV0+79UYM7CEL6Ziv2RiDopI3Wa1hIm3Dm8InWxkl3QwykINlempMNub+r0gwVwLKSQYr+Q/jg1IAw== vso-node-api@6.1.2-preview: version "6.1.2-preview"