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 bf739296d29..2831ba28d9c 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,12 +1,12 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.26.0", + "version": "1.26.1", "repo": "https://github.com/Microsoft/vscode-node-debug" }, { "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/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/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/html-language-features/server/src/modes/javascriptMode.ts b/extensions/html-language-features/server/src/modes/javascriptMode.ts index 191253d0d2c..0c124f2d468 100644 --- a/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ b/extensions/html-language-features/server/src/modes/javascriptMode.ts @@ -131,7 +131,7 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache gotData = true); } else { this.outputChannel.appendLine('stdout is null'); } - const stderr = this.collectData(cmd.stderr); - - let gotData = false; - cmd.stdout.once('data', () => gotData = true); + let stderr: Buffer[]; + if (cmd.stderr) { + // Should be non-null, but #38195 + stderr = this.collectData(cmd.stderr); + } else { + this.outputChannel.appendLine('stderr is null'); + } cmd.on('error', (err: Error) => { onData(err); diff --git a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json index bee4c021973..4c96b7e3ae3 100644 --- a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/TypeScript.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/40288b872220e5c0b844b1de507f1749ed14589b", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/d7df3e324468b6535af67573d2956f9a852aa586", "name": "TypeScript", "scopeName": "source.ts", "patterns": [ @@ -139,6 +139,10 @@ "name": "keyword.control.with.ts", "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])); } } @@ -138,8 +133,7 @@ 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 listening: boolean = false; @@ -153,10 +147,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 +206,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 +236,24 @@ export default class BufferSyncSupport { } syncedBuffer.onContentChanged(e.contentChanges); + this.requestDiagnostic(syncedBuffer); + if (this.pendingGetErr) { this.pendingGetErr.token.cancel(); this.pendingGetErr = undefined; - this.diagnosticDelayer.trigger(() => { - this.sendPendingDiagnostics(); - }, 200); + // In this case we always want to re-trigger all diagnostics + 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,65 +263,55 @@ 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): void { + if (!this.shouldValidate(buffer)) { + return; + } + + this.pendingDiagnostics.set(buffer.resource, Date.now()); + + const lineCount = buffer.lineCount; + const delay = Math.min(Math.max(Math.ceil(lineCount / 20), 300), 800); + this.triggerDiagnostics(delay); + } + 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); + if (fileList.size) { + const files = Array.from(fileList); const args: Proto.GeterrRequestArgs = { delay: 0, - files: fileList + files }; const token = new CancellationTokenSource(); @@ -338,7 +323,7 @@ export default class BufferSyncSupport { this.pendingGetErr = undefined; } }), - files: fileList, + files, token }; } @@ -363,8 +348,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/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/tagCompletion.ts b/extensions/typescript-language-features/src/features/tagCompletion.ts index 8a8495180ca..eecdc80aad5 100644 --- a/extensions/typescript-language-features/src/features/tagCompletion.ts +++ b/extensions/typescript-language-features/src/features/tagCompletion.ts @@ -7,44 +7,100 @@ 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 { VersionDependentRegistration, ConfigurationDependentRegistration, ConditionalRegistration } from '../utils/dependentRegistration'; +import { disposeAll } from '../utils/dispose'; import * as typeConverters from '../utils/typeConverters'; -class TypeScriptTagCompletion implements vscode.CompletionItemProvider { +class TagClosing { + + private _disposed = false; + private timeout: NodeJS.Timer | undefined = undefined; + + private readonly disposables: vscode.Disposable[] = []; + 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)]; + ) { + vscode.workspace.onDidChangeTextDocument( + event => this.onDidChangeTextDocument(event.document, event.contentChanges), + null, + this.disposables); } - private getCompletion(body: Proto.TextInsertion) { - const completion = new vscode.CompletionItem(body.newText); - completion.insertText = this.getTagSnippet(body); - return completion; + public dispose() { + disposeAll(this.disposables); + this._disposed = true; + this.timeout = 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); + } + + 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 () => { + 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); + + try { + const response = await this.client.execute('jsxClosingTag', args, null as any); + body = response && response.body; + if (!body) { + return; + } + } catch { + return; + } + + if (!this._disposed) { + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor) { + const activeDocument = activeEditor.document; + if (document === activeDocument && activeDocument.version === version) { + const selections = activeEditor.selections; + const snippet = this.getTagSnippet(body); + if (selections.length && selections.some(s => s.active.isEqual(position))) { + activeEditor.insertSnippet(snippet, selections.map(s => s.active)); + } else { + activeEditor.insertSnippet(snippet, position); + } + } + } + } + + this.timeout = void 0; + }, 100); } private getTagSnippet(closingTag: Proto.TextInsertion): vscode.SnippetString { @@ -55,12 +111,38 @@ class TypeScriptTagCompletion implements vscode.CompletionItemProvider { } } +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, () => - vscode.languages.registerCompletionItemProvider(selector, - new TypeScriptTagCompletion(client), - '>')); + new ConfigurationDependentRegistration(modeId, 'autoClosingTags', () => + new ActiveDocumentDependentRegistration(selector, () => + new TagClosing(client)))); } diff --git a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts index 6f2d1265275..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}', @@ -223,7 +234,7 @@ export class UpdateImportsOnFileRenameHandler { const isDirectoryRename = fs.lstatSync(newFile).isDirectory(); await this.fileConfigurationManager.ensureConfigurationForDocument(document, undefined); - const args: Proto.GetEditsForFileRenameRequestArgs = { + const args: Proto.GetEditsForFileRenameRequestArgs & { file: string } = { file: targetResource, oldFilePath: oldFile, newFilePath: newFile, @@ -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..c963ef6586c 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/tagCompletion')).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/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index 1ce28c148e5..745eda206d7 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -286,8 +286,7 @@ export default class TypeScriptServiceClientHost { if (diagnostic.code) { converted.code = diagnostic.code; } - // TODO: requires TS 3.0 - const relatedInformation = (diagnostic as any).relatedInformation; + const relatedInformation = diagnostic.relatedInformation; if (relatedInformation) { converted.relatedInformation = relatedInformation.map((info: any) => { let span = info.span; diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index 67be9e314c0..729bda6af6c 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -11,11 +11,6 @@ import { TypeScriptServiceConfiguration } from './utils/configuration'; import Logger from './utils/logger'; import BufferSyncSupport from './features/bufferSyncSupport'; -declare module './protocol' { - export type JsxClosingTagRequestArgs = any; - export type JsxClosingTagResponse = any; -} - export interface ITypeScriptServiceClient { /** * Convert a resource (VS Code) to a normalized path (TypeScript). 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/previewer.ts b/extensions/typescript-language-features/src/utils/previewer.ts index ce10fcbbe04..41ff1fea7d6 100644 --- a/extensions/typescript-language-features/src/utils/previewer.ts +++ b/extensions/typescript-language-features/src/utils/previewer.ts @@ -72,13 +72,18 @@ export function markdownDocumentation( export function addMarkdownDocumentation( out: MarkdownString, - documentation: Proto.SymbolDisplayPart[], - tags: Proto.JSDocTagInfo[] + documentation: Proto.SymbolDisplayPart[] | undefined, + tags: Proto.JSDocTagInfo[] | undefined ): MarkdownString { - out.appendMarkdown(plain(documentation)); - const tagsPreview = tagsMarkdownPreview(tags); - if (tagsPreview) { - out.appendMarkdown('\n\n' + tagsPreview); + if (documentation) { + out.appendMarkdown(plain(documentation)); + } + + if (tags) { + const tagsPreview = tagsMarkdownPreview(tags); + if (tagsPreview) { + out.appendMarkdown('\n\n' + tagsPreview); + } } return out; } 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/extensions/yarn.lock b/extensions/yarn.lock index 7ec0734b094..64d1bf83d0f 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,6 +2,6 @@ # yarn lockfile v1 -typescript@2.9.2: - version "2.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" +typescript@3.0.0-insiders.20180706: + version "3.0.0-insiders.20180706" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.0-insiders.20180706.tgz#684c6c8ca1eefb11e79b6c072a92046ffd75184e" diff --git a/package.json b/package.json index 695afb3d60c..60d3016b27d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.26.0", - "distro": "7ee5cda9f4f44b2ba1f1ab290e48f86416914794", + "distro": "96ae9ca16a7c16ae63bafd206a92d4dc13754af2", "author": { "name": "Microsoft Corporation" }, 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 new file mode 100644 index 00000000000..adbe04a698d --- /dev/null +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-breadcrumbs { + user-select: none; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; +} + +.monaco-breadcrumbs .monaco-breadcrumb-item { + display: flex; + align-items: center; + flex: 0 1 auto; + white-space: nowrap; + cursor: default; + 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; + background-size: 16px; + background-position: 50% 50%; + content: ' '; +} + +.vs-dark .monaco-breadcrumbs .monaco-breadcrumb-item:not(:last-child)::after { + background-image: url(./collpased-dark.svg); +} + diff --git a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts new file mode 100644 index 00000000000..f3189df65e7 --- /dev/null +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -0,0 +1,253 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./breadcrumbsWidget'; +import * as dom from 'vs/base/browser/dom'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Color } from 'vs/base/common/color'; +import { commonPrefixLength, tail } from 'vs/base/common/arrays'; + +export abstract class BreadcrumbsItem { + dispose(): void { } + abstract equals(other: BreadcrumbsItem): boolean; + abstract render(container: HTMLElement): void; +} + +export class SimpleBreadcrumbsItem extends BreadcrumbsItem { + + constructor( + readonly text: string, + readonly title: string = text + ) { + super(); + } + + equals(other: this) { + return other === this || other instanceof SimpleBreadcrumbsItem && other.text === this.text && other.title === this.title; + } + + render(container: HTMLElement): void { + let node = document.createElement('div'); + node.title = this.title; + node.innerText = this.text; + container.appendChild(node); + } +} + +export interface IBreadcrumbsWidgetStyles { + breadcrumbsBackground?: Color; + breadcrumbsForeground?: Color; + breadcrumbsFocusedForeground?: Color; +} + +export interface IBreadcrumbsItemEvent { + type: 'select' | 'focus'; + item: BreadcrumbsItem; + node: HTMLElement; +} + +export class BreadcrumbsWidget { + + private readonly _disposables = new Array(); + private readonly _domNode: HTMLDivElement; + private readonly _styleElement: HTMLStyleElement; + private readonly _scrollable: DomScrollableElement; + + private readonly _onDidSelectItem = new Emitter(); + private readonly _onDidFocusItem = new Emitter(); + private readonly _onDidChangeFocus = new Emitter(); + + readonly onDidSelectItem: Event = this._onDidSelectItem.event; + readonly onDidFocusItem: Event = this._onDidFocusItem.event; + readonly onDidChangeFocus: Event = this._onDidChangeFocus.event; + + private readonly _items = new Array(); + private readonly _nodes = new Array(); + private readonly _freeNodes = new Array(); + + private _focusedItemIdx: number = -1; + private _selectedItemIdx: number = -1; + + constructor( + container: HTMLElement + ) { + this._domNode = document.createElement('div'); + this._domNode.className = 'monaco-breadcrumbs'; + this._domNode.tabIndex = -1; + this._scrollable = new DomScrollableElement(this._domNode, { + vertical: ScrollbarVisibility.Hidden, + horizontal: ScrollbarVisibility.Auto, + horizontalScrollbarSize: 3, + useShadows: false + }); + this._disposables.push(this._scrollable); + this._disposables.push(dom.addStandardDisposableListener(this._domNode, 'click', e => this._onClick(e))); + container.appendChild(this._scrollable.getDomNode()); + + this._styleElement = dom.createStyleSheet(this._domNode); + + let focusTracker = dom.trackFocus(this._domNode); + this._disposables.push(focusTracker); + this._disposables.push(focusTracker.onDidBlur(_ => this._onDidChangeFocus.fire(false))); + this._disposables.push(focusTracker.onDidFocus(_ => this._onDidChangeFocus.fire(true))); + } + + dispose(): void { + dispose(this._disposables); + this._domNode.remove(); + this._disposables.length = 0; + this._nodes.length = 0; + this._freeNodes.length = 0; + } + + layout(dim: dom.Dimension): void { + if (!dim) { + this._scrollable.scanDomNode(); + } else { + this._domNode.style.width = `${dim.width}px`; + this._domNode.style.height = `${dim.height}px`; + this._scrollable.scanDomNode(); + } + } + + style(style: IBreadcrumbsWidgetStyles): void { + let content = ''; + if (style.breadcrumbsBackground) { + content += `.monaco-breadcrumbs { background-color: ${style.breadcrumbsBackground}}`; + } + if (style.breadcrumbsForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item { color: ${style.breadcrumbsForeground}}\n`; + } + if (style.breadcrumbsFocusedForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused { color: ${style.breadcrumbsFocusedForeground}}\n`; + } + if (this._styleElement.innerHTML !== content) { + this._styleElement.innerHTML = content; + } + } + + domFocus(): void { + const focused = this.getFocused() || tail(this._items); + this.setFocused(focused); + this._domNode.focus(); + } + + isDOMFocused(): boolean { + return this._domNode === document.activeElement; + } + + getFocused(): BreadcrumbsItem { + return this._items[this._focusedItemIdx]; + } + + setFocused(item: BreadcrumbsItem): void { + this._focus(this._items.indexOf(item)); + } + + focusPrev(): any { + this._focus((this._focusedItemIdx - 1 + this._nodes.length) % this._nodes.length); + // this._domNode.focus(); + } + + focusNext(): any { + this._focus((this._focusedItemIdx + 1) % this._nodes.length); + // this._domNode.focus(); + } + + private _focus(nth: number): void { + this._focusedItemIdx = -1; + for (let i = 0; i < this._nodes.length; i++) { + const node = this._nodes[i]; + if (i !== nth) { + dom.removeClass(node, 'focused'); + } else { + this._focusedItemIdx = i; + dom.addClass(node, 'focused'); + } + } + this._onDidFocusItem.fire({ type: 'focus', item: this._items[this._focusedItemIdx], node: this._nodes[this._focusedItemIdx] }); + } + + getSelected(): BreadcrumbsItem { + return this._items[this._selectedItemIdx]; + } + + setSelected(item: BreadcrumbsItem): void { + this._select(this._items.indexOf(item)); + } + + private _select(nth: number): void { + this._selectedItemIdx = -1; + for (let i = 0; i < this._nodes.length; i++) { + const node = this._nodes[i]; + if (i !== nth) { + dom.removeClass(node, 'selected'); + } else { + this._selectedItemIdx = i; + dom.addClass(node, 'selected'); + } + } + this._onDidSelectItem.fire({ type: 'select', item: this._items[this._selectedItemIdx], node: this._nodes[this._selectedItemIdx] }); + } + + setItems(items: BreadcrumbsItem[]): void { + let prefix = commonPrefixLength(this._items, items, (a, b) => a.equals(b)); + 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); + } + } + + private _render(start: number): void { + for (; start < this._items.length && start < this._nodes.length; start++) { + let item = this._items[start]; + let node = this._nodes[start]; + this._renderItem(item, node); + } + // case a: more nodes -> remove them + for (; start < this._nodes.length; start++) { + this._nodes[start].remove(); + this._freeNodes.push(this._nodes[start]); + } + this._nodes.length = this._items.length; + + // case b: more items -> render them + for (; start < this._items.length; start++) { + let item = this._items[start]; + let node = this._freeNodes.length > 0 ? this._freeNodes.pop() : document.createElement('div'); + this._renderItem(item, node); + this._domNode.appendChild(node); + this._nodes[start] = node; + } + this.layout(undefined); + } + + private _renderItem(item: BreadcrumbsItem, container: HTMLDivElement): void { + dom.clearNode(container); + container.className = ''; + item.render(container); + dom.append(container); + dom.addClass(container, 'monaco-breadcrumb-item'); + } + + private _onClick(event: IMouseEvent): void { + for (let el = event.target; el; el = el.parentElement) { + let idx = this._nodes.indexOf(el as any); + if (idx >= 0) { + this._focus(idx); + this._select(idx); + break; + } + } + } +} diff --git a/src/vs/base/browser/ui/breadcrumbs/collapsed.svg b/src/vs/base/browser/ui/breadcrumbs/collapsed.svg new file mode 100755 index 00000000000..3a63808c358 --- /dev/null +++ b/src/vs/base/browser/ui/breadcrumbs/collapsed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/base/browser/ui/breadcrumbs/collpased-dark.svg b/src/vs/base/browser/ui/breadcrumbs/collpased-dark.svg new file mode 100755 index 00000000000..cf5c3641aa7 --- /dev/null +++ b/src/vs/base/browser/ui/breadcrumbs/collpased-dark.svg @@ -0,0 +1 @@ + \ 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 1d560a8afec..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 })); @@ -282,6 +282,11 @@ export class ListView implements ISpliceable, IDisposable { private insertItemInDOM(index: number, beforeElement: HTMLElement | null): void { const item = this.items[index]; + if (!item) { + console.log(this.items); + throw new Error(`Got index ${index} and there are ${this.items.length} items. File issue to joao!`); + } + if (!item.row) { item.row = this.cache.alloc(item.templateId); } @@ -311,6 +316,18 @@ export class ListView implements ISpliceable, IDisposable { private removeItemFromDOM(index: number): void { const item = this.items[index]; + + if (!item) { + console.log(this.items); + 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; } @@ -371,7 +388,12 @@ export class ListView implements ISpliceable, IDisposable { } private onScroll(e: ScrollEvent): void { - this.render(e.scrollTop, e.height); + try { + this.render(e.scrollTop, e.height); + } catch (err) { + console.log('Got bad scroll event:', e); + throw err; + } } private onTouchChange(event: GestureEvent): void { 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.css b/src/vs/base/browser/ui/menu/menu.css index fff01a4a278..ef4b8cedfe4 100644 --- a/src/vs/base/browser/ui/menu/menu.css +++ b/src/vs/base/browser/ui/menu/menu.css @@ -39,6 +39,13 @@ background-color: #EEE; } +.monaco-menu .monaco-action-bar.vertical .action-menu-item { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + display: -ms-flexbox; + display: flex; +} + .monaco-menu .monaco-action-bar.vertical .action-label { -ms-flex: 1 1 auto; flex: 1 1 auto; diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 2eb053fbdc0..3851600c6df 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -6,14 +6,16 @@ 'use strict'; import 'vs/css!./menu'; +import * as nls from 'vs/nls'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IActionRunner, IAction, Action } from 'vs/base/common/actions'; -import { ActionBar, IActionItemProvider, ActionsOrientation, Separator, ActionItem, IActionItemOptions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, IActionItemProvider, ActionsOrientation, Separator, ActionItem, IActionItemOptions, BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; import { Event } from 'vs/base/common/event'; -import { addClass, EventType, EventHelper, EventLike } from 'vs/base/browser/dom'; +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; @@ -65,7 +67,7 @@ export class Menu { this.actionBar.push(actions, { icon: true, label: true, isMenu: true }); } - private doGetActionItem(action: IAction, options: IMenuOptions, parentData: ISubMenuData): ActionItem { + private doGetActionItem(action: IAction, options: IMenuOptions, parentData: ISubMenuData): BaseActionItem { if (action instanceof Separator) { return new ActionItem(options.context, action, { icon: true }); } else if (action instanceof SubmenuAction) { @@ -110,42 +112,128 @@ export class Menu { } } -class MenuActionItem extends ActionItem { +class MenuActionItem extends BaseActionItem { static MNEMONIC_REGEX: RegExp = /&&(.)/g; + protected $e: Builder; + protected $label: Builder; + protected options: IActionItemOptions; + private cssClass: string; + constructor(ctx: any, action: IAction, options: IActionItemOptions = {}) { options.isMenu = true; super(action, action, options); - } - private _addMnemonic(action: IAction, actionItemElement: HTMLElement): void { - let matches = MenuActionItem.MNEMONIC_REGEX.exec(action.label); - if (matches && matches.length === 2) { - let mnemonic = matches[1]; - - let ariaLabel = action.label.replace(MenuActionItem.MNEMONIC_REGEX, mnemonic); - - actionItemElement.accessKey = mnemonic.toLocaleLowerCase(); - this.$e.attr('aria-label', ariaLabel); - } else { - this.$e.attr('aria-label', action.label); - } + this.options = options; + this.options.icon = options.icon !== undefined ? options.icon : false; + this.options.label = options.label !== undefined ? options.label : true; + this.cssClass = ''; } public render(container: HTMLElement): void { super.render(container); - this._addMnemonic(this.getAction(), container); - this.$e.attr('role', 'menuitem'); + this.$e = $('a.action-menu-item').appendTo(this.builder); + if (this._action.id === Separator.ID) { + // A separator is a presentation item + this.$e.attr({ role: 'presentation' }); + } else { + this.$e.attr({ role: 'menuitem' }); + } + + this.$label = $('span.action-label').appendTo(this.$e); + + if (this.options.label && this.options.keybinding) { + $('span.keybinding').text(this.options.keybinding).appendTo(this.$e); + } + + this._updateClass(); + this._updateLabel(); + this._updateTooltip(); + this._updateEnabled(); + this._updateChecked(); + } + + public focus(): void { + super.focus(); + this.$e.domFocus(); } public _updateLabel(): void { if (this.options.label) { let label = this.getAction().label; - if (label && this.options.isMenu) { + if (label) { + let matches = MenuActionItem.MNEMONIC_REGEX.exec(label); + if (matches && matches.length === 2) { + let mnemonic = matches[1]; + + let ariaLabel = label.replace(MenuActionItem.MNEMONIC_REGEX, mnemonic); + + this.$e.getHTMLElement().accessKey = mnemonic.toLocaleLowerCase(); + this.$label.attr('aria-label', ariaLabel); + } else { + this.$label.attr('aria-label', label); + } + label = label.replace(MenuActionItem.MNEMONIC_REGEX, '$1\u0332'); } - this.$e.text(label); + + this.$label.text(label); + } + } + + public _updateTooltip(): void { + let title: string = null; + + if (this.getAction().tooltip) { + title = this.getAction().tooltip; + + } else if (!this.options.label && this.getAction().label && this.options.icon) { + title = this.getAction().label; + + if (this.options.keybinding) { + title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding); + } + } + + if (title) { + this.$e.attr({ title: title }); + } + } + + public _updateClass(): void { + if (this.cssClass) { + this.$e.removeClass(this.cssClass); + } + if (this.options.icon) { + this.cssClass = this.getAction().class; + this.$label.addClass('icon'); + if (this.cssClass) { + this.$label.addClass(this.cssClass); + } + this._updateEnabled(); + } else { + this.$label.removeClass('icon'); + } + } + + public _updateEnabled(): void { + if (this.getAction().enabled) { + this.builder.removeClass('disabled'); + this.$e.removeClass('disabled'); + this.$e.attr({ tabindex: 0 }); + } else { + this.builder.addClass('disabled'); + this.$e.addClass('disabled'); + removeTabIndexAndUpdateFocus(this.$e.getHTMLElement()); + } + } + + public _updateChecked(): void { + if (this.getAction().checked) { + this.$label.addClass('checked'); + } else { + this.$label.removeClass('checked'); } } } @@ -154,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, @@ -162,15 +252,28 @@ 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 { super.render(container); - this.builder = $(container); - $(this.builder).addClass('monaco-submenu-item'); - $('span.submenu-indicator').text('\u25B6').appendTo(this.builder); - this.$e.attr('role', 'menu'); + this.$e.addClass('monaco-submenu-item'); + this.$e.attr('aria-haspopup', 'true'); + $('span.submenu-indicator').text('\u25B6').appendTo(this.$e); $(this.builder).on(EventType.KEY_UP, (e) => { let event = new StandardKeyboardEvent(e as KeyboardEvent); @@ -192,33 +295,22 @@ 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(); }); } public onClick(e: EventLike) { // stop clicking from trying to run an action EventHelper.stop(e, true); + + this.createSubmenu(); } private cleanupExistingSubmenu(force: boolean) { @@ -273,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/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/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 e4d0f16503d..822d4063908 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/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 97f712452c9..deaea3955f3 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -43,6 +43,7 @@ import { LocalizationsChannel } from 'vs/platform/localizations/common/localizat import { DialogChannelClient } from 'vs/platform/dialogs/common/dialogIpc'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { DefaultURITransformer } from 'vs/base/common/uriIpc'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -134,7 +135,7 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I instantiationService2.invokeFunction(accessor => { const extensionManagementService = accessor.get(IExtensionManagementService); - const channel = new ExtensionManagementChannel(extensionManagementService); + const channel = new ExtensionManagementChannel(extensionManagementService, DefaultURITransformer); server.registerChannel('extensions', channel); // clean up deprecated extensions diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index cbcf41f5afa..de96fe1c654 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -241,7 +241,7 @@ export class CodeMenu { // Terminal const terminalMenu = new Menu(); - const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu }); + const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "Ter&&minal")), submenu: terminalMenu }); this.setTerminalMenu(terminalMenu); // Debug @@ -435,7 +435,7 @@ export class CodeMenu { } private getPreferencesMenu(): Electron.MenuItem { - const settings = this.createMenuItem(nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings"), 'workbench.action.openSettings'); + const settings = this.createMenuItem(nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings"), 'workbench.action.openSettings2'); const kebindingSettings = this.createMenuItem(nls.localize({ key: 'miOpenKeymap', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts"), 'workbench.action.openGlobalKeybindings'); const keymapExtensions = this.createMenuItem(nls.localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymap Extensions"), 'workbench.extensions.action.showRecommendedKeymapExtensions'); const snippetsSettings = this.createMenuItem(nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets"), 'workbench.action.openSnippets'); 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/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index c86a8b15608..80d248a2819 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -88,7 +88,7 @@ export class QuickFixController implements IEditorContribution { this._activeRequest = e.actions; } - if (e && e.trigger.filter && e.trigger.filter.kind) { + if (e && e.actions && e.trigger.filter && e.trigger.filter.kind) { // Triggered for specific scope // Apply if we only have one action or requested autoApply, otherwise show menu e.actions.then(fixes => { diff --git a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css b/src/vs/editor/contrib/documentSymbols/media/outlineTree.css new file mode 100644 index 00000000000..f62841df77d --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/outlineTree.css @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-tree.focused .selected .outline-element-label, .monaco-tree.focused .selected .outline-element-decoration { + /* make sure selection color wins when a label is being selected */ + color: inherit !important; +} + +.monaco-tree .outline-element { + display: flex; + flex: 1; + flex-flow: row nowrap; + align-items: center; +} + +.monaco-tree .outline-element .outline-element-icon { + padding-right: 3px; +} + +/* .monaco-tree.no-icons .outline-element .outline-element-icon { + display: none; +} */ + +.monaco-tree .outline-element .outline-element-label { + text-overflow: ellipsis; + overflow: hidden; + color: var(--outline-element-color); +} + +.monaco-tree .outline-element .outline-element-label .monaco-highlighted-label .highlight { + font-weight: bold; +} + +.monaco-tree .outline-element .outline-element-detail { + visibility: hidden; + flex: 1; + flex-basis: 10%; + opacity: 0.8; + overflow: hidden; + text-overflow: ellipsis; + font-size: 90%; + padding-left: 4px; + padding-top: 3px; +} + +.monaco-tree .monaco-tree-row.focused .outline-element .outline-element-detail { + visibility: inherit; +} + +.monaco-tree .outline-element .outline-element-decoration { + opacity: 0.75; + font-size: 90%; + font-weight: 600; + padding: 0 12px 0 5px; + margin-left: auto; + text-align: center; + color: var(--outline-element-color); +} + +.monaco-tree .outline-element .outline-element-decoration.bubble { + font-family: octicons; + font-size: 14px; + opacity: 0.4; +} diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/editor/contrib/documentSymbols/outlineTree.ts index 19489cbae98..e1181ea33cd 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineTree.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineTree.ts @@ -5,22 +5,23 @@ 'use strict'; import * as dom from 'vs/base/browser/dom'; -import 'vs/css!./media/symbol-icons'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { values } from 'vs/base/common/collections'; import { createMatches } from 'vs/base/common/filters'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDataSource, IFilter, IRenderer, ISorter, ITree } from 'vs/base/parts/tree/browser/tree'; +import 'vs/css!./media/outlineTree'; +import 'vs/css!./media/symbol-icons'; import { Range } from 'vs/editor/common/core/range'; -import { symbolKindToCssClass, SymbolKind } from 'vs/editor/common/modes'; +import { SymbolKind, symbolKindToCssClass } from 'vs/editor/common/modes'; import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export enum OutlineItemCompareType { ByPosition, 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/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 6e72075c97e..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 }); @@ -878,7 +885,7 @@ export class SuggestWidget implements IContentWidget, IDelegate */ this.telemetryService.publicLog('suggestWidget:collapseDetails', this.editor.getTelemetryData()); } else { - if (this.state !== State.Open && this.state !== State.Details) { + if (this.state !== State.Open && this.state !== State.Details && this.state !== State.Frozen) { return; } diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 1de227bfe06..0aa4e7577a7 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -33,7 +33,7 @@ export class ExtensionManagementChannel implements IExtensionManagementChannel { onUninstallExtension: Event; onDidUninstallExtension: Event; - constructor(private service: IExtensionManagementService) { + constructor(private service: IExtensionManagementService, private uriTransformer: IURITransformer) { this.onInstallExtension = buffer(service.onInstallExtension, true); this.onDidInstallExtension = buffer(service.onDidInstallExtension, true); this.onUninstallExtension = buffer(service.onUninstallExtension, true); @@ -55,15 +55,19 @@ export class ExtensionManagementChannel implements IExtensionManagementChannel { switch (command) { case 'install': return this.service.install(args[0]); case 'installFromGallery': return this.service.installFromGallery(args[0]); - case 'uninstall': return this.service.uninstall(args[0], args[1]); - case 'reinstallFromGallery': return this.service.reinstallFromGallery(args[0]); + case 'uninstall': return this.service.uninstall(this._transform(args[0]), args[1]); + case 'reinstallFromGallery': return this.service.reinstallFromGallery(this._transform(args[0])); case 'getInstalled': return this.service.getInstalled(args[0]); - case 'updateMetadata': return this.service.updateMetadata(args[0], args[1]); + case 'updateMetadata': return this.service.updateMetadata(this._transform(args[0]), args[1]); case 'getExtensionsReport': return this.service.getExtensionsReport(); } throw new Error('Invalid call'); } + + private _transform(extension: ILocalExtension): ILocalExtension { + return extension ? { ...extension, ...{ location: URI.revive(this.uriTransformer.transformIncoming(extension.location)) } } : extension; + } } export class ExtensionManagementChannelClient implements IExtensionManagementService { 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/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 241993fb8f8..b57f7ea9355 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -223,6 +223,9 @@ 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 breadcrumbsForegound = registerColor('breadcrumb.focusForeground', { 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 breadcrumbsFocusedForegound = registerColor('breadcrumb.breadcrumbsFocusedForegound', { light: '#6C6C6C', dark: '#CCCCCC', hc: Color.white }, nls.localize('breadcrumbsFocusForeground', "Color of focused 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 07dfd1d9551..72296db3038 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, breadcrumbsForegound, breadcrumbsFocusedForegound } 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'; @@ -261,4 +261,20 @@ export function attachProgressBarStyler(widget: IThemable, themeService: IThemeS export function attachStylerCallback(themeService: IThemeService, colors: { [name: string]: ColorIdentifier }, callback: styleFn): IDisposable { return attachStyler(themeService, colors, callback); -} \ No newline at end of file +} + +export interface IBreadcrumbsWidgetStyleOverrides extends IStyleOverrides { + breadcrumbsBackground?: ColorIdentifier; + breadcrumbsForeground?: ColorIdentifier; + breadcrumbsFocusedForeground?: ColorIdentifier; +} + +export const defaultBreadcrumbsStyles = { + breadcrumbsBackground: editorBackground, + breadcrumbsForeground: breadcrumbsForegound, + breadcrumbsFocusedForeground: breadcrumbsFocusedForegound +}; + +export function attachBreadcrumbsStyler(widget: IThemable, themeService: IThemeService, style?: IBreadcrumbsWidgetStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultBreadcrumbsStyles, ...style }, widget); +} 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/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..3ce8be20002 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -13,7 +13,6 @@ 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 +29,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 { @@ -518,7 +517,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; @@ -633,19 +632,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( @@ -743,24 +729,6 @@ 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.exists || (this.maxResults && this.resultCount >= this.maxResults)) { @@ -800,12 +768,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/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts new file mode 100644 index 00000000000..6990ff52921 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -0,0 +1,580 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as dom from 'vs/base/browser/dom'; +import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; +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, combinedDisposable } from 'vs/base/common/lifecycle'; +import { dirname, isEqual } 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/breadcrumbs'; +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'; +import { OutlineController, OutlineDataSource, OutlineItemComparator, OutlineRenderer } from 'vs/editor/contrib/documentSymbols/outlineTree'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IConstructorSignature2, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { Registry } from 'vs/platform/registry/common/platform'; +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/breadcrumbsModel'; +import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +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'; + +class Item extends BreadcrumbsItem { + + private readonly _disposables: IDisposable[] = []; + + constructor( + readonly element: BreadcrumbElement, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + } + + dispose(): void { + dispose(this._disposables); + } + + equals(other: BreadcrumbsItem): boolean { + if (!(other instanceof Item)) { + return false; + } + if (this.element instanceof FileElement && other.element instanceof FileElement) { + return isEqual(this.element.uri, other.element.uri); + } + if (this.element instanceof TreeElement && other.element instanceof TreeElement) { + return this.element.id === other.element.id; + } + return false; + } + + 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); + + } else if (this.element instanceof OutlineGroup) { + // provider + let label = new IconLabel(container); + label.setValue(this.element.provider.displayName); + this._disposables.push(label); + + } else if (this.element instanceof OutlineElement) { + // symbol + let label = new IconLabel(container); + label.setValue(this.element.symbol.name.replace(/\r|\n|\r\n/g, '\u23CE')); + this._disposables.push(label); + } + } +} + +export class BreadcrumbsControl { + + static CK_BreadcrumbsVisible = new RawContextKey('breadcrumbsVisible', false); + static CK_BreadcrumbsActive = new RawContextKey('breadcrumbsActive', false); + + private readonly _ckBreadcrumbsVisible: IContextKey; + private readonly _ckBreadcrumbsActive: IContextKey; + + private readonly _domNode: HTMLDivElement; + private readonly _widget: BreadcrumbsWidget; + private _disposables = new Array(); + + private _breadcrumbsDisposables = new Array(); + private _breadcrumbsPickerShowing = false; + + constructor( + container: HTMLElement, + private readonly _editorGroup: EditorGroupView, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IContextViewService private readonly _contextViewService: IContextViewService, + @IEditorService private readonly _editorService: IEditorService, + @IFileService private readonly _fileService: IFileService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @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, 'breadcrumbs-control'); + dom.append(container, this._domNode); + + this._widget = new BreadcrumbsWidget(this._domNode); + this._widget.onDidSelectItem(this._onDidSelectItem, this, this._disposables); + this._widget.onDidFocusItem(this._onDidSelectItem, this, this._disposables); + this._widget.onDidChangeFocus(this._updateCkBreadcrumbsActive, this, this._disposables); + this._disposables.push(attachBreadcrumbsStyler(this._widget, this._themeService)); + + this._ckBreadcrumbsVisible = BreadcrumbsControl.CK_BreadcrumbsVisible.bindTo(this._contextKeyService); + this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService); + + this._disposables.push(breadcrumbsService.register(this._editorGroup.id, this._widget)); + } + + dispose(): void { + this._disposables = dispose(this._disposables); + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); + this._ckBreadcrumbsVisible.reset(); + this._ckBreadcrumbsActive.reset(); + this._widget.dispose(); + this._domNode.remove(); + } + + getPreferredHeight(): number { + return 25; + } + + 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); + + 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; + } + + 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 = [model, listener]; + } + + 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 _onDidSelectItem(event: IBreadcrumbsItemEvent): void { + if (!event.item) { + return; + } + + if (event.type === 'focus' && !this._breadcrumbsPickerShowing) { + // focus change only moves the picker when already active + return; + } + + this._editorGroup.focus(); + this._contextViewService.showContextView({ + getAnchor() { + return event.node; + }, + 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', 'monaco-workbench', '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 }); + let listener = res.onDidPickElement(data => { + this._contextViewService.hideContextView(); + if (!data) { + return; + } + if (URI.isUri(data)) { + // open new editor + this._editorService.openEditor({ resource: data }); + + } else if (data instanceof OutlineElement) { + + let resource: URI; + let candidate = data.parent; + while (candidate) { + if (candidate instanceof OutlineModel) { + resource = candidate.textModel.uri; + break; + } + candidate = candidate.parent; + } + + this._editorService.openEditor({ resource, options: { selection: Range.collapseToStart(data.symbol.selectionRange) } }); + + } + }); + this._breadcrumbsPickerShowing = true; + this._updateCkBreadcrumbsActive(); + + return combinedDisposable([listener, res]); + }, + 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 { + + readonly focus: dom.IFocusTracker; + + protected readonly _onDidPickElement = new Emitter(); + readonly onDidPickElement: Event = this._onDidPickElement.event; + + protected readonly _disposables = new Array(); + protected readonly _domNode: HTMLDivElement; + protected readonly _tree: WorkbenchTree; + + constructor( + container: HTMLElement, + input: BreadcrumbElement, + @IInstantiationService protected readonly _instantiationService: IInstantiationService, + @IThemeService protected readonly _themeService: IThemeService, + ) { + this._domNode = document.createElement('div'); + this._domNode.style.background = this._themeService.getTheme().getColor(SIDE_BAR_BACKGROUND).toString(); + container.appendChild(this._domNode); + + this._tree = this._instantiationService.createInstance(WorkbenchTree, this._domNode, this._completeTreeConfiguration({ dataSource: undefined }), {}); + debounceEvent(this._tree.onDidChangeSelection, (_last, cur) => cur, 0)(this._onDidChangeSelection, this, this._disposables); + + 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.focusFirst(); + this._tree.domFocus(); + }, onUnexpectedError); + } + + dispose(): void { + dispose(this._disposables); + this._onDidPickElement.dispose(); + this._tree.dispose(); + this.focus.dispose(); + } + + layout(dim: dom.Dimension) { + this._domNode.style.width = `${dim.width}px`; + this._domNode.style.height = `${dim.height}px`; + this._tree.layout(dim.height, dim.width); + } + + protected abstract _getInput(input: BreadcrumbElement): any; + protected abstract _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration; + protected abstract _onDidChangeSelection(e: any): void; +} + +export class FileDataSource implements IDataSource { + + private readonly _parents = new WeakMap(); + + constructor( + @IFileService private readonly _fileService: IFileService, + ) { } + + getId(tree: ITree, element: IFileStat | URI): string { + return URI.isUri(element) ? element.toString() : element.resource.toString(); + } + + hasChildren(tree: ITree, element: IFileStat | URI): boolean { + return URI.isUri(element) || element.isDirectory; + } + + getChildren(tree: ITree, element: IFileStat | URI): TPromise { + return this._fileService.resolveFile( + URI.isUri(element) ? element : element.resource + ).then(stat => { + for (const child of stat.children) { + this._parents.set(child, stat); + } + return stat.children; + }); + } + + getParent(tree: ITree, element: IFileStat | URI): TPromise { + return TPromise.as(URI.isUri(element) ? undefined : this._parents.get(element)); + } +} + +export class FileRenderer implements IRenderer { + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { } + + getHeight(tree: ITree, element: any): number { + return 22; + } + + getTemplateId(tree: ITree, element: any): string { + return 'FileStat'; + } + + renderTemplate(tree: ITree, templateId: string, container: HTMLElement) { + return this._instantiationService.createInstance(FileLabel, container, {}); + } + + renderElement(tree: ITree, element: IFileStat, templateId: string, templateData: FileLabel): void { + templateData.setFile(element.resource, { + hidePath: true, + fileKind: element.isDirectory ? FileKind.FOLDER : FileKind.FILE, + fileDecorations: { colors: true, badges: true } + }); + } + + disposeTemplate(tree: ITree, templateId: string, templateData: FileLabel): void { + templateData.dispose(); + } +} + +export class FileSorter implements ISorter { + compare(tree: ITree, a: IFileStat, b: IFileStat): number { + if (a.isDirectory === b.isDirectory) { + // same type -> compare on names + return compareFileNames(a.name, b.name); + } else if (a.isDirectory) { + return -1; + } else { + return 1; + } + } +} + +export class BreadcrumbsFilePicker extends BreadcrumbsPicker { + + + protected _getInput(input: BreadcrumbElement): any { + let { uri } = (input as FileElement); + return dirname(uri); + } + + protected _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration { + // todo@joh reuse explorer implementations? + config.dataSource = this._instantiationService.createInstance(FileDataSource); + config.renderer = this._instantiationService.createInstance(FileRenderer); + config.sorter = new FileSorter(); + return config; + } + + protected _onDidChangeSelection(e: ISelectionEvent): void { + let [first] = e.selection; + let stat = first as IFileStat; + if (stat && !stat.isDirectory) { + this._onDidPickElement.fire(stat.resource); + } + } +} + +export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { + + protected _getInput(input: BreadcrumbElement): any { + return (input as TreeElement).parent; + } + + protected _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration { + config.dataSource = this._instantiationService.createInstance(OutlineDataSource); + config.renderer = this._instantiationService.createInstance(OutlineRenderer); + config.controller = this._instantiationService.createInstance(OutlineController, {}); + config.sorter = new OutlineItemComparator(); + return config; + } + + protected _onDidChangeSelection(e: ISelectionEvent): void { + if (e.payload && e.payload.didClickOnTwistie) { + return; + } + let [first] = e.selection; + if (first instanceof OutlineElement) { + this._onDidPickElement.fire(first); + } + } +} + +//#region config + +export abstract class BreadcrumbsConfig { + + name: string; + value: T; + onDidChange: Event; + abstract dispose(): void; + + private constructor() { + // internal + } + + static IsEnabled = BreadcrumbsConfig._stub('breadcrumbs.enabled'); + + private static _stub(name: string): { bindTo(service: IConfigurationService): BreadcrumbsConfig } { + return { + 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(); + } + }; + } + }; + } +} + +Registry.as(Extensions.Configuration).registerConfiguration({ + id: 'breadcrumbs', + title: localize('title', "Breadcrumb Navigation"), + order: 101, + type: 'object', + properties: { + 'breadcrumbs.enabled': { + 'description': localize('enabled', "Enable/disable navigation breadcrumbss"), + 'type': 'boolean', + 'default': false + } + } +}); + +//#endregion + +//#region commands + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focus', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT, + when: BreadcrumbsControl.CK_BreadcrumbsVisible, + handler(accessor) { + 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, + secondary: [KeyMod.Shift | KeyCode.RightArrow], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + 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, + secondary: [KeyMod.Shift | KeyCode.LeftArrow], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + 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(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + const widget = breadcrumbs.getWidget(groups.activeGroup.id); + widget.setSelected(widget.getFocused()); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.selectEditor', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + breadcrumbs.getWidget(groups.activeGroup.id).setFocused(undefined); + groups.activeGroup.activeControl.focus(); + } +}); + +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts new file mode 100644 index 00000000000..f613d656546 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { equals } from 'vs/base/common/arrays'; +import { TimeoutTimer } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { size } from 'vs/base/common/collections'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { debounceEvent, Emitter, Event } from 'vs/base/common/event'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import * as paths from 'vs/base/common/paths'; +import { isEqual } from 'vs/base/common/resources'; +import URI from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IPosition } from 'vs/editor/common/core/position'; +import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; +import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; + +export class FileElement { + constructor( + readonly uri: URI, + readonly isFile: boolean + ) { } +} + +export type BreadcrumbElement = FileElement | OutlineGroup | OutlineElement; + +export class EditorBreadcrumbsModel { + + private readonly _disposables: IDisposable[] = []; + private readonly _fileElements: FileElement[] = []; + + private _outlineElements: (OutlineGroup | OutlineElement)[] = []; + private _outlineDisposables: IDisposable[] = []; + + private _onDidUpdate = new Emitter(); + readonly onDidUpdate: Event = this._onDidUpdate.event; + + constructor( + private readonly _uri: URI, + private readonly _editor: ICodeEditor | undefined, + @IWorkspaceContextService workspaceService: IWorkspaceContextService, + ) { + this._fileElements = EditorBreadcrumbsModel._getFileElements(this._uri, workspaceService); + this._bindToEditor(); + this._onDidUpdate.fire(this); + } + + dispose(): void { + dispose(this._disposables); + } + + getElements(): ReadonlyArray { + return [].concat(this._fileElements, this._outlineElements); + } + + private static _getFileElements(uri: URI, workspaceService: IWorkspaceContextService): FileElement[] { + let result: FileElement[] = []; + let workspace = workspaceService.getWorkspaceFolder(uri); + let path = uri.path; + while (path !== '/') { + if (workspace && isEqual(workspace.uri, uri)) { + break; + } + result.push(new FileElement(uri, result.length === 0)); + path = paths.dirname(path); + uri = uri.with({ path }); + } + return result.reverse(); + } + + private _bindToEditor(): void { + if (!this._editor) { + return; + } + // update as model changes + this._disposables.push(DocumentSymbolProviderRegistry.onDidChange(_ => this._updateOutline())); + this._disposables.push(this._editor.onDidChangeModel(_ => this._updateOutline())); + this._disposables.push(this._editor.onDidChangeModelLanguage(_ => this._updateOutline())); + this._disposables.push(debounceEvent(this._editor.onDidChangeModelContent, _ => _, 350)(_ => this._updateOutline(true))); + this._updateOutline(); + + // stop when editor dies + this._disposables.push(this._editor.onDidDispose(() => this._outlineDisposables = dispose(this._outlineDisposables))); + } + + private _updateOutline(didChangeContent?: boolean): void { + + this._outlineDisposables = dispose(this._outlineDisposables); + if (!didChangeContent) { + this._updateOutlineElements([]); + } + + const buffer = this._editor.getModel(); + if (!buffer || !DocumentSymbolProviderRegistry.has(buffer) || !isEqual(buffer.uri, this._uri)) { + return; + } + + const source = new CancellationTokenSource(); + const versionIdThen = buffer.getVersionId(); + const timeout = new TimeoutTimer(); + + this._outlineDisposables.push({ + dispose: () => { + source.cancel(); + source.dispose(); + timeout.dispose(); + } + }); + + OutlineModel.create(buffer, source.token).then(model => { + this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); + this._outlineDisposables.push(this._editor.onDidChangeCursorPosition(_ => { + timeout.cancelAndSet(() => { + if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId()) { + this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); + } + }, 150); + })); + }).catch(err => { + this._updateOutlineElements([]); + onUnexpectedError(err); + }); + } + + private _getOutlineElements(model: OutlineModel, position: IPosition): (OutlineGroup | OutlineElement)[] { + if (!model) { + return []; + } + let item: OutlineGroup | OutlineElement = model.getItemEnclosingPosition(position); + let chain: (OutlineGroup | OutlineElement)[] = []; + while (item) { + chain.push(item); + let parent = item.parent; + if (parent instanceof OutlineModel) { + break; + } + if (parent instanceof OutlineGroup && size(parent.parent.children) === 1) { + break; + } + item = parent; + } + return chain.reverse(); + } + + private _updateOutlineElements(elements: (OutlineGroup | OutlineElement)[]): void { + if (!equals(elements, this._outlineElements, EditorBreadcrumbsModel._outlineElementEquals)) { + this._outlineElements = elements; + this._onDidUpdate.fire(this); + } + } + + private static _outlineElementEquals(a: OutlineGroup | OutlineElement, b: OutlineGroup | OutlineElement): boolean { + if (a === b) { + return true; + } else if (!a || !b) { + return false; + } else { + return a.id === b.id; + } + } +} diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 1e4336156a6..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; } @@ -159,4 +160,4 @@ export interface EditorGroupsServiceImpl extends IEditorGroupsService { * A promise that resolves when groups have been restored. */ readonly whenRestored: TPromise; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 23253584873..372e4e6999d 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -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'; @@ -956,7 +956,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.doCloseInactiveEditor(editor); } - // Forward to title control + // Forward to title control & breadcrumbs this.titleAreaControl.closeEditor(editor); } @@ -1344,8 +1344,15 @@ 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.editorControl.layout(new Dimension(this.dimension.width, this.dimension.height - EDITOR_TITLE_HEIGHT)); + 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 { + if (this.dimension) { + const { width, height } = this.dimension; + this.layout(width, height); + } } toJSON(): ISerializedEditorGroup { diff --git a/src/vs/workbench/browser/parts/editor/media/breadcrumbs.css b/src/vs/workbench/browser/parts/editor/media/breadcrumbs.css new file mode 100644 index 00000000000..64d3d29c4b6 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/breadcrumbs.css @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * 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 .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-breadcrumbs .monaco-breadcrumb-item:nth-child(2) { /*first-child is the style-element*/ + padding-left: 8px; +} + +.monaco-breadcrumbs-picker { + border-style: solid; + border-width: 1px; +} + +.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control .monaco-icon-label::before { + height: 18px; /* tweak the icon size of the editor labels when icons are enabled */ +} diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index e7abc5b7357..623775e022e 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -44,8 +44,8 @@ .monaco-workbench > .part.editor > .content .editor-group-container > .title { position: relative; - height: 35px; display: flex; + flex-wrap: wrap; box-sizing: border-box; overflow: hidden; } @@ -112,4 +112,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..b81637b0a06 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css @@ -5,7 +5,7 @@ /* Title Label */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label { +.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container .title-label { line-height: 35px; overflow: hidden; text-overflow: ellipsis; @@ -13,7 +13,7 @@ padding-left: 20px; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { +.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container .monaco-icon-label::before { height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ } @@ -26,4 +26,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..f19919dd009 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -252,4 +252,11 @@ 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 .breadcrumbs-control { + flex: 1 100%; +} 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/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 8934bb9925f..10ff0952b04 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 { BreadcrumbsControl, BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; interface IEditorInputLabel { name: string; @@ -55,6 +57,7 @@ export class TabsTitleControl extends TitleControl { private editorToolbarContainer: HTMLElement; private scrollbar: ScrollableElement; private closeOneEditorAction: CloseOneEditorAction; + private breadcrumbs: BreadcrumbsControl; private tabLabelWidgets: ResourceLabel[] = []; private tabLabels: IEditorInputLabel[] = []; @@ -78,9 +81,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 +112,9 @@ export class TabsTitleControl extends TitleControl { // Close Action this.closeOneEditorAction = this._register(this.instantiationService.createInstance(CloseOneEditorAction, CloseOneEditorAction.ID, CloseOneEditorAction.LABEL)); + + // Breadcrumbs + this.createBreadcrumbsControl(); } private createScrollbar(): void { @@ -128,6 +135,24 @@ export class TabsTitleControl extends TitleControl { this.titleContainer.appendChild(this.scrollbar.getDomNode()); } + private createBreadcrumbsControl(): void { + const config = this._register(BreadcrumbsConfig.IsEnabled.bindTo(this.configurationService)); + config.onDidChange(value => { + if (!value && this.breadcrumbs) { + this.breadcrumbs.dispose(); + this.breadcrumbs = undefined; + this.group.relayout(); + } else if (value && !this.breadcrumbs) { + this.breadcrumbs = this.instantiationService.createInstance(BreadcrumbsControl, this.titleContainer, this.group); + this.breadcrumbs.update(); + this.group.relayout(); + } + }); + if (config.value) { + this.breadcrumbs = this.instantiationService.createInstance(BreadcrumbsControl, this.titleContainer, this.group); + } + } + private registerContainerListeners(): void { // Group dragging @@ -240,6 +265,11 @@ export class TabsTitleControl extends TitleControl { // Redraw all tabs this.redraw(); + + // Update Breadcrumbs + if (this.breadcrumbs) { + this.breadcrumbs.update(); + } } closeEditor(editor: IEditorInput): void { @@ -287,6 +317,11 @@ export class TabsTitleControl extends TitleControl { this.clearEditorActionsToolbar(); } + + // Update Breadcrumbs + if (this.breadcrumbs) { + this.breadcrumbs.update(); + } } moveEditor(editor: IEditorInput, fromIndex: number, targetIndex: number): void { @@ -885,6 +920,10 @@ export class TabsTitleControl extends TitleControl { } } + getPreferredHeight(): number { + return super.getPreferredHeight() + (this.breadcrumbs ? this.breadcrumbs.getPreferredHeight() : 0); + } + layout(dimension: Dimension): void { const activeTab = this.getTab(this.group.activeEditor); if (!activeTab || !dimension) { @@ -910,6 +949,13 @@ export class TabsTitleControl extends TitleControl { return; } + let breadcrumbsHeight = 0; + if (this.breadcrumbs) { + breadcrumbsHeight = this.breadcrumbs.getPreferredHeight(); + this.breadcrumbs.layout({ width: dimension.width, height: dimension.height - super.getPreferredHeight() }); + this.scrollbar.getDomNode().style.height = `${dimension.height - breadcrumbsHeight}px`; + } + const visibleContainerWidth = this.tabsContainer.offsetWidth; const totalContainerWidth = this.tabsContainer.scrollWidth; @@ -1039,6 +1085,7 @@ export class TabsTitleControl extends TitleControl { dispose(): void { super.dispose(); + this.breadcrumbs = dispose(this.breadcrumbs); this.layoutScheduled = dispose(this.layoutScheduled); } } @@ -1198,4 +1245,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..1316298e7f6 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -33,10 +33,11 @@ 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'; export interface IToolbarActions { primary: IAction[]; @@ -72,7 +73,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); @@ -329,6 +331,10 @@ export abstract class TitleControl extends Themable { // Optionally implemented in subclasses } + getPreferredHeight(): number { + return EDITOR_TITLE_HEIGHT; + } + dispose(): void { this.editorToolBarMenuDisposables = dispose(this.editorToolBarMenuDisposables); diff --git a/src/vs/workbench/browser/parts/menubar/media/menubarpart.css b/src/vs/workbench/browser/parts/menubar/media/menubarpart.css index 62b8c849063..55191232bcd 100644 --- a/src/vs/workbench/browser/parts/menubar/media/menubarpart.css +++ b/src/vs/workbench/browser/parts/menubar/media/menubarpart.css @@ -32,12 +32,15 @@ zoom: 1; } + .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: rgba(255, 255, 255, 0.1); } .monaco-workbench > .part.menubar.light > .menubar-menu-button.open, +.monaco-workbench > .part.menubar.light > .menubar-menu-button:focus, .monaco-workbench > .part.menubar.light > .menubar-menu-button:hover { background-color: rgba(0, 0, 0, 0.1); } diff --git a/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts b/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts index e1d7d69ccdd..3d651933593 100644 --- a/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts +++ b/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts @@ -1291,7 +1291,7 @@ function preferencesMenuRegistration() { MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { group: '1_settings', command: { - id: 'workbench.action.openSettings', + id: 'workbench.action.openSettings2', title: nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings") }, order: 1 diff --git a/src/vs/workbench/browser/parts/menubar/menubarPart.ts b/src/vs/workbench/browser/parts/menubar/menubarPart.ts index 31c84306e65..0c2586a7305 100644 --- a/src/vs/workbench/browser/parts/menubar/menubarPart.ts +++ b/src/vs/workbench/browser/parts/menubar/menubarPart.ts @@ -34,13 +34,22 @@ import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, getWorkspaceLabel } from 'vs/platform/workspaces/common/workspaces'; import { getPathLabel } from 'vs/base/common/labels'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { RunOnceScheduler } from 'vs/base/common/async'; interface CustomMenu { title: string; + buttonElement: Builder; titleElement: Builder; actions?: IAction[]; } +enum MenubarState { + HIDDEN, + VISIBLE, + FOCUSED, + OPEN +} + export class MenubarPart extends Part { private keys = [ @@ -74,7 +83,7 @@ export class MenubarPart extends Part { 'Selection': nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection"), 'View': nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View"), 'Go': nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go"), - 'Terminal': nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal"), + 'Terminal': nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "Ter&&minal"), 'Debug': nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug"), 'Tasks': nls.localize({ key: 'mTasks', comment: ['&& denotes a mnemonic'] }, "&&Tasks"), 'Help': nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help") @@ -82,17 +91,22 @@ 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 focusingWithAlt: boolean; private _modifierKeyStatus: IModifierKeyStatus; - private _isFocused: boolean; + private _focusState: MenubarState; + private _onVisibilityChange: Emitter; private initialSizing: { @@ -135,11 +149,11 @@ export class MenubarPart extends Part { this.topLevelMenus['Window'] = this._register(this.menuService.createMenu(MenuId.MenubarWindowMenu, this.contextKeyService)); } + this.menuUpdater = this._register(new RunOnceScheduler(() => this.doSetupMenubar(), 0)); + 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()); @@ -148,10 +162,10 @@ export class MenubarPart extends Part { for (let topLevelMenuName of Object.keys(this.topLevelMenus)) { this._register(this.topLevelMenus[topLevelMenuName].onDidChange(() => this.setupMenubar())); } - this.setupMenubar(); + this.doSetupMenubar(); } - this.isFocused = false; + this._focusState = MenubarState.HIDDEN; this.windowService.getRecentlyOpened().then((recentlyOpened) => { this.recentlyOpened = recentlyOpened; @@ -207,18 +221,105 @@ 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) { - 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._isFocused && this.currentMenubarVisibility === 'toggle') { - if (this.container) { - this.hideMenubar(); + if (this.updatePending) { + this.menuUpdater.schedule(); + this.updatePending = false; } } + + 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 { @@ -241,29 +342,42 @@ export class MenubarPart extends Part { this.container.style('visibility', null); } - private onModifierKeyToggled(modiferKeyStatus: IModifierKeyStatus): void { + private onModifierKeyToggled(modifierKeyStatus: IModifierKeyStatus): void { + const altKeyPressed = (!this._modifierKeyStatus || !this._modifierKeyStatus.altKey) && modifierKeyStatus.altKey; + const altKeyAlone = altKeyPressed && !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 && this.focusingWithAlt) { + if (!this.isFocused) { + this.focusedMenu = { index: 0 }; + this.focusState = MenubarState.FOCUSED; + } else if (!this.isOpen) { + this.focusState = this.currentMenubarVisibility === 'toggle' ? MenubarState.HIDDEN : MenubarState.VISIBLE; + } + } + + this._modifierKeyStatus = modifierKeyStatus; 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); } }); } + + this.focusingWithAlt = altKeyAlone; } private onRecentlyOpenedChange(): void { @@ -282,6 +396,9 @@ export class MenubarPart extends Part { // Listen to update service // this.updateService.onStateChange(() => this.setupMenubar()); + // Listen for context changes + this._register(this.contextKeyService.onDidChangeContext(() => this.setupMenubar())); + // Listen for changes in recently opened menu this._register(this.windowsService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); })); @@ -291,7 +408,7 @@ export class MenubarPart extends Part { this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this)); } - private setupMenubar(): void { + private doSetupMenubar(): void { if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { this.setupCustomMenubar(); } else { @@ -299,6 +416,10 @@ export class MenubarPart extends Part { } } + private setupMenubar(): void { + this.menuUpdater.schedule(); + } + private setupNativeMenubar(): void { // TODO@sbatten: Remove once native menubar is ready if (isMacintosh && isWindows) { @@ -306,6 +427,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(); } @@ -428,10 +554,18 @@ export class MenubarPart extends Part { } private setupCustomMenubar(): void { - this.container.empty(); + // Don't update while using the menu + if (this.isFocused) { + this.updatePending = true; + return; + } + this.container.attr('role', 'menubar'); - this.customMenus = []; + const firstTimeSetup = this.customMenus === undefined; + if (firstTimeSetup) { + this.customMenus = []; + } let idx = 0; @@ -439,26 +573,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(); @@ -486,56 +628,112 @@ 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.customMenus[menuIndex].buttonElement.domFocus(); + } + }); + + this.customMenus[menuIndex].buttonElement.on(EventType.MOUSE_LEAVE, () => { + if (!this.isOpen && this.isFocused) { + this.customMenus[menuIndex].buttonElement.domBlur(); + } + }); + } + } + + 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; } @@ -546,7 +744,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 { @@ -560,7 +764,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 { @@ -619,33 +829,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'); @@ -665,14 +857,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); })); @@ -728,7 +918,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({ @@ -739,7 +929,7 @@ export class MenubarPart extends Part { }); this.customMenus.forEach(customMenu => { - customMenu.titleElement.style({ + customMenu.buttonElement.style({ 'padding': `0 ${this.initialSizing.menuButtonPaddingLeftRight / browser.getZoomFactor()}px` }); }); @@ -756,8 +946,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); } @@ -773,7 +963,7 @@ export class MenubarPart extends Part { // Build the menubar if (this.container) { - this.setupMenubar(); + this.doSetupMenubar(); } return this.container.getHTMLElement(); 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/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index d7112d84a45..61f11d7209c 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -252,13 +252,15 @@ export class TitlebarPart extends Part implements ITitleService { // App Icon (Windows/Linux) if (!isMacintosh) { - this.appIcon = $(this.titleContainer).div({ - class: 'window-appicon', - }).on(EventType.DBLCLICK, e => { - EventHelper.stop(e, true); + this.appIcon = $(this.titleContainer).div({ class: 'window-appicon' }); - this.windowService.closeWindow().then(null, errors.onUnexpectedError); - }); + if (isWindows) { + this.appIcon.on(EventType.DBLCLICK, e => { + EventHelper.stop(e, true); + + this.windowService.closeWindow().then(null, errors.onUnexpectedError); + }); + } } // Title 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/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/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; + 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/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/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/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..1c5deb3215b 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; 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-toc-container { width: 175px; margin-right: 5px; - padding-top: 5px; } .settings-editor > .settings-body .settings-toc-container.hidden { @@ -197,11 +197,18 @@ 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; @@ -210,7 +217,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 { @@ -296,7 +302,7 @@ } .settings-editor > .settings-body > .settings-tree-container .settings-group-level-1.settings-group-first { - padding-top: 7px; + padding-top: 4px; } .settings-editor > .settings-body .settings-feedback-button { diff --git a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts index 9e11c49536a..8ae13de64d4 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts @@ -33,7 +33,7 @@ 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'; @@ -234,6 +234,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 +324,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, @@ -693,7 +704,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 35c087a34b6..16525a9d607 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,6 +26,7 @@ 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 { IOpenerService } from 'vs/platform/opener/common/opener'; import { registerColor, selectBackground, selectBorder } 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'; @@ -97,6 +100,7 @@ export interface ITOCEntry { export class SettingsTreeModel { private _root: SettingsTreeGroupElement; private _treeElementsById = new Map(); + private _treeElementsBySettingName = new Map(); constructor( private _viewState: ISettingsEditorViewState, @@ -125,6 +129,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 +161,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; } } @@ -444,12 +453,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')); } @@ -457,7 +470,7 @@ export class SettingsRenderer implements IRenderer { getHeight(tree: ITree, element: SettingsTreeElement): number { if (element instanceof SettingsTreeGroupElement) { if (element.isFirstGroup) { - return 31; + return 28; } return 40 + (7 * element.level); @@ -677,7 +690,28 @@ 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); this.renderValue(element, isSelected, template); @@ -842,6 +876,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/preferences/electron-browser/preferences.contribution.ts b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts index a38ea158de6..b0ea7b33c62 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts @@ -191,8 +191,8 @@ Registry.as(EditorInputExtensions.EditorInputFactor const category = nls.localize('preferences', "Preferences"); const registry = Registry.as(Extensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenRawDefaultSettingsAction, OpenRawDefaultSettingsAction.ID, OpenRawDefaultSettingsAction.LABEL), 'Preferences: Open Raw Default Settings', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettingsAction, OpenSettingsAction.ID, OpenSettingsAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_COMMA }), 'Preferences: Open Settings', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettings2Action, OpenSettings2Action.ID, OpenSettings2Action.LABEL), 'Preferences: Open Settings (Preview)', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettingsAction, OpenSettingsAction.ID, OpenSettingsAction.LABEL), 'Preferences: Open Settings', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettings2Action, OpenSettings2Action.ID, OpenSettings2Action.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_COMMA }), 'Preferences: Open Settings (Preview)', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalSettingsAction, OpenGlobalSettingsAction.ID, OpenGlobalSettingsAction.LABEL), 'Preferences: Open User Settings', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalKeybindingsAction, OpenGlobalKeybindingsAction.ID, OpenGlobalKeybindingsAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_S) }), 'Preferences: Open Keyboard Shortcuts', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalKeybindingsFileAction, OpenGlobalKeybindingsFileAction.ID, OpenGlobalKeybindingsFileAction.LABEL, { primary: null }), 'Preferences: Open Keyboard Shortcuts File', category); 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..6d43b759884 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -51,7 +51,7 @@ display: none; } -.scm-viewlet .scm-provider > .name > .type { +.scm-viewlet .scm-provider > .type { opacity: 0.7; margin-left: 0.5em; font-size: 0.9em; 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/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/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts index 286854eba1b..c753ae9b9cc 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -625,7 +625,7 @@ export class RunActiveFileInTerminalAction extends Action { if (!instance) { return TPromise.as(void 0); } - const editor = this.codeEditorService.getFocusedCodeEditor(); + const editor = this.codeEditorService.getActiveCodeEditor(); if (!editor) { return TPromise.as(void 0); } diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index 110be30861b..17f4f5930e5 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -277,7 +277,7 @@ class CommandAction extends Action { export class UpdateContribution implements IGlobalActivity { private static readonly showCommandsId = 'workbench.action.showCommands'; - private static readonly openSettingsId = 'workbench.action.openSettings'; + private static readonly openSettingsId = 'workbench.action.openSettings2'; private static readonly openKeybindingsId = 'workbench.action.openGlobalKeybindings'; private static readonly openUserSnippets = 'workbench.action.openSnippets'; private static readonly selectColorThemeId = 'workbench.action.selectTheme'; 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/group/common/editorGroupsService.ts b/src/vs/workbench/services/group/common/editorGroupsService.ts index dc1595f47d1..6bc438e07a4 100644 --- a/src/vs/workbench/services/group/common/editorGroupsService.ts +++ b/src/vs/workbench/services/group/common/editorGroupsService.ts @@ -476,4 +476,4 @@ export interface IEditorGroup { * Invoke a function in the context of the services of this group. */ invokeWithinContext(fn: (accessor: ServicesAccessor) => T): T; -} \ No newline at end of file +} 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 dcfb3c3db8e..91834f3c67a 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; } @@ -384,17 +367,22 @@ export class FileWalker { cb(err, stdout, last); }; + let gotData = false; if (cmd.stdout) { // Should be non-null, but #38195 this.forwardData(cmd.stdout, encoding, onData); + cmd.stdout.once('data', () => gotData = true); } else { onMessage({ message: 'stdout is null' }); } - const stderr = this.collectData(cmd.stderr); - - let gotData = false; - cmd.stdout.once('data', () => gotData = true); + let stderr: Buffer[]; + if (cmd.stderr) { + // Should be non-null, but #38195 + stderr = this.collectData(cmd.stderr); + } else { + onMessage({ message: 'stderr is null' }); + } cmd.on('error', (err: Error) => { onData(err); @@ -513,25 +501,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); }); } @@ -551,18 +525,6 @@ 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; 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/breadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts new file mode 100644 index 00000000000..6e4f476f5e6 --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * 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/breadcrumbsModel'; +import { TestContextService } from 'vs/workbench/test/workbenchTestServices'; + + +suite('Breadcrumb Model', function () { + + const workspaceService = new TestContextService(new Workspace('ffff', 'Test', [new WorkspaceFolder({ uri: URI.parse('foo:/bar/baz/ws'), name: 'ws', index: 0 })])); + + test('only uri, inside workspace', function () { + + let model = new EditorBreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, workspaceService); + let elements = model.getElements(); + + assert.equal(elements.length, 3); + let [one, two, three] = elements as FileElement[]; + assert.equal(one.isFile, false); + assert.equal(two.isFile, false); + assert.equal(three.isFile, true); + assert.equal(one.uri.toString(), 'foo:/bar/baz/ws/some'); + assert.equal(two.uri.toString(), 'foo:/bar/baz/ws/some/path'); + assert.equal(three.uri.toString(), 'foo:/bar/baz/ws/some/path/file.ts'); + }); + + test('only uri, outside workspace', function () { + + let model = new EditorBreadcrumbsModel(URI.parse('foo:/outside/file.ts'), undefined, workspaceService); + let elements = model.getElements(); + + assert.equal(elements.length, 2); + let [one, two] = elements as FileElement[]; + assert.equal(one.isFile, false); + assert.equal(two.isFile, true); + assert.equal(one.uri.toString(), 'foo:/outside'); + assert.equal(two.uri.toString(), 'foo:/outside/file.ts'); + }); +}); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 5ea115ee115..14a6da297de 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -673,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 { diff --git a/test/smoke/src/areas/workbench/viewlet.ts b/test/smoke/src/areas/workbench/viewlet.ts index 2293752b705..33e5a4549bd 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-container .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'));