diff --git a/.github/commands.yml b/.github/commands.yml index ba8c8d7f3ce..56edba1d4da 100644 --- a/.github/commands.yml +++ b/.github/commands.yml @@ -29,7 +29,7 @@ type: 'label', name: '*out-of-scope', action: 'close', - comment: "This issue is being closed to keep the number of issues in our inbox on a manageable level, we are closing issues that have been on the backlog for a long time but have not gained traction: We look at the number of votes the issue has received and the number of duplicate issues filed. If you disagree and feel that this issue is crucial: We are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/vscoderoadmap) and [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nThanks for your understanding and happy coding!" + comment: "This issue is being closed to keep the number of issues in our inbox on a manageable level, we are closing issues that are not going to be addressed in the foreseeable future: We look at the number of votes the issue has received and the number of duplicate issues filed. If you disagree and feel that this issue is crucial: We are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/vscoderoadmap) and [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nThanks for your understanding and happy coding!" }, { type: 'label', @@ -62,5 +62,12 @@ action: 'comment', comment: "Potential duplicates:\n${potentialDuplicates}" }, + { + type: 'comment', + name: 'needsMoreInfo', + action: 'updateLabels', + addLabel: 'needs more info', + comment: "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines. Please take the time to review these and update the issue.\n\nHappy Coding!" + }, ] } diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index ab3a77f657b..2831ba28d9c 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -6,7 +6,7 @@ }, { "name": "ms-vscode.node-debug2", - "version": "1.26.1", + "version": "1.26.2", "repo": "https://github.com/Microsoft/vscode-node-debug2" } ] diff --git a/build/npm/update-localization-extension.js b/build/npm/update-localization-extension.js index 985fbd28cb7..eebf63ed330 100644 --- a/build/npm/update-localization-extension.js +++ b/build/npm/update-localization-extension.js @@ -43,7 +43,12 @@ function update(idOrPath) { let apiToken = process.env.TRANSIFEX_API_TOKEN; let languageId = localization.transifexId || localization.languageId; let translationDataFolder = path.join(locExtFolder, 'translations'); - + if (languageId === "zh-cn") { + languageId = "zh-hans"; + } + if (languageId === "zh-tw") { + languageId = "zh-hant"; + } if (fs.existsSync(translationDataFolder) && fs.existsSync(path.join(translationDataFolder, 'main.i18n.json'))) { console.log('Clearing \'' + translationDataFolder + '\'...'); rimraf.sync(translationDataFolder); diff --git a/build/tfs/continuous-build.yml b/build/tfs/continuous-build.yml index 79d65b7de84..a2cfee54013 100644 --- a/build/tfs/continuous-build.yml +++ b/build/tfs/continuous-build.yml @@ -2,148 +2,14 @@ phases: - phase: Windows queue: Hosted VS2017 steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - powershell: | - yarn - displayName: Install Dependencies - - powershell: | - yarn gulp electron - displayName: Download Electron - - powershell: | - yarn gulp hygiene - displayName: Run Hygiene Checks - - powershell: | - yarn check-monaco-editor-compilation - displayName: Run Monaco Editor Checks - - powershell: | - yarn compile - displayName: Compile Sources - - powershell: | - yarn download-builtin-extensions - displayName: Download Built-in Extensions - - powershell: | - .\scripts\test.bat --tfs "Unit Tests" - displayName: Run Unit Tests - - powershell: | - .\scripts\test-integration.bat --tfs "Integration Tests" - displayName: Run Integration Tests - - powershell: | - yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)\artifacts" --log "$(Build.ArtifactStagingDirectory)\artifacts\smoketest.log" - displayName: Run Smoke Tests - continueOnError: true - - task: PublishBuildArtifacts@1 - displayName: Publish Smoketest Artifacts - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' - ArtifactName: build-artifacts-win32 - publishLocation: Container - condition: eq(variables['System.PullRequest.IsFork'], 'False') - - task: PublishTestResults@2 - displayName: Publish Tests Results - inputs: - testResultsFiles: '*-results.xml' - searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' - condition: succeededOrFailed() + - template: win32/continuous-build-win32.yml - phase: Linux queue: Hosted Linux Preview steps: - - script: | - set -e - apt-get update - apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 libgconf-2-4 dbus xvfb libgtk-3-0 - cp build/tfs/linux/x64/xvfb.init /etc/init.d/xvfb - chmod +x /etc/init.d/xvfb - update-rc.d xvfb defaults - ln -sf /bin/dbus-daemon /usr/bin/dbus-daemon - service xvfb start - service dbus start - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - script: | - yarn - displayName: Install Dependencies - - script: | - yarn gulp electron-x64 - displayName: Download Electron - - script: | - yarn gulp hygiene - displayName: Run Hygiene Checks - - script: | - yarn check-monaco-editor-compilation - displayName: Run Monaco Editor Checks - - script: | - yarn compile - displayName: Compile Sources - - script: | - yarn download-builtin-extensions - displayName: Download Built-in Extensions - - script: | - DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" - displayName: Run Unit Tests - - task: PublishTestResults@2 - displayName: Publish Tests Results - inputs: - testResultsFiles: '*-results.xml' - searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' - condition: succeededOrFailed() + - template: linux/continuous-build-linux.yml - phase: macOS queue: Hosted macOS Preview steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - script: | - yarn - displayName: Install Dependencies - - script: | - yarn gulp electron-x64 - displayName: Download Electron - - script: | - yarn gulp hygiene - displayName: Run Hygiene Checks - - script: | - yarn check-monaco-editor-compilation - displayName: Run Monaco Editor Checks - - script: | - yarn compile - displayName: Compile Sources - - script: | - yarn download-builtin-extensions - displayName: Download Built-in Extensions - - script: | - ./scripts/test.sh --tfs "Unit Tests" - displayName: Run Unit Tests - - script: | - ./scripts/test-integration.sh --tfs "Integration Tests" - displayName: Run Integration Tests - - script: | - yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)/artifacts" --log "$(Build.ArtifactStagingDirectory)/artifacts/smoketest.log" - displayName: Run Smoke Tests - continueOnError: true - - task: PublishBuildArtifacts@1 - displayName: Publish Smoketest Artifacts - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' - ArtifactName: build-artifacts-darwin - publishLocation: Container - condition: eq(variables['System.PullRequest.IsFork'], 'False') - - task: PublishTestResults@2 - displayName: Publish Tests Results - inputs: - testResultsFiles: '*-results.xml' - searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' - condition: succeededOrFailed() \ No newline at end of file + - template: darwin/continuous-build-darwin.yml \ No newline at end of file diff --git a/build/tfs/darwin/continuous-build-darwin.yml b/build/tfs/darwin/continuous-build-darwin.yml new file mode 100644 index 00000000000..20070e08745 --- /dev/null +++ b/build/tfs/darwin/continuous-build-darwin.yml @@ -0,0 +1,48 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" +- script: | + yarn + displayName: Install Dependencies +- script: | + yarn gulp electron-x64 + displayName: Download Electron +- script: | + yarn gulp hygiene + displayName: Run Hygiene Checks +- script: | + yarn check-monaco-editor-compilation + displayName: Run Monaco Editor Checks +- script: | + yarn compile + displayName: Compile Sources +- script: | + yarn download-builtin-extensions + displayName: Download Built-in Extensions +- script: | + ./scripts/test.sh --tfs "Unit Tests" + displayName: Run Unit Tests +- script: | + ./scripts/test-integration.sh --tfs "Integration Tests" + displayName: Run Integration Tests +- script: | + yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)/artifacts" --log "$(Build.ArtifactStagingDirectory)/artifacts/smoketest.log" + displayName: Run Smoke Tests + continueOnError: true +- task: PublishBuildArtifacts@1 + displayName: Publish Smoketest Artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' + ArtifactName: build-artifacts-darwin + publishLocation: Container + condition: eq(variables['System.PullRequest.IsFork'], 'False') +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() \ No newline at end of file diff --git a/build/tfs/darwin/product-build-darwin.yml b/build/tfs/darwin/product-build-darwin.yml new file mode 100644 index 00000000000..0dd31503586 --- /dev/null +++ b/build/tfs/darwin/product-build-darwin.yml @@ -0,0 +1,63 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" + +- script: | + set -e + echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc + yarn + npm run gulp -- hygiene + npm run monaco-compile-check + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin + node build/tfs/common/installDistro.js + node build/lib/builtInExtensions.js + +- script: | + set -e + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ + AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ + npm run gulp -- vscode-darwin-min upload-vscode-sourcemaps + name: build + +- script: | + set -e + ./scripts/test.sh --build --tfs "Unit Tests" + APP_NAME="`ls $(agent.builddirectory)/VSCode-darwin | head -n 1`" + # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-darwin/$APP_NAME" + name: test + +- script: | + set -e + # archive the unsigned build + pushd ../VSCode-darwin && zip -r -X -y ../VSCode-darwin-unsigned.zip * && popd + + # publish the unsigned build + PACKAGEJSON=`ls ../VSCode-darwin/*.app/Contents/Resources/app/package.json` + VERSION=`node -p "require(\"$PACKAGEJSON\").version"` + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + node build/tfs/common/publish.js \ + "$(VSCODE_QUALITY)" \ + darwin \ + archive-unsigned \ + "VSCode-darwin-$(VSCODE_QUALITY)-unsigned.zip" \ + $VERSION \ + false \ + ../VSCode-darwin-unsigned.zip + + # publish hockeyapp symbols + node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_MACOS)" + + # enqueue the unsigned build + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + node build/tfs/darwin/enqueue.js "$(VSCODE_QUALITY)" + + AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ + npm run gulp -- upload-vscode-configuration \ No newline at end of file diff --git a/build/tfs/linux/continuous-build-linux.yml b/build/tfs/linux/continuous-build-linux.yml new file mode 100644 index 00000000000..7ec3aec74b9 --- /dev/null +++ b/build/tfs/linux/continuous-build-linux.yml @@ -0,0 +1,44 @@ +steps: +- script: | + set -e + apt-get update + apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 libgconf-2-4 dbus xvfb libgtk-3-0 + cp build/tfs/linux/x64/xvfb.init /etc/init.d/xvfb + chmod +x /etc/init.d/xvfb + update-rc.d xvfb defaults + ln -sf /bin/dbus-daemon /usr/bin/dbus-daemon + service xvfb start + service dbus start +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" +- script: | + yarn + displayName: Install Dependencies +- script: | + yarn gulp electron-x64 + displayName: Download Electron +- script: | + yarn gulp hygiene + displayName: Run Hygiene Checks +- script: | + yarn check-monaco-editor-compilation + displayName: Run Monaco Editor Checks +- script: | + yarn compile + displayName: Compile Sources +- script: | + yarn download-builtin-extensions + displayName: Download Built-in Extensions +- script: | + DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" + displayName: Run Unit Tests +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() \ No newline at end of file diff --git a/build/tfs/linux/product-build-linux.yml b/build/tfs/linux/product-build-linux.yml new file mode 100644 index 00000000000..b5baa02e143 --- /dev/null +++ b/build/tfs/linux/product-build-linux.yml @@ -0,0 +1,49 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" + +- script: | + set -e + export npm_config_arch="$(VSCODE_ARCH)" + if [[ "$(VSCODE_ARCH)" == "ia32" ]]; then + export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" + fi + + echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc + yarn + npm run gulp -- hygiene + npm run monaco-compile-check + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin + node build/tfs/common/installDistro.js + node build/lib/builtInExtensions.js + +- script: | + set -e + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min + name: build + +- script: | + set -e + npm run gulp -- "electron-$(VSCODE_ARCH)" + DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" + # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" + name: test + +- script: | + set -e + npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" + npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" + #npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-snap" + + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + ./build/tfs/linux/release.sh "$(VSCODE_ARCH)" "$(LINUX_REPO_PASSWORD)" + + # publish hockeyapp symbols + node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX64)" diff --git a/build/tfs/product-build.yml b/build/tfs/product-build.yml index e0ac588a85a..570925d6c99 100644 --- a/build/tfs/product-build.yml +++ b/build/tfs/product-build.yml @@ -1,353 +1,38 @@ phases: - phase: Windows condition: eq(variables['VSCODE_BUILD_WIN32'], 'true') - queue: - name: Hosted VS2017 - parallel: 2 - matrix: - x64: - VSCODE_ARCH: x64 - ia32: - VSCODE_ARCH: ia32 - + queue: Hosted VS2017 + variables: + VSCODE_ARCH: x64 steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" + - template: win32/product-build-win32.yml - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - powershell: | - $ErrorActionPreference = "Stop" - "machine monacotools.visualstudio.com password $(VSO_PAT)" | Out-File "$env:USERPROFILE\_netrc" -Encoding ASCII - $env:npm_config_arch="$(VSCODE_ARCH)" - $env:CHILD_CONCURRENCY="1" - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" - npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - powershell: | - $ErrorActionPreference = "Stop" - $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" - npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-min" - npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-copy-inno-updater" - name: build - - - powershell: | - $ErrorActionPreference = "Stop" - npm run gulp -- "electron-$(VSCODE_ARCH)" - .\scripts\test.bat --build --tfs "Unit Tests" - # yarn smoketest -- --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - name: test - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 - inputs: - ConnectedServiceName: 'ESRP CodeSign' - FolderPath: '$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)' - Pattern: '*.dll,*.exe,*.node' - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "keyCode": "CP-229803", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "VS Code" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://code.visualstudio.com/" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/t \"http://ts4096.gtm.microsoft.com/TSS/AuthenticodeTS\"" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "VS Code" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://code.visualstudio.com/" - }, - { - "parameterName": "Append", - "parameterValue": "/as" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd \"SHA256\"" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolVerify", - "parameters": [ - { - "parameterName": "VerifyAll", - "parameterValue": "/all" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - } - ] - SessionTimeout: 120 - - - task: NuGetCommand@2 - displayName: Install ESRPClient.exe - inputs: - restoreSolution: 'build\tfs\win32\ESRPClient\packages.config' - feedsToUse: config - nugetConfigPath: 'build\tfs\win32\ESRPClient\NuGet.config' - externalFeedCredentials: 3fc0b7f7-da09-4ae7-a9c8-d69824b1819b - restoreDirectory: packages - - - task: ESRPImportCertTask@1 - displayName: Import ESRP Request Signing Certificate - inputs: - ESRP: 'ESRP CodeSign' - - - powershell: | - $ErrorActionPreference = "Stop" - .\build\tfs\win32\import-esrp-auth-cert.ps1 -AuthCertificateBase64 $(ESRP_AUTH_CERTIFICATE) -AuthCertificateKey $(ESRP_AUTH_CERTIFICATE_KEY) - displayName: Import ESRP Auth Certificate - - - powershell: | - $ErrorActionPreference = "Stop" - npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-archive" "vscode-win32-$(VSCODE_ARCH)-system-setup" "vscode-win32-$(VSCODE_ARCH)-user-setup" - - $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:MOONCAKE_STORAGE_ACCESS_KEY = "$(MOONCAKE_STORAGE_ACCESS_KEY)" - $env:AZURE_DOCUMENTDB_MASTERKEY = "$(AZURE_DOCUMENTDB_MASTERKEY)" - - $assetPlatform = if ("$(VSCODE_ARCH)" -eq "ia32") { "win32" } else { "win32-x64" } - - node build/tfs/common/publish.js $Quality "$global:assetPlatform-archive" archive "VSCode-win32-$(VSCODE_ARCH)-$Version.zip" $Version true $Zip - node build/tfs/common/publish.js $Quality "$global:assetPlatform" setup "VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $SystemExe - node build/tfs/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)" } - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" $hockeyAppId +- phase: Windows32 + condition: eq(variables['VSCODE_BUILD_WIN32_32BIT'], 'true') + queue: Hosted VS2017 + variables: + VSCODE_ARCH: ia32 + steps: + - template: win32/product-build-win32.yml - phase: Linux condition: eq(variables['VSCODE_BUILD_LINUX'], 'true') queue: linux-x64 variables: VSCODE_ARCH: x64 - steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - script: | - set -e - export npm_config_arch="$(VSCODE_ARCH)" - if [[ "$(VSCODE_ARCH)" == "ia32" ]]; then - export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" - fi - - echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min - name: build - - - script: | - set -e - npm run gulp -- "electron-$(VSCODE_ARCH)" - DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" - # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" - name: test - - - script: | - set -e - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" - #npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-snap" - - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ - ./build/tfs/linux/release.sh "$(VSCODE_ARCH)" "$(LINUX_REPO_PASSWORD)" - - # publish hockeyapp symbols - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX64)" + - template: linux/product-build-linux.yml - phase: Linux32 - condition: eq(variables['VSCODE_BUILD_LINUX'], 'true') + condition: eq(variables['VSCODE_BUILD_LINUX_32BIT'], 'true') queue: linux-ia32 variables: VSCODE_ARCH: ia32 - steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - script: | - set -e - export npm_config_arch="$(VSCODE_ARCH)" - if [[ "$(VSCODE_ARCH)" == "ia32" ]]; then - export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" - fi - - echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min - name: build - - - script: | - set -e - npm run gulp -- "electron-$(VSCODE_ARCH)" - DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" - # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" - name: test - - - script: | - set -e - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" - #npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-snap" - - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ - ./build/tfs/linux/release.sh "$(VSCODE_ARCH)" "$(LINUX_REPO_PASSWORD)" - - # publish hockeyapp symbols - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX32)" + - template: linux/product-build-linux.yml - phase: macOS condition: eq(variables['VSCODE_BUILD_MACOS'], 'true') queue: Hosted macOS Preview steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - script: | - set -e - echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ - AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ - npm run gulp -- vscode-darwin-min upload-vscode-sourcemaps - name: build - - - script: | - set -e - ./scripts/test.sh --build --tfs "Unit Tests" - APP_NAME="`ls $(agent.builddirectory)/VSCode-darwin | head -n 1`" - # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-darwin/$APP_NAME" - name: test - - - script: | - set -e - # archive the unsigned build - pushd ../VSCode-darwin && zip -r -X -y ../VSCode-darwin-unsigned.zip * && popd - - # publish the unsigned build - PACKAGEJSON=`ls ../VSCode-darwin/*.app/Contents/Resources/app/package.json` - VERSION=`node -p "require(\"$PACKAGEJSON\").version"` - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ - node build/tfs/common/publish.js \ - "$(VSCODE_QUALITY)" \ - darwin \ - archive-unsigned \ - "VSCode-darwin-$(VSCODE_QUALITY)-unsigned.zip" \ - $VERSION \ - false \ - ../VSCode-darwin-unsigned.zip - - # publish hockeyapp symbols - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_MACOS)" - - # enqueue the unsigned build - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - node build/tfs/darwin/enqueue.js "$(VSCODE_QUALITY)" - - AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ - npm run gulp -- upload-vscode-configuration \ No newline at end of file + - template: darwin/product-build-darwin.yml \ No newline at end of file diff --git a/build/tfs/win32/continuous-build-win32.yml b/build/tfs/win32/continuous-build-win32.yml new file mode 100644 index 00000000000..6490c637b78 --- /dev/null +++ b/build/tfs/win32/continuous-build-win32.yml @@ -0,0 +1,66 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn } + displayName: Install Dependencies +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn gulp electron } + displayName: Download Electron +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn gulp hygiene } + displayName: Run Hygiene Checks +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn check-monaco-editor-compilation } + displayName: Run Monaco Editor Checks +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn compile } + displayName: Compile Sources +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn download-builtin-extensions } + displayName: Download Built-in Extensions +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { .\scripts\test.bat --tfs "Unit Tests" } + displayName: Run Unit Tests +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { .\scripts\test-integration.bat --tfs "Integration Tests" } + displayName: Run Integration Tests +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)\artifacts" --log "$(Build.ArtifactStagingDirectory)\artifacts\smoketest.log" } + displayName: Run Smoke Tests + continueOnError: true +- task: PublishBuildArtifacts@1 + displayName: Publish Smoketest Artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' + ArtifactName: build-artifacts-win32 + publishLocation: Container + condition: eq(variables['System.PullRequest.IsFork'], 'False') +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() diff --git a/build/tfs/win32/exec.ps1 b/build/tfs/win32/exec.ps1 new file mode 100644 index 00000000000..826cefdf7dd --- /dev/null +++ b/build/tfs/win32/exec.ps1 @@ -0,0 +1,24 @@ +# Taken from psake https://github.com/psake/psake + +<# +.SYNOPSIS + This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode + to see if an error occcured. If an error is detected then an exception is thrown. + This function allows you to run command-line programs without having to + explicitly check the $lastexitcode variable. + +.EXAMPLE + exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" +#> +function Exec +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, + [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) + ) + & $cmd + if ($lastexitcode -ne 0) { + throw ("Exec: " + $errorMessage) + } +} \ No newline at end of file diff --git a/build/tfs/win32/product-build-win32.yml b/build/tfs/win32/product-build-win32.yml new file mode 100644 index 00000000000..c887ade31fd --- /dev/null +++ b/build/tfs/win32/product-build-win32.yml @@ -0,0 +1,166 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" + +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + "machine monacotools.visualstudio.com password $(VSO_PAT)" | 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 { yarn } + exec { npm run gulp -- hygiene } + exec { npm run monaco-compile-check } + exec { npm run gulp -- mixin } + exec { node build/tfs/common/installDistro.js } + exec { node build/lib/builtInExtensions.js } + +- powershell: | + . build/tfs/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)-copy-inno-updater" } + name: build + +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm run gulp -- "electron-$(VSCODE_ARCH)" } + exec { .\scripts\test.bat --build --tfs "Unit Tests" } + # yarn smoketest -- --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + name: test + +- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + inputs: + ConnectedServiceName: 'ESRP CodeSign' + FolderPath: '$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)' + Pattern: '*.dll,*.exe,*.node' + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "keyCode": "CP-229803", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "VS Code" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "https://code.visualstudio.com/" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/t \"http://ts4096.gtm.microsoft.com/TSS/AuthenticodeTS\"" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "VS Code" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "https://code.visualstudio.com/" + }, + { + "parameterName": "Append", + "parameterValue": "/as" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolVerify", + "parameters": [ + { + "parameterName": "VerifyAll", + "parameterValue": "/all" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + } + ] + SessionTimeout: 120 + +- task: NuGetCommand@2 + displayName: Install ESRPClient.exe + inputs: + restoreSolution: 'build\tfs\win32\ESRPClient\packages.config' + feedsToUse: config + nugetConfigPath: 'build\tfs\win32\ESRPClient\NuGet.config' + externalFeedCredentials: 3fc0b7f7-da09-4ae7-a9c8-d69824b1819b + restoreDirectory: packages + +- task: ESRPImportCertTask@1 + displayName: Import ESRP Request Signing Certificate + inputs: + ESRP: 'ESRP CodeSign' + +- powershell: | + $ErrorActionPreference = "Stop" + .\build\tfs\win32\import-esrp-auth-cert.ps1 -AuthCertificateBase64 $(ESRP_AUTH_CERTIFICATE) -AuthCertificateKey $(ESRP_AUTH_CERTIFICATE_KEY) + displayName: Import ESRP Auth Certificate + +- powershell: | + . build/tfs/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" } + + $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:MOONCAKE_STORAGE_ACCESS_KEY = "$(MOONCAKE_STORAGE_ACCESS_KEY)" + $env:AZURE_DOCUMENTDB_MASTERKEY = "$(AZURE_DOCUMENTDB_MASTERKEY)" + + $assetPlatform = if ("$(VSCODE_ARCH)" -eq "ia32") { "win32" } else { "win32-x64" } + + exec { node build/tfs/common/publish.js $Quality "$global:assetPlatform-archive" archive "VSCode-win32-$(VSCODE_ARCH)-$Version.zip" $Version true $Zip } + exec { node build/tfs/common/publish.js $Quality "$global:assetPlatform" setup "VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $SystemExe } + exec { node build/tfs/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/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" $hockeyAppId } diff --git a/extensions/css-language-features/README.md b/extensions/css-language-features/README.md index 4c72839bdc8..b2f65a5e8e6 100644 --- a/extensions/css-language-features/README.md +++ b/extensions/css-language-features/README.md @@ -1,6 +1,15 @@ -# CSS Language Features +# Language Features for CSS, SCSS and LESS files. -This extension offers CSS/SCSS/Less support in VS Code. +**Notice** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +This extension provides rich language support to CSS/SCSS/LESS files. +- auto-completion +- linting +- diagnostics +- extra information on hover +- navigation to definition and references ## Development @@ -19,7 +28,9 @@ This extension offers CSS/SCSS/Less support in VS Code. ### Contribute to vscode-css-languageservice -[Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice) contains the language smarts for CSS/SCSS/Less, and this extension wraps the service into a Language Server for VS Code. If you want to fix CSS/SCSS/Less issues or make improvements, you should make changes at [Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice). +[Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice) contains the language smarts for CSS/SCSS/Less. +This extension wraps the css language service into a Language Server for VS Code. +If you want to fix CSS/SCSS/Less issues or make improvements, you should make changes at [Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice). However, within this extension, you can run a development version of `vscode-css-languageservice` to debug code or test language features interactively: diff --git a/extensions/emmet/README.md b/extensions/emmet/README.md index a2720bbd541..deda7e16037 100644 --- a/extensions/emmet/README.md +++ b/extensions/emmet/README.md @@ -1,5 +1,7 @@ # Emmet integration in Visual Studio Code +**Notice** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + ## Features See [Emmet in Visual Studio Code](https://code.visualstudio.com/docs/editor/emmet) diff --git a/extensions/git/README.md b/extensions/git/README.md index b97ec81e9a0..469f954eb1f 100644 --- a/extensions/git/README.md +++ b/extensions/git/README.md @@ -1,2 +1,7 @@ # Git integration for Visual Studio Code +**Notice** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +Read [Git support in VS Code](https://code.visualstudio.com/docs/editor/versioncontrol#_git-support) to learn about features of this extension. \ No newline at end of file diff --git a/extensions/git/src/api.ts b/extensions/git/src/api.ts index 6832fda9531..cfce4c9ed8b 100644 --- a/extensions/git/src/api.ts +++ b/extensions/git/src/api.ts @@ -42,19 +42,24 @@ export interface API { export class APIImpl implements API { - constructor(private modelPromise: Promise) { } + constructor(private model: Model) { } async getGitPath(): Promise { - const model = await this.modelPromise; - return model.git.path; + return this.model.git.path; } async getRepositories(): Promise { - const model = await this.modelPromise; - return model.repositories.map(repository => new RepositoryImpl(repository)); + return this.model.repositories.map(repository => new RepositoryImpl(repository)); } } -export function createApi(modelPromise: Promise): API { - return new APIImpl(modelPromise); -} \ No newline at end of file +export class NoopAPIImpl implements API { + + async getGitPath(): Promise { + throw new Error('Git model not found'); + } + + async getRepositories(): Promise { + throw new Error('Git model not found'); + } +} diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index fa155966bbf..6eae1da2c9d 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -16,12 +16,18 @@ import { GitDecorations } from './decorationProvider'; import { Askpass } from './askpass'; import { toDisposable, filterEvent, eventToPromise } from './util'; import TelemetryReporter from 'vscode-extension-telemetry'; -import { API, createApi } from './api'; +import { API, NoopAPIImpl, APIImpl } from './api'; import { GitProtocolHandler } from './protocolHandler'; -let telemetryReporter: TelemetryReporter; +const deactivateTasks: { (): Promise; }[] = []; -async function init(context: ExtensionContext, outputChannel: OutputChannel, disposables: Disposable[]): Promise { +export async function deactivate(): Promise { + for (const task of deactivateTasks) { + await task(); + } +} + +async function createModel(context: ExtensionContext, outputChannel: OutputChannel, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise { const pathHint = workspace.getConfiguration('git').get('path'); const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path))); const askpass = new Askpass(); @@ -63,13 +69,30 @@ async function init(context: ExtensionContext, outputChannel: OutputChannel, dis return model; } -async function _activate(context: ExtensionContext, disposables: Disposable[]): Promise { +export async function activate(context: ExtensionContext): Promise { + const disposables: Disposable[] = []; + context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose())); + const outputChannel = window.createOutputChannel('Git'); commands.registerCommand('git.showOutput', () => outputChannel.show()); disposables.push(outputChannel); + const { name, version, aiKey } = require(context.asAbsolutePath('./package.json')) as { name: string, version: string, aiKey: string }; + const telemetryReporter = new TelemetryReporter(name, version, aiKey); + deactivateTasks.push(() => telemetryReporter.dispose()); + + const config = workspace.getConfiguration('git', null); + const enabled = config.get('enabled'); + + if (!enabled) { + const onConfigChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git')); + const onEnabled = filterEvent(onConfigChange, () => workspace.getConfiguration('git', null).get('enabled') === true); + await eventToPromise(onEnabled); + } + try { - return await init(context, outputChannel, disposables); + const model = await createModel(context, outputChannel, telemetryReporter, disposables); + return new APIImpl(model); } catch (err) { if (!/Git installation not found/.test(err.message || '')) { throw err; @@ -78,60 +101,30 @@ async function _activate(context: ExtensionContext, disposables: Disposable[]): const config = workspace.getConfiguration('git'); const shouldIgnore = config.get('ignoreMissingGitWarning') === true; - if (shouldIgnore) { - return; + if (!shouldIgnore) { + console.warn(err.message); + outputChannel.appendLine(err.message); + outputChannel.show(); + + const download = localize('downloadgit', "Download Git"); + const neverShowAgain = localize('neverShowAgain', "Don't Show Again"); + const choice = await window.showWarningMessage( + localize('notfound', "Git not found. Install it or configure it using the 'git.path' setting."), + download, + neverShowAgain + ); + + if (choice === download) { + commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/')); + } else if (choice === neverShowAgain) { + await config.update('ignoreMissingGitWarning', true, true); + } } - console.warn(err.message); - outputChannel.appendLine(err.message); - outputChannel.show(); - - const download = localize('downloadgit', "Download Git"); - const neverShowAgain = localize('neverShowAgain', "Don't Show Again"); - const choice = await window.showWarningMessage( - localize('notfound', "Git not found. Install it or configure it using the 'git.path' setting."), - download, - neverShowAgain - ); - - if (choice === download) { - commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/')); - } else if (choice === neverShowAgain) { - await config.update('ignoreMissingGitWarning', true, true); - } + return new NoopAPIImpl(); } } -export function activate(context: ExtensionContext): API { - const config = workspace.getConfiguration('git', null); - const enabled = config.get('enabled'); - - const disposables: Disposable[] = []; - context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose())); - - const { name, version, aiKey } = require(context.asAbsolutePath('./package.json')) as { name: string, version: string, aiKey: string }; - telemetryReporter = new TelemetryReporter(name, version, aiKey); - - let activatePromise: Promise; - - if (enabled) { - activatePromise = _activate(context, disposables); - } else { - const onConfigChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git')); - const onEnabled = filterEvent(onConfigChange, () => workspace.getConfiguration('git', null).get('enabled') === true); - - activatePromise = eventToPromise(onEnabled) - .then(() => _activate(context, disposables)); - } - - const modelPromise = activatePromise - .then(model => model || Promise.reject('Git model not found')); - - activatePromise.catch(err => console.error(err)); - - return createApi(modelPromise); -} - async function checkGitVersion(info: IGit): Promise { const config = workspace.getConfiguration('git'); const shouldIgnore = config.get('ignoreLegacyWarning') === true; @@ -159,7 +152,3 @@ async function checkGitVersion(info: IGit): Promise { await config.update('ignoreLegacyWarning', true, true); } } - -export function deactivate(): Promise { - return telemetryReporter ? telemetryReporter.dispose() : Promise.resolve(null); -} diff --git a/extensions/grunt/README.md b/extensions/grunt/README.md index 34132a02dbe..1bb90aedece 100644 --- a/extensions/grunt/README.md +++ b/extensions/grunt/README.md @@ -1,6 +1,8 @@ # Grunt - The JavaScript Task Runner -**Notice** This is a an extension that is bundled with Visual Studio Code. +**Notice** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features This extension supports running [Grunt](https://gruntjs.com/) tasks defined in a `gruntfile.js` file as [VS Code tasks](https://code.visualstudio.com/docs/editor/tasks). Grunt tasks with the name 'build', 'compile', or 'watch' are treated as build tasks. diff --git a/extensions/gulp/README.md b/extensions/gulp/README.md index c06203d7a8d..27a43967db7 100644 --- a/extensions/gulp/README.md +++ b/extensions/gulp/README.md @@ -1,6 +1,8 @@ # Gulp - Automate and enhance your workflow -**Notice** This is a an extension that is bundled with Visual Studio Code. +**Notice** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features This extension supports running [Gulp](https://gulpjs.com/) tasks defined in a `gulpfile.{js,ts}` file as [VS Code tasks](https://code.visualstudio.com/docs/editor/tasks). Gulp tasks with the name 'build', 'compile', or 'watch' are treated as build tasks. diff --git a/extensions/html-language-features/README.md b/extensions/html-language-features/README.md new file mode 100644 index 00000000000..db0905a436a --- /dev/null +++ b/extensions/html-language-features/README.md @@ -0,0 +1,51 @@ +# Language Features for HTML. + +**Notice** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +This extension provides rich language support to HTML files. +- auto-completion +- linting +- diagnostics +- extra information on hover +- navigation to definition and references +- formatting + +## Development + +- Clone [Microsoft/vscode](https://github.com/microsoft/vscode) +- Run `yarn` at `/`, this will install + - Dependencies for `/extension/html-language-features/` + - Dependencies for `/extension/html-language-features/server/` + - devDependencies such as `gulp` +- Open `/extensions/html-language-features/` as the workspace in VS Code +- Run the [`Launch Extension`](https://github.com/Microsoft/vscode/blob/master/extensions/html-language-features/.vscode/launch.json) debug target in the Debug View. This will: + - Launch the `preLaunchTask` task to compile the extension + - Launch a new VS Code instance with the `html-language-features` extension loaded + - You should see a notification saying the development version of `html-language-features` overwrites the bundled version of `html-language-features` +- Test the behavior of this extension by editing html files +- Run `Reload Window` command in the launched instance to reload the extension + +### Contribute to vscode-html-languageservice + +[Microsoft/vscode-html-languageservice](https://github.com/Microsoft/vscode-html-languageservice) contains the language smarts for html. +This extension wraps the html language service into a Language Server for VS Code. +If you want to fix html issues or make improvements, you should make changes at [Microsoft/vscode-html-languageservice](https://github.com/Microsoft/vscode-html-languageservice). + +However, within this extension, you can run a development version of `vscode-html-languageservice` to debug code or test language features interactively: + +#### Linking `vscode-html-languageservice` in `html-language-features/server/` + +- Clone [Microsoft/vscode-html-languageservice](https://github.com/Microsoft/vscode-html-languageservice) +- Run `yarn` in `vscode-html-languageservice` +- Run `yarn link` in `vscode-html-languageservice`. This will compile and link `vscode-html-languageservice` +- In `html-language-features/server/`, run `npm link vscode-html-languageservice` + +#### Testing the development version of `vscode-html-languageservice` + +- Open both `vscode-html-languageservice` and this extension in a single workspace with [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) feature +- Run `yarn watch` at `html-languagefeatures/server/` to recompile this extension with the linked version of `vscode-html-languageservice` +- Make some changes in `vscode-html-languageservice` +- Now when you run `Launch Extension` debug target, the launched instance will use your development version of `vscode-html-languageservice`. You can interactively test the language features. +- You can also run the `Debug Extension and Language Server` debug target, which will launch the extension and attach the debugger to the language server. After successful attach, you should be able to hit breakpoints in both `vscode-html-languageservice` and `html-language-features/server/` diff --git a/extensions/jake/README.md b/extensions/jake/README.md index cbaa043b0a2..603ab4935e2 100644 --- a/extensions/jake/README.md +++ b/extensions/jake/README.md @@ -1,6 +1,8 @@ # Jake - JavaScript build tool -**Notice** This is a an extension that is bundled with Visual Studio Code. +**Notice** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features This extension supports running [Jake](http://jakejs.com/) tasks defined in a `Jakefile.js` file as [VS Code tasks](https://code.visualstudio.com/docs/editor/tasks). Jake tasks with the name 'build', 'compile', or 'watch' are treated as build tasks. diff --git a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json index 8fd13accda3..4774b176fd2 100644 --- a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/88217b1c7d36ed5d35adc3099ba7978aabe2531b", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/d7df3e324468b6535af67573d2956f9a852aa586", "name": "JavaScript (with React support)", "scopeName": "source.js", "patterns": [ @@ -139,6 +139,10 @@ "name": "keyword.control.with.js", "match": "(? { public get allBuffers(): Iterable { return this.values; } +} - public get allResources(): Iterable { - return this.keys; +class PendingDiagnostics extends ResourceMap { + public getFileList(): Set { + return new Set(Array.from(this.entries) + .sort((a, b) => a[1] - b[1]) + .map(entry => entry[0])); + } +} + +class GetErrRequest { + + public static executeGetErrRequest( + client: ITypeScriptServiceClient, + files: string[], + onDone: () => void + ) { + const token = new CancellationTokenSource(); + return new GetErrRequest(client, files, token, onDone); + } + + private _done: boolean = false; + + private constructor( + client: ITypeScriptServiceClient, + public readonly files: string[], + private readonly _token: CancellationTokenSource, + onDone: () => void + ) { + const args: Proto.GeterrRequestArgs = { + delay: 0, + files + }; + + client.executeAsync('geterr', args, _token.token) + .then(undefined, () => { }) + .then(() => { + if (this._done) { + return; + } + this._done = true; + onDone(); + }); + } + + public cancel(): any { + if (!this._done) { + this._token.cancel(); + } + + this._token.dispose(); } } @@ -138,10 +177,9 @@ export default class BufferSyncSupport { private readonly modeIds: Set; private readonly disposables: Disposable[] = []; private readonly syncedBuffers: SyncedBufferMap; - - private readonly pendingDiagnostics = new Map(); + private readonly pendingDiagnostics: PendingDiagnostics; private readonly diagnosticDelayer: Delayer; - private pendingGetErr: { request: Promise, files: string[], token: CancellationTokenSource } | undefined; + private pendingGetErr: GetErrRequest | undefined; private listening: boolean = false; constructor( @@ -153,10 +191,12 @@ export default class BufferSyncSupport { this.diagnosticDelayer = new Delayer(300); - this.syncedBuffers = new SyncedBufferMap(path => this.normalizePath(path)); + const pathNormalizer = (path: Uri) => this.client.normalizedPath(path); + this.syncedBuffers = new SyncedBufferMap(pathNormalizer); + this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer); this.updateConfiguration(); - workspace.onDidChangeConfiguration(() => this.updateConfiguration(), null); + workspace.onDidChangeConfiguration(this.updateConfiguration, this, this.disposables); } private readonly _onDelete = new EventEmitter(); @@ -210,10 +250,10 @@ export default class BufferSyncSupport { return; } - const syncedBuffer = new SyncedBuffer(document, filepath, this, this.client); + const syncedBuffer = new SyncedBuffer(document, filepath, this.client); this.syncedBuffers.set(resource, syncedBuffer); syncedBuffer.open(); - this.requestDiagnostic(resource); + this.requestDiagnostic(syncedBuffer); } public closeResource(resource: Uri): void { @@ -240,25 +280,23 @@ export default class BufferSyncSupport { } syncedBuffer.onContentChanged(e.contentChanges); - if (this.pendingGetErr) { - this.pendingGetErr.token.cancel(); - this.pendingGetErr = undefined; + const didTrigger = this.requestDiagnostic(syncedBuffer); - this.diagnosticDelayer.trigger(() => { - this.sendPendingDiagnostics(); - }, 200); + if (!didTrigger && this.pendingGetErr) { + // In this case we always want to re-trigger all diagnostics + this.pendingGetErr.cancel(); + this.pendingGetErr = undefined; + this.triggerDiagnostics(); } } public requestAllDiagnostics() { for (const buffer of this.syncedBuffers.allBuffers) { if (this.shouldValidate(buffer)) { - this.pendingDiagnostics.set(buffer.filepath, Date.now()); + this.pendingDiagnostics.set(buffer.resource, Date.now()); } } - this.diagnosticDelayer.trigger(() => { - this.sendPendingDiagnostics(); - }, 200); + this.triggerDiagnostics(); } public getErr(resources: Uri[]): any { @@ -268,80 +306,62 @@ export default class BufferSyncSupport { } for (const resource of handledResources) { - const file = this.client.normalizedPath(resource); - if (file) { - this.pendingDiagnostics.set(file, Date.now()); - } + this.pendingDiagnostics.set(resource, Date.now()); } - this.diagnosticDelayer.trigger(() => { - this.sendPendingDiagnostics(); - }, 200); + this.triggerDiagnostics(); } - public requestDiagnostic(resource: Uri): void { - const file = this.client.normalizedPath(resource); - if (!file) { - return; - } - - this.pendingDiagnostics.set(file, Date.now()); - const buffer = this.syncedBuffers.get(resource); - if (!buffer || !this.shouldValidate(buffer)) { - return; - } - - let delay = 300; - const lineCount = buffer.lineCount; - delay = Math.min(Math.max(Math.ceil(lineCount / 20), 300), 800); + private triggerDiagnostics(delay: number = 200) { this.diagnosticDelayer.trigger(() => { this.sendPendingDiagnostics(); }, delay); } + private requestDiagnostic(buffer: SyncedBuffer): boolean { + if (!this.shouldValidate(buffer)) { + return false; + } + + this.pendingDiagnostics.set(buffer.resource, Date.now()); + + const delay = Math.min(Math.max(Math.ceil(buffer.lineCount / 20), 300), 800); + this.triggerDiagnostics(delay); + return true; + } + public hasPendingDiagnostics(resource: Uri): boolean { - const file = this.client.normalizedPath(resource); - return !file || this.pendingDiagnostics.has(file); + return this.pendingDiagnostics.has(resource); } private sendPendingDiagnostics(): void { - const files = new Set(Array.from(this.pendingDiagnostics.entries()) - .sort((a, b) => a[1] - b[1]) - .map(entry => entry[0])); + const fileList = this.pendingDiagnostics.getFileList(); // Add all open TS buffers to the geterr request. They might be visible - for (const file of this.syncedBuffers.allResources) { - if (!this.pendingDiagnostics.get(file)) { - files.add(file); + for (const buffer of this.syncedBuffers.values) { + if (!this.pendingDiagnostics.has(buffer.resource)) { + fileList.add(buffer.filepath); } } if (this.pendingGetErr) { for (const file of this.pendingGetErr.files) { - files.add(file); + fileList.add(file); } } - if (files.size) { - const fileList = Array.from(files); - const args: Proto.GeterrRequestArgs = { - delay: 0, - files: fileList - }; - const token = new CancellationTokenSource(); + if (fileList.size) { + if (this.pendingGetErr) { + this.pendingGetErr.cancel(); + } - const getErr = this.pendingGetErr = { - request: this.client.executeAsync('geterr', args, token.token) - .then(undefined, () => { }) - .then(() => { - if (this.pendingGetErr === getErr) { - this.pendingGetErr = undefined; - } - }), - files: fileList, - token - }; + const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, Array.from(fileList), () => { + if (this.pendingGetErr === getErr) { + this.pendingGetErr = undefined; + } + }); } + this.pendingDiagnostics.clear(); } @@ -363,8 +383,4 @@ export default class BufferSyncSupport { return this._validateTypeScript; } } - - private normalizePath(path: Uri): string | null { - return this.client.normalizedPath(path); - } } diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index 2c4ece54764..cb7e8c89cd5 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -306,12 +306,23 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider triggerCharacter: context.triggerCharacter as Proto.CompletionsTriggerCharacter }; - let msg: Proto.CompletionEntry[] | undefined = undefined; + let msg: ReadonlyArray | undefined = undefined; try { - const response = await this.client.execute('completions', args, token); - msg = response.body; - if (!msg) { - return []; + if (this.client.apiVersion.gte(API.v300)) { + const response = await this.client.execute('completionInfo', args, token); + if (!response.body) { + return []; + } + if (response.body.isNewIdentifierLocation) { + return []; + } + msg = response.body.entries; + } else { + const response = await this.client.execute('completions', args, token); + if (!response.body) { + return []; + } + msg = response.body; } } catch { return []; diff --git a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts index 0cb8c3da539..9bdc174b858 100644 --- a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts @@ -7,8 +7,8 @@ import { workspace as Workspace, FormattingOptions, TextDocument, CancellationTo import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; -import * as languageIds from '../utils/languageModeIds'; import API from '../utils/api'; +import { isTypeScriptDocument } from '../utils/languageModeIds'; function objsAreEqual(a: T, b: T): boolean { let keys = Object.keys(a); @@ -175,8 +175,4 @@ function getImportModuleSpecifierPreference(config: WorkspaceConfiguration) { case 'non-relative': return 'non-relative'; default: return undefined; } -} - -function isTypeScriptDocument(document: TextDocument) { - return document.languageId === languageIds.typescript || document.languageId === languageIds.typescriptreact; -} +} \ No newline at end of file diff --git a/extensions/typescript-language-features/src/features/formatting.ts b/extensions/typescript-language-features/src/features/formatting.ts index e0556c99ec8..029db28f870 100644 --- a/extensions/typescript-language-features/src/features/formatting.ts +++ b/extensions/typescript-language-features/src/features/formatting.ts @@ -51,17 +51,11 @@ class TypeScriptFormattingProvider implements vscode.DocumentRangeFormattingEdit options: vscode.FormattingOptions, token: vscode.CancellationToken ): Promise { - const absPath = this.client.toPath(document.uri); - if (!absPath) { + const file = this.client.toPath(document.uri); + if (!file) { return []; } - const args: Proto.FormatRequestArgs = { - file: absPath, - line: range.start.line + 1, - offset: range.start.character + 1, - endLine: range.end.line + 1, - endOffset: range.end.character + 1 - }; + const args = typeConverters.Range.toFormattingRequestArgs(file, range); return this.doFormat(document, options, args, token); } diff --git a/extensions/typescript-language-features/src/features/resourceMap.ts b/extensions/typescript-language-features/src/features/resourceMap.ts index f2b9c6fd711..73d89182315 100644 --- a/extensions/typescript-language-features/src/features/resourceMap.ts +++ b/extensions/typescript-language-features/src/features/resourceMap.ts @@ -45,12 +45,16 @@ export class ResourceMap { } } + public clear() { + this._map.clear(); + } + public get values(): Iterable { return this._map.values(); } - public get keys(): Iterable { - return this._map.keys(); + public get entries() { + return this._map.entries(); } private toKey(resource: Uri): string | null { diff --git a/extensions/typescript-language-features/src/features/tagClosing.ts b/extensions/typescript-language-features/src/features/tagClosing.ts new file mode 100644 index 00000000000..66af4920ef9 --- /dev/null +++ b/extensions/typescript-language-features/src/features/tagClosing.ts @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ConditionalRegistration, ConfigurationDependentRegistration, VersionDependentRegistration } from '../utils/dependentRegistration'; +import { disposeAll } from '../utils/dispose'; +import * as typeConverters from '../utils/typeConverters'; + +class TagClosing { + + private _disposed = false; + private _timeout: NodeJS.Timer | undefined = undefined; + private _cancel: vscode.CancellationTokenSource | undefined = undefined; + private readonly _disposables: vscode.Disposable[] = []; + + constructor( + private readonly client: ITypeScriptServiceClient + ) { + vscode.workspace.onDidChangeTextDocument( + event => this.onDidChangeTextDocument(event.document, event.contentChanges), + null, + this._disposables); + } + + public dispose() { + this._disposed = true; + + disposeAll(this._disposables); + + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = undefined; + } + + if (this._cancel) { + this._cancel.cancel(); + this._cancel.dispose(); + this._cancel = undefined; + } + } + + private onDidChangeTextDocument( + document: vscode.TextDocument, + changes: vscode.TextDocumentContentChangeEvent[] + ) { + const activeDocument = vscode.window.activeTextEditor && vscode.window.activeTextEditor.document; + if (document !== activeDocument || changes.length === 0) { + return; + } + + const filepath = this.client.toPath(document.uri); + if (!filepath) { + return; + } + + if (typeof this._timeout !== 'undefined') { + clearTimeout(this._timeout); + } + + if (this._cancel) { + this._cancel.cancel(); + this._cancel.dispose(); + this._cancel = undefined; + } + + const lastChange = changes[changes.length - 1]; + const lastCharacter = lastChange.text[lastChange.text.length - 1]; + if (lastChange.rangeLength > 0 || lastCharacter !== '>' && lastCharacter !== '/') { + return; + } + + const secondToLastCharacter = lastChange.text[lastChange.text.length - 2]; + if (secondToLastCharacter === '>') { + return; + } + + const rangeStart = lastChange.range.start; + const version = document.version; + this._timeout = setTimeout(async () => { + this._timeout = undefined; + + if (this._disposed) { + return; + } + + let position = new vscode.Position(rangeStart.line, rangeStart.character + lastChange.text.length); + let body: Proto.TextInsertion | undefined = undefined; + const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position); + + this._cancel = new vscode.CancellationTokenSource(); + try { + const response = await this.client.execute('jsxClosingTag', args, this._cancel.token); + body = response && response.body; + if (!body) { + return; + } + } catch { + return; + } + + if (this._disposed) { + return; + } + + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + + const activeDocument = activeEditor.document; + if (document === activeDocument && activeDocument.version === version) { + activeEditor.insertSnippet( + this.getTagSnippet(body), + this.getInsertionPositions(activeEditor, position)); + } + }, 100); + } + + private getTagSnippet(closingTag: Proto.TextInsertion): vscode.SnippetString { + const snippet = new vscode.SnippetString(); + snippet.appendPlaceholder('', 0); + snippet.appendText(closingTag.newText); + return snippet; + } + + private getInsertionPositions(editor: vscode.TextEditor, position: vscode.Position) { + const activeSelectionPositions = editor.selections.map(s => s.active); + return activeSelectionPositions.some(p => p.isEqual(position)) + ? activeSelectionPositions + : position; + } +} + +export class ActiveDocumentDependentRegistration { + private readonly _registration: ConditionalRegistration; + private readonly _disposables: vscode.Disposable[] = []; + + constructor( + private readonly selector: vscode.DocumentSelector, + register: () => vscode.Disposable, + ) { + this._registration = new ConditionalRegistration(register); + vscode.window.onDidChangeActiveTextEditor(this.update, this, this._disposables); + this.update(); + } + + public dispose() { + disposeAll(this._disposables); + this._registration.dispose(); + } + + private update() { + const editor = vscode.window.activeTextEditor; + const enabled = !!(editor && vscode.languages.match(this.selector, editor.document)); + this._registration.update(enabled); + } +} + +export function register( + selector: vscode.DocumentSelector, + modeId: string, + client: ITypeScriptServiceClient, +) { + return new VersionDependentRegistration(client, API.v300, () => + new ConfigurationDependentRegistration(modeId, 'autoClosingTags', () => + new ActiveDocumentDependentRegistration(selector, () => + new TagClosing(client)))); +} diff --git a/extensions/typescript-language-features/src/features/tagCompletion.ts b/extensions/typescript-language-features/src/features/tagCompletion.ts deleted file mode 100644 index 8a8495180ca..00000000000 --- a/extensions/typescript-language-features/src/features/tagCompletion.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as 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 TypeScriptTagCompletion implements vscode.CompletionItemProvider { - constructor( - private readonly client: ITypeScriptServiceClient - ) { } - - async provideCompletionItems( - document: vscode.TextDocument, - position: vscode.Position, - token: vscode.CancellationToken, - _context: vscode.CompletionContext - ): Promise { - const filepath = this.client.toPath(document.uri); - if (!filepath) { - return undefined; - } - - const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position); - let body: Proto.TextInsertion | undefined = undefined; - try { - const response = await this.client.execute('jsxClosingTag', args, token); - body = response && response.body; - if (!body) { - return undefined; - } - } catch { - return undefined; - } - - return [this.getCompletion(body)]; - } - - private getCompletion(body: Proto.TextInsertion) { - const completion = new vscode.CompletionItem(body.newText); - completion.insertText = this.getTagSnippet(body); - return completion; - } - - private getTagSnippet(closingTag: Proto.TextInsertion): vscode.SnippetString { - const snippet = new vscode.SnippetString(); - snippet.appendPlaceholder('', 0); - snippet.appendText(closingTag.newText); - return snippet; - } -} - -export function register( - selector: vscode.DocumentSelector, - client: ITypeScriptServiceClient, -) { - return new VersionDependentRegistration(client, API.v300, () => - vscode.languages.registerCompletionItemProvider(selector, - new TypeScriptTagCompletion(client), - '>')); -} diff --git a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts index 01cf885a5e6..9b2f3c3ce5f 100644 --- a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts +++ b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts @@ -10,11 +10,11 @@ import * as nls from 'vscode-nls'; import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; -import * as languageIds from '../utils/languageModeIds'; +import * as fileSchemes from '../utils/fileSchemes'; +import { isTypeScriptDocument } from '../utils/languageModeIds'; +import { escapeRegExp } from '../utils/regexp'; import * as typeConverters from '../utils/typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; -import * as fileSchemes from '../utils/fileSchemes'; -import { escapeRegExp } from '../utils/regexp'; const localize = nls.loadMessageBundle(); @@ -83,14 +83,20 @@ export class UpdateImportsOnFileRenameHandler { this.client.bufferSyncSupport.closeResource(targetResource); this.client.bufferSyncSupport.openTextDocument(document); - // Workaround for https://github.com/Microsoft/vscode/issues/52967 - // Never attempt to update import paths if the file does not contain something the looks like an export - const tree = await this.client.execute('navtree', { file: newFile }); - const hasExport = (node: Proto.NavigationTree): boolean => { - return !!node.kindModifiers.match(/\bexport\b/g) || !!(node.childItems && node.childItems.some(hasExport)); - }; - if (!tree.body || !tree.body || !hasExport(tree.body)) { - return; + if (!this.client.apiVersion.gte(API.v300) && !fs.lstatSync(newResource.fsPath).isDirectory()) { + // Workaround for https://github.com/Microsoft/vscode/issues/52967 + // Never attempt to update import paths if the file does not contain something the looks like an export + try { + const tree = await this.client.execute('navtree', { file: newFile }); + const hasExport = (node: Proto.NavigationTree): boolean => { + return !!node.kindModifiers.match(/\bexports?\b/g) || !!(node.childItems && node.childItems.some(hasExport)); + }; + if (!tree.body || !tree.body || !hasExport(tree.body)) { + return; + } + } catch { + // noop + } } const edits = await this.getEditsForFileRename(targetFile, document, oldFile, newFile); @@ -203,7 +209,12 @@ export class UpdateImportsOnFileRenameHandler { return undefined; } - if (this.client.apiVersion.gte(API.v292) && fs.lstatSync(resource.fsPath).isDirectory()) { + const isDirectory = fs.lstatSync(resource.fsPath).isDirectory(); + if (isDirectory && this.client.apiVersion.gte(API.v300)) { + return resource; + } + + if (isDirectory && this.client.apiVersion.gte(API.v292)) { const files = await vscode.workspace.findFiles({ base: resource.fsPath, pattern: '**/*.{ts,tsx,js,jsx}', @@ -293,6 +304,3 @@ export class UpdateImportsOnFileRenameHandler { } } -function isTypeScriptDocument(document: vscode.TextDocument) { - return document.languageId === languageIds.typescript || document.languageId === languageIds.typescriptreact; -} diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 2d7cde5e693..c49323457ae 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -91,7 +91,7 @@ export default class LanguageProvider { this.disposables.push((await import('./features/referencesCodeLens')).register(selector, this.description.id, this.client, cachedResponse)); this.disposables.push((await import('./features/rename')).register(selector, this.client)); this.disposables.push((await import('./features/signatureHelp')).register(selector, this.client)); - this.disposables.push((await import('./features/tagCompletion')).register(selector, this.client)); + this.disposables.push((await import('./features/tagClosing')).register(selector, this.description.id, this.client)); this.disposables.push((await import('./features/typeDefinitions')).register(selector, this.client)); this.disposables.push((await import('./features/workspaceSymbols')).register(this.client, this.description.modeIds)); } diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index 729bda6af6c..709ec47bf5d 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -51,6 +51,7 @@ export interface ITypeScriptServiceClient { execute(command: 'change', args: Proto.ChangeRequestArgs, expectedResult: boolean, token?: CancellationToken): Promise; execute(command: 'quickinfo', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; execute(command: 'completions', args: Proto.CompletionsRequestArgs, token?: CancellationToken): Promise; + execute(command: 'completionInfo', args: Proto.CompletionsRequestArgs, token?: CancellationToken): Promise; execute(command: 'completionEntryDetails', args: Proto.CompletionDetailsRequestArgs, token?: CancellationToken): Promise; execute(command: 'signatureHelp', args: Proto.SignatureHelpRequestArgs, token?: CancellationToken): Promise; execute(command: 'definition', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; diff --git a/extensions/typescript-language-features/src/utils/dependentRegistration.ts b/extensions/typescript-language-features/src/utils/dependentRegistration.ts index 9013dd28bc9..232d6b8e2cf 100644 --- a/extensions/typescript-language-features/src/utils/dependentRegistration.ts +++ b/extensions/typescript-language-features/src/utils/dependentRegistration.ts @@ -8,7 +8,7 @@ import { ITypeScriptServiceClient } from '../typescriptService'; import API from './api'; import { disposeAll } from './dispose'; -class ConditionalRegistration { +export class ConditionalRegistration { private registration: vscode.Disposable | undefined = undefined; public constructor( @@ -75,12 +75,8 @@ export class ConfigurationDependentRegistration { register: () => vscode.Disposable, ) { this._registration = new ConditionalRegistration(register); - this.update(); - - vscode.workspace.onDidChangeConfiguration(() => { - this.update(); - }, null, this._disposables); + vscode.workspace.onDidChangeConfiguration(this.update, this, this._disposables); } public dispose() { diff --git a/extensions/typescript-language-features/src/utils/languageModeIds.ts b/extensions/typescript-language-features/src/utils/languageModeIds.ts index 5fe187e13db..9be48067eda 100644 --- a/extensions/typescript-language-features/src/utils/languageModeIds.ts +++ b/extensions/typescript-language-features/src/utils/languageModeIds.ts @@ -14,4 +14,8 @@ export const jsxTags = 'jsx-tags'; export function isSupportedLanguageMode(doc: vscode.TextDocument) { return vscode.languages.match([typescript, typescriptreact, javascript, javascriptreact], doc) > 0; +} + +export function isTypeScriptDocument(doc: vscode.TextDocument) { + return vscode.languages.match([typescript, typescriptreact], doc) > 0; } \ No newline at end of file diff --git a/extensions/typescript-language-features/src/utils/typeConverters.ts b/extensions/typescript-language-features/src/utils/typeConverters.ts index 8c87338893d..44ad7f4ae5e 100644 --- a/extensions/typescript-language-features/src/utils/typeConverters.ts +++ b/extensions/typescript-language-features/src/utils/typeConverters.ts @@ -24,6 +24,14 @@ export namespace Range { endLine: range.end.line + 1, endOffset: range.end.character + 1 }); + + export const toFormattingRequestArgs = (file: string, range: vscode.Range): Proto.FormatRequestArgs => ({ + file, + line: range.start.line + 1, + offset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 + }); } export namespace Position { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 2e9fe0d0bbf..03548116a3b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -499,17 +499,16 @@ suite('workspace-namespace', () => { }); }); - // TODO@Joh this test fails randomly - // test('findFiles, cancellation', () => { + test('findFiles, cancellation', () => { - // const source = new CancellationTokenSource(); - // const token = source.token; // just to get an instance first - // source.cancel(); + const source = new vscode.CancellationTokenSource(); + const token = source.token; // just to get an instance first + source.cancel(); - // return vscode.workspace.findFiles('*.js', null, 100, token).then((res) => { - // assert.equal(res, void 0); - // }); - // }); + return vscode.workspace.findFiles('*.js', null, 100, token).then((res) => { + assert.deepEqual(res, []); + }); + }); test('findTextInFiles', async () => { const results: vscode.TextSearchResult[] = []; diff --git a/package.json b/package.json index 695afb3d60c..c62aec03bce 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.26.0", - "distro": "7ee5cda9f4f44b2ba1f1ab290e48f86416914794", + "distro": "655112e16611a8427ba96dbd6de2b05ecf2e3f6b", "author": { "name": "Microsoft Corporation" }, @@ -41,7 +41,7 @@ "native-watchdog": "0.3.0", "node-pty": "0.7.6", "semver": "^5.5.0", - "spdlog": "0.6.0", + "spdlog": "0.7.1", "sudo-prompt": "8.2.0", "v8-inspect-profiler": "^0.0.8", "vscode-chokidar": "1.6.2", diff --git a/product.json b/product.json index d25b0eca0f2..d16dca66ecd 100644 --- a/product.json +++ b/product.json @@ -4,6 +4,7 @@ "applicationName": "code-oss", "dataFolderName": ".vscode-oss", "win32MutexName": "vscodeoss", + "enableTelemetry": true, "licenseName": "MIT", "licenseUrl": "https://github.com/Microsoft/vscode/blob/master/LICENSE.txt", "win32DirName": "Microsoft Code OSS", @@ -22,4 +23,4 @@ "ms-vscode.node-debug", "ms-vscode.node-debug2" ] -} \ No newline at end of file +} diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 02b3d4115ea..e8ed9bec491 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -840,6 +840,8 @@ export const EventType = { SUBMIT: 'submit', RESET: 'reset', FOCUS: 'focus', + FOCUS_IN: 'focusin', + FOCUS_OUT: 'focusout', BLUR: 'blur', INPUT: 'input', // Local Storage diff --git a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css index a7f820dee40..8d276f8d2a0 100644 --- a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css @@ -9,6 +9,7 @@ flex-direction: row; flex-wrap: nowrap; justify-content: flex-start; + outline-style: none; } .monaco-breadcrumbs .monaco-breadcrumb-item { @@ -16,13 +17,14 @@ align-items: center; flex: 0 1 auto; white-space: nowrap; - cursor: default; + cursor: pointer; align-self: center; height: 100%; } .monaco-breadcrumbs .monaco-breadcrumb-item:not(:last-child)::after { background-image: url(./collapsed.svg); + opacity: .7; width: 16px; height: 16px; display: inline-block; @@ -35,6 +37,3 @@ background-image: url(./collpased-dark.svg); } -.monaco-breadcrumbs .monaco-breadcrumb-item.focused { - font-weight: bold; -} diff --git a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts index 29cee352905..68f21480418 100644 --- a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -44,11 +44,16 @@ export class SimpleBreadcrumbsItem extends BreadcrumbsItem { export interface IBreadcrumbsWidgetStyles { breadcrumbsBackground?: Color; - breadcrumbsActiveForeground?: Color; - breadcrumbsInactiveForeground?: Color; + breadcrumbsForeground?: Color; + breadcrumbsHoverBackground?: Color; + breadcrumbsHoverForeground?: Color; + breadcrumbsFocusForeground?: Color; + breadcrumbsFocusAndSelectionBackground?: Color; + breadcrumbsFocusAndSelectionForeground?: Color; } export interface IBreadcrumbsItemEvent { + type: 'select' | 'focus'; item: BreadcrumbsItem; node: HTMLElement; } @@ -122,11 +127,23 @@ export class BreadcrumbsWidget { if (style.breadcrumbsBackground) { content += `.monaco-breadcrumbs { background-color: ${style.breadcrumbsBackground}}`; } - if (style.breadcrumbsActiveForeground) { - content += `.monaco-breadcrumbs:focus .monaco-breadcrumb-item { color: ${style.breadcrumbsActiveForeground}}\n`; + if (style.breadcrumbsForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item { color: ${style.breadcrumbsForeground}}\n`; } - if (style.breadcrumbsInactiveForeground) { - content += `.monaco-breadcrumbs .monaco-breadcrumb-item { color: ${style.breadcrumbsInactiveForeground}}\n`; + if (style.breadcrumbsFocusForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused { color: ${style.breadcrumbsFocusForeground}}\n`; + } + if (style.breadcrumbsFocusAndSelectionBackground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused.selected { background-color: ${style.breadcrumbsFocusAndSelectionBackground}}\n`; + } + if (style.breadcrumbsFocusAndSelectionForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused.selected { color: ${style.breadcrumbsFocusAndSelectionForeground}}\n`; + } + if (style.breadcrumbsHoverBackground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item:hover:not(.focused):not(.selected) { background-color: ${style.breadcrumbsHoverBackground}}\n`; + } + if (style.breadcrumbsHoverForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item:hover:not(.focused):not(.selected) { color: ${style.breadcrumbsHoverForeground}}\n`; } if (this._styleElement.innerHTML !== content) { this._styleElement.innerHTML = content; @@ -139,6 +156,10 @@ export class BreadcrumbsWidget { this._domNode.focus(); } + isDOMFocused(): boolean { + return this._domNode === document.activeElement; + } + getFocused(): BreadcrumbsItem { return this._items[this._focusedItemIdx]; } @@ -148,13 +169,15 @@ export class BreadcrumbsWidget { } focusPrev(): any { - this._focus((this._focusedItemIdx - 1 + this._nodes.length) % this._nodes.length); - this._domNode.focus(); + if (this._focusedItemIdx > 0) { + this._focus(this._focusedItemIdx - 1); + } } focusNext(): any { - this._focus((this._focusedItemIdx + 1) % this._nodes.length); - this._domNode.focus(); + if (this._focusedItemIdx + 1 < this._nodes.length) { + this._focus(this._focusedItemIdx + 1); + } } private _focus(nth: number): void { @@ -168,14 +191,29 @@ export class BreadcrumbsWidget { dom.addClass(node, 'focused'); } } - this._onDidFocusItem.fire({ item: this._items[this._focusedItemIdx], node: this._nodes[this._focusedItemIdx] }); + this._reveal(this._focusedItemIdx); + this._onDidFocusItem.fire({ type: 'focus', item: this._items[this._focusedItemIdx], node: this._nodes[this._focusedItemIdx] }); } - getSelected(): BreadcrumbsItem { + reveal(item: BreadcrumbsItem): void { + let idx = this._items.indexOf(item); + if (idx >= 0) { + this._reveal(idx); + } + } + + private _reveal(nth: number): void { + const node = this._nodes[nth]; + if (node) { + this._scrollable.setScrollPosition({ scrollLeft: node.offsetLeft }); + } + } + + getSelection(): BreadcrumbsItem { return this._items[this._selectedItemIdx]; } - setSelected(item: BreadcrumbsItem): void { + setSelection(item: BreadcrumbsItem): void { this._select(this._items.indexOf(item)); } @@ -190,7 +228,7 @@ export class BreadcrumbsWidget { dom.addClass(node, 'selected'); } } - this._onDidSelectItem.fire({ item: this._items[this._selectedItemIdx], node: this._nodes[this._selectedItemIdx] }); + this._onDidSelectItem.fire({ type: 'select', item: this._items[this._selectedItemIdx], node: this._nodes[this._selectedItemIdx] }); } setItems(items: BreadcrumbsItem[]): void { @@ -198,9 +236,7 @@ export class BreadcrumbsWidget { let removed = this._items.splice(prefix, this._items.length - prefix, ...items.slice(prefix)); this._render(prefix); dispose(removed); - if (prefix >= this._focusedItemIdx) { - this._focus(-1); - } + this._focus(-1); } private _render(start: number): void { diff --git a/src/vs/base/browser/ui/contextview/contextview.css b/src/vs/base/browser/ui/contextview/contextview.css index 84942adb3e6..af3ead17a6b 100644 --- a/src/vs/base/browser/ui/contextview/contextview.css +++ b/src/vs/base/browser/ui/contextview/contextview.css @@ -5,5 +5,5 @@ .context-view { position: absolute; - z-index: 1000; + z-index: 2000; } \ No newline at end of file diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index df2fbfde7d3..37ff5439232 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -5,7 +5,7 @@ import { GestureEvent } from 'vs/base/browser/touch'; -export interface IDelegate { +export interface IVirtualDelegate { getHeight(element: T): number; getTemplateId(element: T): string; } @@ -14,6 +14,7 @@ export interface IRenderer { templateId: string; renderTemplate(container: HTMLElement): TTemplateData; renderElement(element: TElement, index: number, templateData: TTemplateData): void; + disposeElement(element: TElement, index: number, templateData: TTemplateData): void; disposeTemplate(templateData: TTemplateData): void; } diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 53ea8be6a37..10094f4e5a6 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -6,7 +6,7 @@ import 'vs/css!./list'; import { IDisposable } from 'vs/base/common/lifecycle'; import { range } from 'vs/base/common/arrays'; -import { IDelegate, IRenderer, IListEvent, IListOpenEvent } from './list'; +import { IVirtualDelegate, IRenderer, IListEvent, IListOpenEvent } from './list'; import { List, IListStyles, IListOptions } from './listWidget'; import { IPagedModel } from 'vs/base/common/paging'; import { Event, mapEvent } from 'vs/base/common/event'; @@ -50,6 +50,10 @@ class PagedRenderer implements IRenderer this.renderer.renderElement(entry, index, data.data)); } + disposeElement(): void { + // noop + } + disposeTemplate(data: ITemplateData): void { data.disposable.dispose(); data.disposable = null; @@ -65,12 +69,12 @@ export class PagedList implements IDisposable { constructor( container: HTMLElement, - delegate: IDelegate, + virtualDelegate: IVirtualDelegate, renderers: IPagedRenderer[], options: IListOptions = {} ) { const pagedRenderers = renderers.map(r => new PagedRenderer>(r, () => this.model)); - this.list = new List(container, delegate, pagedRenderers, options); + this.list = new List(container, virtualDelegate, pagedRenderers, options); } getHTMLElement(): HTMLElement { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index bbfe7f0b134..389493026ec 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -12,7 +12,7 @@ import { domEvent } from 'vs/base/browser/event'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollEvent, ScrollbarVisibility } from 'vs/base/common/scrollable'; import { RangeMap, IRange, relativeComplement, intersect, shift } from './rangeMap'; -import { IDelegate, IRenderer, IListMouseEvent, IListTouchEvent, IListGestureEvent } from './list'; +import { IVirtualDelegate, IRenderer, IListMouseEvent, IListTouchEvent, IListGestureEvent } from './list'; import { RowCache, IRow } from './rowCache'; import { isWindows } from 'vs/base/common/platform'; import * as browser from 'vs/base/browser/browser'; @@ -72,7 +72,7 @@ export class ListView implements ISpliceable, IDisposable { constructor( container: HTMLElement, - private delegate: IDelegate, + private virtualDelegate: IVirtualDelegate, renderers: IRenderer[], options: IListViewOptions = DefaultOptions ) { @@ -156,8 +156,8 @@ export class ListView implements ISpliceable, IDisposable { const inserted = elements.map>(element => ({ id: String(this.itemId++), element, - size: this.delegate.getHeight(element), - templateId: this.delegate.getTemplateId(element), + size: this.virtualDelegate.getHeight(element), + templateId: this.virtualDelegate.getTemplateId(element), row: null })); @@ -322,6 +322,12 @@ export class ListView implements ISpliceable, IDisposable { throw new Error(`Got index ${index} and there are ${this.items.length} items. File issue to joao!`); } + const renderer = this.renderers.get(item.templateId); + + if (renderer.disposeElement) { + renderer.disposeElement(item.element, index, item.row.templateData); + } + this.cache.release(item.row); item.row = null; } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index d297818a787..f8ffd1a0feb 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -15,7 +15,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Event, Emitter, EventBufferer, chain, mapEvent, anyEvent } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; -import { IDelegate, IRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IListOpenEvent } from './list'; +import { IVirtualDelegate, IRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IListOpenEvent } from './list'; import { ListView, IListViewOptions } from './listView'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; @@ -68,6 +68,10 @@ class TraitRenderer implements IRenderer this.trait.renderIndex(index, templateData); } + disposeElement(): void { + // noop + } + splice(start: number, deleteCount: number, insertCount: number): void { const rendered: IRenderedContainer[] = []; @@ -807,6 +811,14 @@ class PipelineRenderer implements IRenderer { } } + disposeElement(element: T, index: number, templateData: any[]): void { + let i = 0; + + for (const renderer of this.renderers) { + renderer.disposeElement(element, index, templateData[i++]); + } + } + disposeTemplate(templateData: any[]): void { let i = 0; @@ -871,7 +883,7 @@ export class List implements ISpliceable, IDisposable { constructor( container: HTMLElement, - delegate: IDelegate, + virtualDelegate: IVirtualDelegate, renderers: IRenderer[], options: IListOptions = DefaultOptions ) { @@ -882,7 +894,7 @@ export class List implements ISpliceable, IDisposable { renderers = renderers.map(r => new PipelineRenderer(r.templateId, [this.focus.renderer, this.selection.renderer, r])); - this.view = new ListView(container, delegate, renderers, options); + this.view = new ListView(container, virtualDelegate, renderers, options); this.view.domNode.setAttribute('role', 'tree'); DOM.addClass(this.view.domNode, this.idPrefix); this.view.domNode.tabIndex = 0; @@ -929,6 +941,14 @@ export class List implements ISpliceable, IDisposable { } splice(start: number, deleteCount: number, elements: T[] = []): void { + if (start < 0 || start > this.view.length) { + throw new Error(`Invalid start index: ${start}`); + } + + if (deleteCount < 0) { + throw new Error(`Invalid delete count: ${deleteCount}`); + } + if (deleteCount === 0 && elements.length === 0) { return; } diff --git a/src/vs/base/browser/ui/list/splice.ts b/src/vs/base/browser/ui/list/splice.ts index e65be2e8432..254e25d6a7b 100644 --- a/src/vs/base/browser/ui/list/splice.ts +++ b/src/vs/base/browser/ui/list/splice.ts @@ -5,9 +5,7 @@ 'use strict'; -export interface ISpliceable { - splice(start: number, deleteCount: number, elements: T[]): void; -} +import { ISpliceable } from 'vs/base/common/sequence'; export interface ISpreadSpliceable { splice(start: number, deleteCount: number, ...elements: T[]): void; diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index f26829f9edd..3851600c6df 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -15,6 +15,7 @@ import { Event } from 'vs/base/common/event'; import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { $, Builder } from 'vs/base/browser/builder'; +import { RunOnceScheduler } from 'vs/base/common/async'; export interface IMenuOptions { context?: any; @@ -241,6 +242,8 @@ class SubmenuActionItem extends MenuActionItem { private mysubmenu: Menu; private submenuContainer: Builder; private mouseOver: boolean; + private showScheduler: RunOnceScheduler; + private hideScheduler: RunOnceScheduler; constructor( action: IAction, @@ -249,6 +252,20 @@ class SubmenuActionItem extends MenuActionItem { private submenuOptions?: IMenuOptions ) { super(action, action, { label: true, isMenu: true }); + + this.showScheduler = new RunOnceScheduler(() => { + if (this.mouseOver) { + this.cleanupExistingSubmenu(false); + this.createSubmenu(); + } + }, 250); + + this.hideScheduler = new RunOnceScheduler(() => { + if (!this.mouseOver && this.parentData.submenu === this.mysubmenu) { + this.parentData.parent.focus(); + this.cleanupExistingSubmenu(true); + } + }, 750); } public render(container: HTMLElement): void { @@ -278,26 +295,14 @@ class SubmenuActionItem extends MenuActionItem { if (!this.mouseOver) { this.mouseOver = true; - setTimeout(() => { - if (this.mouseOver) { - this.cleanupExistingSubmenu(false); - this.createSubmenu(); - } - }, 250); - + this.showScheduler.schedule(); } }); $(this.builder).on(EventType.MOUSE_LEAVE, (e) => { this.mouseOver = false; - setTimeout(() => { - if (!this.mouseOver && this.parentData.submenu === this.mysubmenu) { - this.parentData.parent.focus(); - this.cleanupExistingSubmenu(true); - } - - }, 750); + this.hideScheduler.schedule(); }); } @@ -360,6 +365,9 @@ class SubmenuActionItem extends MenuActionItem { public dispose() { super.dispose(); + this.hideScheduler.dispose(); + this.showScheduler.dispose(); + if (this.mysubmenu) { this.mysubmenu.dispose(); this.mysubmenu = null; diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index fa9bcedd7e3..0c80b4f4b92 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -14,7 +14,7 @@ import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { domEvent } from 'vs/base/browser/event'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ISelectBoxDelegate, ISelectBoxOptions, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox'; @@ -67,12 +67,16 @@ class SelectListRenderer implements IRenderer { +export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate { private static readonly DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN = 32; @@ -238,6 +242,8 @@ export class SelectBoxList implements ISelectBoxDelegate, IDelegate= 0) { + if (this.selected !== this._currentSelection) { + // Reset selected to current if no change this.select(this._currentSelection); } - this._onDidSelect.fire({ - index: this.selectElement.selectedIndex, - selected: this.selectElement.title - }); - this.hideSelectDropDown(false); } // List keyboard controller - // List exit - active - hide ContextView dropdown, return focus to parent select, fire onDidSelect + + // List exit - active - hide ContextView dropdown, reset selection, return focus to parent select private onEscape(e: StandardKeyboardEvent): void { dom.EventHelper.stop(e); + + // Reset selection to value when opened this.select(this._currentSelection); - this.hideSelectDropDown(true); - - this._onDidSelect.fire({ - index: this.selectElement.selectedIndex, - selected: this.selectElement.title - }); } - // List exit - active - hide ContextView dropdown, return focus to parent select, fire onDidSelect + // List exit - active - hide ContextView dropdown, return focus to parent select, fire onDidSelect if change private onEnter(e: StandardKeyboardEvent): void { dom.EventHelper.stop(e); - // Reset current selection - this._currentSelection = -1; + // Only fire if selection change + if (this.selected !== this._currentSelection) { + this._currentSelection = this.selected; + this._onDidSelect.fire({ + index: this.selectElement.selectedIndex, + selected: this.selectElement.title + }); + } this.hideSelectDropDown(true); - this._onDidSelect.fire({ - index: this.selectElement.selectedIndex, - selected: this.selectElement.title - }); } // List navigation - have to handle a disabled option (jump over) diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 6cd72d3311f..18a45d2c74c 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -4,15 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./tree'; -import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IListOptions, List, IIdentityProvider, IMultipleSelectionController } from 'vs/base/browser/ui/list/listWidget'; -import { TreeModel, ITreeNode, ITreeElement } from 'vs/base/browser/ui/tree/treeModel'; -import { IIterator, empty } from 'vs/base/common/iterator'; -import { IDelegate, IRenderer, IListMouseEvent } from 'vs/base/browser/ui/list/list'; +import { TreeModel, ITreeNode, ITreeElement, getNodeLocation } from 'vs/base/browser/ui/tree/treeModel'; +import { Iterator, ISequence } from 'vs/base/common/iterator'; +import { IVirtualDelegate, IRenderer, IListMouseEvent } from 'vs/base/browser/ui/list/list'; import { append, $ } from 'vs/base/browser/dom'; import { Event, Relay, chain } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; +import { tail2 } from 'vs/base/common/arrays'; function toTreeListOptions(options?: IListOptions): IListOptions> { if (!options) { @@ -44,9 +45,9 @@ function toTreeListOptions(options?: IListOptions): IListOptions implements IDelegate> { +class TreeDelegate implements IVirtualDelegate> { - constructor(private delegate: IDelegate) { } + constructor(private delegate: IVirtualDelegate) { } getHeight(element: ITreeNode): number { return this.delegate.getHeight(element.element); @@ -59,10 +60,17 @@ class TreeDelegate implements IDelegate> { interface ITreeListTemplateData { twistie: HTMLElement; - elementDisposable: IDisposable; templateData: T; } +function renderTwistie(node: ITreeNode, twistie: HTMLElement): void { + if (node.children.length === 0 && !node.collapsible) { + twistie.innerText = ''; + } else { + twistie.innerText = node.collapsed ? 'â–¹' : 'â—¢'; + } +} + class TreeRenderer implements IRenderer, ITreeListTemplateData> { readonly templateId: string; @@ -83,21 +91,22 @@ class TreeRenderer implements IRenderer, ITreeLis const contents = append(el, $('.tl-contents')); const templateData = this.renderer.renderTemplate(contents); - return { twistie, elementDisposable: Disposable.None, templateData }; + return { twistie, templateData }; } renderElement(node: ITreeNode, index: number, templateData: ITreeListTemplateData): void { - templateData.elementDisposable.dispose(); - this.renderedNodes.set(node, templateData); - templateData.elementDisposable = toDisposable(() => this.renderedNodes.delete(node)); - templateData.twistie.innerText = node.children.length === 0 ? '' : (node.collapsed ? 'â–¹' : 'â—¢'); templateData.twistie.style.width = `${10 + node.depth * 10}px`; + renderTwistie(node, templateData.twistie); this.renderer.renderElement(node.element, index, templateData.templateData); } + disposeElement(node: ITreeNode): void { + this.renderedNodes.delete(node); + } + disposeTemplate(templateData: ITreeListTemplateData): void { this.renderer.disposeTemplate(templateData.templateData); } @@ -109,7 +118,7 @@ class TreeRenderer implements IRenderer, ITreeLis return; } - templateData.twistie.innerText = node.children.length === 0 ? '' : (node.collapsed ? 'â–¹' : 'â—¢'); + renderTwistie(node, templateData.twistie); } dispose(): void { @@ -118,21 +127,12 @@ class TreeRenderer implements IRenderer, ITreeLis } } -function getLocation(node: ITreeNode): number[] { - const location = []; - - while (node.parent) { - location.push(node.parent.children.indexOf(node)); - node = node.parent; - } - - return location.reverse(); -} - function isInputElement(e: HTMLElement): boolean { return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA'; } +export interface ITreeOptions extends IListOptions { } + export class Tree implements IDisposable { private view: List>; @@ -141,9 +141,9 @@ export class Tree implements IDisposable { constructor( container: HTMLElement, - delegate: IDelegate, + delegate: IVirtualDelegate, renderers: IRenderer[], - options?: IListOptions + options?: ITreeOptions ) { const treeDelegate = new TreeDelegate(delegate); @@ -168,13 +168,13 @@ export class Tree implements IDisposable { onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables); } - splice(location: number[], deleteCount: number, toInsert: IIterator> = empty()): IIterator> { + splice(location: number[], deleteCount: number, toInsert: ISequence> = Iterator.empty()): Iterator> { return this.model.splice(location, deleteCount, toInsert); } private onMouseClick(e: IListMouseEvent>): void { const node = e.element; - const location = getLocation(node); + const location = getNodeLocation(node); this.model.toggleCollapsed(location); } @@ -190,12 +190,17 @@ export class Tree implements IDisposable { } const node = nodes[0]; - const location = getLocation(node); - const didCollapse = this.model.setCollapsed(location, true); + const location = getNodeLocation(node); + const didChange = this.model.setCollapsed(location, true); - if (!didCollapse) { - console.log('should focus parent'); - // this.view.setFocus([]); + if (!didChange) { + if (location.length === 1) { + return; + } + + const [parentLocation] = tail2(location); + const parentListIndex = this.model.getListIndex(parentLocation); + this.view.setFocus([parentListIndex]); } } @@ -210,8 +215,17 @@ export class Tree implements IDisposable { } const node = nodes[0]; - const location = getLocation(node); - this.model.setCollapsed(location, false); + const location = getNodeLocation(node); + const didChange = this.model.setCollapsed(location, false); + + if (!didChange) { + if (node.children.length === 0) { + return; + } + + const [focusedIndex] = this.view.getFocus(); + this.view.setFocus([focusedIndex + 1]); + } } private onSpace(e: StandardKeyboardEvent): void { @@ -225,7 +239,7 @@ export class Tree implements IDisposable { } const node = nodes[0]; - const location = getLocation(node); + const location = getNodeLocation(node); this.model.toggleCollapsed(location); } diff --git a/src/vs/base/browser/ui/tree/treeModel.ts b/src/vs/base/browser/ui/tree/treeModel.ts index 8d3c14b727b..1a30ff92292 100644 --- a/src/vs/base/browser/ui/tree/treeModel.ts +++ b/src/vs/base/browser/ui/tree/treeModel.ts @@ -6,12 +6,13 @@ 'use strict'; import { ISpliceable } from 'vs/base/common/sequence'; -import { IIterator, map, collect, iter, empty } from 'vs/base/common/iterator'; +import { Iterator, ISequence } from 'vs/base/common/iterator'; import { Emitter, Event } from 'vs/base/common/event'; export interface ITreeElement { readonly element: T; - readonly children?: IIterator> | ITreeElement[]; + readonly children?: Iterator> | ITreeElement[]; + readonly collapsible?: boolean; readonly collapsed?: boolean; } @@ -20,6 +21,7 @@ export interface ITreeNode { readonly element: T; readonly children: IMutableTreeNode[]; readonly depth: number; + readonly collapsible: boolean; readonly collapsed: boolean; readonly visibleCount: number; } @@ -49,11 +51,11 @@ function getVisibleNodes(nodes: IMutableTreeNode[], result: ITreeNode[] return result; } -function getTreeElementIterator(elements: IIterator> | ITreeElement[] | undefined): IIterator> { +function getTreeElementIterator(elements: Iterator> | ITreeElement[] | undefined): Iterator> { if (!elements) { - return empty(); + return Iterator.empty(); } else if (Array.isArray(elements)) { - return iter(elements); + return Iterator.iterate(elements); } else { return elements; } @@ -61,15 +63,16 @@ function getTreeElementIterator(elements: IIterator> | ITreeE function treeElementToNode(treeElement: ITreeElement, parent: IMutableTreeNode, visible: boolean, treeListElements: ITreeNode[]): IMutableTreeNode { const depth = parent.depth + 1; - const { element, collapsed } = treeElement; - const node = { parent, element, children: [], depth, collapsed: !!collapsed, visibleCount: 0 }; + const { element, collapsible, collapsed } = treeElement; + const node = { parent, element, children: [], depth, collapsible: !!collapsible, collapsed: !!collapsed, visibleCount: 0 }; if (visible) { treeListElements.push(node); } const children = getTreeElementIterator(treeElement.children); - node.children = collect(map(children, el => treeElementToNode(el, node, visible && !treeElement.collapsed, treeListElements))); + node.children = Iterator.collect(Iterator.map(children, el => treeElementToNode(el, node, visible && !treeElement.collapsed, treeListElements))); + node.collapsible = node.collapsible || node.children.length > 0; node.visibleCount = 1 + getVisibleCount(node.children); return node; @@ -77,11 +80,22 @@ function treeElementToNode(treeElement: ITreeElement, parent: IMutableTree function treeNodeToElement(node: IMutableTreeNode): ITreeElement { const { element, collapsed } = node; - const children = map(iter(node.children), treeNodeToElement); + const children = Iterator.map(Iterator.iterate(node.children), treeNodeToElement); return { element, children, collapsed }; } +export function getNodeLocation(node: ITreeNode): number[] { + const location = []; + + while (node.parent) { + location.push(node.parent.children.indexOf(node)); + node = node.parent; + } + + return location.reverse(); +} + export class TreeModel { private root: IMutableTreeNode = { @@ -89,6 +103,7 @@ export class TreeModel { element: undefined, children: [], depth: 0, + collapsible: false, collapsed: false, visibleCount: 1 }; @@ -98,7 +113,7 @@ export class TreeModel { constructor(private list: ISpliceable>) { } - splice(location: number[], deleteCount: number, toInsert?: IIterator> | ITreeElement[]): IIterator> { + splice(location: number[], deleteCount: number, toInsert?: ISequence>): Iterator> { if (location.length === 0) { throw new Error('Invalid tree location'); } @@ -106,7 +121,7 @@ export class TreeModel { const { parentNode, listIndex, visible } = this.findParentNode(location); const treeListElementsToInsert: ITreeNode[] = []; const elementsToInsert = getTreeElementIterator(toInsert); - const nodesToInsert = collect(map(elementsToInsert, el => treeElementToNode(el, parentNode, visible, treeListElementsToInsert))); + const nodesToInsert = Iterator.collect(Iterator.map(elementsToInsert, el => treeElementToNode(el, parentNode, visible, treeListElementsToInsert))); const lastIndex = location[location.length - 1]; const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert); const visibleDeleteCount = getVisibleCount(deletedNodes); @@ -117,7 +132,11 @@ export class TreeModel { this.list.splice(listIndex, visibleDeleteCount, treeListElementsToInsert); } - return map(iter(deletedNodes), treeNodeToElement); + return Iterator.map(Iterator.iterate(deletedNodes), treeNodeToElement); + } + + getListIndex(location: number[]): number { + return this.findNode(location).listIndex; } setCollapsed(location: number[], collapsed: boolean): boolean { @@ -131,6 +150,10 @@ export class TreeModel { private _setCollapsed(location: number[], collapsed?: boolean | undefined): boolean { const { node, listIndex, visible } = this.findNode(location); + if (!node.collapsible) { + return false; + } + if (typeof collapsed === 'undefined') { collapsed = !node.collapsed; } @@ -142,6 +165,8 @@ export class TreeModel { node.collapsed = collapsed; if (visible) { + this._onDidChangeCollapseState.fire(node); + let visibleCountDiff: number; if (collapsed) { @@ -162,8 +187,6 @@ export class TreeModel { mutableNode.visibleCount += visibleCountDiff; mutableNode = mutableNode.parent; } - - this._onDidChangeCollapseState.fire(node); } return true; diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index cd8d81b84f3..93365a9fb6a 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -247,8 +247,8 @@ const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else export type ParsedPattern = (path: string, basename?: string) => boolean; -// The ParsedExpression returns a Promise iff siblingsFn returns a Promise. -export type ParsedExpression = (path: string, basename?: string, siblingsFn?: () => string[] | TPromise) => string | TPromise /* the matching pattern */; +// The ParsedExpression returns a Promise iff hasSibling returns a Promise. +export type ParsedExpression = (path: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise) => string | TPromise /* the matching pattern */; export interface IGlobOptions { /** @@ -264,9 +264,8 @@ interface ParsedStringPattern { allBasenames?: string[]; allPaths?: string[]; } -type SiblingsPattern = { siblings: string[], name: string }; interface ParsedExpressionPattern { - (path: string, basename: string, siblingsPatternFn: () => SiblingsPattern | TPromise): string | TPromise /* the matching pattern */; + (path: string, basename: string, name: string, hasSibling: (name: string) => boolean | TPromise): string | TPromise /* the matching pattern */; requiresSiblings?: boolean; allBasenames?: string[]; allPaths?: string[]; @@ -436,13 +435,13 @@ function toRegExp(pattern: string): ParsedStringPattern { * - character ranges (using [...]) */ export function match(pattern: string | IRelativePattern, path: string): boolean; -export function match(expression: IExpression, path: string, siblingsFn?: () => string[]): string /* the matching pattern */; -export function match(arg1: string | IExpression | IRelativePattern, path: string, siblingsFn?: () => string[]): any { +export function match(expression: IExpression, path: string, hasSibling?: (name: string) => boolean): string /* the matching pattern */; +export function match(arg1: string | IExpression | IRelativePattern, path: string, hasSibling?: (name: string) => boolean): any { if (!arg1 || !path) { return false; } - return parse(arg1)(path, undefined, siblingsFn); + return parse(arg1)(path, undefined, hasSibling); } /** @@ -482,6 +481,44 @@ export function parse(arg1: string | IExpression | IRelativePattern, options: IG return parsedExpression(arg1, options); } +export function hasSiblingPromiseFn(siblingsFn?: () => TPromise) { + if (!siblingsFn) { + return undefined; + } + + let siblings: TPromise>; + return (name: string) => { + if (!siblings) { + siblings = (siblingsFn() || TPromise.as([])) + .then(list => list ? listToMap(list) : {}); + } + return siblings.then(map => !!map[name]); + }; +} + +export function hasSiblingFn(siblingsFn?: () => string[]) { + if (!siblingsFn) { + return undefined; + } + + let siblings: Record; + return (name: string) => { + if (!siblings) { + const list = siblingsFn(); + siblings = list ? listToMap(list) : {}; + } + return !!siblings[name]; + }; +} + +function listToMap(list: string[]) { + const map: Record = {}; + for (const key of list) { + map[key] = true; + } + return map; +} + export function isRelativePattern(obj: any): obj is IRelativePattern { const rp = obj as IRelativePattern; @@ -493,8 +530,8 @@ export function isRelativePattern(obj: any): obj is IRelativePattern { */ export function parseToAsync(expression: IExpression, options?: IGlobOptions): ParsedExpression { const parsedExpression = parse(expression, options); - return (path: string, basename?: string, siblingsFn?: () => string[] | TPromise): string | TPromise => { - const result = parsedExpression(path, basename, siblingsFn); + return (path: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise): string | TPromise => { + const result = parsedExpression(path, basename, hasSibling); return result instanceof TPromise ? result : TPromise.as(result); }; } @@ -522,7 +559,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return parsedPatterns[0]; } - const resultExpression: ParsedStringPattern = function (path: string, basename: string, siblingsFn?: () => string[]) { + const resultExpression: ParsedStringPattern = function (path: string, basename: string) { for (let i = 0, n = parsedPatterns.length; i < n; i++) { // Pattern matches path const result = (parsedPatterns[i])(path, basename); @@ -547,38 +584,21 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return resultExpression; } - const resultExpression: ParsedStringPattern = function (path: string, basename: string, siblingsFn?: () => string[] | TPromise) { - let siblingsPattern: SiblingsPattern | TPromise; - let siblingsResolved = !siblingsFn; - - function siblingsToSiblingsPattern(siblings: string[]) { - if (siblings && siblings.length) { - if (!basename) { - basename = paths.basename(path); - } - const name = basename.substr(0, basename.length - paths.extname(path).length); - return { siblings, name }; - } - - return undefined; - } - - function siblingsPatternFn() { - // Resolve siblings only once - if (!siblingsResolved) { - siblingsResolved = true; - const siblings = siblingsFn(); - siblingsPattern = TPromise.is(siblings) ? - siblings.then(siblingsToSiblingsPattern) : - siblingsToSiblingsPattern(siblings); - } - - return siblingsPattern; - } + const resultExpression: ParsedStringPattern = function (path: string, basename: string, hasSibling?: (name: string) => boolean | TPromise) { + let name: string; for (let i = 0, n = parsedPatterns.length; i < n; i++) { // Pattern matches path - const result = (parsedPatterns[i])(path, basename, siblingsPatternFn); + const parsedPattern = (parsedPatterns[i]); + if (parsedPattern.requiresSiblings && hasSibling) { + if (!basename) { + basename = paths.basename(path); + } + if (!name) { + name = basename.substr(0, basename.length - paths.extname(path).length); + } + } + const result = parsedPattern(path, basename, name, hasSibling); if (result) { return result; } @@ -619,28 +639,16 @@ function parseExpressionPattern(pattern: string, value: any, options: IGlobOptio if (value) { const when = (value).when; if (typeof when === 'string') { - const siblingsPatternToMatchingPattern = (siblingsPattern: SiblingsPattern): string => { - let clausePattern = when.replace('$(basename)', siblingsPattern.name); - if (siblingsPattern.siblings.indexOf(clausePattern) !== -1) { - return pattern; - } else { - return null; // pattern does not match in the end because the when clause is not satisfied - } - }; - - const result: ParsedExpressionPattern = (path: string, basename: string, siblingsPatternFn: () => SiblingsPattern | TPromise) => { - if (!parsedPattern(path, basename)) { + const result: ParsedExpressionPattern = (path: string, basename: string, name: string, hasSibling: (name: string) => boolean | TPromise) => { + if (!hasSibling || !parsedPattern(path, basename)) { return null; } - const siblingsPattern = siblingsPatternFn(); - if (!siblingsPattern) { - return null; // pattern is malformed or we don't have siblings - } - - return TPromise.is(siblingsPattern) ? - siblingsPattern.then(siblingsPatternToMatchingPattern) : - siblingsPatternToMatchingPattern(siblingsPattern); + const clausePattern = when.replace('$(basename)', name); + const matched = hasSibling(clausePattern); + return TPromise.is(matched) ? + matched.then(m => m ? pattern : null) : + matched ? pattern : null; }; result.requiresSiblings = true; return result; diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 778819cadb8..9e6f3c57461 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -5,74 +5,86 @@ 'use strict'; -export interface IIteratorResult { +export interface IteratorResult { readonly done: boolean; readonly value: T | undefined; } -export interface IIterator { - next(): IIteratorResult; +export interface Iterator { + next(): IteratorResult; } -const _empty: IIterator = { - next() { - return { done: true, value: undefined }; +export module Iterator { + const _empty: Iterator = { + next() { + return { done: true, value: undefined }; + } + }; + + export function empty(): Iterator { + return _empty; } -}; -export function empty(): IIterator { - return _empty; -} + export function iterate(array: T[], index = 0, length = array.length): Iterator { + return { + next(): IteratorResult { + if (index >= length) { + return { done: true, value: undefined }; + } -export function iter(array: T[], index = 0, length = array.length): IIterator { - return { - next(): IIteratorResult { - if (index >= length) { - return { done: true, value: undefined }; + return { done: false, value: array[index++] }; } + }; + } - return { done: false, value: array[index++] }; - } - }; -} - -export function map(iterator: IIterator, fn: (t: T) => R): IIterator { - return { - next() { - const { done, value } = iterator.next(); - return { done, value: done ? undefined : fn(value) }; - } - }; -} - -export function filter(iterator: IIterator, fn: (t: T) => boolean): IIterator { - return { - next() { - while (true) { + export function map(iterator: Iterator, fn: (t: T) => R): Iterator { + return { + next() { const { done, value } = iterator.next(); + return { done, value: done ? undefined : fn(value) }; + } + }; + } - if (done) { - return { done, value: undefined }; - } + export function filter(iterator: Iterator, fn: (t: T) => boolean): Iterator { + return { + next() { + while (true) { + const { done, value } = iterator.next(); - if (fn(value)) { - return { done, value }; + if (done) { + return { done, value: undefined }; + } + + if (fn(value)) { + return { done, value }; + } } } - } - }; -} + }; + } -export function forEach(iterator: IIterator, fn: (t: T) => void): void { - for (let next = iterator.next(); !next.done; next = iterator.next()) { - fn(next.value); + export function forEach(iterator: Iterator, fn: (t: T) => void): void { + for (let next = iterator.next(); !next.done; next = iterator.next()) { + fn(next.value); + } + } + + export function collect(iterator: Iterator): T[] { + const result: T[] = []; + forEach(iterator, value => result.push(value)); + return result; } } -export function collect(iterator: IIterator): T[] { - const result: T[] = []; - forEach(iterator, value => result.push(value)); - return result; +export type ISequence = Iterator | T[]; + +export function getSequenceIterator(arg: Iterator | T[]): Iterator { + if (Array.isArray(arg)) { + return Iterator.iterate(arg); + } else { + return arg; + } } export interface INextIterator { diff --git a/src/vs/base/common/linkedList.ts b/src/vs/base/common/linkedList.ts index d40fd2a2861..9f44422e99c 100644 --- a/src/vs/base/common/linkedList.ts +++ b/src/vs/base/common/linkedList.ts @@ -5,7 +5,7 @@ 'use strict'; -import { IIterator } from 'vs/base/common/iterator'; +import { Iterator } from 'vs/base/common/iterator'; class Node { element: E; @@ -94,7 +94,7 @@ export class LinkedList { }; } - iterator(): IIterator { + iterator(): Iterator { let element = { done: undefined, value: undefined, diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index e234c796330..c39465b4991 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -7,7 +7,7 @@ import URI from 'vs/base/common/uri'; import { CharCode } from 'vs/base/common/charCode'; -import { IIterator } from './iterator'; +import { Iterator } from './iterator'; export function values(set: Set): V[]; export function values(map: Map): V[]; @@ -306,7 +306,7 @@ export class TernarySearchTree { return node && node.value || candidate; } - findSuperstr(key: string): IIterator { + findSuperstr(key: string): Iterator { let iter = this._iter.reset(key); let node = this._root; while (node) { @@ -333,7 +333,7 @@ export class TernarySearchTree { return undefined; } - private _nodeIterator(node: TernarySearchTreeNode): IIterator { + private _nodeIterator(node: TernarySearchTreeNode): Iterator { let res = { done: false, value: undefined diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index eb01c197f74..ebe0dff4cc4 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import * as cp from 'child_process'; import { fork } from 'vs/base/node/stdFork'; import * as nls from 'vs/nls'; -import { PPromise, TPromise, TValueCallback, TProgressCallback, ErrorCallback } from 'vs/base/common/winjs.base'; +import { TPromise, TValueCallback, ErrorCallback } from 'vs/base/common/winjs.base'; import * as Types from 'vs/base/common/types'; import { IStringDictionary } from 'vs/base/common/collections'; import URI from 'vs/base/common/uri'; @@ -19,6 +19,8 @@ import { LineDecoder } from 'vs/base/node/decoder'; import { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode, Executable } from 'vs/base/common/processes'; export { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode }; +export type TProgressCallback = (progress: T) => void; + export interface LineData { line: string; source: Source; @@ -152,18 +154,16 @@ export abstract class AbstractProcess { return 'other'; } - public start(): PPromise { + public start(pp: TProgressCallback): TPromise { if (Platform.isWindows && ((this.options && this.options.cwd && TPath.isUNC(this.options.cwd)) || !this.options && !this.options.cwd && TPath.isUNC(process.cwd()))) { return TPromise.wrapError(new Error(nls.localize('TaskRunner.UNC', 'Can\'t execute a shell command on a UNC drive.'))); } return this.useExec().then((useExec) => { let cc: TValueCallback; let ee: ErrorCallback; - let pp: TProgressCallback; - let result = new PPromise((c, e, p) => { + let result = new TPromise((c, e) => { cc = c; ee = e; - pp = p; }); if (useExec) { diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index ed35efadee0..37555c26b07 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -94,8 +94,8 @@ export interface IChannelClient { * channels (each from a separate client) to pick from. */ export interface IClientRouter { - routeCall(command: string, arg: any): string; - routeEvent(event: string, arg: any): string; + routeCall(command: string, arg: any): TPromise; + routeEvent(event: string, arg: any): TPromise; } /** @@ -433,24 +433,20 @@ export class IPCServer implements IChannelServer, IRoutingChannelClient, IDispos getChannel(channelName: string, router: IClientRouter): T { const call = (command: string, arg: any) => { - const id = router.routeCall(command, arg); + const channelPromise = router.routeCall(command, arg) + .then(id => this.getClient(id)) + .then(client => client.getChannel(channelName)); - if (!id) { - return TPromise.wrapError(new Error('Client id should be provided')); - } - - return getDelayedChannel(this.getClient(id).then(client => client.getChannel(channelName))) + return getDelayedChannel(channelPromise) .call(command, arg); }; const listen = (event: string, arg: any) => { - const id = router.routeEvent(event, arg); + const channelPromise = router.routeEvent(event, arg) + .then(id => this.getClient(id)) + .then(client => client.getChannel(channelName)); - if (!id) { - return TPromise.wrapError(new Error('Client id should be provided')); - } - - return getDelayedChannel(this.getClient(id).then(client => client.getChannel(channelName))) + return getDelayedChannel(channelPromise) .listen(event, arg); }; @@ -462,6 +458,10 @@ export class IPCServer implements IChannelServer, IRoutingChannelClient, IDispos } private getClient(clientId: string): TPromise { + if (!clientId) { + return TPromise.wrapError(new Error('Client id should be provided')); + } + const client = this.channelClients[clientId]; if (client) { diff --git a/src/vs/base/test/browser/ui/list/listView.test.ts b/src/vs/base/test/browser/ui/list/listView.test.ts index 5f4a644c99a..2dc54b4253f 100644 --- a/src/vs/base/test/browser/ui/list/listView.test.ts +++ b/src/vs/base/test/browser/ui/list/listView.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { ListView } from 'vs/base/browser/ui/list/listView'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { range } from 'vs/base/common/arrays'; suite('ListView', function () { @@ -14,7 +14,7 @@ suite('ListView', function () { element.style.height = '200px'; element.style.width = '200px'; - const delegate: IDelegate = { + const delegate: IVirtualDelegate = { getHeight() { return 20; }, getTemplateId() { return 'template'; } }; @@ -25,6 +25,7 @@ suite('ListView', function () { templateId: 'template', renderTemplate() { templatesCount++; }, renderElement() { }, + disposeElement() { }, disposeTemplate() { templatesCount--; } }; diff --git a/src/vs/base/test/browser/ui/tree/treeModel.test.ts b/src/vs/base/test/browser/ui/tree/treeModel.test.ts index a4901161329..f348b38a9d9 100644 --- a/src/vs/base/test/browser/ui/tree/treeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/treeModel.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { TreeModel, ITreeNode } from 'vs/base/browser/ui/tree/treeModel'; -import { ISpliceable } from 'vs/base/browser/ui/list/splice'; -import { iter } from 'vs/base/common/iterator'; +import { ISpliceable } from 'vs/base/common/sequence'; +import { Iterator } from 'vs/base/common/iterator'; function toSpliceable(arr: T[]): ISpliceable { return { @@ -33,7 +33,7 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { element: 0 }, { element: 1 }, { element: 2 } @@ -55,9 +55,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, children: iter([ + element: 0, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -92,9 +92,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, collapsed: true, children: iter([ + element: 0, collapsed: true, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -120,7 +120,7 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { element: 0 }, { element: 1 }, { element: 2 } @@ -145,9 +145,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, children: iter([ + element: 0, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -179,9 +179,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, children: iter([ + element: 0, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -207,9 +207,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, collapsed: true, children: iter([ + element: 0, collapsed: true, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -232,9 +232,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, children: iter([ + element: 0, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -263,9 +263,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, collapsed: true, children: iter([ + element: 0, collapsed: true, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -303,7 +303,7 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { element: 1, children: [ { diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 9cab77a9881..24ef5347f56 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -41,6 +41,95 @@ suite('Async', () => { return result; }); + // Cancelling a sync cancelable promise will fire the cancelled token. + // Also, every `then` callback runs in another execution frame. + test('CancelablePromise execution order (sync)', function () { + const order = []; + + const cancellablePromise = async.createCancelablePromise(token => { + order.push('in callback'); + token.onCancellationRequested(_ => order.push('cancelled')); + return Promise.resolve(1234); + }); + + order.push('afterCreate'); + + const promise = cancellablePromise + .then(null, err => null) + .then(() => order.push('finally')); + + cancellablePromise.cancel(); + order.push('afterCancel'); + + return promise.then(() => assert.deepEqual(order, ['in callback', 'afterCreate', 'cancelled', 'afterCancel', 'finally'])); + }); + + // Cancelling an async cancelable promise is just the same as a sync cancellable promise. + test('CancelablePromise execution order (async)', function () { + const order = []; + + const cancellablePromise = async.createCancelablePromise(token => { + order.push('in callback'); + token.onCancellationRequested(_ => order.push('cancelled')); + return new Promise(c => setTimeout(c(1234), 0)); + }); + + order.push('afterCreate'); + + const promise = cancellablePromise + .then(null, err => null) + .then(() => order.push('finally')); + + cancellablePromise.cancel(); + order.push('afterCancel'); + + return promise.then(() => assert.deepEqual(order, ['in callback', 'afterCreate', 'cancelled', 'afterCancel', 'finally'])); + }); + + // Cancelling a sync tpromise will NOT cancel the promise, since it has resolved already. + // Every `then` callback runs sync in the same execution frame, thus `finally` executes + // before `afterCancel`. + test('TPromise execution order (sync)', function () { + const order = []; + let promise = new TPromise(resolve => { + order.push('in executor'); + resolve(1234); + }, () => order.push('cancelled')); + + order.push('afterCreate'); + + promise = promise + .then(null, err => null) + .then(() => order.push('finally')); + + promise.cancel(); + order.push('afterCancel'); + + return promise.then(() => assert.deepEqual(order, ['in executor', 'afterCreate', 'finally', 'afterCancel'])); + }); + + // Cancelling an async tpromise will cancel the promise. + // Every `then` callback runs sync on the same execution frame as the `cancel` call, + // so finally still executes before `afterCancel`. + test('TPromise execution order (async)', function () { + const order = []; + let promise = new TPromise(resolve => { + order.push('in executor'); + setTimeout(() => resolve(1234)); + }, () => order.push('cancelled')); + + order.push('afterCreate'); + + promise = promise + .then(null, err => null) + .then(() => order.push('finally')); + + promise.cancel(); + order.push('afterCancel'); + + return promise.then(() => assert.deepEqual(order, ['in executor', 'afterCreate', 'cancelled', 'finally', 'afterCancel'])); + }); + test('cancelablePromise - get inner result', async function () { let promise = async.createCancelablePromise(token => { return async.timeout(12).then(_ => 1234); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 23bbe9352ce..07fc6c90f8a 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -8,7 +8,7 @@ import { ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache } from 'vs/base/common/map'; import * as assert from 'assert'; import URI from 'vs/base/common/uri'; -import { IIteratorResult } from 'vs/base/common/iterator'; +import { IteratorResult } from 'vs/base/common/iterator'; suite('Map', () => { @@ -419,7 +419,7 @@ suite('Map', () => { map.set('/user/foo/flip/flop', 3); map.set('/usr/foo', 4); - let item: IIteratorResult; + let item: IteratorResult; let iter = map.findSuperstr('/user'); item = iter.next(); diff --git a/src/vs/base/test/node/glob.test.ts b/src/vs/base/test/node/glob.test.ts index 5d2d82599e2..564399dc659 100644 --- a/src/vs/base/test/node/glob.test.ts +++ b/src/vs/base/test/node/glob.test.ts @@ -430,6 +430,7 @@ suite('Glob', () => { test('expression support (single)', function () { let siblings = ['test.html', 'test.txt', 'test.ts', 'test.js']; + let hasSibling = name => siblings.indexOf(name) !== -1; // { "**/*.js": { "when": "$(basename).ts" } } let expression: glob.IExpression = { @@ -438,9 +439,9 @@ suite('Glob', () => { } }; - assert.strictEqual('**/*.js', glob.match(expression, 'test.js', () => siblings)); - assert.strictEqual(glob.match(expression, 'test.js', () => []), null); - assert.strictEqual(glob.match(expression, 'test.js', () => ['te.ts']), null); + assert.strictEqual('**/*.js', glob.match(expression, 'test.js', hasSibling)); + assert.strictEqual(glob.match(expression, 'test.js', () => false), null); + assert.strictEqual(glob.match(expression, 'test.js', name => name === 'te.ts'), null); assert.strictEqual(glob.match(expression, 'test.js'), null); expression = { @@ -449,22 +450,23 @@ suite('Glob', () => { } }; - assert.strictEqual(glob.match(expression, 'test.js', () => siblings), null); + assert.strictEqual(glob.match(expression, 'test.js', hasSibling), null); expression = { '**/*.js': { } }; - assert.strictEqual('**/*.js', glob.match(expression, 'test.js', () => siblings)); + assert.strictEqual('**/*.js', glob.match(expression, 'test.js', hasSibling)); expression = {}; - assert.strictEqual(glob.match(expression, 'test.js', () => siblings), null); + assert.strictEqual(glob.match(expression, 'test.js', hasSibling), null); }); test('expression support (multiple)', function () { let siblings = ['test.html', 'test.txt', 'test.ts', 'test.js']; + let hasSibling = name => siblings.indexOf(name) !== -1; // { "**/*.js": { "when": "$(basename).ts" } } let expression: glob.IExpression = { @@ -474,11 +476,11 @@ suite('Glob', () => { '**/*.bananas': { bananas: true } }; - assert.strictEqual('**/*.js', glob.match(expression, 'test.js', () => siblings)); - assert.strictEqual('**/*.as', glob.match(expression, 'test.as', () => siblings)); - assert.strictEqual('**/*.bananas', glob.match(expression, 'test.bananas', () => siblings)); + assert.strictEqual('**/*.js', glob.match(expression, 'test.js', hasSibling)); + assert.strictEqual('**/*.as', glob.match(expression, 'test.as', hasSibling)); + assert.strictEqual('**/*.bananas', glob.match(expression, 'test.bananas', hasSibling)); assert.strictEqual('**/*.bananas', glob.match(expression, 'test.bananas')); - assert.strictEqual(glob.match(expression, 'test.foo', () => siblings), null); + assert.strictEqual(glob.match(expression, 'test.foo', hasSibling), null); }); test('brackets', function () { @@ -713,15 +715,16 @@ suite('Glob', () => { '**/*.js': { when: '$(basename).ts' } }; - let sibilings = () => ['foo.ts', 'foo.js', 'foo', 'bar']; + let siblings = ['foo.ts', 'foo.js', 'foo', 'bar']; + let hasSibling = name => siblings.indexOf(name) !== -1; - assert.strictEqual(glob.match(expr, 'bar', sibilings), '**/bar'); - assert.strictEqual(glob.match(expr, 'foo', sibilings), null); - assert.strictEqual(glob.match(expr, 'foo/bar', sibilings), '**/bar'); - assert.strictEqual(glob.match(expr, 'foo\\bar', sibilings), '**/bar'); - assert.strictEqual(glob.match(expr, 'foo/foo', sibilings), null); - assert.strictEqual(glob.match(expr, 'foo.js', sibilings), '**/*.js'); - assert.strictEqual(glob.match(expr, 'bar.js', sibilings), null); + assert.strictEqual(glob.match(expr, 'bar', hasSibling), '**/bar'); + assert.strictEqual(glob.match(expr, 'foo', hasSibling), null); + assert.strictEqual(glob.match(expr, 'foo/bar', hasSibling), '**/bar'); + assert.strictEqual(glob.match(expr, 'foo\\bar', hasSibling), '**/bar'); + assert.strictEqual(glob.match(expr, 'foo/foo', hasSibling), null); + assert.strictEqual(glob.match(expr, 'foo.js', hasSibling), '**/*.js'); + assert.strictEqual(glob.match(expr, 'bar.js', hasSibling), null); }); test('expression with multipe basename globs', function () { @@ -766,10 +769,11 @@ suite('Glob', () => { assert.strictEqual(glob.parse('{**/baz,**/foo}')('baz/foo', 'foo'), true); let expr = { '**/*.js': { when: '$(basename).ts' } }; - let sibilings = () => ['foo.ts', 'foo.js']; + let siblings = ['foo.ts', 'foo.js']; + let hasSibling = name => siblings.indexOf(name) !== -1; - assert.strictEqual(glob.parse(expr)('bar/baz.js', 'baz.js', sibilings), null); - assert.strictEqual(glob.parse(expr)('bar/foo.js', 'foo.js', sibilings), '**/*.js'); + assert.strictEqual(glob.parse(expr)('bar/baz.js', 'baz.js', hasSibling), null); + assert.strictEqual(glob.parse(expr)('bar/foo.js', 'foo.js', hasSibling), '**/*.js'); }); test('expression/pattern basename terms', function () { @@ -809,7 +813,8 @@ suite('Glob', () => { ['bar/nope', null] ]); - const siblingsFn = () => ['baz', 'baz.zip', 'nope']; + const siblings = ['baz', 'baz.zip', 'nope']; + const hasSibling = name => siblings.indexOf(name) !== -1; testOptimizationForBasenames({ '**/foo/**': { when: '$(basename).zip' }, '**/bar/**': true @@ -820,12 +825,12 @@ suite('Glob', () => { ['foo/bar', '**/bar/**'], ], [ null, - siblingsFn, - siblingsFn + hasSibling, + hasSibling ]); }); - function testOptimizationForBasenames(pattern: string | glob.IExpression, basenameTerms: string[], matches: [string, string | boolean][], siblingsFns: (() => string[])[] = []) { + function testOptimizationForBasenames(pattern: string | glob.IExpression, basenameTerms: string[], matches: [string, string | boolean][], siblingsFns: ((name: string) => boolean)[] = []) { const parsed = glob.parse(pattern, { trimForExclusions: true }); assert.deepStrictEqual(glob.getBasenameTerms(parsed), basenameTerms); matches.forEach(([text, result], i) => { @@ -914,7 +919,8 @@ suite('Glob', () => { [nativeSep('/foo/bar/nope'), null] ]); - const siblingsFn = () => ['baz', 'baz.zip', 'nope']; + const siblings = ['baz', 'baz.zip', 'nope']; + let hasSibling = name => siblings.indexOf(name) !== -1; testOptimizationForPaths({ '**/foo/123/**': { when: '$(basename).zip' }, '**/bar/123/**': true @@ -925,12 +931,12 @@ suite('Glob', () => { [nativeSep('foo/bar/123'), '**/bar/123/**'], ], [ null, - siblingsFn, - siblingsFn + hasSibling, + hasSibling ]); }); - function testOptimizationForPaths(pattern: string | glob.IExpression, pathTerms: string[], matches: [string, string | boolean][], siblingsFns: (() => string[])[] = []) { + function testOptimizationForPaths(pattern: string | glob.IExpression, pathTerms: string[], matches: [string, string | boolean][], siblingsFns: ((name: string) => boolean)[] = []) { const parsed = glob.parse(pattern, { trimForExclusions: true }); assert.deepStrictEqual(glob.getPathTerms(parsed), pathTerms); matches.forEach(([text, result], i) => { diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 65980535322..cc305121e2c 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -23,7 +23,7 @@ import { getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IWindowConfiguration, IWindowsService } from 'vs/platform/windows/common/windows'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; @@ -287,9 +287,9 @@ export class IssueReporter extends Disposable { .then(() => connectNet(this.environmentService.sharedIPCHandle, `window:${configuration.windowId}`)); const instantiationService = new InstantiationService(serviceCollection, true); - if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { + if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { const channel = getDelayedChannel(sharedProcess.then(c => c.getChannel('telemetryAppender'))); - const appender = new TelemetryAppenderClient(channel); + const appender = combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(logService)); const commonProperties = resolveCommonProperties(product.commit, pkg.version, configuration.machineId, this.environmentService.installSourcePath); const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath]; const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths }; diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index deaea3955f3..1cac6c41b1f 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -25,7 +25,7 @@ import { ConfigurationService } from 'vs/platform/configuration/node/configurati import { IRequestService } from 'vs/platform/request/node/request'; import { RequestService } from 'vs/platform/request/electron-browser/requestService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { combinedAppender, NullTelemetryService, ITelemetryAppender, NullAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; @@ -67,7 +67,8 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I process.once('exit', () => dispose(disposables)); const environmentService = new EnvironmentService(initData.args, process.execPath); - const logLevelClient = new LogLevelSetterChannelClient(server.getChannel('loglevel', { routeCall: () => 'main', routeEvent: () => 'main' })); + const mainRoute = () => TPromise.as('main'); + const logLevelClient = new LogLevelSetterChannelClient(server.getChannel('loglevel', { routeCall: mainRoute, routeEvent: mainRoute })); const logService = new FollowerLogService(logLevelClient, createSpdLogService('sharedprocess', initData.logLevel, environmentService.logsPath)); disposables.push(logService); @@ -78,46 +79,33 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I services.set(IConfigurationService, new SyncDescriptor(ConfigurationService)); services.set(IRequestService, new SyncDescriptor(RequestService)); - const windowsChannel = server.getChannel('windows', { routeCall: () => 'main', routeEvent: () => 'main' }); + const windowsChannel = server.getChannel('windows', { routeCall: mainRoute, routeEvent: mainRoute }); const windowsService = new WindowsChannelClient(windowsChannel); services.set(IWindowsService, windowsService); const activeWindowManager = new ActiveWindowManager(windowsService); - const dialogChannel = server.getChannel('dialog', { - routeCall: () => { - logService.info('Routing dialog call request to the client', activeWindowManager.activeClientId); - return activeWindowManager.activeClientId; - }, - routeEvent: () => { - logService.info('Routing dialog listen request to the client', activeWindowManager.activeClientId); - return activeWindowManager.activeClientId; - } - }); + const route = () => activeWindowManager.getActiveClientId(); + const dialogChannel = server.getChannel('dialog', { routeCall: route, routeEvent: route }); services.set(IDialogService, new DialogChannelClient(dialogChannel)); const instantiationService = new InstantiationService(services); instantiationService.invokeFunction(accessor => { - const appenders: AppInsightsAppender[] = []; - - if (product.aiConfig && product.aiConfig.asimovKey) { - appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey)); - } - - // It is important to dispose the AI adapter properly because - // only then they flush remaining data. - disposables.push(...appenders); - - const appender = combinedAppender(...appenders); - server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(appender)); - const services = new ServiceCollection(); const environmentService = accessor.get(IEnvironmentService); const { appRoot, extensionsPath, extensionDevelopmentPath, isBuilt, installSourcePath } = environmentService; + const telemetryLogService = new FollowerLogService(logLevelClient, createSpdLogService('telemetry', initData.logLevel, environmentService.logsPath)); - if (isBuilt && !extensionDevelopmentPath && !environmentService.args['disable-telemetry'] && product.enableTelemetry) { + let appInsightsAppender: ITelemetryAppender = NullAppender; + if (product.aiConfig && product.aiConfig.asimovKey && isBuilt) { + appInsightsAppender = new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey, telemetryLogService); + disposables.push(appInsightsAppender); // Ensure the AI appender is disposed so that it flushes remaining data + } + server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(appInsightsAppender)); + + if (!extensionDevelopmentPath && !environmentService.args['disable-telemetry'] && product.enableTelemetry) { const config: ITelemetryServiceConfig = { - appender, + appender: combinedAppender(appInsightsAppender, new LogAppender(logService)), commonProperties: resolveCommonProperties(product.commit, pkg.version, configuration.machineId, installSourcePath), piiPaths: [appRoot, extensionsPath] }; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index f2a7d0cae04..0abf0261e30 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -30,7 +30,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IURLService } from 'vs/platform/url/common/url'; import { URLHandlerChannelClient, URLServiceChannel } from 'vs/platform/url/common/urlIpc'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; @@ -362,9 +362,9 @@ export class CodeApplication { services.set(IMenubarService, new SyncDescriptor(MenubarService)); // Telemtry - if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { + if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { const channel = getDelayedChannel(this.sharedProcessClient.then(c => c.getChannel('telemetryAppender'))); - const appender = new TelemetryAppenderClient(channel); + const appender = combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(this.logService)); const commonProperties = resolveCommonProperties(product.commit, pkg.version, machineId, this.environmentService.installSourcePath); const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath]; const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths }; @@ -426,7 +426,7 @@ export class CodeApplication { // Create a URL handler which forwards to the last active window const activeWindowManager = new ActiveWindowManager(windowsService); - const route = () => activeWindowManager.activeClientId; + const route = () => activeWindowManager.getActiveClientId(); const urlHandlerChannel = this.electronIpcServer.getChannel('urlHandler', { routeCall: route, routeEvent: route }); const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel); diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index de96fe1c654..998479e6247 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -662,11 +662,66 @@ export class CodeMenu { const terminal = this.createMenuItem(nls.localize({ key: 'miToggleTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal"), 'workbench.action.terminal.toggleTerminal'); const problems = this.createMenuItem(nls.localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems"), 'workbench.actions.view.problems'); + // Appearance + + const appearanceMenu = new Menu(); + const fullscreen = new MenuItem(this.withKeybinding('workbench.action.toggleFullScreen', { label: this.mnemonicLabel(nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen")), click: () => this.windowsMainService.getLastActiveWindow().toggleFullScreen(), enabled: this.windowsMainService.getWindowCount() > 0 })); const toggleZenMode = this.createMenuItem(nls.localize('miToggleZenMode', "Toggle Zen Mode"), 'workbench.action.toggleZenMode'); const toggleCenteredLayout = this.createMenuItem(nls.localize('miToggleCenteredLayout', "Toggle Centered Layout"), 'workbench.action.toggleCenteredLayout'); const toggleMenuBar = this.createMenuItem(nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar"), 'workbench.action.toggleMenuBar'); + const toggleSidebar = this.createMenuItem(nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar"), 'workbench.action.toggleSidebarVisibility'); + + let moveSideBarLabel: string; + if (this.currentSidebarLocation !== 'right') { + moveSideBarLabel = nls.localize({ key: 'miMoveSidebarRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Right"); + } else { + moveSideBarLabel = nls.localize({ key: 'miMoveSidebarLeft', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left"); + } + + const moveSidebar = this.createMenuItem(moveSideBarLabel, 'workbench.action.toggleSidebarPosition'); + const togglePanel = this.createMenuItem(nls.localize({ key: 'miTogglePanel', comment: ['&& denotes a mnemonic'] }, "Toggle &&Panel"), 'workbench.action.togglePanel'); + + let statusBarLabel: string; + if (this.currentStatusbarVisible) { + statusBarLabel = nls.localize({ key: 'miHideStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Hide Status Bar"); + } else { + statusBarLabel = nls.localize({ key: 'miShowStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Show Status Bar"); + } + const toggleStatusbar = this.createMenuItem(statusBarLabel, 'workbench.action.toggleStatusbarVisibility'); + + let activityBarLabel: string; + if (this.currentActivityBarVisible) { + activityBarLabel = nls.localize({ key: 'miHideActivityBar', comment: ['&& denotes a mnemonic'] }, "Hide &&Activity Bar"); + } else { + activityBarLabel = nls.localize({ key: 'miShowActivityBar', comment: ['&& denotes a mnemonic'] }, "Show &&Activity Bar"); + } + const toggleActivtyBar = this.createMenuItem(activityBarLabel, 'workbench.action.toggleActivityBarVisibility'); + + const zoomIn = this.createMenuItem(nls.localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In"), 'workbench.action.zoomIn'); + const zoomOut = this.createMenuItem(nls.localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "Zoom O&&ut"), 'workbench.action.zoomOut'); + const resetZoom = this.createMenuItem(nls.localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom"), 'workbench.action.zoomReset'); + + arrays.coalesce([ + fullscreen, + toggleZenMode, + toggleCenteredLayout, + isWindows || isLinux ? toggleMenuBar : void 0, + __separator__(), + moveSidebar, + toggleSidebar, + togglePanel, + toggleStatusbar, + toggleActivtyBar, + __separator__(), + zoomIn, + zoomOut, + resetZoom + ]).forEach(item => appearanceMenu.append(item)); + + const appearance = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miAppearance', comment: ['&& denotes a mnemonic'] }, "&&Appearance")), submenu: appearanceMenu }); + // Editor Layout const editorLayoutMenu = new Menu(); @@ -707,50 +762,18 @@ export class CodeMenu { const editorLayout = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miEditorLayout', comment: ['&& denotes a mnemonic'] }, "Editor &&Layout")), submenu: editorLayoutMenu }); - // Workbench Layout - const toggleSidebar = this.createMenuItem(nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar"), 'workbench.action.toggleSidebarVisibility'); - - let moveSideBarLabel: string; - if (this.currentSidebarLocation !== 'right') { - moveSideBarLabel = nls.localize({ key: 'miMoveSidebarRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Right"); - } else { - moveSideBarLabel = nls.localize({ key: 'miMoveSidebarLeft', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left"); - } - - const moveSidebar = this.createMenuItem(moveSideBarLabel, 'workbench.action.toggleSidebarPosition'); - const togglePanel = this.createMenuItem(nls.localize({ key: 'miTogglePanel', comment: ['&& denotes a mnemonic'] }, "Toggle &&Panel"), 'workbench.action.togglePanel'); - - let statusBarLabel: string; - if (this.currentStatusbarVisible) { - statusBarLabel = nls.localize({ key: 'miHideStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Hide Status Bar"); - } else { - statusBarLabel = nls.localize({ key: 'miShowStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Show Status Bar"); - } - const toggleStatusbar = this.createMenuItem(statusBarLabel, 'workbench.action.toggleStatusbarVisibility'); - - let activityBarLabel: string; - if (this.currentActivityBarVisible) { - activityBarLabel = nls.localize({ key: 'miHideActivityBar', comment: ['&& denotes a mnemonic'] }, "Hide &&Activity Bar"); - } else { - activityBarLabel = nls.localize({ key: 'miShowActivityBar', comment: ['&& denotes a mnemonic'] }, "Show &&Activity Bar"); - } - const toggleActivtyBar = this.createMenuItem(activityBarLabel, 'workbench.action.toggleActivityBarVisibility'); - - // Editor const toggleWordWrap = this.createMenuItem(nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap"), 'editor.action.toggleWordWrap'); const toggleMinimap = this.createMenuItem(nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap"), 'editor.action.toggleMinimap'); const toggleRenderWhitespace = this.createMenuItem(nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace"), 'editor.action.toggleRenderWhitespace'); const toggleRenderControlCharacters = this.createMenuItem(nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters"), 'editor.action.toggleRenderControlCharacter'); - // Zoom - const zoomIn = this.createMenuItem(nls.localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In"), 'workbench.action.zoomIn'); - const zoomOut = this.createMenuItem(nls.localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "Zoom O&&ut"), 'workbench.action.zoomOut'); - const resetZoom = this.createMenuItem(nls.localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom"), 'workbench.action.zoomReset'); - arrays.coalesce([ commands, openView, __separator__(), + appearance, + editorLayout, + __separator__(), explorer, search, scm, @@ -762,27 +785,10 @@ export class CodeMenu { debugConsole, terminal, __separator__(), - fullscreen, - toggleZenMode, - toggleCenteredLayout, - isWindows || isLinux ? toggleMenuBar : void 0, - __separator__(), - editorLayout, - __separator__(), - moveSidebar, - toggleSidebar, - togglePanel, - toggleStatusbar, - toggleActivtyBar, - __separator__(), toggleWordWrap, toggleMinimap, toggleRenderWhitespace, - toggleRenderControlCharacters, - __separator__(), - zoomIn, - zoomOut, - resetZoom + toggleRenderControlCharacters ]).forEach(item => viewMenu.append(item)); } diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 396dd120f12..08bd0945583 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -245,7 +245,7 @@ export function main(argv: ParsedArgs): TPromise { if (isBuilt && !extensionDevelopmentPath && !envService.args['disable-telemetry'] && product.enableTelemetry) { if (product.aiConfig && product.aiConfig.asimovKey) { - appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey)); + appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey, logService)); } const config: ITelemetryServiceConfig = { diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 9774ab0405b..556665f4a6e 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -282,19 +282,19 @@ const editorConfiguration: IConfigurationNode = { 'type': 'number', 'default': EDITOR_MODEL_DEFAULTS.tabSize, 'minimum': 1, - 'description': nls.localize('tabSize', "The number of spaces a tab is equal to. This setting is overridden based on the file contents when `editor.detectIndentation` is on."), + 'description': nls.localize('tabSize', "The number of spaces a tab is equal to. This setting is overridden based on the file contents when [`editor.detectIndentation`](#editor.detectIndentation) is on."), 'errorMessage': nls.localize('tabSize.errorMessage', "Expected 'number'. Note that the value \"auto\" has been replaced by the `editor.detectIndentation` setting.") }, 'editor.insertSpaces': { 'type': 'boolean', 'default': EDITOR_MODEL_DEFAULTS.insertSpaces, - 'description': nls.localize('insertSpaces', "Insert spaces when pressing Tab. This setting is overridden based on the file contents when `editor.detectIndentation` is on."), + 'description': nls.localize('insertSpaces', "Insert spaces when pressing Tab. This setting is overridden based on the file contents when [`editor.detectIndentation`](#editor.detectIndentation) is on."), 'errorMessage': nls.localize('insertSpaces.errorMessage', "Expected 'boolean'. Note that the value \"auto\" has been replaced by the `editor.detectIndentation` setting.") }, 'editor.detectIndentation': { 'type': 'boolean', 'default': EDITOR_MODEL_DEFAULTS.detectIndentation, - 'description': nls.localize('detectIndentation', "When opening a file, `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents.") + 'description': nls.localize('detectIndentation', "When opening a file, [`editor.tabSize`](#editor.tabSize) and [`editor.insertSpaces`](#editor.insertSpaces) will be detected based on the file contents.") }, 'editor.roundedSelection': { 'type': 'boolean', @@ -440,7 +440,7 @@ const editorConfiguration: IConfigurationNode = { '- `ctrlCmd` refers to a value the setting can take and should not be localized.', '- `Control` and `Command` refer to the modifier keys Ctrl or Cmd on the keyboard and can be localized.' ] - }, "The modifier to be used to add multiple cursors with the mouse. `ctrlCmd` maps to `Control` on Windows and Linux and to `Command` on macOS. The Go To Definition and Open Link mouse gestures will adapt such that they do not conflict with the multicursor modifier.") + }, "The modifier to be used to add multiple cursors with the mouse. `ctrlCmd` maps to `Control` on Windows and Linux and to `Command` on macOS. The Go To Definition and Open Link mouse gestures will adapt such that they do not conflict with the multicursor modifier. [Read more](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier)") }, 'editor.multiCursorMergeOverlapping': { 'type': 'boolean', diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index f78056b1667..5e5f192fb1a 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -22,7 +22,7 @@ import { getWordAtText, ensureValidWordDefinition } from 'vs/editor/common/model import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase'; import { IWordAtPosition, EndOfLineSequence } from 'vs/editor/common/model'; import { globals } from 'vs/base/common/platform'; -import { IIterator } from 'vs/base/common/iterator'; +import { Iterator } from 'vs/base/common/iterator'; export interface IMirrorModel { readonly uri: URI; @@ -59,7 +59,7 @@ export interface ICommonModel { getLinesContent(): string[]; getLineCount(): number; getLineContent(lineNumber: number): string; - createWordIterator(wordDefinition: RegExp): IIterator; + createWordIterator(wordDefinition: RegExp): Iterator; getWordUntilPosition(position: IPosition, wordDefinition: RegExp): IWordAtPosition; getValueInRange(range: IRange): string; getWordAtPosition(position: IPosition, wordDefinition: RegExp): Range; @@ -147,7 +147,7 @@ class MirrorModel extends BaseMirrorModel implements ICommonModel { }; } - public createWordIterator(wordDefinition: RegExp): IIterator { + public createWordIterator(wordDefinition: RegExp): Iterator { let obj = { done: false, value: '' diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index 3d695a84c97..9a5cfbdb383 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -475,26 +475,29 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable { next(): boolean { const length = this.hints.signatures.length; + const last = (this.currentSignature % length) === (length - 1); - if (length < 2) { + // If there is only one signature, or we're on last signature of list + if (length < 2 || last) { this.cancel(); return false; } - this.currentSignature = (this.currentSignature + 1) % length; + this.currentSignature++; this.render(); return true; } previous(): boolean { const length = this.hints.signatures.length; + const first = this.currentSignature === 0; - if (length < 2) { + if (length < 2 || first) { this.cancel(); return false; } - this.currentSignature = (this.currentSignature - 1 + length) % length; + this.currentSignature--; this.render(); return true; } diff --git a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts index cb21013f62b..849b2a3d102 100644 --- a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts +++ b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts @@ -138,7 +138,7 @@ let showReferencesCommand: ICommandHandler = (accessor: ServicesAccessor, resour return TPromise.as(controller.toggleWidget( new Range(position.lineNumber, position.column, position.lineNumber, position.column), - createCancelablePromise(_ => Promise.reject(new ReferencesModel(references))), + createCancelablePromise(_ => Promise.resolve(new ReferencesModel(references))), defaultReferenceSearchOptions)).then(() => true); }); }; diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index 9bfe478497b..0df5abe1e5e 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -236,10 +236,16 @@ interface SuggestController extends IEditorContribution { triggerSuggest(onlyFrom?: ISuggestSupport[]): void; } -let _suggestions: ISuggestion[]; + let _provider = new class implements ISuggestSupport { + + onlyOnceSuggestions: ISuggestion[] = []; + provideCompletionItems(): ISuggestResult { - return _suggestions && { suggestions: _suggestions }; + let suggestions = this.onlyOnceSuggestions.slice(0); + let result = { suggestions }; + this.onlyOnceSuggestions.length = 0; + return result; } }; @@ -247,8 +253,7 @@ SuggestRegistry.register('*', _provider); export function showSimpleSuggestions(editor: ICodeEditor, suggestions: ISuggestion[]) { setTimeout(() => { - _suggestions = suggestions; + _provider.onlyOnceSuggestions.push(...suggestions); editor.getContribution('editor.contrib.suggestController').triggerSuggest([_provider]); - _suggestions = undefined; }, 0); } diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 9361c2b2422..3360b6fbda0 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -14,7 +14,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { addClass, append, $, hide, removeClass, show, toggleClass, getDomNodePagePosition, hasClass } from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { IDelegate, IListEvent, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IListEvent, IRenderer } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -173,7 +173,10 @@ class Renderer implements IRenderer { data.readMore.onmousedown = null; data.readMore.onclick = null; } + } + disposeElement(): void { + // noop } disposeTemplate(templateData: ISuggestionTemplateData): void { @@ -349,7 +352,7 @@ export interface ISelectedSuggestion { model: CompletionModel; } -export class SuggestWidget implements IContentWidget, IDelegate, IDisposable { +export class SuggestWidget implements IContentWidget, IVirtualDelegate, IDisposable { private static readonly ID: string = 'editor.widget.suggestWidget'; @@ -601,7 +604,11 @@ export class SuggestWidget implements IContentWidget, IDelegate } else { removeClass(this.element, 'docs-side'); } - }).catch(onUnexpectedError).then(() => this.currentSuggestionDetails = null); + }).catch(onUnexpectedError).then(() => { + if (this.focusedItem === item) { + this.currentSuggestionDetails = null; + } + }); // emit an event this.onDidFocusEmitter.fire({ item, index, model: this.completionModel }); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index c4b1e0f958c..e919252f47a 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -90,6 +90,7 @@ export class MenuId { static readonly MenubarRecentMenu = new MenuId(); static readonly MenubarSelectionMenu = new MenuId(); static readonly MenubarViewMenu = new MenuId(); + static readonly MenubarAppearanceMenu = new MenuId(); static readonly MenubarLayoutMenu = new MenuId(); static readonly MenubarGoMenu = new MenuId(); static readonly MenubarDebugMenu = new MenuId(); diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index ba2b3c673a2..f9667d4d695 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -27,12 +27,12 @@ class WindowRouter implements IClientRouter { constructor(private windowId: number) { } - routeCall(): string { - return `window:${this.windowId}`; + routeCall(): TPromise { + return TPromise.as(`window:${this.windowId}`); } - routeEvent(): string { - return `window:${this.windowId}`; + routeEvent(): TPromise { + return TPromise.as(`window:${this.windowId}`); } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 25e0dcdb540..0bd9ad1eb1f 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -124,6 +124,7 @@ export interface IExtensionManifest { categories?: string[]; activationEvents?: string[]; extensionDependencies?: string[]; + extensionPack?: string[]; contributes?: IExtensionContributions; repository?: { url: string; @@ -135,6 +136,7 @@ export interface IExtensionManifest { export interface IGalleryExtensionProperties { dependencies?: string[]; + extensionPack?: string[]; engine?: string; } @@ -205,6 +207,7 @@ export enum LocalExtensionType { export interface ILocalExtension { type: LocalExtensionType; identifier: IExtensionIdentifier; + galleryIdentifier: IExtensionIdentifier; manifest: IExtensionManifest; metadata: IGalleryMetadata; location: URI; @@ -303,10 +306,10 @@ export interface IExtensionManagementService { onUninstallExtension: Event; onDidUninstallExtension: Event; - install(zipPath: string): TPromise; - installFromGallery(extension: IGalleryExtension): TPromise; + install(zipPath: string): TPromise; + installFromGallery(extension: IGalleryExtension): TPromise; uninstall(extension: ILocalExtension, force?: boolean): TPromise; - reinstallFromGallery(extension: ILocalExtension): TPromise; + reinstallFromGallery(extension: ILocalExtension): TPromise; getInstalled(type?: LocalExtensionType): TPromise; getExtensionsReport(): TPromise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 0aa4e7577a7..13180261b7b 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -17,10 +17,10 @@ export interface IExtensionManagementChannel extends IChannel { listen(event: 'onDidInstallExtension'): Event; listen(event: 'onUninstallExtension'): Event; listen(event: 'onDidUninstallExtension'): Event; - call(command: 'install', args: [string]): TPromise; - call(command: 'installFromGallery', args: [IGalleryExtension]): TPromise; + call(command: 'install', args: [string]): TPromise; + call(command: 'installFromGallery', args: [IGalleryExtension]): TPromise; call(command: 'uninstall', args: [ILocalExtension, boolean]): TPromise; - call(command: 'reinstallFromGallery', args: [ILocalExtension]): TPromise; + call(command: 'reinstallFromGallery', args: [ILocalExtension]): TPromise; call(command: 'getInstalled', args: [LocalExtensionType]): TPromise; call(command: 'getExtensionsReport'): TPromise; call(command: 'updateMetadata', args: [ILocalExtension, IGalleryMetadata]): TPromise; @@ -81,23 +81,20 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer get onUninstallExtension(): Event { return this.channel.listen('onUninstallExtension'); } get onDidUninstallExtension(): Event { return this.channel.listen('onDidUninstallExtension'); } - install(zipPath: string): TPromise { - return this.channel.call('install', [zipPath]) - .then(extension => this._transform(extension)); + install(zipPath: string): TPromise { + return this.channel.call('install', [zipPath]); } - installFromGallery(extension: IGalleryExtension): TPromise { - return this.channel.call('installFromGallery', [extension]) - .then(extension => this._transform(extension)); + installFromGallery(extension: IGalleryExtension): TPromise { + return this.channel.call('installFromGallery', [extension]); } uninstall(extension: ILocalExtension, force = false): TPromise { return this.channel.call('uninstall', [extension, force]); } - reinstallFromGallery(extension: ILocalExtension): TPromise { - return this.channel.call('reinstallFromGallery', [extension]) - .then(extension => this._transform(extension)); + reinstallFromGallery(extension: ILocalExtension): TPromise { + return this.channel.call('reinstallFromGallery', [extension]); } getInstalled(type: LocalExtensionType = null): TPromise { diff --git a/src/vs/platform/extensionManagement/common/multiExtensionManagement.ts b/src/vs/platform/extensionManagement/common/multiExtensionManagement.ts index a7938100654..c9308a0d0f1 100644 --- a/src/vs/platform/extensionManagement/common/multiExtensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/multiExtensionManagement.ts @@ -41,7 +41,7 @@ export class MulitExtensionManagementService implements IExtensionManagementServ return this.getServer(extension).extensionManagementService.uninstall(extension, force); } - reinstallFromGallery(extension: ILocalExtension): TPromise { + reinstallFromGallery(extension: ILocalExtension): TPromise { return this.getServer(extension).extensionManagementService.reinstallFromGallery(extension); } @@ -49,12 +49,12 @@ export class MulitExtensionManagementService implements IExtensionManagementServ return this.getServer(extension).extensionManagementService.updateMetadata(extension, metadata); } - install(zipPath: string): TPromise { + install(zipPath: string): TPromise { return this.servers[0].extensionManagementService.install(zipPath); } - installFromGallery(extension: IGalleryExtension): TPromise { - return this.servers[0].extensionManagementService.installFromGallery(extension); + installFromGallery(extension: IGalleryExtension): TPromise { + return TPromise.join(this.servers.map(server => server.extensionManagementService.installFromGallery(extension))).then(() => null); } getExtensionsReport(): TPromise { diff --git a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts index 87c15f9be41..16af4dbf97a 100644 --- a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts @@ -115,6 +115,7 @@ const AssetType = { const PropertyType = { Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies', + ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack', Engine: 'Microsoft.VisualStudio.Code.Engine' }; @@ -262,8 +263,8 @@ function getVersionAsset(version: IRawGalleryExtensionVersion, type: string): IG }; } -function getDependencies(version: IRawGalleryExtensionVersion): string[] { - const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Dependency) : []; +function getExtensions(version: IRawGalleryExtensionVersion, property: string): string[] { + const values = version.properties ? version.properties.filter(p => p.key === property) : []; const value = values.length > 0 && values[0].value; return value ? value.split(',').map(v => adoptToGalleryExtensionId(v)) : []; } @@ -308,7 +309,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, extensionsGalleryUr ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'), assets, properties: { - dependencies: getDependencies(version), + dependencies: getExtensions(version, PropertyType.Dependency), + extensionPack: getExtensions(version, PropertyType.ExtensionPack), engine: getEngine(version) }, /* __GDPR__FRAGMENT__ @@ -568,7 +570,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return this.getLastValidExtensionVersion(rawExtension, rawExtension.versions) .then(rawVersion => { if (rawVersion) { - extension.properties.dependencies = getDependencies(rawVersion); + extension.properties.dependencies = getExtensions(rawVersion, PropertyType.Dependency); extension.properties.engine = getEngine(rawVersion); extension.assets.download = getVersionAsset(rawVersion, AssetType.VSIX); extension.version = rawVersion.version; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 4cdee22f254..5cc1c5b5bb7 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -11,9 +11,9 @@ import * as pfs from 'vs/base/node/pfs'; import * as errors from 'vs/base/common/errors'; import { assign } from 'vs/base/common/objects'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { flatten, distinct } from 'vs/base/common/arrays'; +import { flatten } from 'vs/base/common/arrays'; import { extract, buffer, ExtractError } from 'vs/base/node/zip'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { TPromise, ValueCallback, ErrorCallback } from 'vs/base/common/winjs.base'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IExtensionManifest, IGalleryMetadata, @@ -53,6 +53,7 @@ const INSTALL_ERROR_LOCAL = 'local'; const INSTALL_ERROR_EXTRACTING = 'extracting'; const INSTALL_ERROR_RENAMING = 'renaming'; const INSTALL_ERROR_DELETING = 'deleting'; +const INSTALL_ERROR_MALICIOUS = 'malicious'; const ERROR_UNKNOWN = 'unknown'; export class ExtensionManagementError extends Error { @@ -112,8 +113,7 @@ export class ExtensionManagementService extends Disposable implements IExtension private uninstalledFileLimiter: Limiter; private reportedExtensions: TPromise | undefined; private lastReportTimestamp = 0; - private readonly installationStartTime: Map = new Map(); - private readonly installingExtensions: Map> = new Map>(); + private readonly installingExtensions: Map> = new Map>(); private readonly uninstallingExtensions: Map> = new Map>(); private readonly manifestCache: ExtensionsManifestCache; private readonly extensionLifecycle: ExtensionsLifecycle; @@ -152,14 +152,14 @@ export class ExtensionManagementService extends Disposable implements IExtension })); } - install(zipPath: string): TPromise { + install(zipPath: string): TPromise { zipPath = path.resolve(zipPath); return validateLocalExtension(zipPath) .then(manifest => { const identifier = { id: getLocalExtensionIdFromManifest(manifest) }; if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode)) { - return TPromise.wrapError(new Error(nls.localize('incompatible', "Unable to install Extension '{0}' as it is not compatible with Code '{1}'.", identifier.id, pkg.version))); + return TPromise.wrapError(new Error(nls.localize('incompatible', "Unable to install Extension '{0}' as it is not compatible with Code '{1}'.", identifier.id, pkg.version))); } return this.removeIfExists(identifier.id) .then( @@ -173,7 +173,7 @@ export class ExtensionManagementService extends Disposable implements IExtension metadata => this.installFromZipPath(identifier, zipPath, metadata, manifest), error => this.installFromZipPath(identifier, zipPath, null, manifest)) .then( - local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; }, + () => { this.logService.info('Successfully installed the extension:', identifier.id); }, e => { this.logService.error('Failed to install the extension:', identifier.id, e.message); return TPromise.wrapError(e); @@ -219,20 +219,7 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(installed => { const operation = this.getOperation({ id: getIdFromLocalExtensionId(identifier.id), uuid: identifier.uuid }, installed); return this.installExtension({ zipPath, id: identifier.id, metadata }) - .then(local => { - if (this.galleryService.isEnabled() && local.manifest.extensionDependencies && local.manifest.extensionDependencies.length) { - return this.getDependenciesToInstall(local.manifest.extensionDependencies) - .then(dependenciesToInstall => { - dependenciesToInstall = metadata ? dependenciesToInstall.filter(d => d.identifier.uuid !== metadata.id) : dependenciesToInstall; - return this.downloadAndInstallExtensions(dependenciesToInstall, dependenciesToInstall.map(d => this.getOperation(d.identifier, installed))); - }) - .then(() => local, error => { - this.setUninstalled(local); - return TPromise.wrapError(new Error(nls.localize('errorInstallingDependencies', "Error while installing dependencies. {0}", error instanceof Error ? error.message : error))); - }); - } - return local; - }) + .then(local => this.installDependenciesAndPackExtensions(local, null).then(() => local, error => this.uninstall(local, true).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error)))) .then( local => { this._onDidInstallExtension.fire({ identifier, zipPath, local, operation }); return local; }, error => { this._onDidInstallExtension.fire({ identifier, zipPath, operation, error }); return TPromise.wrapError(error); } @@ -240,26 +227,60 @@ export class ExtensionManagementService extends Disposable implements IExtension })); } - installFromGallery(extension: IGalleryExtension): TPromise { - this.onInstallExtensions([extension]); - return this.toNonCancellablePromise(this.getInstalled(LocalExtensionType.User) - .then(installed => this.collectExtensionsToInstall(extension) - .then( - extensionsToInstall => { - if (extensionsToInstall.length > 1) { - this.onInstallExtensions(extensionsToInstall.slice(1)); - } - const operataions: InstallOperation[] = extensionsToInstall.map(e => this.getOperation(e.identifier, installed)); - return this.downloadAndInstallExtensions(extensionsToInstall, operataions) - .then( - locals => this.onDidInstallExtensions(extensionsToInstall, locals, operataions, []) - .then(() => locals.filter(l => areSameExtensions({ id: getGalleryExtensionIdFromLocal(l), uuid: l.identifier.uuid }, extension.identifier))[0]), - errors => this.onDidInstallExtensions(extensionsToInstall, [], operataions, errors)); - }, - error => this.onDidInstallExtensions([extension], [], [this.getOperation(extension.identifier, installed)], [error])))); + installFromGallery(extension: IGalleryExtension): TPromise { + let installingExtension = this.installingExtensions.get(extension.identifier.id); + if (!installingExtension) { + + let successCallback: ValueCallback, errorCallback: ErrorCallback; + installingExtension = new TPromise((c, e) => { successCallback = c; errorCallback = e; }); + this.installingExtensions.set(extension.identifier.id, installingExtension); + + try { + const startTime = new Date().getTime(); + const identifier = { id: getLocalExtensionIdFromGallery(extension, extension.version), uuid: extension.identifier.uuid }; + const telemetryData = getGalleryExtensionTelemetryData(extension); + let operation: InstallOperation = InstallOperation.Install; + + this.logService.info('Installing extension:', extension.name); + this._onInstallExtension.fire({ identifier, gallery: extension }); + + this.checkMalicious(extension) + .then(() => this.getInstalled(LocalExtensionType.User)) + .then(installed => { + const existingExtension = installed.filter(i => areSameExtensions(i.galleryIdentifier, extension.identifier))[0]; + operation = existingExtension ? InstallOperation.Update : InstallOperation.Install; + return this.downloadInstallableExtension(extension, operation) + .then(installableExtension => this.installExtension(installableExtension)) + .then(local => this.installDependenciesAndPackExtensions(local, existingExtension) + .then(() => local, error => this.uninstall(local, true).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error)))); + }) + .then( + local => { + this.installingExtensions.delete(extension.identifier.id); + this.logService.info(`Extensions installed successfully:`, extension.identifier.id); + this._onDidInstallExtension.fire({ identifier, gallery: extension, local, operation }); + this.reportTelemetry(this.getTelemetryEvent(operation), telemetryData, new Date().getTime() - startTime, void 0); + successCallback(null); + }, + error => { + this.installingExtensions.delete(extension.identifier.id); + const errorCode = error && (error).code ? (error).code : ERROR_UNKNOWN; + this.logService.error(`Failed to install extension:`, extension.identifier.id, error ? error.message : errorCode); + this._onDidInstallExtension.fire({ identifier, gallery: extension, operation, error: errorCode }); + this.reportTelemetry(this.getTelemetryEvent(operation), telemetryData, new Date().getTime() - startTime, error); + errorCallback(error); + }); + + } catch (error) { + this.installingExtensions.delete(extension.identifier.id); + errorCallback(error); + } + + } + return installingExtension; } - reinstallFromGallery(extension: ILocalExtension): TPromise { + reinstallFromGallery(extension: ILocalExtension): TPromise { if (!this.galleryService.isEnabled()) { return TPromise.wrapError(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"))); } @@ -280,46 +301,19 @@ export class ExtensionManagementService extends Disposable implements IExtension return installed.some(i => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i), uuid: i.identifier.uuid }, extensionToInstall)) ? InstallOperation.Update : InstallOperation.Install; } - private collectExtensionsToInstall(extension: IGalleryExtension): TPromise { - return this.galleryService.loadCompatibleVersion(extension) - .then(compatible => { - if (!compatible) { - return TPromise.wrapError(new ExtensionManagementError(nls.localize('notFoundCompatible', "Unable to install '{0}'; there is no available version compatible with VS Code '{1}'.", extension.identifier.id, pkg.version), INSTALL_ERROR_INCOMPATIBLE)); + private getTelemetryEvent(operation: InstallOperation): string { + return operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install'; + } + + private checkMalicious(extension: IGalleryExtension): TPromise { + return this.getExtensionsReport() + .then(report => { + if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) { + throw new ExtensionManagementError(INSTALL_ERROR_MALICIOUS, nls.localize('malicious extension', "Can't install extension since it was reported to be problematic.")); + } else { + return null; } - return this.getDependenciesToInstall(compatible.properties.dependencies) - .then( - dependenciesToInstall => ([compatible, ...dependenciesToInstall.filter(d => d.identifier.uuid !== compatible.identifier.uuid)]), - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); - }, - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); - } - - private downloadAndInstallExtensions(extensions: IGalleryExtension[], operations: InstallOperation[]): TPromise { - return TPromise.join(extensions.map((extensionToInstall, index) => this.downloadAndInstallExtension(extensionToInstall, operations[index]))) - .then(null, errors => this.rollback(extensions).then(() => TPromise.wrapError(errors), () => TPromise.wrapError(errors))); - } - - private downloadAndInstallExtension(extension: IGalleryExtension, operation: InstallOperation): TPromise { - let installingExtension = this.installingExtensions.get(extension.identifier.id); - if (!installingExtension) { - installingExtension = this.getExtensionsReport() - .then(report => { - if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) { - throw new Error(nls.localize('malicious extension', "Can't install extension since it was reported to be problematic.")); - } else { - return extension; - } - }) - .then(extension => this.downloadInstallableExtension(extension, operation)) - .then(installableExtension => this.installExtension(installableExtension)) - .then( - local => { this.installingExtensions.delete(extension.identifier.id); return local; }, - e => { this.installingExtensions.delete(extension.identifier.id); return TPromise.wrapError(e); } - ); - - this.installingExtensions.set(extension.identifier.id, installingExtension); - } - return installingExtension; + }); } private downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): TPromise { @@ -352,54 +346,6 @@ export class ExtensionManagementService extends Disposable implements IExtension error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); } - private onInstallExtensions(extensions: IGalleryExtension[]): void { - for (const extension of extensions) { - this.logService.info('Installing extension:', extension.name); - this.installationStartTime.set(extension.identifier.id, new Date().getTime()); - const id = getLocalExtensionIdFromGallery(extension, extension.version); - this._onInstallExtension.fire({ identifier: { id, uuid: extension.identifier.uuid }, gallery: extension }); - } - } - - private onDidInstallExtensions(extensions: IGalleryExtension[], locals: ILocalExtension[], operations: InstallOperation[], errors: Error[]): TPromise { - extensions.forEach((gallery, index) => { - const identifier = { id: getLocalExtensionIdFromGallery(gallery, gallery.version), uuid: gallery.identifier.uuid }; - const local = locals[index]; - const error = errors[index]; - const operation = operations[index]; - if (local) { - this.logService.info(`Extensions installed successfully:`, gallery.identifier.id); - this._onDidInstallExtension.fire({ identifier, gallery, local, operation }); - } else { - const errorCode = error && (error).code ? (error).code : ERROR_UNKNOWN; - this.logService.error(`Failed to install extension:`, gallery.identifier.id, error ? error.message : errorCode); - this._onDidInstallExtension.fire({ identifier, gallery, operation, error: errorCode }); - } - const startTime = this.installationStartTime.get(gallery.identifier.id); - this.reportTelemetry(operations[index] === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(gallery), startTime ? new Date().getTime() - startTime : void 0, error); - this.installationStartTime.delete(gallery.identifier.id); - }); - return errors.length ? TPromise.wrapError(this.joinErrors(errors)) : TPromise.as(null); - } - - private getDependenciesToInstall(dependencies: string[]): TPromise { - if (dependencies.length) { - return this.getInstalled() - .then(installed => { - const uninstalledDeps = dependencies.filter(d => installed.every(i => getGalleryExtensionId(i.manifest.publisher, i.manifest.name) !== d)); - if (uninstalledDeps.length) { - return this.galleryService.loadAllDependencies(uninstalledDeps.map(id => ({ id }))) - .then(allDependencies => allDependencies.filter(d => { - const extensionId = getLocalExtensionIdFromGallery(d, d.version); - return installed.every(({ identifier }) => identifier.id !== extensionId); - })); - } - return []; - }); - } - return TPromise.as([]); - } - private installExtension(installableExtension: InstallableExtension): TPromise { return this.unsetUninstalledAndGetLocal(installableExtension.id) .then( @@ -486,11 +432,44 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } + private installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension): TPromise { + if (this.galleryService.isEnabled()) { + const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || []; + if (installed.manifest.extensionPack) { + for (const extension of installed.manifest.extensionPack) { + // add only those extensions which are new in currently installed extension + if (!(existing && existing.manifest.extensionPack && existing.manifest.extensionPack.some(old => areSameExtensions({ id: old }, { id: extension })))) { + if (dependenciesAndPackExtensions.every(e => !areSameExtensions({ id: e }, { id: extension }))) { + dependenciesAndPackExtensions.push(extension); + } + } + } + } + if (dependenciesAndPackExtensions.length) { + return this.getInstalled() + .then(installed => { + // filter out installing and installed extensions + const names = dependenciesAndPackExtensions.filter(id => !this.installingExtensions.has(adoptToGalleryExtensionId(id)) && installed.every(({ galleryIdentifier }) => !areSameExtensions(galleryIdentifier, { id }))); + if (names.length) { + return this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }) + .then(galleryResult => { + const extensionsToInstall = galleryResult.firstPage; + return TPromise.join(extensionsToInstall.map(e => this.installFromGallery(e))) + .then(() => null, errors => this.rollback(extensionsToInstall).then(() => TPromise.wrapError(errors), () => TPromise.wrapError(errors))); + }); + } + return null; + }); + } + } + return TPromise.as(null); + } + private rollback(extensions: IGalleryExtension[]): TPromise { return this.getInstalled(LocalExtensionType.User) .then(installed => TPromise.join(installed.filter(local => extensions.some(galleryExtension => local.identifier.id === getLocalExtensionIdFromGallery(galleryExtension, galleryExtension.version))) // Only check id (pub.name-version) because we want to rollback the exact version - .map(local => this.setUninstalled(local)))) + .map(local => this.uninstall(local, true)))) .then(() => null, () => null); } @@ -558,7 +537,14 @@ export class ExtensionManagementService extends Disposable implements IExtension private checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise { return this.preUninstallExtension(extension) - .then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed, force) : this.uninstallWithDependencies(extension, [], installed)) + .then(() => { + if (force) { + return this.uninstallExtensionAsPack(extension, installed); + } + const hasInstalledExtensionPack = extension.manifest.extensionPack && extension.manifest.extensionPack.length && installed.some(i => extension.manifest.extensionPack.some(dep => areSameExtensions({ id: dep }, i.galleryIdentifier))); + const hasDependencies = extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length > 0; + return hasInstalledExtensionPack || hasDependencies ? this.promptForPackAndUninstall(extension, installed) : this.uninstallExtensions(extension, [], installed); + }) .then(() => this.postUninstallExtension(extension), error => { this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL)); @@ -566,47 +552,45 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private hasDependencies(extension: ILocalExtension, installed: ILocalExtension[]): boolean { - if (extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length) { - return installed.some(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1); - } - return false; - } - - private promptForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise { - if (force) { - const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension); - return this.uninstallWithDependencies(extension, dependencies, installed); - } - - const message = nls.localize('uninstallDependeciesConfirmation', "Would you like to uninstall '{0}' only or its dependencies also?", extension.manifest.displayName || extension.manifest.name); + private promptForPackAndUninstall(extension: ILocalExtension, installed: ILocalExtension[]): TPromise { + const message = nls.localize('uninstallExtensionPackConfirmation', "Would you like to uninstall '{0}' only or as a pack?", extension.manifest.displayName || extension.manifest.name); const buttons = [ - nls.localize('uninstallOnly', "Extension Only"), - nls.localize('uninstallAll', "Uninstall All"), + nls.localize('uninstallPack', "Uninstall Extension Pack"), + nls.localize('uninstallOnly', "Uninstall Extension Only"), nls.localize('cancel', "Cancel") ]; - this.logService.info('Requesting for confirmation to uninstall extension with dependencies', extension.identifier.id); return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 2 }) .then(value => { if (value === 0) { - return this.uninstallWithDependencies(extension, [], installed); + return this.uninstallExtensionAsPack(extension, installed); } if (value === 1) { - const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension); - return this.uninstallWithDependencies(extension, dependencies, installed); + return this.uninstallExtensions(extension, [], installed); } this.logService.info('Cancelled uninstalling extension:', extension.identifier.id); return TPromise.wrapError(errors.canceled()); }, error => TPromise.wrapError(errors.canceled())); } - private uninstallWithDependencies(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): TPromise { - const dependenciesToUninstall = this.filterDependents(extension, dependencies, installed); - let dependents = this.getDependents(extension, installed).filter(dependent => extension !== dependent && dependenciesToUninstall.indexOf(dependent) === -1); - if (dependents.length) { - return TPromise.wrapError(new Error(this.getDependentsErrorMessage(extension, dependents))); + private uninstallExtensionAsPack(extension: ILocalExtension, installed: ILocalExtension[]): TPromise { + const extensionsToUninstall = this.getDependenciesToUninstall(extension, installed); + for (const packExtensionToUninstall of this.getAllPackExtensionsToUninstall(extension, installed)) { + if (extensionsToUninstall.indexOf(packExtensionToUninstall) === -1) { + extensionsToUninstall.push(packExtensionToUninstall); + } } - return TPromise.join([this.uninstallExtension(extension), ...dependenciesToUninstall.map(d => this.doUninstall(d))]).then(() => null); + return this.uninstallExtensions(extension, extensionsToUninstall, installed); + } + + private uninstallExtensions(extension: ILocalExtension, otherExtensionsToUninstall: ILocalExtension[], installed: ILocalExtension[]): TPromise { + const dependents = this.getDependents(extension, installed); + if (dependents.length) { + const remainingDependents = dependents.filter(dependent => extension !== dependent && otherExtensionsToUninstall.indexOf(dependent) === -1); + if (remainingDependents.length) { + return TPromise.wrapError(new Error(this.getDependentsErrorMessage(extension, remainingDependents))); + } + } + return TPromise.join([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]).then(() => null); } private getDependentsErrorMessage(extension: ILocalExtension, dependents: ILocalExtension[]): string { @@ -622,7 +606,23 @@ export class ExtensionManagementService extends Disposable implements IExtension extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name); } - private getDependenciesToUninstallRecursively(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[]): ILocalExtension[] { + private getDependenciesToUninstall(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] { + const dependencies = this.getAllDependenciesToUninstall(extension, installed).filter(e => e !== extension); + + const dependenciesToUninstall = dependencies.slice(0); + for (let index = 0; index < dependencies.length; index++) { + const dep = dependencies[index]; + const dependents = this.getDependents(dep, installed); + // Remove the dependency from the uninstall list if there is a dependent which will not be uninstalled. + if (dependents.some(e => e !== extension && dependencies.indexOf(e) === -1)) { + dependenciesToUninstall.splice(index - (dependencies.length - dependenciesToUninstall.length), 1); + } + } + + return dependenciesToUninstall; + } + + private getAllDependenciesToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] { if (checked.indexOf(extension) !== -1) { return []; } @@ -630,29 +630,32 @@ export class ExtensionManagementService extends Disposable implements IExtension if (!extension.manifest.extensionDependencies || extension.manifest.extensionDependencies.length === 0) { return []; } - const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1); + const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.some(id => areSameExtensions({ id }, i.galleryIdentifier))); const depsOfDeps = []; for (const dep of dependenciesToUninstall) { - depsOfDeps.push(...this.getDependenciesToUninstallRecursively(dep, installed, checked)); + depsOfDeps.push(...this.getAllDependenciesToUninstall(dep, installed, checked)); } return [...dependenciesToUninstall, ...depsOfDeps]; } - private filterDependents(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): ILocalExtension[] { - installed = installed.filter(i => i !== extension && i.manifest.extensionDependencies && i.manifest.extensionDependencies.length > 0); - let result = dependencies.slice(0); - for (let i = 0; i < dependencies.length; i++) { - const dep = dependencies[i]; - const dependents = this.getDependents(dep, installed).filter(e => dependencies.indexOf(e) === -1); - if (dependents.length) { - result.splice(i - (dependencies.length - result.length), 1); - } + private getAllPackExtensionsToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] { + if (checked.indexOf(extension) !== -1) { + return []; } - return result; + checked.push(extension); + if (!extension.manifest.extensionPack || extension.manifest.extensionPack.length === 0) { + return []; + } + const packedExtensions = installed.filter(i => extension.manifest.extensionPack.some(id => areSameExtensions({ id }, i.galleryIdentifier))); + const packOfPackedExtensions = []; + for (const packedExtension of packedExtensions) { + packOfPackedExtensions.push(...this.getAllPackExtensionsToUninstall(packedExtension, installed, checked)); + } + return [...packedExtensions, ...packOfPackedExtensions]; } private getDependents(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] { - return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(extension)) !== -1); + return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.galleryIdentifier))); } private doUninstall(extension: ILocalExtension): TPromise { @@ -761,8 +764,12 @@ export class ExtensionManagementService extends Disposable implements IExtension if (manifest.extensionDependencies) { manifest.extensionDependencies = manifest.extensionDependencies.map(id => adoptToGalleryExtensionId(id)); } + if (manifest.extensionPack) { + manifest.extensionPack = manifest.extensionPack.map(id => adoptToGalleryExtensionId(id)); + } const identifier = { id: type === LocalExtensionType.System ? folderName : getLocalExtensionIdFromManifest(manifest), uuid: metadata ? metadata.id : null }; - return { type, identifier, manifest, metadata, location: URI.file(extensionPath), readmeUrl, changelogUrl }; + const galleryIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: identifier.uuid }; + return { type, identifier, galleryIdentifier, manifest, metadata, location: URI.file(extensionPath), readmeUrl, changelogUrl }; })) .then(null, () => null); } diff --git a/src/vs/platform/history/electron-main/historyMainService.ts b/src/vs/platform/history/electron-main/historyMainService.ts index 8b8dbc2fe10..9b7309f7c55 100644 --- a/src/vs/platform/history/electron-main/historyMainService.ts +++ b/src/vs/platform/history/electron-main/historyMainService.ts @@ -62,29 +62,33 @@ export class HistoryMainService implements IHistoryMainService { const mru = this.getRecentlyOpened(); // Workspaces - workspaces.forEach(workspace => { - const isUntitledWorkspace = !isSingleFolderWorkspaceIdentifier(workspace) && this.workspacesMainService.isUntitledWorkspace(workspace); - if (isUntitledWorkspace) { - return; // only store saved workspaces - } + if (Array.isArray(workspaces)) { + workspaces.forEach(workspace => { + const isUntitledWorkspace = !isSingleFolderWorkspaceIdentifier(workspace) && this.workspacesMainService.isUntitledWorkspace(workspace); + if (isUntitledWorkspace) { + return; // only store saved workspaces + } - mru.workspaces.unshift(workspace); - mru.workspaces = arrays.distinct(mru.workspaces, workspace => this.distinctFn(workspace)); + mru.workspaces.unshift(workspace); + mru.workspaces = arrays.distinct(mru.workspaces, workspace => this.distinctFn(workspace)); - // We do not add to recent documents here because on Windows we do this from a custom - // JumpList and on macOS we fill the recent documents in one go from all our data later. - }); + // We do not add to recent documents here because on Windows we do this from a custom + // JumpList and on macOS we fill the recent documents in one go from all our data later. + }); + } // Files - files.forEach((path) => { - mru.files.unshift(path); - mru.files = arrays.distinct(mru.files, file => this.distinctFn(file)); + if (Array.isArray(files)) { + files.forEach((path) => { + mru.files.unshift(path); + mru.files = arrays.distinct(mru.files, file => this.distinctFn(file)); - // Add to recent documents (Windows only, macOS later) - if (isWindows) { - app.addRecentDocument(path); - } - }); + // Add to recent documents (Windows only, macOS later) + if (isWindows) { + app.addRecentDocument(path); + } + }); + } // Make sure its bounded mru.workspaces = mru.workspaces.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES); diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index bc246f6ed0c..d4448b8b1bf 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -10,7 +10,7 @@ import { createDecorator, IInstantiationService } from 'vs/platform/instantiatio import { IDisposable, toDisposable, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { PagedList, IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; -import { IDelegate, IRenderer, IListMouseEvent, IListTouchEvent } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer, IListMouseEvent, IListTouchEvent } from 'vs/base/browser/ui/list/list'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { attachListStyler, defaultListStyles, computeStyles } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -90,6 +90,7 @@ export class ListService implements IListService { const RawWorkbenchListFocusContextKey = new RawContextKey('listFocus', true); export const WorkbenchListSupportsMultiSelectContextKey = new RawContextKey('listSupportsMultiselect', true); export const WorkbenchListFocusContextKey = ContextKeyExpr.and(RawWorkbenchListFocusContextKey, ContextKeyExpr.not(InputFocusedContextKey)); +export const WorkbenchListHasSelectionOrFocus = new RawContextKey('listHasSelectionOrFocus', false); export const WorkbenchListDoubleSelection = new RawContextKey('listDoubleSelection', false); export const WorkbenchListMultiSelection = new RawContextKey('listMultiSelection', false); @@ -199,6 +200,7 @@ export class WorkbenchList extends List { readonly contextKeyService: IContextKeyService; + private listHasSelectionOrFocus: IContextKey; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; @@ -206,7 +208,7 @@ export class WorkbenchList extends List { constructor( container: HTMLElement, - delegate: IDelegate, + delegate: IVirtualDelegate, renderers: IRenderer[], options: IListOptions, @IContextKeyService contextKeyService: IContextKeyService, @@ -225,6 +227,7 @@ export class WorkbenchList extends List { ); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); @@ -236,8 +239,17 @@ export class WorkbenchList extends List { attachListStyler(this, themeService), this.onSelectionChange(() => { const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); this.listMultiSelection.set(selection.length > 1); this.listDoubleSelection.set(selection.length === 2); + }), + this.onFocusChange(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); }) ])); @@ -267,7 +279,7 @@ export class WorkbenchPagedList extends PagedList { constructor( container: HTMLElement, - delegate: IDelegate, + delegate: IVirtualDelegate, renderers: IPagedRenderer[], options: IListOptions, @IContextKeyService contextKeyService: IContextKeyService, @@ -323,6 +335,7 @@ export class WorkbenchTree extends Tree { protected disposables: IDisposable[]; + private listHasSelectionOrFocus: IContextKey; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; @@ -352,6 +365,7 @@ export class WorkbenchTree extends Tree { this.disposables = []; this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); @@ -366,10 +380,20 @@ export class WorkbenchTree extends Tree { this.disposables.push(this.onDidChangeSelection(() => { const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set((selection && selection.length > 0) || !!focus); this.listDoubleSelection.set(selection && selection.length === 2); this.listMultiSelection.set(selection && selection.length > 1); })); + this.disposables.push(this.onDidChangeFocus(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set((selection && selection.length > 0) || !!focus); + })); + this.disposables.push(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(openModeSettingKey)) { this._openOnSingleClick = useSingleClickToOpen(configurationService); diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 3d75158aa79..9f2f46c6ce3 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -12,6 +12,7 @@ import URI from 'vs/base/common/uri'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService, KeybindingSource } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { ILogService } from 'vs/platform/log/common/log'; export const NullTelemetryService = new class implements ITelemetryService { _serviceBrand: undefined; @@ -42,6 +43,27 @@ export function combinedAppender(...appenders: ITelemetryAppender[]): ITelemetry export const NullAppender: ITelemetryAppender = { log: () => null, dispose: () => TPromise.as(null) }; + +export class LogAppender implements ITelemetryAppender { + + private commonPropertiesRegex = /^sessionID$|^version$|^timestamp$|^common\./; + constructor(@ILogService private readonly _logService: ILogService) { } + + dispose(): TPromise { + return TPromise.as(undefined); + } + + log(eventName: string, data: any): void { + const strippedData = {}; + Object.keys(data).forEach(key => { + if (!this.commonPropertiesRegex.test(key)) { + strippedData[key] = data[key]; + } + }); + this._logService.trace(`telemetry/${eventName}`, strippedData); + } +} + /* __GDPR__FRAGMENT__ "URIDescriptor" : { "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, diff --git a/src/vs/platform/telemetry/node/appInsightsAppender.ts b/src/vs/platform/telemetry/node/appInsightsAppender.ts index 898f5bfc02c..e8a7ead4570 100644 --- a/src/vs/platform/telemetry/node/appInsightsAppender.ts +++ b/src/vs/platform/telemetry/node/appInsightsAppender.ts @@ -9,6 +9,7 @@ import { isObject } from 'vs/base/common/types'; import { safeStringify, mixin } from 'vs/base/common/objects'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ILogService } from 'vs/platform/log/common/log'; let _initialized = false; @@ -53,7 +54,8 @@ export class AppInsightsAppender implements ITelemetryAppender { constructor( private _eventPrefix: string, private _defaultData: { [key: string]: any }, - aiKeyOrClientFactory: string | (() => typeof appInsights.client) // allow factory function for testing + aiKeyOrClientFactory: string | (() => typeof appInsights.client), // allow factory function for testing + @ILogService private _logService?: ILogService ) { if (!this._defaultData) { this._defaultData = Object.create(null); @@ -133,8 +135,12 @@ export class AppInsightsAppender implements ITelemetryAppender { return; } data = mixin(data, this._defaultData); - let { properties, measurements } = AppInsightsAppender._getData(data); - this._aiClient.trackEvent(this._eventPrefix + '/' + eventName, properties, measurements); + data = AppInsightsAppender._getData(data); + + if (this._logService) { + this._logService.trace(`telemetry/${eventName}`, data); + } + this._aiClient.trackEvent(this._eventPrefix + '/' + eventName, data.properties, data.measurements); } dispose(): TPromise { diff --git a/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts b/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts index a298f26aa91..040a51185f9 100644 --- a/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; +import { ILogService, AbstractLogService, LogLevel, DEFAULT_LOG_LEVEL } from 'vs/platform/log/common/log'; interface IAppInsightsEvent { eventName: string; @@ -39,11 +40,61 @@ class AppInsightsMock { } } +class TestableLogService extends AbstractLogService implements ILogService { + _serviceBrand: any; + + public logs: string[] = []; + + constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) { + super(); + this.setLevel(logLevel); + } + + trace(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Trace) { + this.logs.push(message + JSON.stringify(args)); + } + } + + debug(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Debug) { + this.logs.push(message); + } + } + + info(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Info) { + this.logs.push(message); + } + } + + warn(message: string | Error, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Warning) { + this.logs.push(message.toString()); + } + } + + error(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Error) { + this.logs.push(message); + } + } + + critical(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Critical) { + this.logs.push(message); + } + } + + dispose(): void { } +} + suite('AIAdapter', () => { var appInsightsMock: AppInsightsMock; var adapter: AppInsightsAppender; var prefix = 'prefix'; + setup(() => { appInsightsMock = new AppInsightsMock(); adapter = new AppInsightsAppender(prefix, undefined, () => appInsightsMock); @@ -141,4 +192,27 @@ suite('AIAdapter', () => { assert.equal(appInsightsMock.events[0].properties['nestedObj.nestedObj2.nestedObj3'], JSON.stringify({ 'testProperty': 'test' })); assert.equal(appInsightsMock.events[0].measurements['nestedObj.testMeasurement'], 1); }); + + test('Do not Log Telemetry if log level is not trace', () => { + const logService = new TestableLogService(LogLevel.Info); + adapter = new AppInsightsAppender(prefix, { 'common.platform': 'Windows' }, () => appInsightsMock, logService); + adapter.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 }); + assert.equal(logService.logs.length, 0); + }); + + test('Log Telemetry if log level is trace', () => { + const logService = new TestableLogService(LogLevel.Trace); + adapter = new AppInsightsAppender(prefix, { 'common.platform': 'Windows' }, () => appInsightsMock, logService); + adapter.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 }); + assert.equal(logService.logs.length, 1); + assert.equal(logService.logs[0], 'telemetry/testEvent' + JSON.stringify([{ + properties: { + hello: 'world', + 'common.platform': 'Windows' + }, + measurements: { + isTrue: 1, numberBetween1And3: 2 + } + }])); + }); }); \ No newline at end of file diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 241993fb8f8..facc14315a1 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -223,6 +223,11 @@ export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.ac export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hc: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations.")); +export const breadcrumbsForeground = registerColor('breadcrumb.breadcrumbsForeground', { light: Color.fromHex('#6C6C6C').transparent(.7), dark: Color.fromHex('#CCCCCC').transparent(.7), hc: Color.white.transparent(.7) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); +export const breadcrumbsFocusForeground = registerColor('breadcrumb.breadcrumbsFocusForeground', { light: '#6C6C6C', dark: '#CCCCCC', hc: Color.white }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); +export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.breadcrumbsActiveSelectionForeground', { light: '#6C6C6C', dark: '#CCCCCC', hc: Color.white }, nls.localize('breadcrumbsSelectedForegound', "Color of selected breadcrumb items.")); +export const breadcrumbsActiveSelectionBackground = registerColor('breadcrumb.breadcrumbsActiveSelectionBackground', { light: '#F3F3F3', dark: '#252526', hc: Color.black }, nls.localize('breadcrumbsSelectedBackground', "Background color of selected breadcrumb items.")); + /** * Editor background color. * Because of bug https://monacotools.visualstudio.com/DefaultCollection/Monaco/_workitems/edit/13254 diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index a14be6f3154..c8921a0a5e3 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -6,7 +6,7 @@ 'use strict'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, lighten, badgeBackground, badgeForeground, progressBarBackground } from 'vs/platform/theme/common/colorRegistry'; +import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, lighten, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionBackground, breadcrumbsActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; @@ -265,28 +265,22 @@ export function attachStylerCallback(themeService: IThemeService, colors: { [nam export interface IBreadcrumbsWidgetStyleOverrides extends IStyleOverrides { breadcrumbsBackground?: ColorIdentifier; - breadcrumbsItemHoverBackground?: ColorIdentifier; - breadcrumbsItemHoverForeground?: ColorIdentifier; - breadcrumbsItemFocusBackground?: ColorIdentifier; - breadcrumbsItemFocusForeground?: ColorIdentifier; - breadcrumbsActiveItemSelectionBackground?: ColorIdentifier; - breadcrumbsActiveItemSelectionForeground?: ColorIdentifier; - breadcrumbsInactiveItemSelectionBackground?: ColorIdentifier; - breadcrumbsInactiveItemSelectionForeground?: ColorIdentifier; + breadcrumbsForeground?: ColorIdentifier; + breadcrumbsHoverBackground?: ColorIdentifier; + breadcrumbsHoverForeground?: ColorIdentifier; + breadcrumbsFocusForeground?: ColorIdentifier; + breadcrumbsFocusAndSelectionBackground?: ColorIdentifier; + breadcrumbsFocusAndSelectionForeground?: ColorIdentifier; } export const defaultBreadcrumbsStyles = { breadcrumbsBackground: editorBackground, - breadcrumbsItemHoverBackground: listHoverBackground, - breadcrumbsItemHoverForeground: listHoverForeground, - breadcrumbsItemFocusBackground: listFocusBackground, - breadcrumbsItemFocusForeground: listFocusForeground, - breadcrumbsItemSelectionBackground: listActiveSelectionBackground, - breadcrumbsItemSelectionForeground: listActiveSelectionForeground, - breadcrumbsActiveItemSelectionBackground: listActiveSelectionBackground, - breadcrumbsActiveItemSelectionForeground: listActiveSelectionForeground, - breadcrumbsInactiveItemSelectionBackground: editorBackground, - breadcrumbsInactiveItemSelectionForeground: listInactiveSelectionForeground, + breadcrumbsForeground: breadcrumbsForeground, + breadcrumbsHoverBackground: editorBackground, + breadcrumbsHoverForeground: breadcrumbsFocusForeground, + breadcrumbsFocusForeground: breadcrumbsFocusForeground, + breadcrumbsFocusAndSelectionBackground: breadcrumbsActiveSelectionBackground, + breadcrumbsFocusAndSelectionForeground: breadcrumbsActiveSelectionForeground, }; export function attachBreadcrumbsStyler(widget: IThemable, themeService: IThemeService, style?: IBreadcrumbsWidgetStyleOverrides): IDisposable { diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 9c94e242e06..65855f4b03e 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -162,6 +162,7 @@ export interface IWindowsService { getWindowCount(): TPromise; log(severity: string, ...messages: string[]): TPromise; showItemInFolder(path: string): TPromise; + getActiveWindowId(): TPromise; // This needs to be handled from browser process to prevent // foreground ordering issues on Windows @@ -357,19 +358,31 @@ export interface IRunActionInWindowRequest { export class ActiveWindowManager implements IDisposable { private disposables: IDisposable[] = []; - private _activeWindowId: number; + private firstActiveWindowIdPromise: TPromise | null; + private _activeWindowId: number | undefined; constructor(@IWindowsService windowsService: IWindowsService) { const onActiveWindowChange = latch(anyEvent(windowsService.onWindowOpen, windowsService.onWindowFocus)); onActiveWindowChange(this.setActiveWindow, this, this.disposables); + + this.firstActiveWindowIdPromise = windowsService.getActiveWindowId() + .then(id => (typeof this._activeWindowId === 'undefined') && this.setActiveWindow(id)); } private setActiveWindow(windowId: number) { + if (this.firstActiveWindowIdPromise) { + this.firstActiveWindowIdPromise = null; + } + this._activeWindowId = windowId; } - get activeClientId(): string { - return `window:${this._activeWindowId}`; + getActiveClientId(): TPromise { + if (this.firstActiveWindowIdPromise) { + return this.firstActiveWindowIdPromise; + } + + return TPromise.as(`window:${this._activeWindowId}`); } dispose() { diff --git a/src/vs/platform/windows/common/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts index 0adca2f73b4..fb9cd086488 100644 --- a/src/vs/platform/windows/common/windowsIpc.ts +++ b/src/vs/platform/windows/common/windowsIpc.ts @@ -68,6 +68,7 @@ export interface IWindowsChannel extends IChannel { call(command: 'toggleSharedProcess'): TPromise; call(command: 'log', arg: [string, string[]]): TPromise; call(command: 'showItemInFolder', arg: string): TPromise; + call(command: 'getActiveWindowId'): TPromise; call(command: 'openExternal', arg: string): TPromise; call(command: 'startCrashReporter', arg: CrashReporterStartOptions): TPromise; call(command: 'openAccessibilityOptions'): TPromise; @@ -166,6 +167,7 @@ export class WindowsChannel implements IWindowsChannel { case 'quit': return this.service.quit(); case 'log': return this.service.log(arg[0], arg[1]); case 'showItemInFolder': return this.service.showItemInFolder(arg); + case 'getActiveWindowId': return this.service.getActiveWindowId(); case 'openExternal': return this.service.openExternal(arg); case 'startCrashReporter': return this.service.startCrashReporter(arg); case 'openAccessibilityOptions': return this.service.openAccessibilityOptions(); @@ -364,6 +366,10 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('showItemInFolder', path); } + getActiveWindowId(): TPromise { + return this.channel.call('getActiveWindowId'); + } + openExternal(url: string): TPromise { return this.channel.call('openExternal', url); } diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index 2987443c204..dcc72f14b81 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -14,7 +14,7 @@ import product from 'vs/platform/node/product'; import { IWindowsService, OpenContext, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IDevToolsOptions } from 'vs/platform/windows/common/windows'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { shell, crashReporter, app, Menu, clipboard, BrowserWindow } from 'electron'; -import { Event, fromNodeEventEmitter, mapEvent, filterEvent, anyEvent } from 'vs/base/common/event'; +import { Event, fromNodeEventEmitter, mapEvent, filterEvent, anyEvent, latch } from 'vs/base/common/event'; import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IWindowsMainService, ISharedProcess } from 'vs/platform/windows/electron-main/windows'; @@ -32,6 +32,8 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable private disposables: IDisposable[] = []; + private _activeWindowId: number | undefined; + readonly onWindowOpen: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-created', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); readonly onWindowFocus: Event = anyEvent( mapEvent(filterEvent(mapEvent(this.windowsMainService.onWindowsCountChanged, () => this.windowsMainService.getLastActiveWindow()), w => !!w), w => w.id), @@ -54,6 +56,10 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable @ILogService private logService: ILogService ) { urlService.registerHandler(this); + + // remember last active window id + latch(anyEvent(this.onWindowOpen, this.onWindowFocus)) + (id => this._activeWindowId = id, null, this.disposables); } pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise { @@ -435,6 +441,10 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } + getActiveWindowId(): TPromise { + return TPromise.as(this._activeWindowId); + } + openExternal(url: string): TPromise { this.logService.trace('windowsService#openExternal'); return TPromise.as(shell.openExternal(url)); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 6dcc58cc6df..cd04e4a88a9 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -189,12 +189,45 @@ declare module 'vscode' { provideTextSearchResults?(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Thenable; } + /** + * Options that can be set on a findTextInFiles search. + */ export interface FindTextInFilesOptions { + /** + * A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern + * will be matched against the file paths of files relative to their workspace. Use a [relative pattern](#RelativePattern) + * to restrict the search results to a [workspace folder](#WorkspaceFolder). + */ include?: GlobPattern; - exclude?: GlobPattern; + + /** + * A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will + * apply, when `null` no excludes will apply. + */ + exclude?: GlobPattern | null; + + /** + * The maximum number of results to search for + */ maxResults?: number; + + /** + * Whether external files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useIgnoreFiles"`. + */ useIgnoreFiles?: boolean; + + /** + * Whether symlinks should be followed while searching. + * See the vscode setting `"search.followSymlinks"`. + */ followSymlinks?: boolean; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ encoding?: string; } @@ -210,6 +243,24 @@ declare module 'vscode' { */ export function registerSearchProvider(scheme: string, provider: SearchProvider): Disposable; + + /** + * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. + * @param callback A callback, called for each result + * @param token A token that can be used to signal cancellation to the underlying search engine. + * @return A thenable that resolves when the search is complete. + */ + export function findTextInFiles(query: TextSearchQuery, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; + + /** + * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. + * @param options An optional set of query options. Include and exclude patterns, maxResults, etc. + * @param callback A callback, called for each result + * @param token A token that can be used to signal cancellation to the underlying search engine. + * @return A thenable that resolves when the search is complete. + */ export function findTextInFiles(query: TextSearchQuery, options: FindTextInFilesOptions, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index a3e9c819851..cafb5b305b2 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTask.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTask.ts @@ -332,7 +332,7 @@ namespace TaskDTO { let definition = TaskDefinitionDTO.to(task.definition, executeOnly); let id = `${task.source.extensionId}.${definition._key}`; let result: ContributedTask = { - _id: id, // uuidMap.getUUID(identifier), + _id: id, // uuidMap.getUUID(identifier) _source: source, _label: label, type: definition.type, diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index eea4190baed..41d84006f0f 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -500,7 +500,19 @@ export function createApiFactory( findFiles: (include, exclude, maxResults?, token?) => { return extHostWorkspace.findFiles(typeConverters.GlobPattern.from(include), typeConverters.GlobPattern.from(exclude), maxResults, extension.id, token); }, - findTextInFiles: (query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, callback: (result: vscode.TextSearchResult) => void, token?: vscode.CancellationToken) => { + findTextInFiles: (query: vscode.TextSearchQuery, optionsOrCallback, callbackOrToken?, token?: vscode.CancellationToken) => { + let options: vscode.FindTextInFilesOptions; + let callback: (result: vscode.TextSearchResult) => void; + + if (typeof optionsOrCallback === 'object') { + options = optionsOrCallback; + callback = callbackOrToken; + } else { + options = {}; + callback = optionsOrCallback; + token = callbackOrToken; + } + return extHostWorkspace.findTextInFiles(query, options || {}, callback, extension.id, token); }, saveAll: (includeUntitled?) => { diff --git a/src/vs/workbench/api/node/extHostProgress.ts b/src/vs/workbench/api/node/extHostProgress.ts index fc4be5b66e6..f4a17349909 100644 --- a/src/vs/workbench/api/node/extHostProgress.ts +++ b/src/vs/workbench/api/node/extHostProgress.ts @@ -71,11 +71,10 @@ export class ExtHostProgress implements ExtHostProgressShape { function mergeProgress(result: IProgressStep, currentValue: IProgressStep): IProgressStep { result.message = currentValue.message; - if (typeof currentValue.increment === 'number' && typeof result.message === 'number') { - result.increment += currentValue.increment; - } else if (typeof currentValue.increment === 'number') { + if (typeof currentValue.increment === 'number') { result.increment = currentValue.increment; } + return result; } diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index d35c2401007..7dca897dd92 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -9,11 +9,9 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as glob from 'vs/base/common/glob'; import * as resources from 'vs/base/common/resources'; -import * as strings from 'vs/base/common/strings'; import URI, { UriComponents } from 'vs/base/common/uri'; import { PPromise, TPromise } from 'vs/base/common/winjs.base'; import * as extfs from 'vs/base/node/extfs'; -import * as pfs from 'vs/base/node/pfs'; import { IFileMatch, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchCompleteStats, ISearchQuery } from 'vs/platform/search/common/search'; import * as vscode from 'vscode'; import { ExtHostSearchShape, IMainContext, MainContext, MainThreadSearchShape } from './extHost.protocol'; @@ -30,9 +28,9 @@ export class ExtHostSearch implements ExtHostSearchShape { private _fileSearchManager: FileSearchManager; - constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer, private _extfs = extfs, private _pfs = pfs) { + constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer, private _extfs = extfs) { this._proxy = mainContext.getProxy(MainContext.MainThreadSearch); - this._fileSearchManager = new FileSearchManager(this._pfs); + this._fileSearchManager = new FileSearchManager(); } private _transformScheme(scheme: string): string { @@ -317,12 +315,12 @@ class QueryGlobTester { /** * Guaranteed sync - siblingsFn should not return a promise. */ - public includedInQuerySync(testPath: string, basename?: string, siblingsFn?: () => string[]): boolean { - if (this._parsedExcludeExpression && this._parsedExcludeExpression(testPath, basename, siblingsFn)) { + public includedInQuerySync(testPath: string, basename?: string, hasSibling?: (name: string) => boolean): boolean { + if (this._parsedExcludeExpression && this._parsedExcludeExpression(testPath, basename, hasSibling)) { return false; } - if (this._parsedIncludeExpression && !this._parsedIncludeExpression(testPath, basename, siblingsFn)) { + if (this._parsedIncludeExpression && !this._parsedIncludeExpression(testPath, basename, hasSibling)) { return false; } @@ -332,9 +330,9 @@ class QueryGlobTester { /** * Guaranteed async. */ - public includedInQuery(testPath: string, basename?: string, siblingsFn?: () => string[] | TPromise): TPromise { + public includedInQuery(testPath: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise): TPromise { const excludeP = this._parsedExcludeExpression ? - TPromise.as(this._parsedExcludeExpression(testPath, basename, siblingsFn)).then(result => !!result) : + TPromise.as(this._parsedExcludeExpression(testPath, basename, hasSibling)).then(result => !!result) : TPromise.wrap(false); return excludeP.then(excluded => { @@ -343,7 +341,7 @@ class QueryGlobTester { } return this._parsedIncludeExpression ? - TPromise.as(this._parsedIncludeExpression(testPath, basename, siblingsFn)).then(result => !!result) : + TPromise.as(this._parsedIncludeExpression(testPath, basename, hasSibling)).then(result => !!result) : TPromise.wrap(true); }).then(included => { return included; @@ -429,13 +427,13 @@ class TextSearchEngine { const testingPs = []; const progress = { report: (result: vscode.TextSearchResult) => { - const siblingFn = folderQuery.folder.scheme === 'file' && (() => { + const hasSibling = folderQuery.folder.scheme === 'file' && glob.hasSiblingPromiseFn(() => { return this.readdir(path.dirname(result.uri.fsPath)); }); const relativePath = path.relative(folderQuery.folder.fsPath, result.uri.fsPath); testingPs.push( - queryTester.includedInQuery(relativePath, path.basename(relativePath), siblingFn) + queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling) .then(included => { if (included) { onResult(result); @@ -506,7 +504,6 @@ function patternInfoToQuery(patternInfo: IPatternInfo): vscode.TextSearchQuery { class FileSearchEngine { private filePattern: string; - private normalizedFilePatternLowercase: string; private includePattern: glob.ParsedExpression; private maxResults: number; private exists: boolean; @@ -518,7 +515,7 @@ class FileSearchEngine { private globalExcludePattern: glob.ParsedExpression; - constructor(private config: ISearchQuery, private provider: vscode.SearchProvider, private _pfs: typeof pfs) { + constructor(private config: ISearchQuery, private provider: vscode.SearchProvider) { this.filePattern = config.filePattern; this.includePattern = config.includePattern && glob.parse(config.includePattern); this.maxResults = config.maxResults || null; @@ -527,10 +524,6 @@ class FileSearchEngine { this.isLimitHit = false; this.activeCancellationTokens = new Set(); - if (this.filePattern) { - this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase(); - } - this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern); } @@ -633,19 +626,6 @@ class FileSearchEngine { return null; } - if (noSiblingsClauses && this.isLimitHit) { - // If the limit was hit, check whether filePattern is an exact relative match because it must be included - return this.checkFilePatternRelativeMatch(fq.folder).then(({ exists, size }) => { - if (exists) { - onResult({ - base: fq.folder, - relativePath: this.filePattern, - basename: path.basename(this.filePattern), - }); - } - }); - } - this.matchDirectoryTree(tree, queryTester, onResult); return null; }).then( @@ -712,6 +692,7 @@ class FileSearchEngine { const self = this; const filePattern = this.filePattern; function matchDirectory(entries: IDirectoryEntry[]) { + const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename)); for (let i = 0, n = entries.length; i < n; i++) { const entry = entries[i]; const { relativePath, basename } = entry; @@ -720,7 +701,7 @@ class FileSearchEngine { // If the user searches for the exact file name, we adjust the glob matching // to ignore filtering by siblings because the user seems to know what she // is searching for and we want to include the result in that case anyway - if (!queryTester.includedInQuerySync(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) { + if (!queryTester.includedInQuerySync(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { continue; } @@ -743,26 +724,8 @@ class FileSearchEngine { matchDirectory(rootEntries); } - private checkFilePatternRelativeMatch(base: URI): TPromise<{ exists: boolean, size?: number }> { - if (!this.filePattern || path.isAbsolute(this.filePattern) || base.scheme !== 'file') { - return TPromise.wrap({ exists: false }); - } - - const absolutePath = path.join(base.fsPath, this.filePattern); - return this._pfs.stat(absolutePath).then(stat => { - return { - exists: !stat.isDirectory(), - size: stat.size - }; - }, err => { - return { - exists: false - }; - }); - } - private matchFile(onResult: (result: IInternalFileMatch) => void, candidate: IInternalFileMatch): void { - if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) { + if (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename)) { if (this.exists || (this.maxResults && this.resultCount >= this.maxResults)) { this.isLimitHit = true; this.cancel(); @@ -773,20 +736,6 @@ class FileSearchEngine { } } } - - private isFilePatternMatch(path: string): boolean { - // Check for search pattern - if (this.filePattern) { - if (this.filePattern === '*') { - return true; // support the all-matching wildcard - } - - return strings.fuzzyContains(path, this.normalizedFilePatternLowercase); - } - - // No patterns means we match all - return true; - } } interface IInternalSearchComplete { @@ -800,12 +749,10 @@ class FileSearchManager { private readonly expandedCacheKeys = new Map(); - constructor(private _pfs: typeof pfs) { } - fileSearch(config: ISearchQuery, provider: vscode.SearchProvider): PPromise { let searchP: PPromise; return new PPromise((c, e, p) => { - const engine = new FileSearchEngine(config, provider, this._pfs); + const engine = new FileSearchEngine(config, provider); searchP = this.doSearch(engine, FileSearchManager.BATCH_SIZE).then( result => { diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index c8a40bb16a8..ee825674099 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -871,7 +871,8 @@ export class ExtHostTask implements ExtHostTaskShape { if (task.definition && validTypes[task.definition.type] === true) { sanitized.push(task); } else { - console.error(`Dropping task [${task.source}, ${task.name}]. Its type is not known to the system.`); + sanitized.push(task); + console.warn(`The task [${task.source}, ${task.name}] uses an undefined task type. The task will be ignored in the future.`); } } let workspaceFolders = this._workspaceService.getWorkspaceFolders(); diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index 94f68267136..f9f85749d90 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -30,9 +30,12 @@ padding-left: 12px; } -.monaco-workbench > .part > .title > .title-label span { +.monaco-workbench > .part > .title > .title-label h2 { font-size: 11px; cursor: default; + font-weight: normal; + -webkit-margin-before: 0; + -webkit-margin-after: 0; } .monaco-workbench > .part > .title > .title-label a { diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 386e2981306..52578559e00 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -434,7 +434,7 @@ export abstract class CompositePart extends Part { $(parent).div({ 'class': 'title-label' }, div => { - titleLabel = div.span(); + titleLabel = div.element('h2'); }); const $this = this; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts new file mode 100644 index 00000000000..27d5727d3c6 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { BreadcrumbsWidget } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { GroupIdentifier } from 'vs/workbench/common/editor'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export const IBreadcrumbsService = createDecorator('IEditorBreadcrumbsService'); + +export interface IBreadcrumbsService { + + _serviceBrand: any; + + register(group: GroupIdentifier, widget: BreadcrumbsWidget): IDisposable; + + getWidget(group: GroupIdentifier): BreadcrumbsWidget; +} + + +export class BreadcrumbsService implements IBreadcrumbsService { + + _serviceBrand: any; + + private readonly _map = new Map(); + + register(group: number, widget: BreadcrumbsWidget): IDisposable { + if (this._map.has(group)) { + throw new Error(`group (${group}) has already a widget`); + } + this._map.set(group, widget); + return { + dispose: () => this._map.delete(group) + }; + } + + getWidget(group: number): BreadcrumbsWidget { + return this._map.get(group); + } +} + +registerSingleton(IBreadcrumbsService, BreadcrumbsService); diff --git a/src/vs/workbench/browser/parts/editor/editorBreadcrumbs.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts similarity index 62% rename from src/vs/workbench/browser/parts/editor/editorBreadcrumbs.ts rename to src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index d271ac2a475..562096a0273 100644 --- a/src/vs/workbench/browser/parts/editor/editorBreadcrumbs.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -11,12 +11,12 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { compareFileNames } from 'vs/base/common/comparers'; import { debounceEvent, Emitter, Event } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { dirname, isEqual } from 'vs/base/common/resources'; +import { dispose, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { dirname, isEqual, basenameOrAuthority } from 'vs/base/common/resources'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDataSource, IRenderer, ISelectionEvent, ISorter, ITree, ITreeConfiguration } from 'vs/base/parts/tree/browser/tree'; -import 'vs/css!./media/editorbreadcrumbs'; +import 'vs/css!./media/breadcrumbscontrol'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { Range } from 'vs/editor/common/core/range'; import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; @@ -35,12 +35,15 @@ import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { FileLabel } from 'vs/workbench/browser/labels'; -import { BreadcrumbElement, EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/editorBreadcrumbsModel'; +import { BreadcrumbElement, EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; -import { EditorInput } from 'vs/workbench/common/editor'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorBreadcrumbs, IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IBreadcrumbsService } from 'vs/workbench/browser/parts/editor/breadcrumbs'; +import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; +import { symbolKindToCssClass } from 'vs/editor/common/modes'; class Item extends BreadcrumbsItem { @@ -48,6 +51,7 @@ class Item extends BreadcrumbsItem { constructor( readonly element: BreadcrumbElement, + readonly options: IBreadcrumbsControlOptions, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -73,12 +77,20 @@ class Item extends BreadcrumbsItem { render(container: HTMLElement): void { if (this.element instanceof FileElement) { // file/folder - let label = this._instantiationService.createInstance(FileLabel, container, {}); - label.setFile(this.element.uri, { - hidePath: true, - fileKind: this.element.isFile ? FileKind.FILE : FileKind.FOLDER - }); - this._disposables.push(label); + if (this.options.showIcons) { + let label = this._instantiationService.createInstance(FileLabel, container, {}); + label.setFile(this.element.uri, { + hidePath: true, + fileKind: this.element.isFile ? FileKind.FILE : FileKind.FOLDER, + fileDecorations: { colors: this.options.showDecorationColors, badges: false } + }); + this._disposables.push(label); + + } else { + let label = new IconLabel(container); + label.setValue(basenameOrAuthority(this.element.uri)); + this._disposables.push(label); + } } else if (this.element instanceof OutlineGroup) { // provider @@ -88,31 +100,45 @@ class Item extends BreadcrumbsItem { } else if (this.element instanceof OutlineElement) { // symbol + + if (this.options.showIcons) { + let icon = document.createElement('div'); + icon.className = `symbol-icon ${symbolKindToCssClass(this.element.symbol.kind)}`; + container.appendChild(icon); + } + let label = new IconLabel(container); - label.setValue(this.element.symbol.name); + label.setValue(this.element.symbol.name.replace(/\r|\n|\r\n/g, '\u23CE')); this._disposables.push(label); } } } -export class EditorBreadcrumbs implements IEditorBreadcrumbs { +export interface IBreadcrumbsControlOptions { + showIcons: boolean; + showDecorationColors: boolean; +} + +export class BreadcrumbsControl { + + static HEIGHT = 25; static CK_BreadcrumbsVisible = new RawContextKey('breadcrumbsVisible', false); - static CK_BreadcrumbsFocused = new RawContextKey('breadcrumbsFocused', false); + static CK_BreadcrumbsActive = new RawContextKey('breadcrumbsActive', false); private readonly _ckBreadcrumbsVisible: IContextKey; - private readonly _ckBreadcrumbsFocused: IContextKey; + private readonly _ckBreadcrumbsActive: IContextKey; - private readonly _cfEnabled: Config; - - private readonly _disposables = new Array(); - private readonly _domNode: HTMLDivElement; + readonly domNode: HTMLDivElement; private readonly _widget: BreadcrumbsWidget; + private _disposables = new Array(); private _breadcrumbsDisposables = new Array(); + private _breadcrumbsPickerShowing = false; constructor( container: HTMLElement, + private readonly _options: IBreadcrumbsControlOptions, private readonly _editorGroup: EditorGroupView, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IContextViewService private readonly _contextViewService: IContextViewService, @@ -122,101 +148,78 @@ export class EditorBreadcrumbs implements IEditorBreadcrumbs { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IThemeService private readonly _themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, + @IBreadcrumbsService breadcrumbsService: IBreadcrumbsService, ) { - this._domNode = document.createElement('div'); - dom.addClasses(this._domNode, 'editor-breadcrumbs', 'show-file-icons'); - dom.append(container, this._domNode); + this.domNode = document.createElement('div'); + dom.addClasses(this.domNode, 'breadcrumbs-control'); + dom.append(container, this.domNode); - this._widget = new BreadcrumbsWidget(this._domNode); - this._widget.onDidSelectItem(this._onDidSelectItem, this, this._disposables); - this._widget.onDidChangeFocus(val => this._ckBreadcrumbsFocused.set(val), undefined, this._disposables); + this._widget = new BreadcrumbsWidget(this.domNode); + this._widget.onDidSelectItem(this._onSelectEvent, this, this._disposables); + this._widget.onDidFocusItem(this._onFocusEvent, this, this._disposables); + this._widget.onDidChangeFocus(this._updateCkBreadcrumbsActive, this, this._disposables); this._disposables.push(attachBreadcrumbsStyler(this._widget, this._themeService)); - this._cfEnabled = Config.create(configurationService, 'breadcrumbs.enabled'); - this._disposables.push(this._cfEnabled.onDidChange(value => { - if (!value) { - this.closeEditor(undefined); - this._editorGroup.relayout(); - } else if (this._editorGroup.activeEditor) { - this.openEditor(this._editorGroup.activeEditor); - this._editorGroup.relayout(); - } - })); + this._ckBreadcrumbsVisible = BreadcrumbsControl.CK_BreadcrumbsVisible.bindTo(this._contextKeyService); + this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService); - this._ckBreadcrumbsVisible = EditorBreadcrumbs.CK_BreadcrumbsVisible.bindTo(this._contextKeyService); - this._ckBreadcrumbsFocused = EditorBreadcrumbs.CK_BreadcrumbsFocused.bindTo(this._contextKeyService); + this._disposables.push(breadcrumbsService.register(this._editorGroup.id, this._widget)); } dispose(): void { - dispose(this._disposables); - this._widget.dispose(); + this._disposables = dispose(this._disposables); + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); this._ckBreadcrumbsVisible.reset(); - this._cfEnabled.dispose(); - } - - getPreferredHeight(): number { - return this._cfEnabled.value ? 25 : 0; + this._ckBreadcrumbsActive.reset(); + this._widget.dispose(); + this.domNode.remove(); } layout(dim: dom.Dimension): void { - this._domNode.style.width = `${dim.width}px`; - this._domNode.style.height = `${dim.height}px`; this._widget.layout(dim); } - setActive(value: boolean): void { - dom.toggleClass(this._domNode, 'active', value); - } + update(): void { + const input = this._editorGroup.activeEditor; + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); - openEditor(input: EditorInput): void { - if (!this._cfEnabled.value) { - // not enabled -> return early + if (!input || !input.getResource() || !this._fileService.canHandleResource(input.getResource())) { + // cleanup and return when there is no input or when + // we cannot handle this input + this._ckBreadcrumbsVisible.set(false); + dom.toggleClass(this.domNode, 'hidden', true); return; } - this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); - - let uri = input.getResource(); - if (!uri || !this._fileService.canHandleResource(uri)) { - return this.closeEditor(undefined); - } - - dom.toggleClass(this._domNode, 'hidden', false); + dom.toggleClass(this.domNode, 'hidden', false); this._ckBreadcrumbsVisible.set(true); let control = this._editorGroup.activeControl.getControl() as ICodeEditor; let model = new EditorBreadcrumbsModel(input.getResource(), isCodeEditor(control) ? control : undefined, this._workspaceService); - let listener = model.onDidUpdate(_ => this._widget.setItems(model.getElements().map(element => new Item(element, this._instantiationService)))); - this._widget.setItems(model.getElements().map(element => new Item(element, this._instantiationService))); - this._breadcrumbsDisposables.push(model, listener); + let updateBreadcrumbs = () => { + let items = model.getElements().map(element => new Item(element, this._options, this._instantiationService)); + this._widget.setItems(items); + this._widget.reveal(items[items.length - 1]); + }; + let listener = model.onDidUpdate(updateBreadcrumbs); + updateBreadcrumbs(); + this._breadcrumbsDisposables = [model, listener]; } - closeEditor(input: EditorInput): void { + clear(): void { + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); this._ckBreadcrumbsVisible.set(false); - dom.toggleClass(this._domNode, 'hidden', true); + dom.toggleClass(this.domNode, 'hidden', true); } - focus(): void { - this._widget.domFocus(); - } - - focusNext(): void { - this._widget.focusNext(); - } - - focusPrev(): void { - this._widget.focusPrev(); - } - - select(): void { - const item = this._widget.getFocused(); - if (item) { - this._widget.setSelected(item); + private _onFocusEvent(event: IBreadcrumbsItemEvent): void { + if (event.item && this._breadcrumbsPickerShowing) { + return this._widget.setSelection(event.item); } } - private _onDidSelectItem(event: IBreadcrumbsItemEvent): void { + private _onSelectEvent(event: IBreadcrumbsItemEvent): void { if (!event.item) { return; } @@ -226,17 +229,24 @@ export class EditorBreadcrumbs implements IEditorBreadcrumbs { getAnchor() { return event.node; }, - render: (container: HTMLElement) => { - dom.addClasses(container, 'monaco-breadcrumbs-picker', 'monaco-workbench', 'show-file-icons'); - let color = this._themeService.getTheme().getColor(SIDE_BAR_BACKGROUND); - container.style.borderColor = color.darken(.2).toString(); - container.style.boxShadow = `${color.toString()} 6px 6px 6px -6px;`; + render: (parent: HTMLElement) => { + const container = document.createElement('div'); + parent.appendChild(container); + const theme = this._themeService.getTheme(); + const color = theme.getColor(editorBackground).darken(theme.type === 'dark' ? .2 : .1); + container.style.borderColor = color.toString(); + container.style.boxShadow = `2px 2px 3px ${color.toString()}`; + container.style.position = 'absolute'; + container.style.zIndex = '1000'; + dom.addClasses(container, 'monaco-breadcrumbs-picker', 'show-file-icons'); + let { element } = event.item as Item; let ctor: IConstructorSignature2 = element instanceof FileElement ? BreadcrumbsFilePicker : BreadcrumbsOutlinePicker; let res = this._instantiationService.createInstance(ctor, container, element); - res.layout({ width: 250, height: 300 }); - res.onDidPickElement(data => { + res.layout({ width: 330, height: 220 }); + let listener = res.onDidPickElement(data => { this._contextViewService.hideContextView(); + this._widget.setSelection(undefined); if (!data) { return; } @@ -260,14 +270,22 @@ export class EditorBreadcrumbs implements IEditorBreadcrumbs { } }); - return res; + this._breadcrumbsPickerShowing = true; + this._updateCkBreadcrumbsActive(); + + return combinedDisposable([listener, res]); }, - onHide: () => { - this._widget.setSelected(undefined); - // this._widget.setFocused(undefined); + onHide: (data) => { + this._breadcrumbsPickerShowing = false; + this._updateCkBreadcrumbsActive(); } }); } + + private _updateCkBreadcrumbsActive(): void { + const value = this._widget.isDOMFocused() || this._breadcrumbsPickerShowing; + this._ckBreadcrumbsActive.set(value); + } } export abstract class BreadcrumbsPicker { @@ -296,8 +314,17 @@ export abstract class BreadcrumbsPicker { this.focus = dom.trackFocus(this._domNode); this.focus.onDidBlur(_ => this._onDidPickElement.fire(undefined), undefined, this._disposables); + this._tree.setInput(this._getInput(input)).then(_ => { + this._tree.domFocus(); + let selection = this._getInitialSelection(this._tree, input); + if (selection) { + this._tree.setFocus(selection); + this._tree.reveal(selection); + } else { + this._tree.focusFirst(); + } - this._tree.setInput(this._getInput(input)); + }, onUnexpectedError); } dispose(): void { @@ -314,6 +341,7 @@ export abstract class BreadcrumbsPicker { } protected abstract _getInput(input: BreadcrumbElement): any; + protected abstract _getInitialSelection(tree: ITree, input: BreadcrumbElement): any; protected abstract _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration; protected abstract _onDidChangeSelection(e: any): void; } @@ -402,6 +430,17 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { return dirname(uri); } + protected _getInitialSelection(tree: ITree, input: BreadcrumbElement): any { + let { uri } = (input as FileElement); + let nav = tree.getNavigator(); + while (nav.next()) { + if (isEqual(uri, (nav.current() as IFileStat).resource)) { + return nav.current(); + } + } + return undefined; + } + protected _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration { // todo@joh reuse explorer implementations? config.dataSource = this._instantiationService.createInstance(FileDataSource); @@ -425,6 +464,10 @@ export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { return (input as TreeElement).parent; } + protected _getInitialSelection(_tree: ITree, input: BreadcrumbElement): any { + return input; + } + protected _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration { config.dataSource = this._instantiationService.createInstance(OutlineDataSource); config.renderer = this._instantiationService.createInstance(OutlineRenderer); @@ -446,32 +489,41 @@ export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { //#region config -abstract class Config { +export abstract class BreadcrumbsConfig { name: string; value: T; onDidChange: Event; abstract dispose(): void; - static create(service: IConfigurationService, name: string): Config { + private constructor() { + // internal + } - let value: T = service.getValue(name); - let onDidChange = new Emitter(); - - let listener = service.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(name)) { - value = service.getValue(name); - onDidChange.fire(value); - } - }); + static IsEnabled = BreadcrumbsConfig._stub('breadcrumbs.enabled'); + private static _stub(name: string): { bindTo(service: IConfigurationService): BreadcrumbsConfig } { return { - name, - get value() { return value; }, - onDidChange: onDidChange.event, - dispose(): void { - listener.dispose(); - onDidChange.dispose(); + bindTo(service) { + let value: T = service.getValue(name); + let onDidChange = new Emitter(); + + let listener = service.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(name)) { + value = service.getValue(name); + onDidChange.fire(value); + } + }); + + return { + name, + get value() { return value; }, + onDidChange: onDidChange.event, + dispose(): void { + listener.dispose(); + onDidChange.dispose(); + } + }; } }; } @@ -499,50 +551,62 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'breadcrumbs.focus', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT, - when: EditorBreadcrumbs.CK_BreadcrumbsVisible, + when: BreadcrumbsControl.CK_BreadcrumbsVisible, handler(accessor) { - let groups = accessor.get(IEditorGroupsService); - groups.activeGroup.breadcrumbs.focus(); + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + //todo@joh focus last? + breadcrumbs.getWidget(groups.activeGroup.id).domFocus(); } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'breadcrumbs.focusNext', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), primary: KeyCode.RightArrow, - when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + secondary: [KeyMod.Shift | KeyCode.RightArrow], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), handler(accessor) { - let groups = accessor.get(IEditorGroupsService); - groups.activeGroup.breadcrumbs.focusNext(); + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + breadcrumbs.getWidget(groups.activeGroup.id).focusNext(); } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'breadcrumbs.focusPrevious', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), primary: KeyCode.LeftArrow, - when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + secondary: [KeyMod.Shift | KeyCode.LeftArrow], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), handler(accessor) { - let groups = accessor.get(IEditorGroupsService); - groups.activeGroup.breadcrumbs.focusPrev(); + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + breadcrumbs.getWidget(groups.activeGroup.id).focusPrev(); } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'breadcrumbs.selectFocused', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), primary: KeyCode.Enter, - secondary: [KeyCode.UpArrow, KeyCode.Space], - when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + secondary: [KeyCode.DownArrow, KeyCode.Space], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), handler(accessor) { - let groups = accessor.get(IEditorGroupsService); - groups.activeGroup.breadcrumbs.select(); + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + const widget = breadcrumbs.getWidget(groups.activeGroup.id); + widget.setSelection(widget.getFocused()); } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'breadcrumbs.selectEditor', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), primary: KeyCode.Escape, - when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + secondary: [KeyMod.Shift | KeyCode.Escape], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), handler(accessor) { - let groups = accessor.get(IEditorGroupsService); + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + breadcrumbs.getWidget(groups.activeGroup.id).setFocused(undefined); + breadcrumbs.getWidget(groups.activeGroup.id).setSelection(undefined); groups.activeGroup.activeControl.focus(); } }); diff --git a/src/vs/workbench/browser/parts/editor/editorBreadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts similarity index 98% rename from src/vs/workbench/browser/parts/editor/editorBreadcrumbsModel.ts rename to src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index bb1e22f0e68..f613d656546 100644 --- a/src/vs/workbench/browser/parts/editor/editorBreadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -117,7 +117,7 @@ export class EditorBreadcrumbsModel { this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); this._outlineDisposables.push(this._editor.onDidChangeCursorPosition(_ => { timeout.cancelAndSet(() => { - if (versionIdThen === buffer.getVersionId()) { + if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId()) { this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); } }, 150); diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 5e63f57bfad..248fa7871f5 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -117,6 +117,7 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito isEmpty(): boolean; setActive(isActive: boolean): void; setLabel(label: string): void; + relayout(): void; shutdown(): void; } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index a31d41c5797..372e4e6999d 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -19,7 +19,7 @@ import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { Themable, EDITOR_GROUP_HEADER_TABS_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_EMPTY_BACKGROUND, EDITOR_GROUP_FOCUSED_EMPTY_BORDER } from 'vs/workbench/common/theme'; -import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, EditorsOrder, GroupsOrder, IEditorBreadcrumbs } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, EditorsOrder, GroupsOrder } from 'vs/workbench/services/group/common/editorGroupsService'; import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl'; import { EditorControl } from 'vs/workbench/browser/parts/editor/editorControl'; import { IProgressService } from 'vs/platform/progress/common/progress'; @@ -34,7 +34,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; -import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptionsChangeEvent, EDITOR_TITLE_HEIGHT, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptionsChangeEvent, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { join } from 'vs/base/common/paths'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -46,7 +46,6 @@ import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions' import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { EditorBreadcrumbs } from 'vs/workbench/browser/parts/editor/editorBreadcrumbs'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -105,9 +104,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private titleContainer: HTMLElement; private titleAreaControl: TitleControl; - private breadcrumbsContainer: HTMLElement; - private breadcrumbsControl: EditorBreadcrumbs; - private progressBar: ProgressBar; private editorContainer: HTMLElement; @@ -194,14 +190,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Title control this.createTitleAreaControl(); - // Breadcrumbs container - this.breadcrumbsContainer = document.createElement('div'); - addClass(this.breadcrumbsContainer, 'editor-breadcrumbs'); - this.element.appendChild(this.breadcrumbsContainer); - - // Breadcrumbs control - this.createEditorBreadcrumbs(); - // Editor container this.editorContainer = document.createElement('div'); addClass(this.editorContainer, 'editor-container'); @@ -409,16 +397,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } - private createEditorBreadcrumbs(): void { - - if (this.breadcrumbsControl) { - this.breadcrumbsControl.dispose(); - clearNode(this.breadcrumbsContainer); - } - - this.breadcrumbsControl = this.scopedInstantiationService.createInstance(EditorBreadcrumbs, this.breadcrumbsContainer, this); - } - private restoreEditors(from: IEditorGroupView | ISerializedEditorGroup): TPromise { if (this._group.count === 0) { return TPromise.as(void 0); // nothing to show @@ -615,10 +593,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this._label; } - get breadcrumbs(): IEditorBreadcrumbs { - return this.breadcrumbsControl; - } - get disposed(): boolean { return this._disposed; } @@ -644,8 +618,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Update title control this.titleAreaControl.setActive(isActive); - this.breadcrumbsControl.setActive(isActive); - // Update styles this.updateStyles(); @@ -817,8 +789,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Show in title control after editor control because some actions depend on it this.titleAreaControl.openEditor(editor); - this.breadcrumbsControl.openEditor(editor); - return openEditorPromise; } @@ -988,7 +958,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Forward to title control & breadcrumbs this.titleAreaControl.closeEditor(editor); - this.breadcrumbsControl.closeEditor(editor); } private doCloseActiveEditor(focusNext = this.accessor.activeGroup === this, fromError?: boolean): void { @@ -1375,9 +1344,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.dimension = new Dimension(width, height); // Forward to controls - this.titleAreaControl.layout(new Dimension(this.dimension.width, EDITOR_TITLE_HEIGHT)); - this.breadcrumbsControl.layout(new Dimension(this.dimension.width, this.breadcrumbsControl.getPreferredHeight())); - this.editorControl.layout(new Dimension(this.dimension.width, this.dimension.height - (EDITOR_TITLE_HEIGHT + this.breadcrumbsControl.getPreferredHeight()))); + this.titleAreaControl.layout(new Dimension(this.dimension.width, this.titleAreaControl.getPreferredHeight())); + this.editorControl.layout(new Dimension(this.dimension.width, this.dimension.height - this.titleAreaControl.getPreferredHeight())); } relayout(): void { @@ -1404,8 +1372,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleAreaControl.dispose(); - this.breadcrumbsControl.dispose(); - super.dispose(); } } diff --git a/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css new file mode 100644 index 00000000000..b74ae0063cb --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control.hidden { + display: none; +} + +.monaco-workbench>.part.editor>.content .editor-group-container:not(.active) .breadcrumbs-control { + opacity: .8; +} + +.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item .symbol-icon{ + margin-left: 4px; + padding-right: 2px; +} + +.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumbs .monaco-breadcrumb-item:nth-child(2) { + /*first-child is the style-element*/ + margin-left: 8px; +} + +.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumbs .monaco-breadcrumb-item:last-child { + /*first-child is the style-element*/ + margin-right: 8px; +} + +.monaco-breadcrumbs-picker { + border-style: solid; + border-width: 1px; +} diff --git a/src/vs/workbench/browser/parts/editor/media/editorbreadcrumbs.css b/src/vs/workbench/browser/parts/editor/media/editorbreadcrumbs.css deleted file mode 100644 index e40ca9ba696..00000000000 --- a/src/vs/workbench/browser/parts/editor/media/editorbreadcrumbs.css +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench>.part.editor>.content .editor-group-container:not(.active) .editor-breadcrumbs { - opacity: .8; -} - - -.monaco-workbench>.part.editor>.content .editor-group-container .editor-breadcrumbs .monaco-breadcrumbs .monaco-breadcrumb-item:nth-child(2) { /*first-child is the style-element*/ - padding-left: 8px; -} - -.monaco-breadcrumbs-picker { - border: 1px #dddddd solid; - box-shadow: #dddddd 6px 6px 6px -6px; -} diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index e7abc5b7357..455de722f55 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -44,12 +44,16 @@ .monaco-workbench > .part.editor > .content .editor-group-container > .title { position: relative; - height: 35px; display: flex; + flex-wrap: nowrap; box-sizing: border-box; overflow: hidden; } +.monaco-workbench > .part.editor > .content .editor-group-container > .title.tabs { + flex-wrap: wrap; +} + .monaco-workbench > .part.editor > .content .editor-group-container > .title.title-border-bottom::after { content: ''; position: absolute; @@ -112,4 +116,4 @@ .monaco-workbench > .part.editor > .content .grid-view-container { width: 100%; height: 100%; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css index 7a2683b4f03..17069e6c634 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css @@ -13,10 +13,48 @@ padding-left: 20px; } +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs.title-label { + flex: none; +} + .monaco-workbench > .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ } +/* Breadcrumbs */ +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs { + flex: 1 50%; + overflow: hidden; + line-height: 35px; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { + font-size: 0.9em; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:nth-child(2) { + padding-left: 2px; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { + padding-right: 2px; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs .breadcrumbs-control.preview .monaco-breadcrumb-item { + font-style: italic; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:not(:last-child)::after { + content: '/'; + height: inherit; + width: inherit; + background-image: none; +} + +.monaco-workbench.windows > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:not(:last-child)::after { + content: '\\'; +} + /* Title Actions */ .monaco-workbench > .part.editor > .content .editor-group-container > .title .title-actions { display: flex; @@ -26,4 +64,4 @@ .monaco-workbench > .part.editor > .content .editor-group-container.active > .title .title-actions { opacity: 1; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index 2dc523ad8aa..065b844c4b2 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -252,4 +252,21 @@ cursor: default; flex: initial; padding-left: 4px; -} \ No newline at end of file + height: 35px; +} + +/* Breadcrumbs */ + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control { + flex: 1 100%; + height: 25px; + cursor: default; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-icon-label::before { + height: 18px; /* tweak the icon size of the editor labels when icons are enabled */ +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { + padding-right: 8px; +} diff --git a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css index da2df0dfeca..41e78a153f2 100644 --- a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css @@ -21,7 +21,7 @@ .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .monaco-icon-label::before, .monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label a, .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label a, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label span, +.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label h2, .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label span { cursor: pointer; } diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 0d85263c31c..452b06b4eeb 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -11,8 +11,8 @@ import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor import { ResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; -import { addDisposableListener, EventType, addClass, EventHelper, removeClass } from 'vs/base/browser/dom'; -import { IEditorPartOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { addDisposableListener, EventType, addClass, EventHelper, removeClass, toggleClass } from 'vs/base/browser/dom'; +import { IEditorPartOptions, EDITOR_TITLE_HEIGHT } from 'vs/workbench/browser/parts/editor/editor'; import { IAction } from 'vs/base/common/actions'; import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; @@ -35,6 +35,12 @@ export class NoTabsTitleControl extends TitleControl { this.editorLabel = this._register(this.instantiationService.createInstance(ResourceLabel, this.titleContainer, void 0)); this._register(this.editorLabel.onClick(e => this.onTitleLabelClick(e))); + // Breadcrumbs + const breadcrumbsContainer = document.createElement('div'); + addClass(breadcrumbsContainer, 'no-tabs-breadcrumbs'); + this.titleContainer.appendChild(breadcrumbsContainer); + this.createBreadcrumbsControl(breadcrumbsContainer, { showIcons: false, showDecorationColors: false }); + // Right Actions Container const actionsContainer = document.createElement('div'); addClass(actionsContainer, 'title-actions'); @@ -84,6 +90,10 @@ export class NoTabsTitleControl extends TitleControl { } } + getPreferredHeight(): number { + return EDITOR_TITLE_HEIGHT; + } + openEditor(editor: IEditorInput): void { this.ifActiveEditorChanged(() => this.redraw()); } @@ -156,6 +166,9 @@ export class NoTabsTitleControl extends TitleControl { const editor = this.group.activeEditor; this.lastRenderedActiveEditor = editor; + const isEditorPinned = this.group.isPinned(this.group.activeEditor); + const isGroupActive = this.accessor.activeGroup === this.group; + // Clear if there is no editor if (!editor) { removeClass(this.titleContainer, 'dirty'); @@ -165,9 +178,6 @@ export class NoTabsTitleControl extends TitleControl { // Otherwise render it else { - const isEditorPinned = this.group.isPinned(this.group.activeEditor); - const isGroupActive = this.accessor.activeGroup === this.group; - // Dirty state this.updateEditorDirty(editor); @@ -177,7 +187,9 @@ export class NoTabsTitleControl extends TitleControl { const { labelFormat } = this.accessor.partOptions; let description: string; - if (labelFormat === 'default' && !isGroupActive) { + if (this.breadcrumbsControl) { + description = ''; // hide description when showing breadcrumbs + } else if (labelFormat === 'default' && !isGroupActive) { description = ''; // hide description when group is not active and style is 'default' } else { description = editor.getDescription(this.getVerbosity(labelFormat)) || ''; @@ -188,7 +200,7 @@ export class NoTabsTitleControl extends TitleControl { title = ''; // dont repeat what is already shown } - this.editorLabel.setLabel({ name, description, resource }, { title, italic: !isEditorPinned, extraClasses: ['title-label'] }); + this.editorLabel.setLabel({ name, description, resource }, { title, italic: !isEditorPinned, extraClasses: ['no-tabs', 'title-label'] }); if (isGroupActive) { this.editorLabel.element.style.color = this.getColor(TAB_ACTIVE_FOREGROUND); } else { @@ -198,6 +210,16 @@ export class NoTabsTitleControl extends TitleControl { // Update Editor Actions Toolbar this.updateEditorActionsToolbar(); } + + // Update Breadcrumbs + if (this.breadcrumbsControl) { + if (isGroupActive) { + this.breadcrumbsControl.update(); + toggleClass(this.breadcrumbsControl.domNode, 'preview', !isEditorPinned); + } else { + this.breadcrumbsControl.clear(); + } + } } private getVerbosity(style: string): Verbosity { diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index b262ab1f852..b38e32f681a 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -54,7 +54,7 @@ export class SideBySideEditor extends BaseEditor { private onDidCreateEditors = this._register(new Emitter<{ width: number; height: number; }>()); private _onDidSizeConstraintsChange = this._register(new Relay<{ width: number; height: number; }>()); - get onDidSizeConstraintsChange(): Event<{ width: number; height: number; }> { return anyEvent(this.onDidCreateEditors.event, this._onDidSizeConstraintsChange.event); } + readonly onDidSizeConstraintsChange: Event<{ width: number; height: number; }> = anyEvent(this.onDidCreateEditors.event, this._onDidSizeConstraintsChange.event); constructor( @ITelemetryService telemetryService: ITelemetryService, diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 8934bb9925f..45103a5535b 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -39,6 +39,8 @@ import { addClass, addDisposableListener, hasClass, EventType, EventHelper, remo import { localize } from 'vs/nls'; import { IEditorGroupsAccessor, IEditorPartOptions, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BreadcrumbsControl } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; interface IEditorInputLabel { name: string; @@ -78,9 +80,10 @@ export class TabsTitleControl extends TitleControl { @IMenuService menuService: IMenuService, @IQuickOpenService quickOpenService: IQuickOpenService, @IThemeService themeService: IThemeService, - @IExtensionService extensionService: IExtensionService + @IExtensionService extensionService: IExtensionService, + @IConfigurationService configurationService: IConfigurationService ) { - super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService); + super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService, configurationService); } protected create(parent: HTMLElement): void { @@ -108,6 +111,12 @@ export class TabsTitleControl extends TitleControl { // Close Action this.closeOneEditorAction = this._register(this.instantiationService.createInstance(CloseOneEditorAction, CloseOneEditorAction.ID, CloseOneEditorAction.LABEL)); + + // Breadcrumbs + const breadcrumbsContainer = document.createElement('div'); + addClass(breadcrumbsContainer, 'tabs-breadcrumbs'); + this.titleContainer.appendChild(breadcrumbsContainer); + this.createBreadcrumbsControl(breadcrumbsContainer, { showIcons: true, showDecorationColors: false }); } private createScrollbar(): void { @@ -240,6 +249,11 @@ export class TabsTitleControl extends TitleControl { // Redraw all tabs this.redraw(); + + // Update Breadcrumbs + if (this.breadcrumbsControl) { + this.breadcrumbsControl.update(); + } } closeEditor(editor: IEditorInput): void { @@ -287,6 +301,11 @@ export class TabsTitleControl extends TitleControl { this.clearEditorActionsToolbar(); } + + // Update Breadcrumbs + if (this.breadcrumbsControl) { + this.breadcrumbsControl.update(); + } } moveEditor(editor: IEditorInput, fromIndex: number, targetIndex: number): void { @@ -910,6 +929,11 @@ export class TabsTitleControl extends TitleControl { return; } + if (this.breadcrumbsControl) { + this.breadcrumbsControl.layout({ width: dimension.width, height: BreadcrumbsControl.HEIGHT }); + this.scrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`; + } + const visibleContainerWidth = this.tabsContainer.offsetWidth; const totalContainerWidth = this.tabsContainer.scrollWidth; @@ -1198,4 +1222,4 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { `); } } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 8ddd3329287..ea29413fb7f 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -33,10 +33,12 @@ import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Dimension, addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IEditorGroupsAccessor, IEditorPartOptions, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorPartOptions, IEditorGroupView, EDITOR_TITLE_HEIGHT } from 'vs/workbench/browser/parts/editor/editor'; import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { LocalSelectionTransfer, DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; import { applyDragImage } from 'vs/base/browser/dnd'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BreadcrumbsConfig, BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; export interface IToolbarActions { primary: IAction[]; @@ -48,6 +50,8 @@ export abstract class TitleControl extends Themable { protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); protected readonly editorTransfer = LocalSelectionTransfer.getInstance(); + protected breadcrumbsControl: BreadcrumbsControl; + private currentPrimaryEditorActionIds: string[] = []; private currentSecondaryEditorActionIds: string[] = []; protected editorActionsToolbar: ToolBar; @@ -72,7 +76,8 @@ export abstract class TitleControl extends Themable { @IMenuService private menuService: IMenuService, @IQuickOpenService protected quickOpenService: IQuickOpenService, @IThemeService themeService: IThemeService, - @IExtensionService private extensionService: IExtensionService + @IExtensionService private extensionService: IExtensionService, + @IConfigurationService protected configurationService: IConfigurationService ) { super(themeService); @@ -89,6 +94,24 @@ export abstract class TitleControl extends Themable { protected abstract create(parent: HTMLElement): void; + protected createBreadcrumbsControl(container: HTMLElement, options: IBreadcrumbsControlOptions): void { + const config = this._register(BreadcrumbsConfig.IsEnabled.bindTo(this.configurationService)); + config.onDidChange(value => { + if (!value && this.breadcrumbsControl) { + this.breadcrumbsControl.dispose(); + this.breadcrumbsControl = undefined; + this.group.relayout(); + } else if (value && !this.breadcrumbsControl) { + this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group); + this.breadcrumbsControl.update(); + this.group.relayout(); + } + }); + if (config.value) { + this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group); + } + } + protected createEditorActionsToolBar(container: HTMLElement): void { const context = { groupId: this.group.id } as IEditorCommandsContext; @@ -327,9 +350,18 @@ export abstract class TitleControl extends Themable { layout(dimension: Dimension): void { // Optionally implemented in subclasses + + if (this.breadcrumbsControl) { + this.breadcrumbsControl.layout(undefined); + } + } + + getPreferredHeight(): number { + return EDITOR_TITLE_HEIGHT + (this.breadcrumbsControl ? BreadcrumbsControl.HEIGHT : 0); } dispose(): void { + this.breadcrumbsControl = dispose(this.breadcrumbsControl); this.editorToolBarMenuDisposables = dispose(this.editorToolBarMenuDisposables); super.dispose(); diff --git a/src/vs/workbench/browser/parts/menubar/media/menubarpart.css b/src/vs/workbench/browser/parts/menubar/media/menubarpart.css index 62b8c849063..e1ee844a335 100644 --- a/src/vs/workbench/browser/parts/menubar/media/menubarpart.css +++ b/src/vs/workbench/browser/parts/menubar/media/menubarpart.css @@ -32,16 +32,6 @@ zoom: 1; } -.monaco-workbench > .part.menubar > .menubar-menu-button.open, -.monaco-workbench > .part.menubar > .menubar-menu-button:hover { - background-color: rgba(255, 255, 255, 0.1); -} - -.monaco-workbench > .part.menubar.light > .menubar-menu-button.open, -.monaco-workbench > .part.menubar.light > .menubar-menu-button:hover { - background-color: rgba(0, 0, 0, 0.1); -} - .menubar-menu-items-holder { position: absolute; left: 0px; diff --git a/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts b/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts index 3d651933593..d010dfb31ec 100644 --- a/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts +++ b/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts @@ -12,6 +12,7 @@ fileMenuRegistration(); editMenuRegistration(); selectionMenuRegistration(); viewMenuRegistration(); +appearanceMenuRegistration(); layoutMenuRegistration(); goMenuRegistration(); debugMenuRegistration(); @@ -494,9 +495,25 @@ function viewMenuRegistration() { order: 2 }); + // TODO: Appearance Submenu + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_appearance', + title: nls.localize({ key: 'miAppearance', comment: ['&& denotes a mnemonic'] }, "&&Appearance"), + submenu: MenuId.MenubarAppearanceMenu, + order: 1 + }); + + // TODO: Editor Layout Submenu + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_appearance', + title: nls.localize({ key: 'miEditorLayout', comment: ['&& denotes a mnemonic'] }, "Editor &&Layout"), + submenu: MenuId.MenubarLayoutMenu, + order: 2 + }); + // Viewlets MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '2_views', + group: '3_views', command: { id: 'workbench.view.explorer', title: nls.localize({ key: 'miViewExplorer', comment: ['&& denotes a mnemonic'] }, "&&Explorer") @@ -505,7 +522,7 @@ function viewMenuRegistration() { }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '2_views', + group: '3_views', command: { id: 'workbench.view.search', title: nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search") @@ -514,7 +531,7 @@ function viewMenuRegistration() { }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '2_views', + group: '3_views', command: { id: 'workbench.view.scm', title: nls.localize({ key: 'miViewSCM', comment: ['&& denotes a mnemonic'] }, "S&&CM") @@ -523,7 +540,7 @@ function viewMenuRegistration() { }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '2_views', + group: '3_views', command: { id: 'workbench.view.debug', title: nls.localize({ key: 'miViewDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug") @@ -532,7 +549,7 @@ function viewMenuRegistration() { }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '2_views', + group: '3_views', command: { id: 'workbench.view.extensions', title: nls.localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions") @@ -542,7 +559,7 @@ function viewMenuRegistration() { // Panels MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_panels', + group: '4_panels', command: { id: 'workbench.action.output.toggleOutput', title: nls.localize({ key: 'miToggleOutput', comment: ['&& denotes a mnemonic'] }, "&&Output") @@ -551,7 +568,7 @@ function viewMenuRegistration() { }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_panels', + group: '4_panels', command: { id: 'workbench.debug.action.toggleRepl', title: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") @@ -560,7 +577,7 @@ function viewMenuRegistration() { }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_panels', + group: '4_panels', command: { id: 'workbench.action.terminal.toggleTerminal', title: nls.localize({ key: 'miToggleIntegratedTerminal', comment: ['&& denotes a mnemonic'] }, "&&Integrated Terminal") @@ -569,7 +586,7 @@ function viewMenuRegistration() { }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_panels', + group: '4_panels', command: { id: 'workbench.actions.view.problems', title: nls.localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems") @@ -577,101 +594,9 @@ function viewMenuRegistration() { order: 4 }); - // Toggle View - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_toggle_view', - command: { - id: 'workbench.action.toggleFullScreen', - title: nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_toggle_view', - command: { - id: 'workbench.action.toggleZenMode', - title: nls.localize('miToggleZenMode', "Toggle Zen Mode") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_toggle_view', - command: { - id: 'workbench.action.toggleCenteredLayout', - title: nls.localize('miToggleCenteredLayout', "Toggle Centered Layout") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_toggle_view', - command: { - id: 'workbench.action.toggleMenuBar', - title: nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar") - }, - order: 4 - }); - - // TODO: Editor Layout Submenu - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - title: nls.localize({ key: 'miEditorLayout', comment: ['&& denotes a mnemonic'] }, "Editor &&Layout"), - submenu: MenuId.MenubarLayoutMenu, - group: '5_layout', - order: 1 - }); - - - // Workbench Layout - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '6_workbench_layout', - command: { - id: 'workbench.action.toggleSidebarVisibility', - title: nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '6_workbench_layout', - command: { - id: 'workbench.action.toggleSidebarPosition', - title: nls.localize({ key: 'miMoveSidebarLeftRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left/Right") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '6_workbench_layout', - command: { - id: 'workbench.action.toggleStatusbarVisibility', - title: nls.localize({ key: 'miToggleStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Status Bar") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '6_workbench_layout', - command: { - id: 'workbench.action.toggleActivityBarVisibility', - title: nls.localize({ key: 'miToggleActivityBar', comment: ['&& denotes a mnemonic'] }, "Toggle &&Activity Bar") - }, - order: 4 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '6_workbench_layout', - command: { - id: 'workbench.action.togglePanel', - title: nls.localize({ key: 'miTogglePanel', comment: ['&& denotes a mnemonic'] }, "Toggle &&Panel") - }, - order: 5 - }); - // Toggle Editor Settings MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '8_editor', + group: '5_editor', command: { id: 'workbench.action.toggleWordWrap', title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap") @@ -680,7 +605,7 @@ function viewMenuRegistration() { }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '8_editor', + group: '5_editor', command: { id: 'workbench.action.toggleMinimap', title: nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap") @@ -689,7 +614,7 @@ function viewMenuRegistration() { }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '8_editor', + group: '5_editor', command: { id: 'workbench.action.toggleRenderWhitespace', title: nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace") @@ -698,17 +623,100 @@ function viewMenuRegistration() { }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '8_editor', + group: '5_editor', command: { id: 'workbench.action.toggleRenderControlCharacters', title: nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters") }, order: 4 }); +} + +function appearanceMenuRegistration() { + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: 'workbench.action.toggleFullScreen', + title: nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: 'workbench.action.toggleZenMode', + title: nls.localize('miToggleZenMode', "Toggle Zen Mode") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: 'workbench.action.toggleCenteredLayout', + title: nls.localize('miToggleCenteredLayout', "Toggle Centered Layout") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: 'workbench.action.toggleMenuBar', + title: nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar") + }, + order: 4 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: 'workbench.action.toggleSidebarVisibility', + title: nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: 'workbench.action.toggleSidebarPosition', + title: nls.localize({ key: 'miMoveSidebarLeftRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left/Right") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: 'workbench.action.toggleStatusbarVisibility', + title: nls.localize({ key: 'miToggleStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Status Bar") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: 'workbench.action.toggleActivityBarVisibility', + title: nls.localize({ key: 'miToggleActivityBar', comment: ['&& denotes a mnemonic'] }, "Toggle &&Activity Bar") + }, + order: 4 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: 'workbench.action.togglePanel', + title: nls.localize({ key: 'miTogglePanel', comment: ['&& denotes a mnemonic'] }, "Toggle &&Panel") + }, + order: 5 + }); // Zoom - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '9_zoom', + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '3_zoom', command: { id: 'workbench.action.zoomIn', title: nls.localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In") @@ -716,8 +724,8 @@ function viewMenuRegistration() { order: 1 }); - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '9_zoom', + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '3_zoom', command: { id: 'workbench.action.zoomOut', title: nls.localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out") @@ -725,8 +733,8 @@ function viewMenuRegistration() { order: 2 }); - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '9_zoom', + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '3_zoom', command: { id: 'workbench.action.zoomReset', title: nls.localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom") diff --git a/src/vs/workbench/browser/parts/menubar/menubarPart.ts b/src/vs/workbench/browser/parts/menubar/menubarPart.ts index e33dc07f208..f86a7e2d944 100644 --- a/src/vs/workbench/browser/parts/menubar/menubarPart.ts +++ b/src/vs/workbench/browser/parts/menubar/menubarPart.ts @@ -12,21 +12,19 @@ import * as browser from 'vs/base/browser/browser'; import { Part } from 'vs/workbench/browser/part'; import { IMenubarService, IMenubarMenu, IMenubarMenuItemAction, IMenubarData } from 'vs/platform/menubar/common/menubar'; import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { IWindowService, MenuBarVisibility, IWindowsService } from 'vs/platform/windows/common/windows'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ActionRunner, IActionRunner, IAction, Action } from 'vs/base/common/actions'; import { Builder, $ } from 'vs/base/browser/builder'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { EventType, Dimension, toggleClass } from 'vs/base/browser/dom'; -import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; +import { EventType, Dimension } from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { Menu, IMenuOptions, SubmenuAction } from 'vs/base/browser/ui/menu/menu'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { Color } from 'vs/base/common/color'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { domEvent } from 'vs/base/browser/event'; @@ -35,13 +33,22 @@ import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderW import { getPathLabel } from 'vs/base/common/labels'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { MENUBAR_SELECTION_FOREGROUND, MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_BORDER, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, MENU_BACKGROUND, MENU_FOREGROUND, MENU_SELECTION_BACKGROUND, MENU_SELECTION_FOREGROUND, MENU_SELECTION_BORDER } from 'vs/workbench/common/theme'; interface CustomMenu { title: string; + buttonElement: Builder; titleElement: Builder; actions?: IAction[]; } +enum MenubarState { + HIDDEN, + VISIBLE, + FOCUSED, + OPEN +} + export class MenubarPart extends Part { private keys = [ @@ -83,19 +90,21 @@ export class MenubarPart extends Part { private focusedMenu: { index: number; - holder: Builder; - widget: Menu; + holder?: Builder; + widget?: Menu; }; private customMenus: CustomMenu[]; private menuUpdater: RunOnceScheduler; private actionRunner: IActionRunner; + private focusToReturn: Builder; private container: Builder; private recentlyOpened: IRecentlyOpened; private updatePending: boolean; private _modifierKeyStatus: IModifierKeyStatus; - private _isFocused: boolean; + private _focusState: MenubarState; + private _onVisibilityChange: Emitter; private initialSizing: { @@ -142,9 +151,7 @@ export class MenubarPart extends Part { this.actionRunner = this._register(new ActionRunner()); this._register(this.actionRunner.onDidBeforeRun(() => { - if (this.focusedMenu && this.focusedMenu.holder) { - this.focusedMenu.holder.hide(); - } + this.focusState = this.currentMenubarVisibility === 'toggle' ? MenubarState.HIDDEN : MenubarState.VISIBLE; })); this._onVisibilityChange = this._register(new Emitter()); @@ -156,7 +163,7 @@ export class MenubarPart extends Part { this.doSetupMenubar(); } - this.isFocused = false; + this._focusState = MenubarState.HIDDEN; this.windowService.getRecentlyOpened().then((recentlyOpened) => { this.recentlyOpened = recentlyOpened; @@ -212,12 +219,12 @@ export class MenubarPart extends Part { return this.configurationService.getValue('window.titleBarStyle'); } - private get isFocused(): boolean { - return this._isFocused; + private get focusState(): MenubarState { + return this._focusState; } - private set isFocused(value: boolean) { - if (this._isFocused && !value) { + private set focusState(value: MenubarState) { + if (this._focusState >= MenubarState.FOCUSED && value < MenubarState.FOCUSED) { // Losing focus, update the menu if needed if (this.updatePending) { @@ -226,19 +233,107 @@ export class MenubarPart extends Part { } } - this._isFocused = value; - - if (!this._isFocused && this.currentMenubarVisibility === 'toggle') { - if (this.container) { - this.hideMenubar(); - } + if (value === this._focusState) { + return; } + + switch (value) { + case MenubarState.HIDDEN: + if (this.isVisible) { + this.hideMenubar(); + } + + if (this.isOpen) { + this.cleanupCustomMenu(); + } + + if (this.isFocused) { + this.focusedMenu = null; + + if (this.focusToReturn) { + this.focusToReturn.domFocus(); + this.focusToReturn = null; + } + } + + + break; + case MenubarState.VISIBLE: + if (!this.isVisible) { + this.showMenubar(); + } + + if (this.isOpen) { + this.cleanupCustomMenu(); + } + + if (this.isFocused) { + if (this.focusedMenu) { + this.customMenus[this.focusedMenu.index].buttonElement.domBlur(); + } + + this.focusedMenu = null; + + if (this.focusToReturn) { + this.focusToReturn.domFocus(); + this.focusToReturn = null; + } + } + + break; + case MenubarState.FOCUSED: + if (!this.isVisible) { + this.showMenubar(); + } + + if (this.isOpen) { + this.cleanupCustomMenu(); + } + + if (this.focusedMenu) { + this.customMenus[this.focusedMenu.index].buttonElement.domFocus(); + } + break; + case MenubarState.OPEN: + if (!this.isVisible) { + this.showMenubar(); + } + + if (this.focusedMenu) { + this.showCustomMenu(this.focusedMenu.index); + } + break; + } + + this._focusState = value; + } + + private get isVisible(): boolean { + return this.focusState >= MenubarState.VISIBLE; + } + + private get isFocused(): boolean { + return this.focusState >= MenubarState.FOCUSED; + } + + private get isOpen(): boolean { + return this.focusState >= MenubarState.OPEN; } private onDidChangeFullscreen(): void { this.updateStyles(); } + private onDidChangeWindowFocus(hasFocus: boolean): void { + if (this.container) { + if (hasFocus) { + this.container.removeClass('inactive'); + } else { + this.container.addClass('inactive'); + } + } + } + private onConfigurationUpdated(event: IConfigurationChangeEvent): void { if (this.keys.some(key => event.affectsConfiguration(key))) { this.setupMenubar(); @@ -255,26 +350,35 @@ export class MenubarPart extends Part { this.container.style('visibility', null); } - private onModifierKeyToggled(modiferKeyStatus: IModifierKeyStatus): void { + private onModifierKeyToggled(modifierKeyStatus: IModifierKeyStatus): void { + this._modifierKeyStatus = modifierKeyStatus; + const altKeyAlone = modifierKeyStatus.lastKeyPressed === 'alt' && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey; + const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey; + if (this.currentMenubarVisibility === 'toggle') { - const altKeyPressed = (!this._modifierKeyStatus || !this._modifierKeyStatus.altKey) && modiferKeyStatus.altKey; - if (altKeyPressed && !modiferKeyStatus.ctrlKey && !modiferKeyStatus.shiftKey) { - this.showMenubar(); - } else if (!this.isFocused) { - this.hideMenubar(); + if (altKeyAlone) { + if (!this.isVisible) { + this.focusState = MenubarState.VISIBLE; + } + } else if (!allModifiersReleased && !this.isFocused) { + this.focusState = MenubarState.HIDDEN; } } - this._modifierKeyStatus = modiferKeyStatus; + if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') { + if (!this.isFocused) { + this.focusedMenu = { index: 0 }; + this.focusState = MenubarState.FOCUSED; + } else if (!this.isOpen) { + this.focusState = this.currentMenubarVisibility === 'toggle' ? MenubarState.HIDDEN : MenubarState.VISIBLE; + } + } if (this.currentEnableMenuBarMnemonics && this.customMenus) { this.customMenus.forEach(customMenu => { let child = customMenu.titleElement.child(); if (child) { - let grandChild = child.child(); - if (grandChild) { - grandChild.style('text-decoration', modiferKeyStatus.altKey ? 'underline' : null); - } + child.style('text-decoration', modifierKeyStatus.altKey ? 'underline' : null); } }); } @@ -288,8 +392,6 @@ export class MenubarPart extends Part { } private registerListeners(): void { - this._register(browser.onDidChangeFullscreen(() => this.onDidChangeFullscreen())); - // Update when config changes this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); @@ -305,7 +407,17 @@ export class MenubarPart extends Part { // Listen to keybindings change this._register(this.keybindingService.onDidUpdateKeybindings(() => this.setupMenubar())); - this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this)); + // These listeners only apply when the custom menubar is being used + if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { + // Listen to fullscreen changes + this._register(browser.onDidChangeFullscreen(() => this.onDidChangeFullscreen())); + + // Listen for alt key presses + this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this)); + + // Listen for window focus changes + this._register(this.windowService.onDidChangeFocus(e => this.onDidChangeWindowFocus(e))); + } } private doSetupMenubar(): void { @@ -327,6 +439,11 @@ export class MenubarPart extends Part { } } + + private clearMnemonic(topLevelElement: HTMLElement): void { + topLevelElement.accessKey = null; + } + private registerMnemonic(topLevelElement: HTMLElement, mnemonic: string): void { topLevelElement.accessKey = mnemonic.toLocaleLowerCase(); } @@ -455,10 +572,12 @@ export class MenubarPart extends Part { return; } - this.container.empty(); this.container.attr('role', 'menubar'); - this.customMenus = []; + const firstTimeSetup = this.customMenus === undefined; + if (firstTimeSetup) { + this.customMenus = []; + } let idx = 0; @@ -466,26 +585,34 @@ export class MenubarPart extends Part { const menu: IMenu = this.topLevelMenus[menuTitle]; let menuIndex = idx++; - let titleElement = $(this.container).div({ class: 'menubar-menu-button' }); + // Create the top level menu button element + if (firstTimeSetup) { + const buttonElement = $(this.container).div({ class: 'menubar-menu-button' }).attr({ 'role': 'menu', 'tabindex': 0 }); + buttonElement.attr('aria-label', this.topLevelTitles[menuTitle].replace(/&&(.)/g, '$1')); + + const titleElement = $(buttonElement).div({ class: 'menubar-menu-title', 'aria-hidden': true }); + + this.customMenus.push({ + title: menuTitle, + buttonElement: buttonElement, + titleElement: titleElement + }); + } + + // Update the button label to reflect mnemonics let displayTitle = this.topLevelTitles[menuTitle].replace(/&&(.)/g, this.currentEnableMenuBarMnemonics ? '$1' : '$1'); - let legibleTitle = this.topLevelTitles[menuTitle].replace(/&&(.)/g, '$1'); - $(titleElement).div({ class: 'menubar-menu-title', 'aria-hidden': true }).innerHtml(displayTitle); - - titleElement.attr('aria-label', legibleTitle); - titleElement.attr('role', 'menu'); + $(this.customMenus[menuIndex].titleElement).innerHtml(displayTitle); + // Clear and register mnemonics due to updated settings + this.clearMnemonic(this.customMenus[menuIndex].buttonElement.getHTMLElement()); if (this.currentEnableMenuBarMnemonics) { let mnemonic = (/&&(.)/g).exec(this.topLevelTitles[menuTitle]); if (mnemonic && mnemonic[1]) { - this.registerMnemonic(titleElement.getHTMLElement(), mnemonic[1]); + this.registerMnemonic(this.customMenus[menuIndex].buttonElement.getHTMLElement(), mnemonic[1]); } } - this.customMenus.push({ - title: menuTitle, - titleElement: titleElement - }); - + // Update the menu actions const updateActions = (menu: IMenu, target: IAction[]) => { target.splice(0); let groups = menu.getActions(); @@ -513,56 +640,108 @@ export class MenubarPart extends Part { }; this.customMenus[menuIndex].actions = []; - this._register(menu.onDidChange(() => updateActions(menu, this.customMenus[menuIndex].actions))); + if (firstTimeSetup) { + this._register(menu.onDidChange(() => updateActions(menu, this.customMenus[menuIndex].actions))); + } + updateActions(menu, this.customMenus[menuIndex].actions); - this.customMenus[menuIndex].titleElement.on(EventType.CLICK, () => { - if (this._modifierKeyStatus && (this._modifierKeyStatus.shiftKey || this._modifierKeyStatus.ctrlKey)) { - return; // supress keyboard shortcuts that shouldn't conflict + if (firstTimeSetup) { + this.customMenus[menuIndex].buttonElement.on(EventType.KEY_UP, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + let eventHandled = true; + + if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) { + this.focusedMenu = { index: menuIndex }; + this.focusState = MenubarState.OPEN; + } else { + eventHandled = false; + } + + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); + } + }); + + this.customMenus[menuIndex].buttonElement.on(EventType.CLICK, () => { + if (this._modifierKeyStatus && (this._modifierKeyStatus.shiftKey || this._modifierKeyStatus.ctrlKey)) { + return; // supress keyboard shortcuts that shouldn't conflict + } + + if (this.isOpen) { + if (this.isCurrentMenu(menuIndex)) { + this.focusState = this.currentMenubarVisibility === 'toggle' ? MenubarState.HIDDEN : MenubarState.VISIBLE; + } else { + this.cleanupCustomMenu(); + this.showCustomMenu(menuIndex); + } + } else { + this.focusedMenu = { index: menuIndex }; + this.focusState = MenubarState.OPEN; + } + }); + + this.customMenus[menuIndex].buttonElement.on(EventType.MOUSE_ENTER, () => { + if (this.isOpen && !this.isCurrentMenu(menuIndex)) { + this.customMenus[menuIndex].buttonElement.domFocus(); + this.cleanupCustomMenu(); + this.showCustomMenu(menuIndex); + } else if (this.isFocused && !this.isOpen) { + this.focusedMenu = { index: menuIndex }; + this.customMenus[menuIndex].buttonElement.domFocus(); + } + }); + + } + } + + if (firstTimeSetup) { + this.container.on(EventType.KEY_DOWN, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + let eventHandled = true; + + if (event.equals(KeyCode.LeftArrow) || (event.shiftKey && event.keyCode === KeyCode.Tab)) { + this.focusPrevious(); + } else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Tab)) { + this.focusNext(); + } else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) { + this.focusState = this.currentMenubarVisibility === 'toggle' ? MenubarState.HIDDEN : MenubarState.VISIBLE; + } else { + eventHandled = false; } - this.toggleCustomMenu(menuIndex); - this.isFocused = !this.isFocused; - }); - - this.customMenus[menuIndex].titleElement.on(EventType.MOUSE_ENTER, () => { - if (this.isFocused && !this.isCurrentMenu(menuIndex)) { - this.toggleCustomMenu(menuIndex); + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); } }); - - this.customMenus[menuIndex].titleElement.on(EventType.MOUSE_LEAVE, () => { - if (!this.isFocused) { - this.cleanupCustomMenu(); - } - }); - - this.customMenus[menuIndex].titleElement.on(EventType.BLUR, () => { - this.cleanupCustomMenu(); - }); } - this.container.off(EventType.KEY_DOWN); - this.container.on(EventType.KEY_DOWN, (e) => { - let event = new StandardKeyboardEvent(e as KeyboardEvent); - let eventHandled = true; + this.container.on(EventType.FOCUS_IN, (e) => { + let event = e as FocusEvent; - if (event.equals(KeyCode.LeftArrow) || (event.shiftKey && event.keyCode === KeyCode.Tab)) { - this.focusPrevious(); - } else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Tab)) { - this.focusNext(); - } else { - eventHandled = false; + if (event.relatedTarget) { + if (!this.container.getHTMLElement().contains(event.relatedTarget as HTMLElement)) { + this.focusToReturn = $(event.relatedTarget as HTMLElement); + } } + }); - if (eventHandled) { - event.preventDefault(); - event.stopPropagation(); + this.container.on(EventType.FOCUS_OUT, (e) => { + let event = e as FocusEvent; + + if (event.relatedTarget) { + if (!this.container.getHTMLElement().contains(event.relatedTarget as HTMLElement)) { + this.focusToReturn = null; + this.focusState = this.currentMenubarVisibility === 'toggle' ? MenubarState.HIDDEN : MenubarState.VISIBLE; + } } }); } private focusPrevious(): void { + if (!this.focusedMenu) { return; } @@ -573,7 +752,13 @@ export class MenubarPart extends Part { return; } - this.toggleCustomMenu(newFocusedIndex); + if (this.isOpen) { + this.cleanupCustomMenu(); + this.showCustomMenu(newFocusedIndex); + } else if (this.isFocused) { + this.focusedMenu.index = newFocusedIndex; + this.customMenus[newFocusedIndex].buttonElement.domFocus(); + } } private focusNext(): void { @@ -587,7 +772,13 @@ export class MenubarPart extends Part { return; } - this.toggleCustomMenu(newFocusedIndex); + if (this.isOpen) { + this.cleanupCustomMenu(); + this.showCustomMenu(newFocusedIndex); + } else if (this.isFocused) { + this.focusedMenu.index = newFocusedIndex; + this.customMenus[newFocusedIndex].buttonElement.domFocus(); + } } private getMenubarMenus(): IMenubarData { @@ -646,33 +837,15 @@ export class MenubarPart extends Part { if (this.focusedMenu.widget) { this.focusedMenu.widget.dispose(); } + + this.focusedMenu = { index: this.focusedMenu.index }; } - - this.focusedMenu = null; } - public focusCustomMenu(menuTitle: string): void { - this.toggleCustomMenu(0); - } - - private toggleCustomMenu(menuIndex: number): void { + private showCustomMenu(menuIndex: number): void { const customMenu = this.customMenus[menuIndex]; - if (this.focusedMenu) { - let hiding: boolean = this.isCurrentMenu(menuIndex); - - // Need to cleanup currently displayed menu - this.cleanupCustomMenu(); - - // Hiding this menu - if (hiding) { - return; - } - } - - customMenu.titleElement.domFocus(); - - let menuHolder = $(customMenu.titleElement).div({ class: 'menubar-menu-items-holder' }); + let menuHolder = $(customMenu.buttonElement).div({ class: 'menubar-menu-items-holder' }); $(menuHolder.getHTMLElement().parentElement).addClass('open'); @@ -692,14 +865,12 @@ export class MenubarPart extends Part { let menuWidget = this._register(new Menu(menuHolder.getHTMLElement(), customMenu.actions, menuOptions)); this._register(menuWidget.onDidCancel(() => { - this.cleanupCustomMenu(); - this.isFocused = false; + this.focusState = MenubarState.FOCUSED; })); this._register(menuWidget.onDidBlur(() => { setTimeout(() => { this.cleanupCustomMenu(); - this.isFocused = false; }, 100); })); @@ -712,25 +883,6 @@ export class MenubarPart extends Part { }; } - updateStyles(): void { - super.updateStyles(); - - // Part container - if (this.container) { - const fgColor = this.getColor(TITLE_BAR_ACTIVE_FOREGROUND); - const bgColor = this.getColor(TITLE_BAR_ACTIVE_BACKGROUND); - - this.container.style('color', fgColor); - if (browser.isFullscreen()) { - this.container.style('background-color', bgColor); - } else { - this.container.style('background-color', null); - } - - toggleClass(this.container.getHTMLElement(), 'light', Color.fromHex(bgColor).isLighter()); - } - } - public get onVisibilityChange(): Event { return this._onVisibilityChange.event; } @@ -755,7 +907,7 @@ export class MenubarPart extends Part { } if (typeof this.initialSizing.menuButtonPaddingLeftRight !== 'number') { - this.initialSizing.menuButtonPaddingLeftRight = parseInt(this.customMenus[0].titleElement.getComputedStyle().paddingLeft, 10); + this.initialSizing.menuButtonPaddingLeftRight = parseInt(this.customMenus[0].buttonElement.getComputedStyle().paddingLeft, 10); } this.container.style({ @@ -766,7 +918,7 @@ export class MenubarPart extends Part { }); this.customMenus.forEach(customMenu => { - customMenu.titleElement.style({ + customMenu.buttonElement.style({ 'padding': `0 ${this.initialSizing.menuButtonPaddingLeftRight / browser.getZoomFactor()}px` }); }); @@ -783,8 +935,8 @@ export class MenubarPart extends Part { public getMenubarItemsDimensions(): Dimension { if (this.customMenus) { - const left = this.customMenus[0].titleElement.getHTMLElement().getBoundingClientRect().left; - const right = this.customMenus[this.customMenus.length - 1].titleElement.getHTMLElement().getBoundingClientRect().right; + const left = this.customMenus[0].buttonElement.getHTMLElement().getBoundingClientRect().left; + const right = this.customMenus[this.customMenus.length - 1].buttonElement.getHTMLElement().getBoundingClientRect().right; return new Dimension(right - left, this.container.getClientArea().height); } @@ -807,76 +959,205 @@ export class MenubarPart extends Part { } } +registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { + const menubarActiveWindowFgColor = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); + if (menubarActiveWindowFgColor) { + collector.addRule(` + .monaco-workbench > .part.menubar > .menubar-menu-button { + color: ${menubarActiveWindowFgColor}; + } + `); + } + + const menubarInactiveWindowFgColor = theme.getColor(TITLE_BAR_INACTIVE_FOREGROUND); + if (menubarInactiveWindowFgColor) { + collector.addRule(` + .monaco-workbench > .part.menubar.inactive > .menubar-menu-button { + color: ${menubarInactiveWindowFgColor}; + } + `); + } + + + const menubarSelectedFgColor = theme.getColor(MENUBAR_SELECTION_FOREGROUND); + if (menubarSelectedFgColor) { + collector.addRule(` + .monaco-workbench > .part.menubar > .menubar-menu-button.open, + .monaco-workbench > .part.menubar > .menubar-menu-button:focus, + .monaco-workbench > .part.menubar > .menubar-menu-button:hover { + color: ${menubarSelectedFgColor}; + } + `); + } + + const menubarSelectedBgColor = theme.getColor(MENUBAR_SELECTION_BACKGROUND); + if (menubarSelectedBgColor) { + collector.addRule(` + .monaco-workbench > .part.menubar > .menubar-menu-button.open, + .monaco-workbench > .part.menubar > .menubar-menu-button:focus, + .monaco-workbench > .part.menubar > .menubar-menu-button:hover { + background-color: ${menubarSelectedBgColor}; + } + `); + } + + const menubarSelectedBorderColor = theme.getColor(MENUBAR_SELECTION_BORDER); + if (menubarSelectedBorderColor) { + collector.addRule(` + .monaco-workbench > .part.menubar > .menubar-menu-button:hover { + outline: dashed 1px; + } + + .monaco-workbench > .part.menubar > .menubar-menu-button.open, + .monaco-workbench > .part.menubar > .menubar-menu-button:focus { + outline: solid 1px; + } + + .monaco-workbench > .part.menubar > .menubar-menu-button.open, + .monaco-workbench > .part.menubar > .menubar-menu-button:focus, + .monaco-workbench > .part.menubar > .menubar-menu-button:hover { + outline-offset: -1px; + outline-color: ${menubarSelectedBorderColor}; + } + `); + } + + const menuBgColor = theme.getColor(MENU_BACKGROUND); + if (menuBgColor) { + collector.addRule(` + .monaco-menu .monaco-action-bar.vertical, + .monaco-menu .monaco-action-bar.vertical .action-menu-item { + background-color: ${menuBgColor}; + } + `); + } + + const menuFgColor = theme.getColor(MENU_FOREGROUND); + if (menuFgColor) { + collector.addRule(` + .monaco-menu .monaco-action-bar.vertical, + .monaco-menu .monaco-action-bar.vertical .action-menu-item { + color: ${menuFgColor}; + } + `); + } + + const selectedMenuItemBgColor = theme.getColor(MENU_SELECTION_BACKGROUND); + if (menuBgColor) { + collector.addRule(` + .monaco-menu .monaco-action-bar.vertical .action-item.focused > .action-menu-item, + .monaco-menu .monaco-action-bar.vertical .action-menu-item:hover { + background-color: ${selectedMenuItemBgColor}; + } + `); + } + + const selectedMenuItemFgColor = theme.getColor(MENU_SELECTION_FOREGROUND); + if (selectedMenuItemFgColor) { + collector.addRule(` + .monaco-menu .monaco-action-bar.vertical .action-item.focused > .action-menu-item, + .monaco-menu .monaco-action-bar.vertical .action-menu-item:hover { + color: ${selectedMenuItemFgColor}; + } + `); + } + + const selectedMenuItemBorderColor = theme.getColor(MENU_SELECTION_BORDER); + if (selectedMenuItemBorderColor) { + collector.addRule(` + .monaco-menu .monaco-action-bar.vertical .action-item.focused > .action-menu-item { + outline: solid 1px; + } + + .monaco-menu .monaco-action-bar.vertical .action-menu-item:hover { + outline: dashed 1px; + } + + .monaco-menu .monaco-action-bar.vertical .action-item.focused > .action-menu-item, + .monaco-menu .monaco-action-bar.vertical .action-menu-item:hover { + outline-offset: -1px; + outline-color: ${selectedMenuItemBorderColor}; + } + `); + } +}); + +type ModifierKey = 'alt' | 'ctrl' | 'shift'; + interface IModifierKeyStatus { altKey: boolean; shiftKey: boolean; ctrlKey: boolean; + lastKeyPressed?: ModifierKey; + lastKeyReleased?: ModifierKey; } + class ModifierKeyEmitter extends Emitter { private _subscriptions: IDisposable[] = []; - private _isPressed: IModifierKeyStatus; + private _keyStatus: IModifierKeyStatus; private static instance: ModifierKeyEmitter; private constructor() { super(); - this._isPressed = { + this._keyStatus = { altKey: false, shiftKey: false, ctrlKey: false }; this._subscriptions.push(domEvent(document.body, 'keydown')(e => { - if (e.altKey || e.shiftKey || e.ctrlKey) { - this.isPressed = { - altKey: e.altKey, - ctrlKey: e.ctrlKey, - shiftKey: e.shiftKey - }; + if (e.altKey && !this._keyStatus.altKey) { + this._keyStatus.lastKeyPressed = 'alt'; + } else if (e.ctrlKey && !this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyPressed = 'ctrl'; + } else if (e.shiftKey && !this._keyStatus.shiftKey) { + this._keyStatus.lastKeyPressed = 'shift'; + } else { + this._keyStatus.lastKeyPressed = undefined; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyPressed) { + this.fire(this._keyStatus); } })); this._subscriptions.push(domEvent(document.body, 'keyup')(e => { - if ((!e.altKey && this.isPressed.altKey) || - (!e.shiftKey && this.isPressed.shiftKey) || - (!e.ctrlKey && this.isPressed.ctrlKey) - ) { + if (!e.altKey && this._keyStatus.altKey) { + this._keyStatus.lastKeyReleased = 'alt'; + } else if (!e.ctrlKey && this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyReleased = 'ctrl'; + } else if (!e.shiftKey && this._keyStatus.shiftKey) { + this._keyStatus.lastKeyReleased = 'shift'; + } else { + this._keyStatus.lastKeyReleased = undefined; + } - this.isPressed = { - altKey: e.altKey, - shiftKey: e.shiftKey, - ctrlKey: e.ctrlKey - }; + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyReleased) { + this.fire(this._keyStatus); } })); - this._subscriptions.push(domEvent(document.body, 'blur')(e => { - if (this.isPressed.altKey || this.isPressed.shiftKey || this.isPressed.ctrlKey) { - this.isPressed = { - altKey: false, - shiftKey: false, - ctrlKey: false - }; - } + this._subscriptions.push(domEvent(window, 'blur')(e => { + this._keyStatus.lastKeyPressed = undefined; + this._keyStatus.lastKeyReleased = undefined; + this._keyStatus.altKey = false; + this._keyStatus.shiftKey = false; + this._keyStatus.shiftKey = false; + + this.fire(this._keyStatus); })); } - get isPressed(): IModifierKeyStatus { - return this._isPressed; - } - - set isPressed(value: IModifierKeyStatus) { - if (this._isPressed.altKey === value.altKey && - this._isPressed.shiftKey === value.shiftKey && - this._isPressed.ctrlKey === value.ctrlKey) { - return; - } - - this._isPressed = value; - this.fire(this._isPressed); - } - static getInstance() { if (!ModifierKeyEmitter.instance) { ModifierKeyEmitter.instance = new ModifierKeyEmitter(); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 2ecb5ec8da0..7e88bde2337 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -5,7 +5,7 @@ 'use strict'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { clearNode, addClass, removeClass, toggleClass, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import URI from 'vs/base/common/uri'; @@ -26,7 +26,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Severity } from 'vs/platform/notification/common/notification'; -export class NotificationsListDelegate implements IDelegate { +export class NotificationsListDelegate implements IVirtualDelegate { private static readonly ROW_HEIGHT = 42; private static readonly LINE_HEIGHT = 22; @@ -278,6 +278,10 @@ export class NotificationRenderer implements IRenderer { +class ListElementDelegate implements IVirtualDelegate { getHeight(element: ListElement): number { return element.item.detail ? 44 : 22; diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 312dae4c272..9a5f5bd6e00 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -12,7 +12,7 @@ visibility: hidden !important; } -.monaco-workbench > .sidebar > .title > .title-label span { +.monaco-workbench > .sidebar > .title > .title-label h2 { text-transform: uppercase; } diff --git a/src/vs/workbench/browser/parts/views/media/panelviewlet.css b/src/vs/workbench/browser/parts/views/media/panelviewlet.css index adddb24399d..7d6c4e36d60 100644 --- a/src/vs/workbench/browser/parts/views/media/panelviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/panelviewlet.css @@ -3,8 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-panel-view .panel > .panel-header > .title { +.monaco-panel-view .panel > .panel-header h3.title { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; + font-size: 11px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + display: flex; } diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index 8bfdfdae01c..a647bf39021 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -100,7 +100,7 @@ export abstract class ViewletPanel extends Panel implements IView { protected renderHeader(container: HTMLElement): void { this.headerContainer = container; - this.renderHeaderTitle(container); + this.renderHeaderTitle(container, this.title); const actions = append(container, $('.actions')); this.toolbar = new ToolBar(actions, this.contextMenuService, { @@ -119,8 +119,8 @@ export abstract class ViewletPanel extends Panel implements IView { this.updateActionsVisibility(); } - protected renderHeaderTitle(container: HTMLElement): void { - append(container, $('.title', null, this.title)); + protected renderHeaderTitle(container: HTMLElement, title: string): void { + append(container, $('h3.title', null, title)); } focus(): void { diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index cb1b5e23623..307a1f1c24b 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, listActiveSelectionForeground, listActiveSelectionBackground } from 'vs/platform/theme/common/colorRegistry'; import { Disposable } from 'vs/base/common/lifecycle'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; @@ -388,6 +388,56 @@ export const TITLE_BAR_BORDER = registerColor('titleBar.border', { hc: contrastBorder }, nls.localize('titleBarBorder', "Title bar border color. Note that this color is currently only supported on macOS.")); +// < --- Menubar --- > + +export const MENUBAR_SELECTION_FOREGROUND = registerColor('menubar.selectionForeground', { + dark: TITLE_BAR_ACTIVE_FOREGROUND, + light: TITLE_BAR_ACTIVE_FOREGROUND, + hc: TITLE_BAR_ACTIVE_FOREGROUND +}, nls.localize('menubarSelectionForeground', "Foreground color of the selected menu item in the menubar.")); + +export const MENUBAR_SELECTION_BACKGROUND = registerColor('menubar.selectionBackground', { + dark: transparent(Color.white, 0.1), + light: transparent(Color.black, 0.1), + hc: null +}, nls.localize('menubarSelectionBackground', "Background color of the selected menu item in the menubar.")); + +export const MENUBAR_SELECTION_BORDER = registerColor('menubar.selectionBorder', { + dark: null, + light: null, + hc: activeContrastBorder +}, nls.localize('menubarSelectionBorder', "Border color of the selected menu item in the menubar.")); + +export const MENU_FOREGROUND = registerColor('menu.foreground', { + dark: SIDE_BAR_FOREGROUND, + light: SIDE_BAR_FOREGROUND, + hc: SIDE_BAR_FOREGROUND +}, nls.localize('menuForeground', "Foreground color of menu items.")); + +export const MENU_BACKGROUND = registerColor('menu.background', { + dark: SIDE_BAR_BACKGROUND, + light: SIDE_BAR_BACKGROUND, + hc: SIDE_BAR_BACKGROUND +}, nls.localize('menuBackground', "Background color of menu items.")); + +export const MENU_SELECTION_FOREGROUND = registerColor('menu.selectionForeground', { + dark: listActiveSelectionForeground, + light: listActiveSelectionForeground, + hc: listActiveSelectionForeground +}, nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); + +export const MENU_SELECTION_BACKGROUND = registerColor('menu.selectionBackground', { + dark: listActiveSelectionBackground, + light: listActiveSelectionBackground, + hc: listActiveSelectionBackground +}, nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); + +export const MENU_SELECTION_BORDER = registerColor('menu.selectionBorder', { + dark: null, + light: null, + hc: activeContrastBorder +}, nls.localize('menuSelectionBorder', "Border color of the selected menu item in menus.")); + // < --- Notifications --- > export const NOTIFICATIONS_CENTER_BORDER = registerColor('notificationCenter.border', { diff --git a/src/vs/workbench/electron-browser/commands.ts b/src/vs/workbench/electron-browser/commands.ts index ecb6e315f43..63fce9d272b 100644 --- a/src/vs/workbench/electron-browser/commands.ts +++ b/src/vs/workbench/electron-browser/commands.ts @@ -13,7 +13,7 @@ import { IWindowsService, IWindowService } from 'vs/platform/windows/common/wind import { List } from 'vs/base/browser/ui/list/listWidget'; import * as errors from 'vs/base/common/errors'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget } from 'vs/platform/list/browser/listService'; +import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget, WorkbenchListHasSelectionOrFocus } from 'vs/platform/list/browser/listService'; import { PagedList } from 'vs/base/browser/ui/list/listPaging'; import { range } from 'vs/base/common/arrays'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -471,13 +471,30 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.clear', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: WorkbenchListFocusContextKey, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListHasSelectionOrFocus), primary: KeyCode.Escape, handler: (accessor) => { const focused = accessor.get(IListService).lastFocusedList; - // Tree only - if (focused && !(focused instanceof List || focused instanceof PagedList)) { + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + if (list.getSelection().length > 0) { + list.setSelection([]); + + return void 0; + } + + if (list.getFocus().length > 0) { + list.setFocus([]); + + return void 0; + } + } + + // Tree + else if (focused) { const tree = focused; if (tree.getSelection().length) { diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 6518e436b43..d72216a4c8d 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -463,7 +463,7 @@ configurationRegistry.registerConfiguration({ 'type': 'boolean', 'default': false, 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('window.smoothScrollingWorkaround', "Enable this workaround if scrolling is no longer smooth after restoring a minimized VS Code window. This is a workaround for an issue (https://github.com/Microsoft/vscode/issues/13612) where scrolling starts to lag on devices with precision trackpads like the Surface devices from Microsoft. Enabling this workaround can result in a little bit of layout flickering after restoring the window from minimized state but is otherwise harmless."), + 'description': nls.localize('window.smoothScrollingWorkaround', "Enable this workaround if scrolling is no longer smooth after restoring a minimized VS Code window. This is a workaround for an issue (https://github.com/Microsoft/vscode/issues/13612) where scrolling starts to lag on devices with precision trackpads like the Surface devices from Microsoft. Enabling this workaround can result in a little bit of layout flickering after restoring the window from minimized state but is otherwise harmless. Note: in order for this workaround to function, make sure to also set 'window.titleBarStyle: native'."), 'included': isWindows }, 'window.clickThroughInactive': { diff --git a/src/vs/workbench/electron-browser/media/shell.css b/src/vs/workbench/electron-browser/media/shell.css index 64869f0de97..0463dc38c96 100644 --- a/src/vs/workbench/electron-browser/media/shell.css +++ b/src/vs/workbench/electron-browser/media/shell.css @@ -15,11 +15,11 @@ /* Font Families (with CJK support) */ -.monaco-shell { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; } -.monaco-shell:lang(zh-Hans) { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } -.monaco-shell:lang(zh-Hant) { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; } -.monaco-shell:lang(ja) { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Meiryo", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; } -.monaco-shell:lang(ko) { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } +.monaco-shell { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; } +.monaco-shell:lang(zh-Hans) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } +.monaco-shell:lang(zh-Hant) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; } +.monaco-shell:lang(ja) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Meiryo", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; } +.monaco-shell:lang(ko) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } diff --git a/src/vs/workbench/electron-browser/media/workbench.css b/src/vs/workbench/electron-browser/media/workbench.css index 0968fc3040e..28c4a57c000 100644 --- a/src/vs/workbench/electron-browser/media/workbench.css +++ b/src/vs/workbench/electron-browser/media/workbench.css @@ -3,10 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench-container { - position: absolute; -} - .monaco-workbench { font-size: 13px; line-height: 1.4em; diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 1096104def6..73dde75f2bb 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -16,10 +16,9 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import product from 'vs/platform/node/product'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import pkg from 'vs/platform/node/package'; -import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { Workbench, IWorkbenchStartedInfo } from 'vs/workbench/electron-browser/workbench'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService, configurationTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { NullTelemetryService, configurationTelemetry, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; @@ -46,7 +45,6 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { ILifecycleService, LifecyclePhase, ShutdownReason, StartupKind } from 'vs/platform/lifecycle/common/lifecycle'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -93,7 +91,7 @@ import { NotificationService } from 'vs/workbench/services/notification/common/n import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { DialogService } from 'vs/workbench/services/dialogs/electron-browser/dialogService'; import { DialogChannel } from 'vs/platform/dialogs/common/dialogIpc'; -import { EventType, addDisposableListener, addClass, getClientArea } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, addClass } from 'vs/base/browser/dom'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { SearchHistoryService } from 'vs/workbench/services/search/node/searchHistoryService'; @@ -120,7 +118,6 @@ export class WorkbenchShell extends Disposable { private storageService: IStorageService; private environmentService: IEnvironmentService; private logService: ILogService; - private contextViewService: ContextViewService; private configurationService: IConfigurationService; private contextService: IWorkspaceContextService; private telemetryService: ITelemetryService; @@ -135,8 +132,6 @@ export class WorkbenchShell extends Disposable { private container: HTMLElement; private previousErrorValue: string; private previousErrorTime: number; - private content: HTMLElement; - private contentsContainer: HTMLElement; private configuration: IWindowConfiguration; private workbench: Workbench; @@ -160,20 +155,15 @@ export class WorkbenchShell extends Disposable { this.previousErrorTime = 0; } - private createContents(parent: HTMLElement): HTMLElement { - + private renderContents(): void { // ARIA aria.setARIAContainer(document.body); - // Workbench Container - const workbenchContainer = document.createElement('div'); - parent.appendChild(workbenchContainer); - // Instantiation service with services - const [instantiationService, serviceCollection] = this.initServiceCollection(parent); + const [instantiationService, serviceCollection] = this.initServiceCollection(this.container); // Workbench - this.workbench = this.createWorkbench(instantiationService, serviceCollection, parent, workbenchContainer); + this.workbench = this.createWorkbench(instantiationService, serviceCollection, this.container); // Window this.workbench.getInstantiationService().createInstance(ElectronWindow); @@ -186,13 +176,11 @@ export class WorkbenchShell extends Disposable { this.lifecycleService.when(LifecyclePhase.Running).then(() => { clearTimeout(timeoutHandle); }); - - return workbenchContainer; } - private createWorkbench(instantiationService: IInstantiationService, serviceCollection: ServiceCollection, parent: HTMLElement, workbenchContainer: HTMLElement): Workbench { + private createWorkbench(instantiationService: IInstantiationService, serviceCollection: ServiceCollection, container: HTMLElement): Workbench { try { - const workbench = instantiationService.createInstance(Workbench, parent, workbenchContainer, this.configuration, serviceCollection, this.lifecycleService, this.mainProcessClient); + const workbench = instantiationService.createInstance(Workbench, container, this.configuration, serviceCollection, this.lifecycleService, this.mainProcessClient); // Set lifecycle phase to `Restoring` this.lifecycleService.phase = LifecyclePhase.Restoring; @@ -362,14 +350,12 @@ export class WorkbenchShell extends Disposable { serviceCollection.set(IHashService, new SyncDescriptor(HashService)); // Telemetry - if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { - const channel = getDelayedChannel(sharedProcess.then(c => c.getChannel('telemetryAppender'))); - const commit = product.commit; - const version = pkg.version; + if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { + const channel = getDelayedChannel(sharedProcess.then(c => c.getChannel('telemetryAppender'))); const config: ITelemetryServiceConfig = { - appender: new TelemetryAppenderClient(channel), - commonProperties: resolveWorkbenchCommonProperties(this.storageService, commit, version, this.configuration.machineId, this.environmentService.installSourcePath), + appender: combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(this.logService)), + commonProperties: resolveWorkbenchCommonProperties(this.storageService, product.commit, pkg.version, this.configuration.machineId, this.environmentService.installSourcePath), piiPaths: [this.environmentService.appRoot, this.environmentService.extensionsPath] }; @@ -418,9 +404,6 @@ export class WorkbenchShell extends Disposable { serviceCollection.set(ICommandService, new SyncDescriptor(CommandService)); - this.contextViewService = instantiationService.createInstance(ContextViewService, this.container); - serviceCollection.set(IContextViewService, this.contextViewService); - serviceCollection.set(IMarkerService, new SyncDescriptor(MarkerService)); serviceCollection.set(IModeService, new SyncDescriptor(WorkbenchModeServiceImpl)); @@ -472,13 +455,8 @@ export class WorkbenchShell extends Disposable { // Shell Class for CSS Scoping addClass(this.container, 'monaco-shell'); - // Controls - this.content = document.createElement('div'); - addClass(this.content, 'monaco-shell-content'); - this.container.appendChild(this.content); - // Create Contents - this.contentsContainer = this.createContents(this.content); + this.renderContents(); // Layout this.layout(); @@ -521,12 +499,6 @@ export class WorkbenchShell extends Disposable { } private layout(): void { - const clientArea = getClientArea(this.container); - - this.contentsContainer.style.width = `${clientArea.width}px`; - this.contentsContainer.style.height = `${clientArea.height}px`; - - this.contextViewService.layout(); this.workbench.layout(); } diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 01b3628a51a..00aae6933f9 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -191,7 +191,6 @@ export class Workbench extends Disposable implements IPartService { _serviceBrand: any; private workbenchParams: WorkbenchParams; - private workbenchContainer: Builder; private workbench: Builder; private workbenchStarted: boolean; private workbenchCreated: boolean; @@ -200,6 +199,7 @@ export class Workbench extends Disposable implements IPartService { private editorService: EditorService; private editorGroupService: IEditorGroupsService; private viewletService: IViewletService; + private contextViewService: ContextViewService; private contextKeyService: IContextKeyService; private keybindingService: IKeybindingService; private backupFileService: IBackupFileService; @@ -236,7 +236,6 @@ export class Workbench extends Disposable implements IPartService { private closeEmptyWindowScheduler: RunOnceScheduler = this._register(new RunOnceScheduler(() => this.onAllEditorsClosed(), 50)); constructor( - private parent: HTMLElement, private container: HTMLElement, private configuration: IWindowConfiguration, serviceCollection: ServiceCollection, @@ -250,7 +249,6 @@ export class Workbench extends Disposable implements IPartService { @IEnvironmentService private environmentService: IEnvironmentService, @IWindowService private windowService: IWindowService, @INotificationService private notificationService: NotificationService, - @IContextViewService private contextViewService: ContextViewService, @ITelemetryService private telemetryService: TelemetryService ) { super(); @@ -300,11 +298,10 @@ export class Workbench extends Disposable implements IPartService { } private createWorkbench(): void { - this.workbenchContainer = $('.monaco-workbench-container'); this.workbench = $().div({ 'class': `monaco-workbench ${isWindows ? 'windows' : isLinux ? 'linux' : 'mac'}`, id: Identifiers.WORKBENCH_CONTAINER - }).appendTo(this.workbenchContainer); + }); } private createGlobalActions(): void { @@ -355,6 +352,10 @@ export class Workbench extends Disposable implements IPartService { // List serviceCollection.set(IListService, this.instantiationService.createInstance(ListService)); + // Context view service + this.contextViewService = this.instantiationService.createInstance(ContextViewService, this.workbench.getHTMLElement()); + serviceCollection.set(IContextViewService, this.contextViewService); + // Use themable context menus when custom titlebar is enabled to match custom menubar if (!isMacintosh && this.getCustomTitleBarStyle() === 'custom') { serviceCollection.set(IContextMenuService, new SyncDescriptor(HTMLContextMenuService, null, this.telemetryService, this.notificationService, this.contextViewService)); @@ -975,12 +976,6 @@ export class Workbench extends Disposable implements IPartService { // Apply font aliasing this.setFontAliasing(this.fontAliasing); - // Apply title style if shown - const titleStyle = this.getCustomTitleBarStyle(); - if (titleStyle) { - DOM.addClass(this.parent, `titlebar-style-${titleStyle}`); - } - // Apply fullscreen state if (browser.isFullscreen()) { this.workbench.addClass('fullscreen'); @@ -999,7 +994,7 @@ export class Workbench extends Disposable implements IPartService { this.createNotificationsHandlers(); // Add Workbench to DOM - this.workbenchContainer.build(this.container); + this.workbench.appendTo(this.container); } private createTitlebarPart(): void { @@ -1292,6 +1287,8 @@ export class Workbench extends Disposable implements IPartService { } layout(options?: ILayoutOptions): void { + this.contextViewService.layout(); + if (this.workbenchStarted && !this.workbenchShutdown) { this.workbenchLayout.layout(options); } diff --git a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts index 1ee06237e2e..04fa7816807 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts @@ -26,7 +26,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IModelService } from 'vs/editor/common/services/modelService'; import { SimpleCommentEditor } from './simpleCommentEditor'; import URI from 'vs/base/common/uri'; -import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { transparent, editorForeground, inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -270,7 +270,7 @@ export class ReviewZoneWidget extends ZoneWidget { } protected _doLayout(heightInPixel: number, widthInPixel: number): void { - this._commentEditor.layout({ height: (this._commentEditor.hasWidgetFocus() ? 5 : 1) * 18, width: widthInPixel - 20 /* margin */ }); + this._commentEditor.layout({ height: (this._commentEditor.hasWidgetFocus() ? 5 : 1) * 18, width: widthInPixel - 40 /* margin */ }); } display(lineNumber: number) { @@ -353,6 +353,22 @@ export class ReviewZoneWidget extends ZoneWidget { attachButtonStyler(button, this.themeService); button.label = 'Add comment'; button.onDidClick(async () => { + if (!this._commentEditor.getValue()) { + this._commentEditor.focus(); + this._commentEditor.getDomNode().style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`; + + + this._disposables.push(this._commentEditor.onDidChangeModelContent(_ => { + if (!this._commentEditor.getValue()) { + this._commentEditor.getDomNode().style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`; + } else { + this._commentEditor.getDomNode().style.outline = ''; + } + })); + + return; + } + let newCommentThread; if (this._commentThread.threadId) { // reply diff --git a/src/vs/workbench/parts/comments/electron-browser/media/review.css b/src/vs/workbench/parts/comments/electron-browser/media/review.css index 9854ade1898..b39b972ab61 100644 --- a/src/vs/workbench/parts/comments/electron-browser/media/review.css +++ b/src/vs/workbench/parts/comments/electron-browser/media/review.css @@ -55,6 +55,7 @@ .monaco-editor .review-widget .body .review-comment .review-comment-contents { margin-left: 20px; + user-select: text; } .monaco-editor .review-widget .body pre { diff --git a/src/vs/workbench/parts/debug/browser/breakpointsView.ts b/src/vs/workbench/parts/debug/browser/breakpointsView.ts index 77f2cd56085..edf399c0e2a 100644 --- a/src/vs/workbench/parts/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/parts/debug/browser/breakpointsView.ts @@ -23,7 +23,7 @@ import { basename } from 'vs/base/common/paths'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { TPromise } from 'vs/base/common/winjs.base'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IDelegate, IListContextMenuEvent, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IListContextMenuEvent, IRenderer } from 'vs/base/browser/ui/list/list'; import { IEditor } from 'vs/workbench/common/editor'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -221,7 +221,7 @@ export class BreakpointsView extends ViewletPanel { } } -class BreakpointsDelegate implements IDelegate { +class BreakpointsDelegate implements IVirtualDelegate { constructor(private debugService: IDebugService) { // noop @@ -338,6 +338,10 @@ class BreakpointsRenderer implements IRenderer Promise; + parent: IExtensionData; +} + export class DataSource implements IDataSource { - public getId(tree: ITree, element: IExtensionDependencies): string { - let id = element.identifier; - this.getParent(tree, element).then(parent => { - id = parent ? this.getId(tree, parent) + '/' + id : id; - }); - return id; + public getId(tree: ITree, { extension, parent }: IExtensionData): string { + return parent ? this.getId(tree, parent) + '/' + extension.id : extension.id; } - public hasChildren(tree: ITree, element: IExtensionDependencies): boolean { - return element.hasDependencies; + public hasChildren(tree: ITree, { hasChildren }: IExtensionData): boolean { + return hasChildren; } - public getChildren(tree: ITree, element: IExtensionDependencies): Promise { - return TPromise.as(element.dependencies); + public getChildren(tree: ITree, extensionData: IExtensionData): Promise { + return extensionData.getChildren(); } - public getParent(tree: ITree, element: IExtensionDependencies): Promise { - return TPromise.as(element.dependent); + public getParent(tree: ITree, { parent }: IExtensionData): Promise { + return TPromise.as(parent); } } @@ -62,12 +67,12 @@ export class Renderer implements IRenderer { constructor(@IInstantiationService private instantiationService: IInstantiationService) { } - public getHeight(tree: ITree, element: IExtensionDependencies): number { + public getHeight(tree: ITree, element: IExtensionData): number { return 62; } - public getTemplateId(tree: ITree, element: IExtensionDependencies): string { - return element.extension ? Renderer.EXTENSION_TEMPLATE_ID : Renderer.UNKNOWN_EXTENSION_TEMPLATE_ID; + public getTemplateId(tree: ITree, { extension }: IExtensionData): string { + return extension ? Renderer.EXTENSION_TEMPLATE_ID : Renderer.UNKNOWN_EXTENSION_TEMPLATE_ID; } public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any { @@ -78,7 +83,7 @@ export class Renderer implements IRenderer { } private renderExtensionTemplate(tree: ITree, container: HTMLElement): IExtensionTemplateData { - dom.addClass(container, 'dependency'); + dom.addClass(container, 'extension'); const icon = dom.append(container, dom.$('img.icon')); const details = dom.append(container, dom.$('.details')); @@ -87,8 +92,8 @@ export class Renderer implements IRenderer { const name = dom.append(header, dom.$('span.name')); const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction); const extensionDisposables = [dom.addDisposableListener(name, 'click', (e: MouseEvent) => { - tree.setFocus(openExtensionAction.extensionDependencies); - tree.setSelection([openExtensionAction.extensionDependencies]); + tree.setFocus(openExtensionAction.extensionData); + tree.setSelection([openExtensionAction.extensionData]); openExtensionAction.run(e.ctrlKey || e.metaKey); e.stopPropagation(); e.preventDefault(); @@ -103,22 +108,22 @@ export class Renderer implements IRenderer { identifier, author, extensionDisposables, - set extensionDependencies(e: IExtensionDependencies) { - openExtensionAction.extensionDependencies = e; + set extensionData(extensionData: IExtensionData) { + openExtensionAction.extensionData = extensionData; } }; } private renderUnknownExtensionTemplate(tree: ITree, container: HTMLElement): IUnknownExtensionTemplateData { - const messageContainer = dom.append(container, dom.$('div.unknown-dependency')); + const messageContainer = dom.append(container, dom.$('div.unknown-extension')); dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error"); - dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Dependency', "Unknown Dependency:"); + dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Extension', "Unknown Extension:"); const identifier = dom.append(messageContainer, dom.$('span.message')); return { identifier }; } - public renderElement(tree: ITree, element: IExtensionDependencies, templateId: string, templateData: any): void { + public renderElement(tree: ITree, element: IExtensionData, templateId: string, templateData: any): void { if (templateId === Renderer.EXTENSION_TEMPLATE_ID) { this.renderExtension(tree, element, templateData); return; @@ -126,9 +131,8 @@ export class Renderer implements IRenderer { this.renderUnknownExtension(tree, element, templateData); } - private renderExtension(tree: ITree, element: IExtensionDependencies, data: IExtensionTemplateData): void { - const extension = element.extension; - + private renderExtension(tree: ITree, extensionData: IExtensionData, data: IExtensionTemplateData): void { + const extension = extensionData.extension; const onError = once(domEvent(data.icon, 'error')); onError(() => data.icon.src = extension.iconUrlFallback, null, data.extensionDisposables); data.icon.src = extension.iconUrl; @@ -143,11 +147,11 @@ export class Renderer implements IRenderer { data.name.textContent = extension.displayName; data.identifier.textContent = extension.id; data.author.textContent = extension.publisherDisplayName; - data.extensionDependencies = element; + data.extensionData = extensionData; } - private renderUnknownExtension(tree: ITree, element: IExtensionDependencies, data: IUnknownExtensionTemplateData): void { - data.identifier.textContent = element.identifier; + private renderUnknownExtension(tree: ITree, { extension }: IExtensionData, data: IUnknownExtensionTemplateData): void { + data.identifier.textContent = extension.id; } public disposeTemplate(tree: ITree, templateId: string, templateData: any): void { @@ -169,10 +173,10 @@ export class Controller extends WorkbenchTreeController { this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, (tree: ITree, event: any) => this.openExtension(tree, true)); } - protected onLeftClick(tree: ITree, element: IExtensionDependencies, event: IMouseEvent): boolean { + protected onLeftClick(tree: ITree, element: IExtensionData, event: IMouseEvent): boolean { let currentFocused = tree.getFocus(); if (super.onLeftClick(tree, element, event)) { - if (element.dependent === null) { + if (element.parent === null) { if (currentFocused) { tree.setFocus(currentFocused); } else { @@ -185,7 +189,7 @@ export class Controller extends WorkbenchTreeController { } public openExtension(tree: ITree, sideByside: boolean): boolean { - const element: IExtensionDependencies = tree.getFocus(); + const element: IExtensionData = tree.getFocus(); if (element.extension) { this.extensionsWorkdbenchService.open(element.extension, sideByside); return true; @@ -196,21 +200,58 @@ export class Controller extends WorkbenchTreeController { class OpenExtensionAction extends Action { - private _extensionDependencies: IExtensionDependencies; + private _extensionData: IExtensionData; constructor(@IExtensionsWorkbenchService private extensionsWorkdbenchService: IExtensionsWorkbenchService) { - super('extensions.action.openDependency', ''); + super('extensions.action.openExtension', ''); } - public set extensionDependencies(extensionDependencies: IExtensionDependencies) { - this._extensionDependencies = extensionDependencies; + public set extensionData(extension: IExtensionData) { + this._extensionData = extension; } - public get extensionDependencies(): IExtensionDependencies { - return this._extensionDependencies; + public get extensionData(): IExtensionData { + return this._extensionData; } run(sideByside: boolean): TPromise { - return this.extensionsWorkdbenchService.open(this._extensionDependencies.extension, sideByside); + return this.extensionsWorkdbenchService.open(this.extensionData.extension, sideByside); + } +} + +export class ExtensionsTree extends WorkbenchTree { + + constructor( + input: IExtensionData, + container: HTMLElement, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService + ) { + const renderer = instantiationService.createInstance(Renderer); + const controller = instantiationService.createInstance(Controller); + + super( + container, + { + dataSource: new DataSource(), + renderer, + controller + }, { + indentPixels: 40, + twistiePixels: 20 + }, + contextKeyService, listService, themeService, instantiationService, configurationService + ); + + this.setInput(input); + + this.disposables.push(this.onDidChangeSelection(event => { + if (event && event.payload && event.payload.origin === 'keyboard') { + controller.openExtension(this, false); + } + })); } } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/common/extensions.ts b/src/vs/workbench/parts/extensions/common/extensions.ts index 64b3a90a7b2..bf50d9d44ae 100644 --- a/src/vs/workbench/parts/extensions/common/extensions.ts +++ b/src/vs/workbench/parts/extensions/common/extensions.ts @@ -49,11 +49,14 @@ export interface IExtension { outdated: boolean; enablementState: EnablementState; dependencies: string[]; + extensionPack: string[]; telemetryData: any; preview: boolean; getManifest(): TPromise; getReadme(): TPromise; + hasReadme(): boolean; getChangelog(): TPromise; + hasChangelog(): boolean; local?: ILocalExtension; locals?: ILocalExtension[]; gallery?: IGalleryExtension; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts index f3a1da645e0..5e247ac5bdd 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/extensionEditor'; import { localize } from 'vs/nls'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { TPromise, Promise } from 'vs/base/common/winjs.base'; import { marked } from 'vs/base/common/marked/marked'; import { always } from 'vs/base/common/async'; import * as arrays from 'vs/base/common/arrays'; @@ -27,7 +27,6 @@ import { IExtensionManifest, IKeyBinding, IView, IExtensionTipsService, LocalExt import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput'; import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, IExtension, IExtensionDependencies } from 'vs/workbench/parts/extensions/common/extensions'; -import { Renderer, DataSource, Controller } from 'vs/workbench/parts/extensions/browser/dependenciesViewer'; import { RatingsWidget, InstallCountWidget } from 'vs/workbench/parts/extensions/browser/extensionsWidgets'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -46,10 +45,10 @@ import { Command } from 'vs/editor/browser/editorExtensions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Color } from 'vs/base/common/color'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { assign } from 'vs/base/common/objects'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ExtensionsTree, IExtensionData } from 'vs/workbench/parts/extensions/browser/extensionsViewer'; /** A context key that is set when an extension editor webview has focus. */ export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_WEBVIEW_FOCUS = new RawContextKey('extensionEditorWebviewFocus', undefined); @@ -138,7 +137,8 @@ const NavbarSection = { Readme: 'readme', Contributions: 'contributions', Changelog: 'changelog', - Dependencies: 'dependencies' + Dependencies: 'dependencies', + ExtensionPack: 'extensionPack' }; interface ILayoutParticipant { @@ -423,12 +423,27 @@ export class ExtensionEditor extends BaseEditor { this.navbar.clear(); this.navbar.onChange(this.onNavbarChange.bind(this, extension), this, this.transientDisposables); - this.navbar.push(NavbarSection.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); - this.navbar.push(NavbarSection.Contributions, localize('contributions', "Contributions"), localize('contributionstooltip', "Lists contributions to VS Code by this extension")); - this.navbar.push(NavbarSection.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file")); - this.navbar.push(NavbarSection.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on")); - this.editorLoadComplete = true; + if (extension.hasReadme()) { + this.navbar.push(NavbarSection.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); + } + this.extensionManifest.get() + .then(manifest => { + if (extension.extensionPack.length) { + this.navbar.push(NavbarSection.ExtensionPack, localize('extensionPack', "Extension Pack"), localize('extensionsPack', "Set of extensions that can be installed together")); + } + if (manifest.contributes) { + this.navbar.push(NavbarSection.Contributions, localize('contributions', "Contributions"), localize('contributionstooltip', "Lists contributions to VS Code by this extension")); + } + if (extension.hasChangelog()) { + this.navbar.push(NavbarSection.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file")); + } + if (extension.dependencies.length) { + this.navbar.push(NavbarSection.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on")); + } + this.editorLoadComplete = true; + }); + return super.setInput(input, options, token); } @@ -459,6 +474,7 @@ export class ExtensionEditor extends BaseEditor { case NavbarSection.Contributions: return this.openContributions(); case NavbarSection.Changelog: return this.openChangelog(); case NavbarSection.Dependencies: return this.openDependencies(extension); + case NavbarSection.ExtensionPack: return this.openExtensionPack(extension); } } @@ -550,16 +566,16 @@ export class ExtensionEditor extends BaseEditor { append(this.content, scrollableContent.getDomNode()); this.contentDisposables.push(scrollableContent); - const tree = this.renderDependencies(content, extensionDependencies); + const dependenciesTree = this.renderDependencies(content, extensionDependencies); const layout = () => { scrollableContent.scanDomNode(); const scrollDimensions = scrollableContent.getScrollDimensions(); - tree.layout(scrollDimensions.height); + dependenciesTree.layout(scrollDimensions.height); }; const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); this.contentDisposables.push(toDisposable(removeLayoutParticipant)); - this.contentDisposables.push(tree); + this.contentDisposables.push(dependenciesTree); scrollableContent.scanDomNode(); }, error => { append(this.content, $('p.nocontent')).textContent = error; @@ -569,26 +585,83 @@ export class ExtensionEditor extends BaseEditor { } private renderDependencies(container: HTMLElement, extensionDependencies: IExtensionDependencies): Tree { - const renderer = this.instantiationService.createInstance(Renderer); - const controller = this.instantiationService.createInstance(Controller); - const tree = this.instantiationService.createInstance(WorkbenchTree, container, { - dataSource: new DataSource(), - renderer, - controller - }, { - indentPixels: 40, - twistiePixels: 20 - }); + class ExtensionData implements IExtensionData { - tree.setInput(extensionDependencies); + private readonly extensionDependencies: IExtensionDependencies; - this.contentDisposables.push(tree.onDidChangeSelection(event => { - if (event && event.payload && event.payload.origin === 'keyboard') { - controller.openExtension(tree, false); + constructor(extensionDependencies: IExtensionDependencies) { + this.extensionDependencies = extensionDependencies; } - })); - return tree; + get extension(): IExtension { + return this.extensionDependencies.extension; + } + + get parent(): IExtensionData { + return this.extensionDependencies.dependent ? new ExtensionData(this.extensionDependencies.dependent) : null; + } + + get hasChildren(): boolean { + return this.extensionDependencies.hasDependencies; + } + + getChildren(): Promise { + return this.extensionDependencies.dependencies ? TPromise.as(this.extensionDependencies.dependencies.map(d => new ExtensionData(d))) : null; + } + } + + return this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extensionDependencies), container); + } + + private openExtensionPack(extension: IExtension) { + return this.loadContents(() => { + const content = $('div', { class: 'subcontent' }); + const scrollableContent = new DomScrollableElement(content, {}); + append(this.content, scrollableContent.getDomNode()); + this.contentDisposables.push(scrollableContent); + + const dependenciesTree = this.renderExtensionPack(content, extension); + const layout = () => { + scrollableContent.scanDomNode(); + const scrollDimensions = scrollableContent.getScrollDimensions(); + dependenciesTree.layout(scrollDimensions.height); + }; + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); + this.contentDisposables.push(toDisposable(removeLayoutParticipant)); + + this.contentDisposables.push(dependenciesTree); + scrollableContent.scanDomNode(); + return TPromise.as(null); + }); + } + + private renderExtensionPack(container: HTMLElement, extension: IExtension): Tree { + const extensionsWorkbenchService = this.extensionsWorkbenchService; + class ExtensionData implements IExtensionData { + + readonly extension: IExtension; + readonly parent: IExtensionData; + + constructor(extension: IExtension, parent?: IExtensionData) { + this.extension = extension; + this.parent = parent; + } + + get hasChildren(): boolean { + return this.extension.extensionPack.length > 0; + } + + getChildren(): Promise { + if (this.hasChildren) { + const names = arrays.distinct(this.extension.extensionPack, e => e.toLowerCase()); + return extensionsWorkbenchService.queryGallery({ names, pageSize: names.length }) + .then(result => result.firstPage.map(extension => new ExtensionData(extension, this))); + } + return TPromise.as(null); + } + } + + return this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extension), container); } private renderSettings(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index 784c508d624..fbaf724d4ad 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -81,7 +81,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe private _workspaceIgnoredRecommendations: string[] = []; private _extensionsRecommendationsUrl: string; private _disposables: IDisposable[] = []; - public loadRecommendationsPromise: TPromise; + public loadWorkspaceConfigPromise: TPromise; private proactiveRecommendationsFetched: boolean = false; private readonly _onRecommendationChange: Emitter = new Emitter(); @@ -120,22 +120,19 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe let globallyIgnored = JSON.parse(this.storageService.get('extensionsAssistant/ignored_recommendations', StorageScope.GLOBAL, '[]')); this._globallyIgnoredRecommendations = globallyIgnored.map(id => id.toLowerCase()); - this.loadRecommendationsPromise = this.getWorkspaceRecommendations() - .then(() => { - // these must be called after workspace configs have been refreshed. - this.fetchCachedDynamicWorkspaceRecommendations(); - this.fetchFileBasedRecommendations(); - this.fetchExperimentalRecommendations(); - return this.promptWorkspaceRecommendations(); - }).then(() => { - this._modelService.onModelAdded(this.promptFiletypeBasedRecommendations, this, this._disposables); - this._modelService.getModels().forEach(model => this.promptFiletypeBasedRecommendations(model)); - }); - + this.fetchCachedDynamicWorkspaceRecommendations(); + this.fetchFileBasedRecommendations(); + this.fetchExperimentalRecommendations(); if (!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { this.fetchProactiveRecommendations(true); } + this.loadWorkspaceConfigPromise = this.getWorkspaceRecommendations().then(() => { + this.promptWorkspaceRecommendations(); + this._modelService.onModelAdded(this.promptFiletypeBasedRecommendations, this, this._disposables); + this._modelService.getModels().forEach(model => this.promptFiletypeBasedRecommendations(model)); + }); + this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); this._register(this.configurationService.onDidChangeConfiguration(e => { if (!this.proactiveRecommendationsFetched && !this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { @@ -411,7 +408,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe if (this._exeBasedRecommendations[extensionId]) { sources.push('executable'); } - if (this._dynamicWorkspaceRecommendations[extensionId]) { + if (this._dynamicWorkspaceRecommendations.indexOf(extensionId) !== -1) { sources.push('dynamic'); } return ({ extensionId, sources }); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts index 88964425ea0..ce4e5210290 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts @@ -2321,7 +2321,12 @@ export class AddToWorkspaceFolderRecommendationsAction extends AbstractConfigure } return this.addExtensionToWorkspaceFolderConfig(configurationFile, extensionId, shouldRecommend).then(() => { - this.notificationService.info(localize('AddToWorkspaceFolderRecommendations.success', 'The extension was successfully added to this workspace folder\'s recommendations.')); + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceFolderRecommendations.success', 'The extension was successfully added to this workspace folder\'s recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openExtensionsFile(configurationFile) + }]); }, err => { this.notificationService.error(localize('AddToWorkspaceFolderRecommendations.failure', 'Failed to write to extensions.json. {0}', err)); }); @@ -2333,7 +2338,12 @@ export class AddToWorkspaceFolderRecommendationsAction extends AbstractConfigure } return this.addExtensionToWorkspaceFolderConfig(configurationFile, extensionId, shouldRecommend).then(() => { - this.notificationService.info(localize('AddToWorkspaceFolderIgnoredRecommendations.success', 'The extension was successfully added to this workspace folder\'s unwanted recommendations.')); + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceFolderIgnoredRecommendations.success', 'The extension was successfully added to this workspace folder\'s unwanted recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openExtensionsFile(configurationFile) + }]); }, err => { this.notificationService.error(localize('AddToWorkspaceFolderRecommendations.failure', 'Failed to write to extensions.json. {0}', err)); }); @@ -2381,7 +2391,13 @@ export class AddToWorkspaceRecommendationsAction extends AbstractConfigureRecomm } return this.addExtensionToWorkspaceConfig(workspaceConfig, extensionId, shouldRecommend).then(() => { - this.notificationService.info(localize('AddToWorkspaceRecommendations.success', 'The extension was successfully added to this workspace\'s recommendations.')); + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceRecommendations.success', 'The extension was successfully added to this workspace\'s recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openWorkspaceConfigurationFile(workspaceConfig) + }]); + }, err => { this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write. {0}', err)); }); @@ -2392,7 +2408,12 @@ export class AddToWorkspaceRecommendationsAction extends AbstractConfigureRecomm } return this.addExtensionToWorkspaceConfig(workspaceConfig, extensionId, shouldRecommend).then(() => { - this.notificationService.info(localize('AddToWorkspaceUnwantedRecommendations.success', 'The extension was successfully added to this workspace\'s unwanted recommendations.')); + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceUnwantedRecommendations.success', 'The extension was successfully added to this workspace\'s unwanted recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openWorkspaceConfigurationFile(workspaceConfig) + }]); }, err => { this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write. {0}', err)); }); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts index 1f032611899..1153fd57fb8 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts @@ -11,7 +11,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Action } from 'vs/base/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDelegate } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { once } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; @@ -38,7 +38,7 @@ export interface ITemplateData { extensionDisposables: IDisposable[]; } -export class Delegate implements IDelegate { +export class Delegate implements IVirtualDelegate { getHeight() { return 62; } getTemplateId() { return 'extension'; } } @@ -194,6 +194,10 @@ export class Renderer implements IPagedRenderer { }); } + disposeElement(): void { + // noop + } + private updateRecommendationStatus(extension: IExtension, data: ITemplateData) { const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); let ariaLabel = extension.displayName + '. '; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index a03d325d251..80e64760492 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -71,9 +71,8 @@ export class ExtensionsListView extends ViewletPanel { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService); } - renderHeader(container: HTMLElement): void { - const titleDiv = append(container, $('div.title')); - append(titleDiv, $('span')).textContent = this.options.title; + renderHeaderTitle(container: HTMLElement): void { + super.renderHeaderTitle(container, this.options.title); this.badgeContainer = append(container, $('.count-badge-wrapper')); this.badge = new CountBadge(this.badgeContainer); diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css index 10efc9daede..7a8a61811e5 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css @@ -311,11 +311,11 @@ border-color: rgb(238, 238, 238); } -.extension-editor .subcontent .monaco-tree-row .content .unknown-dependency { +.extension-editor .subcontent .monaco-tree-row .content .unknown-extension { line-height: 62px; } -.extension-editor .subcontent .monaco-tree-row .content .unknown-dependency > .error-marker { +.extension-editor .subcontent .monaco-tree-row .content .unknown-extension > .error-marker { background-color: #BE1100; padding: 2px 4px; font-weight: bold; @@ -323,46 +323,46 @@ color: #CCC; } -.extension-editor .subcontent .monaco-tree-row .unknown-dependency > .message { +.extension-editor .subcontent .monaco-tree-row .unknown-extension > .message { padding-left: 10px; font-weight: bold; font-size: 14px; } -.extension-editor .subcontent .monaco-tree-row .dependency { +.extension-editor .subcontent .monaco-tree-row .extension { display: flex; align-items: center; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details { +.extension-editor .subcontent .monaco-tree-row .extension > .details { flex: 1; overflow: hidden; padding-left: 10px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header { display: flex; align-items: center; height: 19px; overflow: hidden; } -.extension-editor .subcontent .monaco-tree-row .dependency > .icon { +.extension-editor .subcontent .monaco-tree-row .extension > .icon { height: 40px; width: 40px; object-fit: contain; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header > .name { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .name { font-weight: bold; font-size: 16px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header > .name:hover { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .name:hover { text-decoration: underline; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header > .identifier { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .identifier { font-size: 90%; opacity: 0.6; margin-left: 10px; @@ -371,14 +371,14 @@ border-radius: 4px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .footer { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .footer { display: flex; height: 19px; overflow: hidden; padding-top: 5px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .footer > .author { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .footer > .author { font-size: 90%; font-weight: 600; opacity: 0.6; diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css index 848c0382f9a..a879a47e71a 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css @@ -6,4 +6,9 @@ .monaco-workbench > .activitybar > .content .monaco-action-bar .action-label.extensions { -webkit-mask: url('extensions-dark.svg') no-repeat 50% 50%; -webkit-mask-size: 21px; +} + +.extensions .split-view-view .panel-header .count-badge-wrapper { + position: absolute; + right: 12px; } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts index ed9c80d3b1a..0d1d08f98b9 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -21,7 +21,7 @@ import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/parts/exte import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService, IExtensionDescription, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom'; import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -216,7 +216,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { const TEMPLATE_ID = 'runtimeExtensionElementTemplate'; - const delegate = new class implements IDelegate{ + const delegate = new class implements IVirtualDelegate{ getHeight(element: IRuntimeExtension): number { return 62; } @@ -369,6 +369,8 @@ export class RuntimeExtensionsEditor extends BaseEditor { } }, + disposeElement: () => null, + disposeTemplate: (data: IRuntimeExtensionTemplateData): void => { data.disposables = dispose(data.disposables); } diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index 03a9cc7540c..01ae0465430 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -218,6 +218,18 @@ class Extension implements IExtension { return TPromise.as(this.local.manifest); } + hasReadme(): boolean { + if (this.gallery && !this.isGalleryOutdated() && this.gallery.assets.readme) { + return true; + } + + if (this.local && this.local.readmeUrl) { + return true; + } + + return this.type === LocalExtensionType.System; + } + getReadme(): TPromise { if (this.gallery && !this.isGalleryOutdated()) { if (this.gallery.assets.readme) { @@ -233,7 +245,8 @@ class Extension implements IExtension { if (this.type === LocalExtensionType.System) { return TPromise.as(`# ${this.displayName || this.name} -**Notice** This is a an extension that is bundled with Visual Studio Code. +**Notice** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. +## Features ${this.description} `); } @@ -241,6 +254,19 @@ ${this.description} return TPromise.wrapError(new Error('not available')); } + hasChangelog(): boolean { + if (this.gallery && this.gallery.assets.changelog && !this.isGalleryOutdated()) { + return true; + } + + if (this.local && this.local.changelogUrl) { + const uri = URI.parse(this.local.changelogUrl); + return uri.scheme === 'file'; + } + + return false; + } + getChangelog(): TPromise { if (this.gallery && this.gallery.assets.changelog && !this.isGalleryOutdated()) { return this.galleryService.getChangelog(this.gallery); @@ -264,13 +290,24 @@ ${this.description} get dependencies(): string[] { const { local, gallery } = this; if (gallery && !this.isGalleryOutdated()) { - return gallery.properties.dependencies; + return gallery.properties.dependencies || []; } if (local && local.manifest.extensionDependencies) { return local.manifest.extensionDependencies; } return []; } + + get extensionPack(): string[] { + const { local, gallery } = this; + if (gallery && !this.isGalleryOutdated()) { + return gallery.properties.extensionPack || []; + } + if (local && local.manifest.extensionPack) { + return local.manifest.extensionPack; + } + return []; + } } class ExtensionDependencies implements IExtensionDependencies { @@ -302,7 +339,7 @@ class ExtensionDependencies implements IExtensionDependencies { if (!this.hasDependencies) { return []; } - return this._extension.dependencies.map(d => new ExtensionDependencies(this._map.get(d), d, this._map, this)); + return this._extension.dependencies.map(id => new ExtensionDependencies(this._map.get(id), id, this._map, this)); } private computeHasDependencies(): boolean { @@ -438,7 +475,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, .then(([allRecommendations, report]) => { const maliciousSet = getMaliciousExtensionsSet(report); - return this.galleryService.loadAllDependencies((extension).dependencies.map(id => { id })) + return this.galleryService.loadAllDependencies((extension).dependencies.map(id => ({ id }))) .then(galleryExtensions => galleryExtensions.map(galleryExtension => this.fromGallery(galleryExtension, maliciousSet, allRecommendations))) .then(extensions => [...this.local, ...extensions]) .then(extensions => { @@ -654,7 +691,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, location: ProgressLocation.Extensions, title: nls.localize('installingMarketPlaceExtension', 'Installing extension from Marketplace....'), source: `${extension.id}` - }, () => this.extensionService.installFromGallery(gallery).then(() => null)); + }, () => this.extensionService.installFromGallery(gallery)); } setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): TPromise { @@ -701,46 +738,31 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } private promptAndSetEnablement(extensions: IExtension[], enablementState: EnablementState): TPromise { - const allDependencies = this.getDependenciesRecursively(extensions, this.local, enablementState, []); - if (allDependencies.length > 0) { - if (enablementState === EnablementState.Enabled || enablementState === EnablementState.WorkspaceEnabled) { - return this.promptForDependenciesAndEnable(extensions, allDependencies, enablementState); + const allDependenciesAndPackedExtensions = this.getDependenciesAndPackedExtensionsRecursively(extensions, this.local, enablementState); + if (allDependenciesAndPackedExtensions.length > 0) { + if (extensions.length === 1 && (enablementState === EnablementState.Disabled || enablementState === EnablementState.WorkspaceDisabled)) { + return this.promptForDependenciesAndDisable(extensions[0], allDependenciesAndPackedExtensions, enablementState); } else { - return this.promptForDependenciesAndDisable(extensions, allDependencies, enablementState); + return this.checkAndSetEnablement(extensions, allDependenciesAndPackedExtensions, enablementState); } } return this.checkAndSetEnablement(extensions, [], enablementState); } - private promptForDependenciesAndEnable(extensions: IExtension[], dependencies: IExtension[], enablementState: EnablementState): TPromise { - const message = nls.localize('enableDependeciesConfirmation', "Enabling an extension also enables its dependencies. Would you like to continue?"); + private promptForDependenciesAndDisable(extension: IExtension, dependencies: IExtension[], enablementState: EnablementState): TPromise { + const message = nls.localize('disableExtensionPackConfirmation', "Would you like to disable '{0}' only or as a pack?", extension.displayName); const buttons = [ - nls.localize('enable', "Yes"), - nls.localize('doNotEnable', "No") - ]; - return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 1 }) - .then(value => { - if (value === 0) { - return this.checkAndSetEnablement(extensions, dependencies, enablementState); - } - return TPromise.as(null); - }); - } - - private promptForDependenciesAndDisable(extensions: IExtension[], dependencies: IExtension[], enablementState: EnablementState): TPromise { - const message = nls.localize('disableDependeciesConfirmation', "Would you like to disable the dependencies of the extensions also?"); - const buttons = [ - nls.localize('yes', "Yes"), - nls.localize('no', "No"), + nls.localize('disablePack', "Disable Extension Pack"), + nls.localize('disableOnly', "Disable Extension Only"), nls.localize('cancel', "Cancel") ]; - return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 2 }) + return this.dialogService.show(Severity.Info, message, buttons) .then(value => { if (value === 0) { - return this.checkAndSetEnablement(extensions, dependencies, enablementState); + return this.checkAndSetEnablement([extension], dependencies, enablementState); } if (value === 1) { - return this.checkAndSetEnablement(extensions, [], enablementState); + return this.checkAndSetEnablement([extension], [], enablementState); } return TPromise.as(null); }); @@ -760,25 +782,26 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return TPromise.join(allExtensions.map(e => this.doSetEnablement(e, enablementState))); } - private getDependenciesRecursively(extensions: IExtension[], installed: IExtension[], enablementState: EnablementState, checked: IExtension[]): IExtension[] { + private getDependenciesAndPackedExtensionsRecursively(extensions: IExtension[], installed: IExtension[], enablementState: EnablementState, checked: IExtension[] = []): IExtension[] { const toCheck = extensions.filter(e => checked.indexOf(e) === -1); if (toCheck.length) { for (const extension of toCheck) { checked.push(extension); } - const dependenciesToDisable = installed.filter(i => { + const extensionsToDisable = installed.filter(i => { if (checked.indexOf(i) !== -1) { return false; } if (i.enablementState === enablementState) { return false; } - return i.type === LocalExtensionType.User && extensions.some(extension => extension.dependencies.indexOf(i.id) !== -1); + return i.type === LocalExtensionType.User && + extensions.some(extension => extension.dependencies.some(id => areSameExtensions({ id }, i)) || extension.extensionPack.some(id => areSameExtensions({ id }, i))); }); - if (dependenciesToDisable.length) { - const depsOfDeps = this.getDependenciesRecursively(dependenciesToDisable, installed, enablementState, checked); - return [...dependenciesToDisable, ...depsOfDeps]; + if (extensionsToDisable.length) { + extensionsToDisable.push(...this.getDependenciesAndPackedExtensionsRecursively(extensionsToDisable, installed, enablementState, checked)); } + return extensionsToDisable; } return []; } @@ -797,12 +820,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, if (extensionsToDisable.indexOf(i) !== -1) { return false; } - return i.dependencies.some(dep => { - if (extension.id === dep) { - return true; - } - return extensionsToDisable.some(d => d.id === dep); - }); + return i.dependencies.some(dep => [extension, ...extensionsToDisable].some(d => d.id === dep)); }); } diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts index faa0bf3f29f..89cbd28c3d6 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts @@ -287,7 +287,7 @@ suite('ExtensionsTipsService Test', () => { function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); }); @@ -297,7 +297,7 @@ suite('ExtensionsTipsService Test', () => { function testNoPromptOrRecommendationsForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - assert.equal(!testObject.loadRecommendationsPromise, true); + assert.equal(!testObject.loadWorkspaceConfigPromise, true); assert.ok(!prompted); return testObject.getWorkspaceRecommendations().then(() => { @@ -327,7 +327,7 @@ suite('ExtensionsTipsService Test', () => { test('ExtensionTipsService: Prompt for valid workspace recommendations', () => { return setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length); @@ -359,7 +359,7 @@ suite('ExtensionsTipsService Test', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, 0); assert.ok(!prompted); }); @@ -387,7 +387,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-vscode.csharp']); // stored recommendation that has been globally ignored assert.ok(recommendations['ms-python.python']); // stored recommendation @@ -407,7 +407,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-vscode.csharp']); // stored recommendation that has been workspace ignored assert.ok(recommendations['ms-python.python']); // stored recommendation @@ -435,7 +435,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(recommendations['ms-python.python']); @@ -462,7 +462,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(recommendations['ms-python.python']); assert.ok(recommendations['mockpublisher1.mockextension1']); @@ -516,7 +516,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.equal(recommendations.length, 2); assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-vscode.csharp')); // stored recommendation that exists in product.extensionTips @@ -535,7 +535,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.equal(recommendations.length, 2); assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-vscode.csharp')); // stored recommendation that exists in product.extensionTips diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 2b3f43d0114..f3f0e333ec4 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -40,7 +40,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { URLService } from 'vs/platform/url/common/urlService'; import URI from 'vs/base/common/uri'; -suite('ExtensionsWorkbenchService Test', () => { +suite('ExtensionsWorkbenchServiceTest', () => { let instantiationService: TestInstantiationService; let testObject: IExtensionsWorkbenchService; @@ -851,6 +851,27 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); + test('test disable extension pack disable only itself', () => { + const extensionA = aLocalExtension('a', { extensionPack: ['pub.b'] }); + const extensionB = aLocalExtension('b'); + const extensionC = aLocalExtension('c'); + + return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Enabled) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Enabled)) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) + .then(() => { + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); + instantiationService.stubPromise(IDialogService, 'show', 1); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) + .then(() => { + assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); + assert.equal(testObject.local[1].enablementState, EnablementState.Enabled); + }); + }); + }); + test('test disable extension with dependencies disable all', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); @@ -872,6 +893,27 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); + test('test disable extension pack disable all', () => { + const extensionA = aLocalExtension('a', { extensionPack: ['pub.b'] }); + const extensionB = aLocalExtension('b'); + const extensionC = aLocalExtension('c'); + + return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Enabled) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Enabled)) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) + .then(() => { + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); + instantiationService.stubPromise(IDialogService, 'show', 0); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) + .then(() => { + assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); + assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); + }); + }); + }); + test('test disable extension fails if extension is a dependent of other', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); @@ -887,6 +929,24 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); + test('test disable extension when extension is part of a pack', () => { + const extensionA = aLocalExtension('a', { extensionPack: ['pub.b'] }); + const extensionB = aLocalExtension('b'); + const extensionC = aLocalExtension('c'); + + return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Enabled) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Enabled)) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) + .then(() => { + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + return testObject.setEnablement(testObject.local[1], EnablementState.Disabled) + .then(() => { + assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); + }); + }); + }); + test('test disable both dependency and dependent do not promot and do not fail', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); @@ -1076,27 +1136,6 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); - test('test enable extension with dependencies does not enable if cancelled', () => { - const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); - const extensionB = aLocalExtension('b'); - const extensionC = aLocalExtension('c'); - - return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Disabled) - .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Disabled)) - .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Disabled)) - .then(() => { - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 1); - testObject = instantiationService.createInstance(ExtensionsWorkbenchService); - - return testObject.setEnablement(testObject.local[0], EnablementState.Enabled) - .then(() => { - assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); - assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); - }); - }); - }); - test('test enable extension with dependencies does not prompt if dependency is enabled already', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index bdb52e71e26..92490a50246 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -279,19 +279,6 @@ export class ExplorerItem { return this.children.size; } - public getChildrenNames(): string[] { - if (!this.children) { - return []; - } - - const names: string[] = []; - this.children.forEach(child => { - names.push(child.name); - }); - - return names; - } - /** * Removes a child element from this folder. */ diff --git a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts index 013fab15004..f6991efd4c5 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts @@ -436,6 +436,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: EditorContextKeys.focus.toNegated(), primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_C, + win: { + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C) + }, id: COPY_RELATIVE_PATH_COMMAND_ID, handler: (accessor, resource: URI | object) => { const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService)); diff --git a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts index dfe8322c5ae..c152f150f4d 100644 --- a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts @@ -251,12 +251,12 @@ configurationRegistry.registerConfiguration({ nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "A dirty file is automatically saved when the window loses focus.") ], 'default': AutoSaveConfiguration.OFF, - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty files. Accepted values: '{0}', '{1}', '{2}' (editor loses focus), '{3}' (window loses focus). If set to '{4}', you can configure the delay in 'files.autoSaveDelay'.", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) + 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty files. Accepted values: '{0}', '{1}', '{2}' (editor loses focus), '{3}' (window loses focus). If set to '{4}', you can configure the delay in [`files.autoSaveDelay`](#files.autoSaveDelay). Read more about autosave [here](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save)", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) }, 'files.autoSaveDelay': { 'type': 'number', 'default': 1000, - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in ms after which a dirty file is saved automatically. Only applies when 'files.autoSave' is set to '{0}'", AutoSaveConfiguration.AFTER_DELAY) + 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in ms after which a dirty file is saved automatically. Only applies when [`files.autoSave`](#files.autoSave) is set to '{0}'", AutoSaveConfiguration.AFTER_DELAY) }, 'files.watcherExclude': { 'type': 'object', diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 09615b2bae6..a59212c44df 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -275,7 +275,11 @@ export class FileRenderer implements IRenderer { const extraClasses = ['explorer-item', 'explorer-item-edited']; const fileKind = stat.isRoot ? FileKind.ROOT_FOLDER : (stat.isDirectory || (stat instanceof NewStatPlaceholder && stat.isDirectoryPlaceholder())) ? FileKind.FOLDER : FileKind.FILE; const labelOptions: IFileLabelOptions = { hidePath: true, hideLabel: true, fileKind, extraClasses }; - label.setFile(stat.resource, labelOptions); + + const parent = stat.name ? resources.dirname(stat.resource) : stat.resource; + const value = stat.name || ''; + + label.setFile(parent.with({ path: paths.join(parent.path, value || ' ') }), labelOptions); // Use icon for ' ' if name is empty. // Input field for name const inputBox = new InputBox(label.element, this.contextViewService, { @@ -286,12 +290,10 @@ export class FileRenderer implements IRenderer { }); const styler = attachInputBoxStyler(inputBox, this.themeService); - const parent = resources.dirname(stat.resource); inputBox.onDidChange(value => { - label.setFile(parent.with({ path: paths.join(parent.path, value) }), labelOptions); // update label icon while typing! + label.setFile(parent.with({ path: paths.join(parent.path, value || ' ') }), labelOptions); // update label icon while typing! }); - const value = stat.name || ''; const lastDot = value.lastIndexOf('.'); inputBox.value = value; @@ -683,18 +685,21 @@ export class FileSorter implements ISorter { } // Explorer Filter +interface CachedParsedExpression { + original: glob.IExpression; + parsed: glob.ParsedExpression; +} + export class FileFilter implements IFilter { - private static readonly MAX_SIBLINGS_FILTER_THRESHOLD = 2000; - - private hiddenExpressionPerRoot: Map; + private hiddenExpressionPerRoot: Map; private workspaceFolderChangeListener: IDisposable; constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService, @IConfigurationService private configurationService: IConfigurationService ) { - this.hiddenExpressionPerRoot = new Map(); + this.hiddenExpressionPerRoot = new Map(); this.registerListeners(); } @@ -707,9 +712,16 @@ export class FileFilter implements IFilter { let needsRefresh = false; this.contextService.getWorkspace().folders.forEach(folder => { const configuration = this.configurationService.getValue({ resource: folder.uri }); - const excludesConfig = (configuration && configuration.files && configuration.files.exclude) || Object.create(null); - needsRefresh = needsRefresh || !objects.equals(this.hiddenExpressionPerRoot.get(folder.uri.toString()), excludesConfig); - this.hiddenExpressionPerRoot.set(folder.uri.toString(), objects.deepClone(excludesConfig)); // do not keep the config, as it gets mutated under our hoods + const excludesConfig: glob.IExpression = (configuration && configuration.files && configuration.files.exclude) || Object.create(null); + + if (!needsRefresh) { + const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString()); + needsRefresh = !cached || !objects.equals(cached.original, excludesConfig); + } + + const excludesConfigCopy = objects.deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods + + this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) } as CachedParsedExpression); }); return needsRefresh; @@ -724,18 +736,9 @@ export class FileFilter implements IFilter { return true; // always visible } - // Workaround for O(N^2) complexity (https://github.com/Microsoft/vscode/issues/9962) - let siblingsFn: () => string[]; - let siblingCount = stat.parent && stat.parent.getChildrenCount(); - if (siblingCount && siblingCount > FileFilter.MAX_SIBLINGS_FILTER_THRESHOLD) { - siblingsFn = () => void 0; - } else { - siblingsFn = () => stat.parent ? stat.parent.getChildrenNames() : void 0; - } - // Hide those that match Hidden Patterns - const expression = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()) || Object.create(null); - if (glob.match(expression, paths.normalize(path.relative(stat.root.resource.path, stat.resource.path), true), siblingsFn)) { + const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); + if (cached && cached.parsed(paths.normalize(path.relative(stat.root.resource.path, stat.resource.path), true), stat.name, name => !!stat.parent.getChild(name))) { return false; // hidden through pattern } diff --git a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts index e949d6f6fe6..52205ce5b1d 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts @@ -26,7 +26,7 @@ import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { IDelegate, IRenderer, IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer, IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { EditorLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -176,8 +176,7 @@ export class OpenEditorsView extends ViewletPanel { } protected renderHeaderTitle(container: HTMLElement): void { - const title = dom.append(container, $('.title')); - dom.append(title, $('span', null, this.title)); + super.renderHeaderTitle(container, this.title); const count = dom.append(container, $('.count')); this.dirtyCountElement = dom.append(count, $('.monaco-count-badge')); @@ -489,7 +488,7 @@ class OpenEditorActionRunner extends ActionRunner { } } -class OpenEditorsDelegate implements IDelegate { +class OpenEditorsDelegate implements IVirtualDelegate { public static readonly ITEM_HEIGHT = 22; @@ -595,6 +594,10 @@ class EditorGroupRenderer implements IRenderer { +class Delegate implements IVirtualDelegate { getHeight(element: IListEntry) { if (element.templateId === KEYBINDING_ENTRY_TEMPLATE_ID) { @@ -667,6 +667,9 @@ class KeybindingHeaderRenderer implements IRenderer { renderElement(entry: IListEntry, index: number, template: any): void { } + disposeElement(): void { + } + disposeTemplate(template: any): void { } } @@ -704,6 +707,8 @@ class KeybindingItemRenderer implements IRenderer .settings-body > .settings-tree-container .setting-item .setting-item-description { - opacity: 0.7; margin-top: 3px; overflow: hidden; - white-space: pre; text-overflow: ellipsis; height: 18px; } +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description * { + margin: 0px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description code { + line-height: 15px; /** For some reason, this is needed, otherwise will take up 20px height */ +} + .settings-editor > .settings-body > .settings-tree-container .setting-measure-container.monaco-tree-row { position: absolute; visibility: hidden; @@ -209,7 +216,6 @@ .settings-editor > .settings-body > .settings-tree-container .setting-item.is-expanded .setting-item-description, .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-measure-helper .setting-item-description { height: initial; - white-space: pre-wrap; } .settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-item-value-description { diff --git a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts index 9e11c49536a..e3c77593bb1 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts @@ -9,7 +9,7 @@ import * as arrays from 'vs/base/common/arrays'; import { Delayer, ThrottledDelayer } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import * as collections from 'vs/base/common/collections'; -import { Color } from 'vs/base/common/color'; +import { Color, RGBA } from 'vs/base/common/color'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -24,7 +24,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { editorBackground, foreground, listActiveSelectionBackground, listInactiveSelectionBackground } from 'vs/platform/theme/common/colorRegistry'; +import { editorBackground, focusBorder, foreground, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; @@ -33,13 +33,19 @@ import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbenc import { commonlyUsedData, tocData } from 'vs/workbench/parts/preferences/browser/settingsLayout'; import { ISettingsEditorViewState, NonExpandableTree, resolveExtensionsSettings, resolveSettingsTree, SearchResultIdx, SearchResultModel, SettingsAccessibilityProvider, SettingsDataSource, SettingsRenderer, SettingsTreeController, SettingsTreeElement, SettingsTreeFilter, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTree'; import { TOCDataSource, TOCRenderer, TOCTreeModel } from 'vs/workbench/parts/preferences/browser/tocTree'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_FIRST_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, IPreferencesSearchService, ISearchProvider, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_TOC_ROW_FOCUS } from 'vs/workbench/parts/preferences/common/preferences'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_FIRST_ROW_FOCUS, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, IPreferencesSearchService, ISearchProvider } from 'vs/workbench/parts/preferences/common/preferences'; import { IPreferencesService, ISearchResult, ISettingsEditorModel } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; const $ = DOM.$; +export const settingItemInactiveSelectionBorder = registerColor('settings.inactiveSelectedItemBorder', { + dark: '#3F3F46', + light: '#CCCEDB', + hc: null +}, localize('settingItemInactiveSelectionBorder', "The color of the selected setting row border, when the settings list does not have focus.")); + export class SettingsEditor2 extends BaseEditor { public static readonly ID: string = 'workbench.editor.settings2'; @@ -234,6 +240,16 @@ export class SettingsEditor2 extends BaseEditor { this._register(DOM.addDisposableListener(this.showConfiguredSettingsOnlyCheckbox, 'change', e => this.onShowConfiguredOnlyClicked())); } + private revealSetting(settingName: string): void { + const element = this.settingsTreeModel.getElementByName(settingName); + if (element) { + this.settingsTree.setSelection([element]); + this.settingsTree.setFocus(element); + this.settingsTree.reveal(element, 0); + this.settingsTree.domFocus(); + } + } + private openSettingsFile(): TPromise { const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget; @@ -314,6 +330,7 @@ export class SettingsEditor2 extends BaseEditor { const renderer = this.instantiationService.createInstance(SettingsRenderer, this.settingsTreeContainer); this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value))); this._register(renderer.onDidOpenSettings(() => this.openSettingsFile())); + this._register(renderer.onDidClickSettingLink(settingName => this.revealSetting(settingName))); const treeClass = 'settings-editor-tree'; this.settingsTree = this.instantiationService.createInstance(NonExpandableTree, this.settingsTreeContainer, @@ -333,15 +350,23 @@ export class SettingsEditor2 extends BaseEditor { }); this._register(registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { - const activeBorderColor = theme.getColor(listActiveSelectionBackground); + const activeBorderColor = theme.getColor(focusBorder); if (activeBorderColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree:focus .monaco-tree-row.focused {outline: solid 1px ${activeBorderColor}; outline-offset: -1px; }`); } - const inactiveBorderColor = theme.getColor(listInactiveSelectionBackground); + const inactiveBorderColor = theme.getColor(settingItemInactiveSelectionBorder); if (inactiveBorderColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree .monaco-tree-row.focused {outline: solid 1px ${inactiveBorderColor}; outline-offset: -1px; }`); } + + const foregroundColor = theme.getColor(foreground); + if (foregroundColor) { + // Links appear inside other elements in markdown. CSS opacity acts like a mask. So we have to dynamically compute the description color to avoid + // applying an opacity to the link color. + const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, .7)); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description { color: ${fgWithOpacity}; }`); + } })); this.settingsTree.getHTMLElement().classList.add(treeClass); @@ -693,7 +718,10 @@ export class SettingsEditor2 extends BaseEditor { // Count unique results const counts = {}; const filterResult = results[SearchResultIdx.Local]; - counts['filterResult'] = filterResult.filterMatches.length; + if (filterResult) { + counts['filterResult'] = filterResult.filterMatches.length; + } + if (nlpResult) { counts['nlpResult'] = nlpResult.filterMatches.length; } diff --git a/src/vs/workbench/parts/preferences/browser/settingsLayout.ts b/src/vs/workbench/parts/preferences/browser/settingsLayout.ts index caf7d55ff89..db7fd6c80ac 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsLayout.ts @@ -61,7 +61,7 @@ export const tocData: ITOCEntry = { { id: 'editor/suggestions', label: localize('suggestions', "Suggestions"), - settings: ['editor.*suggestion*'] + settings: ['editor.*suggest*'] }, { id: 'editor/files', diff --git a/src/vs/workbench/parts/preferences/browser/settingsTree.ts b/src/vs/workbench/parts/preferences/browser/settingsTree.ts index 95a40dc3071..8ba896f0253 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsTree.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { Button } from 'vs/base/browser/ui/button/button'; @@ -12,11 +13,12 @@ import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import * as arrays from 'vs/base/common/arrays'; import { Color } from 'vs/base/common/color'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; -import { escapeRegExpCharacters } from 'vs/base/common/strings'; +import { escapeRegExpCharacters, startsWith } from 'vs/base/common/strings'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAccessibilityProvider, IDataSource, IFilter, IRenderer, ITree } from 'vs/base/parts/tree/browser/tree'; @@ -24,7 +26,8 @@ import { localize } from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; -import { registerColor, selectBackground, selectBorder } from 'vs/platform/theme/common/colorRegistry'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { inputBackground, inputBorder, inputForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { SettingsTarget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; @@ -39,25 +42,46 @@ export const modifiedItemForeground = registerColor('settings.modifiedItemForegr hc: '#73C991' }, localize('modifiedItemForeground', "(For settings editor preview) The foreground color for a modified setting.")); +// Enum control colors +export const settingsSelectBackground = registerColor('settings.dropdownBackground', { dark: selectBackground, light: selectBackground, hc: selectBackground }, localize('settingsDropdownBackground', "Settings editor dropdown background.")); +export const settingsSelectForeground = registerColor('settings.dropdownForeground', { dark: selectForeground, light: selectForeground, hc: selectForeground }, localize('settingsDropdownForeground', "Settings editor dropdown foreground.")); +export const settingsSelectBorder = registerColor('settings.dropdownBorder', { dark: selectBorder, light: selectBorder, hc: selectBorder }, localize('settingsDropdownBorder', "Settings editor dropdown border.")); + +// Bool control colors +export const settingsCheckboxBackground = registerColor('settings.checkboxBackground', { dark: selectBackground, light: selectBackground, hc: selectBackground }, localize('settingsCheckboxBackground', "Settings editor checkbox background.")); +export const settingsCheckboxForeground = registerColor('settings.checkboxForeground', { dark: selectForeground, light: selectForeground, hc: selectForeground }, localize('settingsCheckboxForeground', "Settings editor checkbox foreground.")); +export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', { dark: selectBorder, light: selectBorder, hc: selectBorder }, localize('settingsCheckboxBorder', "Settings editor checkbox border.")); + +// Text control colors +export const settingsTextInputBackground = registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); +export const settingsTextInputForeground = registerColor('settings.textInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('textInputBoxForeground', "Settings editor text input box foreground.")); +export const settingsTextInputBorder = registerColor('settings.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "Settings editor text input box border.")); + +// Number control colors +export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); +export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('numberInputBoxForeground', "Settings editor number input box foreground.")); +export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('numberInputBoxBorder', "Settings editor number input box border.")); + registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const modifiedItemForegroundColor = theme.getColor(modifiedItemForeground); if (modifiedItemForegroundColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.is-configured .setting-item-is-configured-label { color: ${modifiedItemForegroundColor}; }`); } -}); -registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { - // TODO@roblou Hacks! Make checkbox background themeable - const selectBackgroundColor = theme.getColor(selectBackground); - if (selectBackgroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { background-color: ${selectBackgroundColor} !important; }`); + const checkboxBackgroundColor = theme.getColor(settingsCheckboxBackground); + if (checkboxBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { background-color: ${checkboxBackgroundColor} !important; }`); } - // TODO@roblou Hacks! Use proper inputbox theming instead of !important - const selectBorderColor = theme.getColor(selectBorder); - if (selectBorderColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { border-color: ${selectBorderColor} !important; }`); - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-control > .monaco-inputbox { border: solid 1px ${selectBorderColor} !important; }`); + const checkboxBorderColor = theme.getColor(settingsCheckboxBorder); + if (checkboxBorderColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { border-color: ${checkboxBorderColor} !important; }`); + } + + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description a { color: ${link}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description a > code { color: ${link}; }`); } }); @@ -76,7 +100,6 @@ export class SettingsTreeGroupElement extends SettingsTreeElement { export class SettingsTreeSettingElement extends SettingsTreeElement { setting: ISetting; - isExpanded: boolean; displayCategory: string; displayLabel: string; value: any; @@ -97,6 +120,7 @@ export interface ITOCEntry { export class SettingsTreeModel { private _root: SettingsTreeGroupElement; private _treeElementsById = new Map(); + private _treeElementsBySettingName = new Map(); constructor( private _viewState: ISettingsEditorViewState, @@ -125,6 +149,10 @@ export class SettingsTreeModel { return this._treeElementsById.get(id); } + getElementByName(name: string): SettingsTreeElement { + return this._treeElementsBySettingName.get(name); + } + private createSettingsTreeGroupElement(tocEntry: ITOCEntry, parent?: SettingsTreeGroupElement): SettingsTreeGroupElement { const element = new SettingsTreeGroupElement(); element.id = tocEntry.id; @@ -153,6 +181,7 @@ export class SettingsTreeModel { private createSettingsTreeSettingElement(setting: ISetting, parent: SettingsTreeGroupElement): SettingsTreeSettingElement { const element = createSettingsTreeSettingElement(setting, parent, this._viewState.settingsTarget, this._configurationService); this._treeElementsById.set(element.id, element); + this._treeElementsBySettingName.set(setting.key, element); return element; } } @@ -182,7 +211,6 @@ function createSettingsTreeSettingElement(setting: ISetting, parent: any, settin element.setting = setting; element.displayLabel = displayKeyFormat.label; element.displayCategory = displayKeyFormat.category; - element.isExpanded = false; element.value = displayValue; element.isConfigured = isConfigured; @@ -395,8 +423,6 @@ interface IDisposableTemplate { } interface ISettingItemTemplate extends IDisposableTemplate { - parent: HTMLElement; - containerElement: HTMLElement; categoryElement: HTMLElement; labelElement: HTMLElement; @@ -444,12 +470,16 @@ export class SettingsRenderer implements IRenderer { private readonly _onDidOpenSettings: Emitter = new Emitter(); public readonly onDidOpenSettings: Event = this._onDidOpenSettings.event; + private readonly _onDidClickSettingLink: Emitter = new Emitter(); + public readonly onDidClickSettingLink: Event = this._onDidClickSettingLink.event; + private measureContainer: HTMLElement; constructor( _measureContainer: HTMLElement, @IThemeService private themeService: IThemeService, - @IContextViewService private contextViewService: IContextViewService + @IContextViewService private contextViewService: IContextViewService, + @IOpenerService private readonly openerService: IOpenerService, ) { this.measureContainer = DOM.append(_measureContainer, $('.setting-measure-container.monaco-tree-row')); } @@ -556,7 +586,6 @@ export class SettingsRenderer implements IRenderer { const toDispose = []; const template: ISettingItemTemplate = { - parent: container, toDispose, containerElement: container, @@ -667,8 +696,8 @@ export class SettingsRenderer implements IRenderer { const isSelected = !!this.elementIsSelected(tree, element); const setting = element.setting; - DOM.toggleClass(template.parent, 'is-configured', element.isConfigured); - DOM.toggleClass(template.parent, 'is-expanded', isSelected); + DOM.toggleClass(template.containerElement, 'is-configured', element.isConfigured); + DOM.toggleClass(template.containerElement, 'is-expanded', isSelected); template.containerElement.id = element.id.replace(/\./g, '_'); const titleTooltip = setting.key; @@ -677,7 +706,31 @@ export class SettingsRenderer implements IRenderer { template.labelElement.textContent = element.displayLabel; template.labelElement.title = titleTooltip; - template.descriptionElement.textContent = element.description; + + const enumDescriptionText = element.setting.enumDescriptions ? + '\n' + element.setting.enumDescriptions + .map((desc, i) => ` - \`${element.setting.enum[i]}\`: ${desc}`) + .join('\n') : + ''; + const descriptionText = element.description + enumDescriptionText; + const renderedDescription = renderMarkdown({ value: descriptionText }, { + actionHandler: { + callback: (content: string) => { + if (startsWith(content, '#')) { + this._onDidClickSettingLink.fire(content.substr(1)); + } else { + this.openerService.open(URI.parse(content)).then(void 0, onUnexpectedError); + } + }, + disposeables: template.toDispose + } + }); + renderedDescription.classList.add('setting-item-description-markdown'); + template.descriptionElement.innerHTML = ''; + template.descriptionElement.appendChild(renderedDescription); + (renderedDescription.querySelectorAll('a')).forEach(aElement => { + aElement.tabIndex = isSelected ? 0 : -1; + }); this.renderValue(element, isSelected, template); @@ -710,17 +763,12 @@ export class SettingsRenderer implements IRenderer { valueControlElement.setAttribute('class', 'setting-item-control'); if (element.enum && (element.valueType === 'string' || !element.valueType)) { - valueControlElement.classList.add('setting-type-enum'); this.renderEnum(element, isSelected, template, valueControlElement, onChange); } else if (element.valueType === 'string') { - valueControlElement.classList.add('setting-type-string'); this.renderText(element, isSelected, template, valueControlElement, onChange); } else if (element.valueType === 'number' || element.valueType === 'integer') { - valueControlElement.classList.add('setting-type-number'); - const parseFn = element.valueType === 'integer' ? parseInt : parseFloat; - this.renderText(element, isSelected, template, valueControlElement, value => onChange(parseFn(value))); + this.renderNumber(element, isSelected, template, valueControlElement, onChange); } else { - valueControlElement.classList.add('setting-type-complex'); this.renderEditInSettingsJson(element, isSelected, template, valueControlElement); } } @@ -734,10 +782,15 @@ export class SettingsRenderer implements IRenderer { } private renderEnum(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingItemTemplate, element: HTMLElement, onChange: (value: string) => void): void { + element.classList.add('setting-type-enum'); const idx = dataElement.enum.indexOf(dataElement.value); const displayOptions = dataElement.enum.map(escapeInvisibleChars); const selectBox = new SelectBox(displayOptions, idx, this.contextViewService); - template.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService)); + template.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService, { + selectBackground: settingsSelectBackground, + selectForeground: settingsSelectForeground, + selectBorder: settingsSelectBorder + })); selectBox.render(element); if (element.firstElementChild) { element.firstElementChild.setAttribute('tabindex', isSelected ? '0' : '-1'); @@ -748,8 +801,13 @@ export class SettingsRenderer implements IRenderer { } private renderText(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingItemTemplate, element: HTMLElement, onChange: (value: string) => void): void { + element.classList.add('setting-type-string'); const inputBox = new InputBox(element, this.contextViewService); - template.toDispose.push(attachInputBoxStyler(inputBox, this.themeService)); + template.toDispose.push(attachInputBoxStyler(inputBox, this.themeService, { + inputBackground: settingsTextInputBackground, + inputForeground: settingsTextInputForeground, + inputBorder: settingsTextInputBorder + })); template.toDispose.push(inputBox); inputBox.value = dataElement.value; inputBox.inputElement.tabIndex = isSelected ? 0 : -1; @@ -758,7 +816,26 @@ export class SettingsRenderer implements IRenderer { inputBox.onDidChange(e => onChange(e))); } + private renderNumber(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingItemTemplate, element: HTMLElement, onChange: (value: number) => void): void { + element.classList.add('setting-type-number'); + const parseFn = dataElement.valueType === 'integer' ? parseInt : parseFloat; + + const inputBox = new InputBox(element, this.contextViewService); + template.toDispose.push(attachInputBoxStyler(inputBox, this.themeService, { + inputBackground: settingsNumberInputBackground, + inputForeground: settingsNumberInputForeground, + inputBorder: settingsNumberInputBorder + })); + template.toDispose.push(inputBox); + inputBox.value = dataElement.value; + inputBox.inputElement.tabIndex = isSelected ? 0 : -1; + + template.toDispose.push( + inputBox.onDidChange(value => onChange(parseFn(value)))); + } + private renderEditInSettingsJson(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingItemTemplate, element: HTMLElement): void { + element.classList.add('setting-type-complex'); const openSettingsButton = new Button(element, { title: true, buttonBackground: null, buttonHoverBackground: null }); openSettingsButton.onDidClick(() => this._onDidOpenSettings.fire()); openSettingsButton.label = localize('editInSettingsJson', "Edit in settings.json"); @@ -842,6 +919,21 @@ export class SettingsTreeController extends WorkbenchTreeController { ) { super({}, configurationService); } + + protected onLeftClick(tree: ITree, element: any, eventish: IMouseEvent, origin?: string): boolean { + const isLink = eventish.target.tagName.toLowerCase() === 'a' || + eventish.target.parentElement.tagName.toLowerCase() === 'a'; // inside + + if (isLink && DOM.findParentWithClass(eventish.target, 'setting-item-description-markdown', tree.getHTMLElement())) { + return true; + } + + // Without this, clicking on the setting description causes the tree to lose focus. I don't know why. + // The superclass does not always call it because of DND which is not used here. + eventish.preventDefault(); + + return super.onLeftClick(tree, element, eventish, origin); + } } export class SettingsAccessibilityProvider implements IAccessibilityProvider { diff --git a/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css b/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css index e55cb63c612..de4d95ed717 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css @@ -19,6 +19,7 @@ border-top: 4px solid transparent; border-bottom: 4px solid transparent; transition: border-top-width 80ms linear, border-bottom-width 80ms linear, bottom 80ms linear; + pointer-events: none; } .monaco-editor .dirty-diff-glyph:before { diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index f6e3ec0c18a..8638431c24b 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -51,6 +51,7 @@ display: none; } +.scm-viewlet .scm-provider > .type, .scm-viewlet .scm-provider > .name > .type { opacity: 0.7; margin-left: 0.5em; diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index b369614adb0..6d60c47a9f4 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -17,7 +17,7 @@ import { PanelViewlet, ViewletPanel, IViewletPanelOptions } from 'vs/workbench/b import { append, $, addClass, toggleClass, trackFocus, Dimension, addDisposableListener } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; import { VIEWLET_ID, VIEW_CONTAINER } from 'vs/workbench/parts/scm/common/scm'; import { FileLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -75,7 +75,7 @@ export interface IViewModel { hide(repository: ISCMRepository): void; } -class ProvidersListDelegate implements IDelegate { +class ProvidersListDelegate implements IVirtualDelegate { getHeight(element: ISCMRepository): number { return 22; @@ -190,6 +190,10 @@ class ProviderRenderer implements IRenderer { template.elementDisposable = combinedDisposable(disposables); } + disposeElement(): void { + // noop + } + disposeTemplate(template: ResourceTemplate): void { template.elementDisposable.dispose(); template.dispose(); } } -class ProviderListDelegate implements IDelegate { +class ProviderListDelegate implements IVirtualDelegate { getHeight() { return 22; } @@ -765,19 +777,20 @@ export class RepositoryPanel extends ViewletPanel { } protected renderHeaderTitle(container: HTMLElement): void { - const header = append(container, $('.title.scm-provider')); - const name = append(header, $('.name')); - const title = append(name, $('span.title')); - const type = append(name, $('span.type')); + let title: string; + let type: string; if (this.repository.provider.rootUri) { - title.textContent = basename(this.repository.provider.rootUri.fsPath); - type.textContent = this.repository.provider.label; + title = basename(this.repository.provider.rootUri.fsPath); + type = this.repository.provider.label; } else { - title.textContent = this.repository.provider.label; - type.textContent = ''; + title = this.repository.provider.label; + type = ''; } + super.renderHeaderTitle(container, title); + addClass(container, 'scm-provider'); + append(container, $('span.type', null, type)); const onContextMenu = mapEvent(stop(domEvent(container, 'contextmenu')), e => new StandardMouseEvent(e)); onContextMenu(this.onContextMenu, this, this.disposables); } diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index bd2aa0463a9..50925449d9a 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -84,15 +84,6 @@ export const toggleRegexCommand = (accessor: ServicesAccessor) => { searchView.toggleRegex(); }; -export const FocusActiveEditorCommand = (accessor: ServicesAccessor) => { - const editorService = accessor.get(IEditorService); - const activeControl = editorService.activeControl; - if (activeControl) { - activeControl.focus(); - } - return TPromise.as(true); -}; - export class FocusNextInputAction extends Action { public static readonly ID = 'search.focus.nextInputBox'; diff --git a/src/vs/workbench/parts/search/browser/searchResultsView.ts b/src/vs/workbench/parts/search/browser/searchResultsView.ts index d30adb28b21..dd4b0933013 100644 --- a/src/vs/workbench/parts/search/browser/searchResultsView.ts +++ b/src/vs/workbench/parts/search/browser/searchResultsView.ts @@ -237,7 +237,8 @@ export class SearchRenderer extends Disposable implements IRenderer { private renderFolderMatch(tree: ITree, folderMatch: FolderMatch, templateData: IFolderMatchTemplate): void { if (folderMatch.hasRoot()) { - const fileKind = resources.isEqual(this.contextService.getWorkspaceFolder(folderMatch.resource()).uri, folderMatch.resource()) ? + const workspaceFolder = this.contextService.getWorkspaceFolder(folderMatch.resource()); + const fileKind = workspaceFolder && resources.isEqual(workspaceFolder.uri, folderMatch.resource()) ? FileKind.ROOT_FOLDER : FileKind.FOLDER; diff --git a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts index cfef04d170f..bff2ceb88c9 100644 --- a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts +++ b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts @@ -531,7 +531,7 @@ configurationRegistry.registerConfiguration({ properties: { 'search.exclude': { type: 'object', - description: nls.localize('exclude', "Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the files.exclude setting."), + description: nls.localize('exclude', "Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the [`files.exclude`](#files-exclude) setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."), default: { '**/node_modules': true, '**/bower_components': true }, additionalProperties: { anyOf: [ diff --git a/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts b/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts index ae5c397ea09..e624664bbe6 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts @@ -21,6 +21,8 @@ export class Snippet { private _codeSnippet: string; private _isBogous: boolean; + readonly prefixLow: string; + constructor( readonly scopes: string[], readonly name: string, @@ -31,6 +33,7 @@ export class Snippet { readonly isFromExtension?: boolean, ) { // + this.prefixLow = prefix.toLowerCase(); } get codeSnippet(): string { diff --git a/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts b/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts index eecdc73b271..047e0c2ae96 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts @@ -4,33 +4,33 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { localize } from 'vs/nls'; -import { ITextModel } from 'vs/editor/common/model'; -import { ISuggestSupport, ISuggestResult, ISuggestion, LanguageId, SuggestionType, SnippetType, SuggestContext } from 'vs/editor/common/modes'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/suggest'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { Position } from 'vs/editor/common/core/position'; -import { overlap, compare, startsWith, isFalsyOrWhitespace, endsWith } from 'vs/base/common/strings'; -import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { join, basename, extname } from 'path'; -import * as resources from 'vs/base/common/resources'; -import { mkdirp, readdir, exists } from 'vs/base/node/pfs'; -import { watch } from 'vs/base/node/extfs'; -import { SnippetFile, Snippet } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile'; -import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; +import { basename, extname, join } from 'path'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { values } from 'vs/base/common/map'; +import * as resources from 'vs/base/common/resources'; +import { compare, endsWith, isFalsyOrWhitespace } from 'vs/base/common/strings'; +import URI from 'vs/base/common/uri'; +import { watch } from 'vs/base/node/extfs'; +import { exists, mkdirp, readdir } from 'vs/base/node/pfs'; +import { Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import { ISuggestion, ISuggestResult, ISuggestSupport, LanguageId, SnippetType, SuggestContext, SuggestionType } from 'vs/editor/common/modes'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; +import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/suggest'; +import { localize } from 'vs/nls'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { values } from 'vs/base/common/map'; -import URI from 'vs/base/common/uri'; -import { IFileService } from 'vs/platform/files/common/files'; +import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution'; +import { Snippet, SnippetFile } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; namespace schema { @@ -326,35 +326,46 @@ export class SnippetSuggestProvider implements ISuggestSupport { const languageId = this._getLanguageIdAtPosition(model, position); return this._snippets.getSnippets(languageId).then(snippets => { - const suggestions: SnippetSuggestion[] = []; + let suggestions: SnippetSuggestion[]; + let pos = { lineNumber: position.lineNumber, column: Math.max(1, position.column - 100) }; + let lineOffsets: number[] = []; + let linePrefixLow = model.getLineContent(position.lineNumber).substr(Math.max(0, position.column - 100), position.column - 1).toLowerCase(); - const lowWordUntil = model.getWordUntilPosition(position).word.toLowerCase(); - const lowLineUntil = model.getLineContent(position.lineNumber).substr(Math.max(0, position.column - 100), position.column - 1).toLowerCase(); + while (pos.column < position.column) { + let word = model.getWordAtPosition(pos); + if (word) { + // at a word + lineOffsets.push(word.startColumn - 1); + pos.column = word.endColumn + 1; - for (const snippet of snippets) { + if (word.endColumn - 1 < linePrefixLow.length && !/\s/.test(linePrefixLow[word.endColumn - 1])) { + lineOffsets.push(word.endColumn - 1); + } - const lowPrefix = snippet.prefix.toLowerCase(); - let overwriteBefore = 0; - let accetSnippet = true; - - if (typeof context.triggerCharacter === 'string') { - // cheap match on the trigger-character - overwriteBefore = context.triggerCharacter.length; - accetSnippet = startsWith(lowPrefix, context.triggerCharacter.toLowerCase()); - - } else if (lowWordUntil.length > 0 && startsWith(lowPrefix, lowWordUntil)) { - // cheap match on the (none-empty) current word - overwriteBefore = lowWordUntil.length; - accetSnippet = true; - - } else if (lowLineUntil.length > 0 && lowLineUntil.match(/[^\s]$/)) { - // compute overlap between snippet and (none-empty) line on text - overwriteBefore = overlap(lowLineUntil, snippet.prefix.toLowerCase()); - accetSnippet = overwriteBefore > 0 && !model.getWordAtPosition(new Position(position.lineNumber, position.column - overwriteBefore)); + } else if (!/\s/.test(linePrefixLow[pos.column - 1])) { + // at a none-whitespace character + lineOffsets.push(pos.column - 1); + pos.column += 1; + } else { + // always advance! + pos.column += 1; } + } - if (accetSnippet) { - suggestions.push(new SnippetSuggestion(snippet, overwriteBefore)); + if (lineOffsets.length === 0) { + // no interesting spans found -> pick all snippets + suggestions = snippets.map(snippet => new SnippetSuggestion(snippet, 0)); + + } else { + let consumed = new Set(); + suggestions = []; + for (const start of lineOffsets) { + for (const snippet of snippets) { + if (!consumed.has(snippet) && matches(linePrefixLow, start, snippet.prefixLow, 0)) { + suggestions.push(new SnippetSuggestion(snippet, linePrefixLow.length - start)); + consumed.add(snippet); + } + } } } @@ -394,6 +405,16 @@ export class SnippetSuggestProvider implements ISuggestSupport { } } +function matches(pattern: string, patternStart: number, word: string, wordStart: number): boolean { + while (patternStart < pattern.length && wordStart < word.length) { + if (pattern[patternStart] === word[wordStart]) { + patternStart += 1; + } + wordStart += 1; + } + return patternStart === pattern.length; +} + export function getNonWhitespacePrefix(model: ISimpleModel, position: Position): string { /** * Do not analyze more characters diff --git a/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts b/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts index 6c469038ba7..70f0ab60b99 100644 --- a/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts +++ b/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts @@ -83,10 +83,63 @@ suite('SnippetsService', function () { assert.equal(result.incomplete, undefined); assert.equal(result.suggestions.length, 1); assert.equal(result.suggestions[0].label, 'bar'); + assert.equal(result.suggestions[0].overwriteBefore, 3); assert.equal(result.suggestions[0].insertText, 'barCodeSnippet'); }); }); + test('snippet completions - with different prefixes', async function () { + + snippetService = new SimpleSnippetService([new Snippet( + ['fooLang'], + 'barTest', + 'bar', + '', + 's1', + '' + ), new Snippet( + ['fooLang'], + 'name', + 'bar-bar', + '', + 's2', + '' + )]); + + const provider = new SnippetSuggestProvider(modeService, snippetService); + const model = TextModel.createFromString('bar-bar', undefined, modeService.getLanguageIdentifier('fooLang')); + + await provider.provideCompletionItems(model, new Position(1, 3), suggestContext).then(result => { + assert.equal(result.incomplete, undefined); + assert.equal(result.suggestions.length, 2); + assert.equal(result.suggestions[0].label, 'bar'); + assert.equal(result.suggestions[0].insertText, 's1'); + assert.equal(result.suggestions[0].overwriteBefore, 2); + assert.equal(result.suggestions[1].label, 'bar-bar'); + assert.equal(result.suggestions[1].insertText, 's2'); + assert.equal(result.suggestions[1].overwriteBefore, 2); + }); + + await provider.provideCompletionItems(model, new Position(1, 5), suggestContext).then(result => { + assert.equal(result.incomplete, undefined); + assert.equal(result.suggestions.length, 1); + assert.equal(result.suggestions[0].label, 'bar-bar'); + assert.equal(result.suggestions[0].insertText, 's2'); + assert.equal(result.suggestions[0].overwriteBefore, 4); + }); + + await provider.provideCompletionItems(model, new Position(1, 6), suggestContext).then(result => { + assert.equal(result.incomplete, undefined); + assert.equal(result.suggestions.length, 2); + assert.equal(result.suggestions[0].label, 'bar'); + assert.equal(result.suggestions[0].insertText, 's1'); + assert.equal(result.suggestions[0].overwriteBefore, 1); + assert.equal(result.suggestions[1].label, 'bar-bar'); + assert.equal(result.suggestions[1].insertText, 's2'); + assert.equal(result.suggestions[1].overwriteBefore, 5); + }); + }); + test('Cannot use " { assert.equal(result.suggestions.length, 1); + assert.equal(result.suggestions[0].overwriteBefore, 2); model.dispose(); model = TextModel.createFromString('a { - - assert.equal(result.suggestions.length, 0); + assert.equal(result.suggestions.length, 1); + assert.equal(result.suggestions[0].overwriteBefore, 2); model.dispose(); }); }); @@ -170,4 +224,27 @@ suite('SnippetsService', function () { assert.equal(second.label, 'second'); }); }); + + test('Dash in snippets prefix broken #53945', async function () { + snippetService = new SimpleSnippetService([new Snippet( + ['fooLang'], + 'p-a', + 'p-a', + '', + 'second', + '' + )]); + const provider = new SnippetSuggestProvider(modeService, snippetService); + + let model = TextModel.createFromString('p-', undefined, modeService.getLanguageIdentifier('fooLang')); + + let result = await provider.provideCompletionItems(model, new Position(1, 2), suggestContext); + assert.equal(result.suggestions.length, 1); + + result = await provider.provideCompletionItems(model, new Position(1, 3), suggestContext); + assert.equal(result.suggestions.length, 1); + + result = await provider.provideCompletionItems(model, new Position(1, 3), { triggerCharacter: '-', triggerKind: SuggestTriggerKind.TriggerCharacter }); + assert.equal(result.suggestions.length, 1); + }); }); diff --git a/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts b/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts index 8169d1f3c8c..8cec848aedb 100644 --- a/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts +++ b/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts @@ -12,7 +12,7 @@ import * as Strings from 'vs/base/common/strings'; import * as Collections from 'vs/base/common/collections'; import { CommandOptions, Source, ErrorData } from 'vs/base/common/processes'; -import { LineProcess } from 'vs/base/node/processes'; +import { LineProcess, LineData } from 'vs/base/node/processes'; import { IFileService } from 'vs/platform/files/common/files'; @@ -269,7 +269,20 @@ export class ProcessRunnerDetector { private runDetection(process: LineProcess, command: string, isShellCommand: boolean, matcher: TaskDetectorMatcher, problemMatchers: string[], list: boolean): TPromise { let tasks: string[] = []; matcher.init(); - return process.start().then((success) => { + + const onProgress = (progress: LineData) => { + if (progress.source === Source.stderr) { + this._stderr.push(progress.line); + return; + } + let line = Strings.removeAnsiEscapeCodes(progress.line); + let matches = matcher.match(tasks, line); + if (matches && matches.length > 0) { + tasks.push(matches[1]); + } + }; + + return process.start(onProgress).then((success) => { if (tasks.length === 0) { if (success.cmdCode !== 0) { if (command === 'gulp') { @@ -305,16 +318,6 @@ export class ProcessRunnerDetector { this._stderr.push(nls.localize('TaskSystemDetector.noProgram', 'Program {0} was not found. Message is {1}', command, error.message)); } return { config: null, stdout: this._stdout, stderr: this._stderr }; - }, (progress) => { - if (progress.source === Source.stderr) { - this._stderr.push(progress.line); - return; - } - let line = Strings.removeAnsiEscapeCodes(progress.line); - let matches = matcher.match(tasks, line); - if (matches && matches.length > 0) { - tasks.push(matches[1]); - } }); } diff --git a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts index 8ec0759e9e1..810cfd8615b 100644 --- a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts @@ -249,7 +249,21 @@ export class ProcessTaskSystem implements ITaskSystem { this.activeTask = task; const inactiveEvent = TaskEvent.create(TaskEventKind.Inactive, task); let processStartedSignaled: boolean = false; - const startPromise = this.childProcess.start(); + const onProgress = (progress: LineData) => { + let line = Strings.removeAnsiEscapeCodes(progress.line); + this.outputChannel.append(line + '\n'); + watchingProblemMatcher.processLine(line); + if (delayer === null) { + delayer = new Async.Delayer(3000); + } + delayer.trigger(() => { + watchingProblemMatcher.forceDelivery(); + return null; + }).then(() => { + delayer = null; + }); + }; + const startPromise = this.childProcess.start(onProgress); this.childProcess.pid.then(pid => { if (pid !== -1) { processStartedSignaled = true; @@ -287,19 +301,6 @@ export class ProcessTaskSystem implements ITaskSystem { } eventCounter = 0; return this.handleError(task, error); - }, (progress: LineData) => { - let line = Strings.removeAnsiEscapeCodes(progress.line); - this.outputChannel.append(line + '\n'); - watchingProblemMatcher.processLine(line); - if (delayer === null) { - delayer = new Async.Delayer(3000); - } - delayer.trigger(() => { - watchingProblemMatcher.forceDelivery(); - return null; - }).then(() => { - delayer = null; - }); }); let result: ITaskExecuteResult = (task).tscWatch ? { kind: TaskExecuteKind.Started, started: { restartOnFileChanges: '**/*.ts' }, promise: this.activeTaskPromise } @@ -312,7 +313,12 @@ export class ProcessTaskSystem implements ITaskSystem { this.activeTask = task; const inactiveEvent = TaskEvent.create(TaskEventKind.Inactive, task); let processStartedSignaled: boolean = false; - const startPromise = this.childProcess.start(); + const onProgress = (progress) => { + let line = Strings.removeAnsiEscapeCodes(progress.line); + this.outputChannel.append(line + '\n'); + startStopProblemMatcher.processLine(line); + }; + const startPromise = this.childProcess.start(onProgress); this.childProcess.pid.then(pid => { if (pid !== -1) { processStartedSignaled = true; @@ -340,10 +346,6 @@ export class ProcessTaskSystem implements ITaskSystem { this._onDidStateChange.fire(inactiveEvent); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task)); return this.handleError(task, error); - }, (progress) => { - let line = Strings.removeAnsiEscapeCodes(progress.line); - this.outputChannel.append(line + '\n'); - startStopProblemMatcher.processLine(line); }); return { kind: TaskExecuteKind.Started, started: {}, promise: this.activeTaskPromise }; } diff --git a/src/vs/workbench/parts/tasks/node/tasks.ts b/src/vs/workbench/parts/tasks/node/tasks.ts index c0cd15ad06d..a24e94c013a 100644 --- a/src/vs/workbench/parts/tasks/node/tasks.ts +++ b/src/vs/workbench/parts/tasks/node/tasks.ts @@ -27,7 +27,10 @@ namespace TaskDefinition { export function createTaskIdentifier(external: TaskIdentifier, reporter: { error(message: string): void; }): KeyedTaskIdentifier | undefined { let definition = TaskDefinitionRegistry.get(external.type); if (definition === void 0) { - return undefined; + // We have no task definition so we can't sanitize the literal. Take it as is + let copy = Objects.deepClone(external); + delete copy._key; + return KeyedTaskIdentifier.create(copy); } let literal: { type: string;[name: string]: any } = Object.create(null); diff --git a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts index e9860c97d85..55cc22df61d 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts @@ -24,7 +24,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { Schemas } from 'vs/base/common/network'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { getInstalledExtensions, IExtensionStatus, onExtensionChanged, isKeymapExtension } from 'vs/workbench/parts/extensions/electron-browser/extensionsUtils'; -import { IExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, EnablementState, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; import { used } from 'vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page'; import { ILifecycleService, StartupKind } from 'vs/platform/lifecycle/common/lifecycle'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -39,6 +39,7 @@ import { IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor'; import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { TimeoutTimer } from 'vs/base/common/async'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; used(); @@ -420,7 +421,9 @@ class WelcomePage { return null; } return this.extensionManagementService.installFromGallery(extension) - .then(local => { + .then(() => this.extensionManagementService.getInstalled(LocalExtensionType.User)) + .then(installed => { + const local = installed.filter(i => areSameExtensions(extension.identifier, i.galleryIdentifier))[0]; // TODO: Do this as part of the install to avoid multiple events. return this.extensionEnablementService.setEnablement(local, EnablementState.Disabled).then(() => local); }); diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 146bafc26a3..722deba856e 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -14,7 +14,7 @@ import { LinkedList } from 'vs/base/common/linkedList'; import { createStyleSheet, createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { IdGenerator } from 'vs/base/common/idGenerator'; -import { IIterator } from 'vs/base/common/iterator'; +import { Iterator } from 'vs/base/common/iterator'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { isPromiseCanceledError } from 'vs/base/common/errors'; @@ -174,7 +174,7 @@ class DecorationStyles { }); } - cleanUp(iter: IIterator): void { + cleanUp(iter: Iterator): void { // remove every rule for which no more // decoration (data) is kept. this isn't cheap let usedDecorations = new Set(); diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts index 610c5ef1946..c2c1ee52f58 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts @@ -13,6 +13,7 @@ import { isLinux, isWindows } from 'vs/base/common/platform'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions } from 'vs/platform/dialogs/common/dialogs'; +import { ILogService } from 'vs/platform/log/common/log'; interface IMassagedMessageBoxOptions { @@ -34,10 +35,13 @@ export class DialogService implements IDialogService { _serviceBrand: any; constructor( - @IWindowService private windowService: IWindowService + @IWindowService private windowService: IWindowService, + @ILogService private logService: ILogService ) { } confirm(confirmation: IConfirmation): TPromise { + this.logService.trace('DialogService#confirm', confirmation.message); + const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation)); return this.windowService.showMessageBox(options).then(result => { @@ -86,6 +90,8 @@ export class DialogService implements IDialogService { } show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): TPromise { + this.logService.trace('DialogService#show', message); + const { options, buttonIndexMap } = this.massageMessageBoxOptions({ message, buttons, diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index c21f0dd7858..6b3a2b4c7d5 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -288,6 +288,15 @@ const schema: IJSONSchema = { pattern: EXTENSION_IDENTIFIER_PATTERN } }, + extensionPack: { + description: nls.localize('vscode.extension.contributes.extensionPack', "A set of extensions that can be installed together. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."), + type: 'array', + uniqueItems: true, + items: { + type: 'string', + pattern: EXTENSION_IDENTIFIER_PATTERN + } + }, scripts: { type: 'object', properties: { diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index 90cfe42920d..802d0be085c 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -615,4 +615,4 @@ export class ExtensionScanner { return []; }); } -} +} \ No newline at end of file diff --git a/src/vs/workbench/services/group/common/editorGroupsService.ts b/src/vs/workbench/services/group/common/editorGroupsService.ts index e744ba63b0f..6bc438e07a4 100644 --- a/src/vs/workbench/services/group/common/editorGroupsService.ts +++ b/src/vs/workbench/services/group/common/editorGroupsService.ts @@ -312,13 +312,6 @@ export interface IGroupChangeEvent { editorIndex?: number; } -export interface IEditorBreadcrumbs { - focus(): void; - focusNext(): void; - focusPrev(): void; - select(): void; -} - export interface IEditorGroup { /** @@ -339,11 +332,6 @@ export interface IEditorGroup { */ readonly label: string; - /** - * - */ - readonly breadcrumbs: IEditorBreadcrumbs; - /** * The active control is the currently visible control of the group. */ diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 8c649215708..93be98d9f2b 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -48,6 +48,7 @@ export interface ISetting { // TODO@roblou maybe need new type and new EditorModel for GUI editor instead of ISetting which is used for text settings editor type?: string | string[]; enum?: string[]; + enumDescriptions?: string[]; } export interface IExtensionSetting extends ISetting { diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 4f3ca88dfe0..304a0794663 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -473,7 +473,8 @@ export class DefaultSettings extends Disposable { valueRange: null, overrides: [], type: setting.type, - enum: setting.enum + enum: setting.enum, + enumDescriptions: setting.enumDescriptions }; } return null; @@ -547,7 +548,7 @@ export class DefaultSettings extends Disposable { const value = prop.default; const description = (prop.description || '').split('\n'); const overrides = OVERRIDE_PROPERTY_PATTERN.test(key) ? this.parseOverrideSettings(prop.default) : []; - result.push({ key, value, description, range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides, type: prop.type, enum: prop.enum }); + result.push({ key, value, description, range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides, type: prop.type, enum: prop.enum, enumDescriptions: prop.enumDescriptions }); } } return result; diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 1d6dc527cae..b1683441fd7 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -206,7 +206,6 @@ export class FileWalker { const useRipgrep = this.useRipgrep; let noSiblingsClauses: boolean; - let filePatternSeen = false; if (useRipgrep) { const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder).expression); cmd = ripgrep.cmd; @@ -265,9 +264,6 @@ export class FileWalker { if (useRipgrep && noSiblingsClauses) { for (const relativePath of relativeFiles) { - if (relativePath === this.filePattern) { - filePatternSeen = true; - } const basename = path.basename(relativePath); this.matchFile(onResult, { base: rootFolder, relativePath, basename }); if (this.isLimitHit) { @@ -276,22 +272,9 @@ export class FileWalker { } } if (last || this.isLimitHit) { - if (!filePatternSeen) { - this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => { - if (match) { - this.resultCount++; - onResult({ - base: folderQuery.folder, - relativePath: this.filePattern, - basename: path.basename(this.filePattern), - }); - } - done(); - }); - } else { - done(); - } + done(); } + return; } @@ -479,6 +462,7 @@ export class FileWalker { const filePattern = this.filePattern; function matchDirectory(entries: IDirectoryEntry[]) { self.directoriesWalked++; + const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename)); for (let i = 0, n = entries.length; i < n; i++) { const entry = entries[i]; const { relativePath, basename } = entry; @@ -487,7 +471,7 @@ export class FileWalker { // If the user searches for the exact file name, we adjust the glob matching // to ignore filtering by siblings because the user seems to know what she // is searching for and we want to include the result in that case anyway - if (excludePattern.test(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) { + if (excludePattern.test(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { continue; } @@ -518,25 +502,11 @@ export class FileWalker { return done(); } - // Support relative paths to files from a root resource (ignores excludes) - return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => { - if (this.isCanceled || this.isLimitHit) { - return done(); - } + if (this.isCanceled || this.isLimitHit) { + return done(); + } - // Report result from file pattern if matching - if (match) { - this.resultCount++; - onResult({ - base: folderQuery.folder, - relativePath: this.filePattern, - basename: path.basename(this.filePattern), - size - }); - } - - return this.doWalk(folderQuery, '', files, onResult, done); - }); + return this.doWalk(folderQuery, '', files, onResult, done); }); } @@ -556,22 +526,11 @@ export class FileWalker { }; } - private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void { - if (!this.filePattern || path.isAbsolute(this.filePattern)) { - return clb(null); - } - - const absolutePath = path.join(basePath, this.filePattern); - - return fs.stat(absolutePath, (error, stat) => { - return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files - }); - } - private doWalk(folderQuery: IFolderSearch, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void { const rootFolder = folderQuery.folder; // Execute tasks on each file in parallel to optimize throughput + const hasSibling = glob.hasSiblingFn(() => files); flow.parallel(files, (file: string, clb: (error: Error, result: {}) => void): void => { // Check canceled @@ -579,17 +538,12 @@ export class FileWalker { return clb(null, undefined); } + // Check exclude pattern // If the user searches for the exact file name, we adjust the glob matching // to ignore filtering by siblings because the user seems to know what she // is searching for and we want to include the result in that case anyway - let siblings = files; - if (this.config.filePattern === file) { - siblings = []; - } - - // Check exclude pattern let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file; - if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, () => siblings)) { + if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, this.config.filePattern !== file ? hasSibling : undefined)) { return clb(null, undefined); } @@ -776,9 +730,9 @@ class AbsoluteAndRelativeParsedExpression { this.relativeParsedExpr = relativeGlobExpr && glob.parse(relativeGlobExpr, { trimForExclusions: true }); } - public test(_path: string, basename?: string, siblingsFn?: () => string[] | TPromise): string | TPromise { - return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, siblingsFn)) || - (this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, siblingsFn)); + public test(_path: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise): string | TPromise { + return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, hasSibling)) || + (this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, hasSibling)); } public getBasenameTerms(): string[] { diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts index 81898c0880d..c9e4a7f498c 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts @@ -79,7 +79,7 @@ export class RipgrepEngine { this.ripgrepParser = new RipgrepParser(this.config.maxResults, cwd, this.config.extraFiles); this.ripgrepParser.on('result', (match: ISerializedFileMatch) => { if (this.postProcessExclusions) { - const handleResultP = (>this.postProcessExclusions(match.path, undefined, () => getSiblings(match.path))) + const handleResultP = (>this.postProcessExclusions(match.path, undefined, glob.hasSiblingPromiseFn(() => getSiblings(match.path)))) .then(globMatch => { if (!globMatch) { onResult(match); diff --git a/src/vs/workbench/services/search/test/node/search.test.ts b/src/vs/workbench/services/search/test/node/search.test.ts index 1d19c3d0c76..fda8eed27b0 100644 --- a/src/vs/workbench/services/search/test/node/search.test.ts +++ b/src/vs/workbench/services/search/test/node/search.test.ts @@ -600,29 +600,6 @@ suite('FileSearchEngine', () => { }); }); - test('Files: relative path to file ignores excludes', function (done: () => void) { - this.timeout(testTimeout); - let engine = new FileSearchEngine({ - folderQueries: ROOT_FOLDER_QUERY, - filePattern: path.normalize(path.join('examples', 'company.js')), - excludePattern: { '**/*.js': true } - }); - - let count = 0; - let res: IRawFileMatch; - engine.search((result) => { - if (result) { - count++; - } - res = result; - }, () => { }, (error) => { - assert.ok(!error); - assert.equal(count, 1); - assert.equal(path.basename(res.relativePath), 'company.js'); - done(); - }); - }); - test('Files: Include pattern, single files', function (done: () => void) { this.timeout(testTimeout); let engine = new FileSearchEngine({ diff --git a/src/vs/workbench/test/browser/parts/editor/editorBreadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts similarity index 97% rename from src/vs/workbench/test/browser/parts/editor/editorBreadcrumbModel.test.ts rename to src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts index c0942cd5dc4..6e4f476f5e6 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorBreadcrumbModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/editorBreadcrumbsModel'; +import { EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; import { TestContextService } from 'vs/workbench/test/workbenchTestServices'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts index 49d33da63b2..bad1d62b9d7 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -606,37 +606,6 @@ suite('ExtHostSearch', () => { assert.equal(cancels, 2, 'Expected all invocations to be canceled when hitting limit'); }); - test('respects filePattern', async () => { - const reportedResults = [ - joinPath(rootFolderA, 'file1.ts'), - joinPath(rootFolderA, 'file2.ts'), - joinPath(rootFolderA, 'file3.ts'), - ]; - - await registerTestSearchProvider({ - provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(r)); - return TPromise.wrap(null); - } - }); - - const query: ISearchQuery = { - type: QueryType.File, - - filePattern: 'file3', - - folderQueries: [ - { - folder: rootFolderA - } - ] - }; - - const { results } = await runFileSearch(query); - assert.equal(results.length, 1); - compareURIs(results, reportedResults.slice(2)); - }); - test('works with non-file schemes', async () => { const reportedResults = [ joinPath(fancySchemeFolderA, 'file1.ts'), diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 35a157ea0c1..46060e5bc2d 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -67,7 +67,7 @@ import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensi import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, EditorsOrder, IFindGroupScope, EditorGroupLayout, IEditorBreadcrumbs } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, EditorsOrder, IFindGroupScope, EditorGroupLayout } from 'vs/workbench/services/group/common/editorGroupsService'; import { IEditorService, IOpenEditorOverrideHandler } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; @@ -583,7 +583,6 @@ export class TestEditorGroup implements IEditorGroupView { constructor(public id: number) { } group: EditorGroup = void 0; - breadcrumbs: IEditorBreadcrumbs; activeControl: IEditor; activeEditor: IEditorInput; previewEditor: IEditorInput; @@ -674,6 +673,7 @@ export class TestEditorGroup implements IEditorGroupView { dispose(): void { } toJSON(): object { return Object.create(null); } layout(width: number, height: number): void { } + relayout() { } } export class TestEditorService implements EditorServiceImpl { @@ -1295,6 +1295,10 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } + getActiveWindowId(): TPromise { + return TPromise.as(undefined); + } + // This needs to be handled from browser process to prevent // foreground ordering issues on Windows openExternal(url: string): TPromise { diff --git a/test/smoke/src/areas/workbench/viewlet.ts b/test/smoke/src/areas/workbench/viewlet.ts index 2293752b705..84ccb601012 100644 --- a/test/smoke/src/areas/workbench/viewlet.ts +++ b/test/smoke/src/areas/workbench/viewlet.ts @@ -12,6 +12,6 @@ export abstract class Viewlet { constructor(protected code: Code) { } async waitForTitle(fn: (title: string) => boolean): Promise { - await this.code.waitForTextContent('.monaco-workbench-container .part.sidebar > .title > .title-label > span', undefined, fn); + await this.code.waitForTextContent('.monaco-workbench .part.sidebar > .title > .title-label > h2', undefined, fn); } } \ No newline at end of file diff --git a/test/tree/server.js b/test/tree/server.js index 2a931b51727..b1105bfb741 100644 --- a/test/tree/server.js +++ b/test/tree/server.js @@ -23,7 +23,7 @@ async function getTree(fsPath, level) { const childNames = await fs.readdir(fsPath); const children = await Promise.all(childNames.map(async childName => await getTree(path.join(fsPath, childName), level + 1))); - return { element, collapsed: false, children }; + return { element, collapsible: true, collapsed: false, children }; } app.use(serve('public')); diff --git a/yarn.lock b/yarn.lock index f49e290a360..283cfdac357 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5436,9 +5436,9 @@ sparkles@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" -spdlog@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.6.0.tgz#20632ed4f1558ffa46e8a5827a5e97c61e0fa9ed" +spdlog@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.7.1.tgz#960b8cdced12e8c482d1df9fa65220789f276979" dependencies: bindings "^1.3.0" mkdirp "^0.5.1"