diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index cbcde9bb109..fdd03c400cc 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -4,6 +4,11 @@ "version": "1.0.8", "resolved": "ghcr.io/devcontainers/features/desktop-lite@sha256:e7dc4d37ab9e3d6e7ebb221bac741f5bfe07dae47025399d038b17af2ed8ddb7", "integrity": "sha256:e7dc4d37ab9e3d6e7ebb221bac741f5bfe07dae47025399d038b17af2ed8ddb7" + }, + "ghcr.io/devcontainers/features/rust:1": { + "version": "1.1.3", + "resolved": "ghcr.io/devcontainers/features/rust@sha256:aba6f47303b197976902bf544c786b5efecc03c238ff593583e5e74dfa9c7ccb", + "integrity": "sha256:aba6f47303b197976902bf544c786b5efecc03c238ff593583e5e74dfa9c7ccb" } } } \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f5adc4e1c46..75076a0f8b6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,8 @@ "dockerfile": "Dockerfile" }, "features": { - "ghcr.io/devcontainers/features/desktop-lite:1": {} + "ghcr.io/devcontainers/features/desktop-lite:1": {}, + "ghcr.io/devcontainers/features/rust:1": {} }, "containerEnv": { "DISPLAY": "" // Allow the Dev Containers extension to set DISPLAY, post-create.sh will add it back in ~/.bashrc and ~/.zshrc if not set. diff --git a/.eslintrc.json b/.eslintrc.json index c39a66311e4..8dafc03b087 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -755,7 +755,8 @@ "vs/base/~", "vs/base/parts/*/~", "vs/platform/*/~", - "vs/editor/~" + "vs/editor/~", + "@vscode/tree-sitter-wasm" // node module allowed even in /common/ ] }, { @@ -1032,7 +1033,7 @@ "restrictions": [] }, { - "target": "src/{bootstrap-amd.js,bootstrap-fork.js,bootstrap-node.js,bootstrap-window.js,bootstrap.js,cli.js,main.js,server-cli.js,server-main.js}", + "target": "src/{bootstrap-amd.js,bootstrap-fork.js,bootstrap-node.js,bootstrap-window.js,cli.js,main.js,server-cli.js,server-main.js}", "restrictions": [] } ] diff --git a/.github/commands.json b/.github/commands.json index df9fc791fd5..38da97915a2 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -156,6 +156,37 @@ "addLabel": "confirmation-pending", "removeLabel": "confirmed" }, + { + "type": "comment", + "name": "needsMoreInfo", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "updateLabels", + "addLabel": "~info-needed" + }, + { + "type": "comment", + "name": "needsPerfInfo", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "addLabel": "info-needed", + "comment": "Thanks for creating this issue regarding performance! Please follow this guide to help us diagnose performance issues: https://github.com/microsoft/vscode/wiki/Performance-Issues \n\nHappy Coding!" + }, + { + "type": "comment", + "name": "jsDebugLogs", + "action": "updateLabels", + "addLabel": "info-needed", + "comment": "Please collect trace logs using the following instructions:\n\n> If you're able to, add `\"trace\": true` to your `launch.json` and reproduce the issue. The location of the log file on your disk will be written to the Debug Console. Share that with us.\n>\n> ⚠️ This log file will not contain source code, but will contain file paths. You can drop it into https://microsoft.github.io/vscode-pwa-analyzer/index.html to see what it contains. If you'd rather not share the log publicly, you can email it to connor@xbox.com" + }, { "type": "comment", "name": "closedWith", @@ -169,6 +200,19 @@ "reason": "completed", "addLabel": "unreleased" }, + { + "type": "comment", + "name": "spam", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "close", + "reason": "not_planned", + "addLabel": "invalid" + }, { "type": "comment", "name": "a11ymas", @@ -414,6 +458,32 @@ "addLabel": "*caused-by-extension", "comment": "It looks like this is caused by the Copilot extension. Please file the issue in the [Copilot Discussion Forum](https://github.com/community/community/discussions/categories/copilot). Make sure to check their issue reporting template and provide them relevant information such as the extension version you're using. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting) for more information.\n\nHappy Coding!" }, + { + "type": "comment", + "name": "gifPlease", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "comment", + "addLabel": "info-needed", + "comment": "Thanks for reporting this issue! Unfortunately, it's hard for us to understand what issue you're seeing. Please help us out by providing a screen recording showing exactly what isn't working as expected. While we can work with most standard formats, `.gif` files are preferred as they are displayed inline on GitHub. You may find https://gifcap.dev helpful as a browser-based gif recording tool.\n\nIf the issue depends on keyboard input, you can help us by enabling screencast mode for the recording (`Developer: Toggle Screencast Mode` in the command palette). Lastly, please attach this file via the GitHub web interface as emailed responses will strip files out from the issue.\n\nHappy coding!" + }, + { + "type": "comment", + "name": "confirmPlease", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "comment", + "addLabel": "info-needed", + "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" + }, { "__comment__": "Allows folks on the team to label issues by commenting: `\\label My-Label` ", "type": "comment", @@ -444,5 +514,29 @@ "addLabel": "verification-steps-needed", "removeLabel": "~verification-steps-needed", "comment": "Friendly ping! Looks like this issue requires some further steps to be verified. Please provide us with the steps necessary to verify this issue." + }, + { + "type": "label", + "name": "~info-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~info-needed", + "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 guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~version-info-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~version-info-needed", + "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~confirmation-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~confirmation-needed", + "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" } ] diff --git a/.github/workflows/author-verified.yml b/.github/workflows/author-verified.yml deleted file mode 100644 index f914be2f71b..00000000000 --- a/.github/workflows/author-verified.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Author Verified -on: - issues: - types: [closed] - -# also make changes in ./on-label.yml -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - if: contains(github.event.issue.labels.*.name, 'author-verification-requested') && contains(github.event.issue.labels.*.name, 'insiders-released') - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - if: contains(github.event.issue.labels.*.name, 'author-verification-requested') && contains(github.event.issue.labels.*.name, 'insiders-released') - run: npm install --production --prefix ./actions - - name: Run Author Verified - if: contains(github.event.issue.labels.*.name, 'author-verification-requested') && contains(github.event.issue.labels.*.name, 'insiders-released') - uses: ./actions/author-verified - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - requestVerificationComment: "This bug has been fixed in the latest release of [VS Code Insiders](https://code.visualstudio.com/insiders/)!\n\n@${author}, you can help us out by commenting `/verified` if things are now working as expected.\n\nIf things still don't seem right, please ensure you're on version ${commit} of Insiders (today's or later - you can use `Help: About` in the command palette to check), and leave a comment letting us know what isn't working as expected.\n\nHappy Coding!" - releasedLabel: insiders-released - verifiedLabel: verified - authorVerificationRequestedLabel: author-verification-requested diff --git a/.github/workflows/bad-tag.yml b/.github/workflows/bad-tag.yml deleted file mode 100644 index bc964fb0582..00000000000 --- a/.github/workflows/bad-tag.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Bad Tag -on: - create - -jobs: - main: - runs-on: ubuntu-latest - if: github.event.ref == '1.999.0' - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run Bad Tag - uses: ./actions/tag-alert - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - tag-name: '1.999.0' diff --git a/.github/workflows/deep-classifier-assign-monitor.yml b/.github/workflows/deep-classifier-assign-monitor.yml deleted file mode 100644 index a61f9cfb137..00000000000 --- a/.github/workflows/deep-classifier-assign-monitor.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: "Deep Classifier: Assign Monitor" -on: - issues: - types: [assigned] - -jobs: - main: - runs-on: ubuntu-latest - if: ${{ contains(github.event.issue.labels.*.name, 'triage-needed') }} - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: "Run Classifier: Monitor" - uses: ./actions/classifier-deep/monitor - with: - botName: VSCodeTriageBot - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml deleted file mode 100644 index 7145de06db5..00000000000 --- a/.github/workflows/deep-classifier-runner.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: "Deep Classifier: Runner" - -permissions: - id-token: write - contents: read - -on: - schedule: - - cron: 0 * * * * - workflow_dispatch: - repository_dispatch: - types: [trigger-deep-classifier-runner] - -jobs: - main: - runs-on: ubuntu-latest - environment: main - steps: - - uses: azure/login@v2 - with: - client-id: ${{ vars.AZURE_CLIENT_ID }} - tenant-id: ${{ vars.AZURE_TENANT_ID }} - allow-no-subscriptions: true - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Install Additional Dependencies - # Pulls in a bunch of other packages that arent needed for the rest of the actions - run: npm install @azure/storage-blob@12.1.1 mongodb@2.2.31 - - name: "Run Classifier: Scraper" - uses: ./actions/classifier-deep/apply/fetch-sources - with: - # slightly overlapping to protect against issues slipping through the cracks if a run is delayed - until: 5 - excludeLabels: feature-request|testplan-item - configPath: classifier - blobContainerName: vscode-issue-classifier - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - - name: Set up Python 3.7 - uses: actions/setup-python@v5 - with: - python-version: 3.7 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install --upgrade numpy==1.20.0 scipy==1.6.0 scikit-learn==0.24.1 joblib==1.0.0 nltk==3.5 simpletransformers==0.51.16 torch==1.7.1 torchvision==0.8.2 - - name: "Run Classifier: Generator" - run: python ./actions/classifier-deep/apply/generate-labels/main.py - - name: "Run Classifier: Labeler" - uses: ./actions/classifier-deep/apply/apply-labels - with: - configPath: classifier - allowLabels: "info-needed|new release|error-telemetry|*english-please|translation-required" - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/deep-classifier-scraper.yml b/.github/workflows/deep-classifier-scraper.yml deleted file mode 100644 index e663372fad0..00000000000 --- a/.github/workflows/deep-classifier-scraper.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: "Deep Classifier: Scraper" - -permissions: - id-token: write - contents: read - -on: - schedule: - - cron: 0 0 15 * * # 15th of the month - workflow_dispatch: - repository_dispatch: - types: [trigger-deep-classifier-scraper] - -jobs: - main: - runs-on: ubuntu-latest - environment: main - steps: - - uses: azure/login@v2 - with: - client-id: ${{ vars.AZURE_CLIENT_ID }} - tenant-id: ${{ vars.AZURE_TENANT_ID }} - allow-no-subscriptions: true - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Install Additional Dependencies - # Pulls in a bunch of other packages that arent needed for the rest of the actions - run: npm install @azure/storage-blob@12.1.1 - - name: "Run Classifier: Scraper" - uses: ./actions/classifier-deep/train/fetch-issues - with: - blobContainerName: vscode-issue-classifier - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/deep-classifier-unassign-monitor.yml b/.github/workflows/deep-classifier-unassign-monitor.yml deleted file mode 100644 index 52ac0d3ddcd..00000000000 --- a/.github/workflows/deep-classifier-unassign-monitor.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: "Deep Classifier: Unassign Monitor" -on: - issues: - types: [unassigned] - -jobs: - main: - runs-on: ubuntu-latest - if: ${{ ! contains(github.event.issue.labels.*.name, 'triage-needed') }} - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: "Run Classifier: Monitor" - uses: ./actions/classifier-deep/monitor - with: - botName: VSCodeTriageBot - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/english-please.yml b/.github/workflows/english-please.yml deleted file mode 100644 index 9e04d6d549c..00000000000 --- a/.github/workflows/english-please.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: English Please -on: - issues: - types: [edited] - -# also make changes in ./on-label.yml and ./on-open.yml -jobs: - main: - runs-on: ubuntu-latest - if: contains(github.event.issue.labels.*.name, '*english-please') - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run English Please - uses: ./actions/english-please - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - cognitiveServicesAPIKey: ${{secrets.AZURE_TEXT_TRANSLATOR_KEY}} - nonEnglishLabel: "*english-please" - needsMoreInfoLabel: "info-needed" - translatorRequestedLabelPrefix: "translation-required-" - translatorRequestedLabelColor: "c29cff" diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml deleted file mode 100644 index 83c0a9705c5..00000000000 --- a/.github/workflows/feature-request.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Feature Request Manager -on: - repository_dispatch: - types: [trigger-feature-request-manager] - issues: - types: [milestoned] - schedule: - - cron: 20 2 * * * # 4:20am Zurich - -# also make changes in ./on-label.yml -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') - run: npm install --production --prefix ./actions - - name: Run Feature Request Manager - if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') - uses: ./actions/feature-request - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - candidateMilestoneID: 107 - candidateMilestoneName: Backlog Candidates - backlogMilestoneID: 8 - featureRequestLabel: feature-request - upvotesRequired: 20 - numCommentsOverride: 20 - initComment: "This feature request is now a candidate for our backlog. The community has 60 days to [upvote](https://github.com/microsoft/vscode/wiki/Issues-Triaging#up-voting-a-feature-request) the issue. If it receives 20 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - warnComment: "This feature request has not yet received the 20 community [upvotes](https://github.com/microsoft/vscode/wiki/Issues-Triaging#up-voting-a-feature-request) it takes to make to our backlog. 10 days to go. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - acceptComment: ":slightly_smiling_face: This feature request received a sufficient number of community upvotes and we moved it to our backlog. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - rejectComment: ":slightly_frowning_face: In the last 60 days, this feature request has received less than 20 community upvotes and we closed it. Still a big Thank You to you for taking the time to create this issue! To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - warnDays: 10 - closeDays: 60 - milestoneDelaySeconds: 60 diff --git a/.github/workflows/latest-release-monitor.yml b/.github/workflows/latest-release-monitor.yml deleted file mode 100644 index f7392dc24a8..00000000000 --- a/.github/workflows/latest-release-monitor.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Latest Release Monitor -on: - schedule: - - cron: 0/5 * * * * - repository_dispatch: - types: [trigger-latest-release-monitor] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Install Storage Module - run: npm install @azure/storage-blob@12.1.1 - - name: Run Latest Release Monitor - uses: ./actions/latest-release-monitor - with: - storageKey: ${{secrets.AZURE_BLOB_STORAGE_CONNECTION_STRING}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml deleted file mode 100644 index ef775ce8fdf..00000000000 --- a/.github/workflows/locker.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Locker -on: - schedule: - - cron: 20 23 * * * # 4:20pm Redmond - repository_dispatch: - types: [trigger-locker] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run Locker - uses: ./actions/locker - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - daysSinceClose: 45 - daysSinceUpdate: 3 - ignoredLabel: "*out-of-scope,accessibility" - ignoreLabelUntil: "author-verification-requested" - ignoredMilestones: "Backlog Candidates" - labelUntil: "verified" diff --git a/.github/workflows/needs-more-info-closer.yml b/.github/workflows/needs-more-info-closer.yml deleted file mode 100644 index 8db8a4246a3..00000000000 --- a/.github/workflows/needs-more-info-closer.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: info-needed Closer -on: - schedule: - - cron: 20 11 * * * # 4:20am Redmond - repository_dispatch: - types: [trigger-needs-more-info] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run info-needed Closer - uses: ./actions/needs-more-info-closer - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - label: info-needed - closeDays: 7 - additionalTeam: "cleidigh|usernamehw|gjsjohnmurray|IllusionMH" - closeComment: "This issue has been closed automatically because it needs more information and has not had recent activity. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" - pingDays: 80 - pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information." diff --git a/.github/workflows/on-comment.yml b/.github/workflows/on-comment.yml deleted file mode 100644 index 089aa77c1e7..00000000000 --- a/.github/workflows/on-comment.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: On Comment -on: - issue_comment: - types: [created] - -# also make changes in ./on-label.yml -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run Commands - uses: ./actions/commands - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - config-path: commands - - name: "Run Release Pipeline Labeler" - uses: ./actions/release-pipeline - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - notYetReleasedLabel: unreleased - insidersReleasedLabel: insiders-released diff --git a/.github/workflows/on-label.yml b/.github/workflows/on-label.yml deleted file mode 100644 index bf563734017..00000000000 --- a/.github/workflows/on-label.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: On Label -on: - issues: - types: [labeled] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - # source of truth in ./author-verified.yml - - name: Run Author Verified - if: contains(github.event.issue.labels.*.name, 'author-verification-requested') && contains(github.event.issue.labels.*.name, 'insiders-released') - uses: ./actions/author-verified - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - requestVerificationComment: "This bug has been fixed in the latest release of [VS Code Insiders](https://code.visualstudio.com/insiders/)!\n\n@${author}, you can help us out by commenting `/verified` if things are now working as expected.\n\nIf things still don't seem right, please ensure you're on version ${commit} of Insiders (today's or later - you can use `Help: About` in the command palette to check), and leave a comment letting us know what isn't working as expected.\n\nHappy Coding!" - releasedLabel: insiders-released - verifiedLabel: verified - authorVerificationRequestedLabel: author-verification-requested - - - # also make changes in ./on-comment.yml - - name: Run Commands - uses: ./actions/commands - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - config-path: commands - - # source of truth in ./feature-request.yml - - name: Run Feature Request Manager - if: contains(github.event.issue.labels.*.name, 'feature-request') - uses: ./actions/feature-request - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - candidateMilestoneID: 107 - candidateMilestoneName: Backlog Candidates - backlogMilestoneID: 8 - featureRequestLabel: feature-request - upvotesRequired: 20 - numCommentsOverride: 20 - initComment: "This feature request is now a candidate for our backlog. The community has 60 days to upvote the issue. If it receives 20 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - warnComment: "This feature request has not yet received the 20 community upvotes it takes to make to our backlog. 10 days to go. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding" - acceptComment: ":slightly_smiling_face: This feature request received a sufficient number of community upvotes and we moved it to our backlog. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - rejectComment: ":slightly_frowning_face: In the last 60 days, this feature request has received less than 20 community upvotes and we closed it. Still a big Thank You to you for taking the time to create this issue! To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - warnDays: 10 - closeDays: 60 - milestoneDelaySeconds: 60 - - # source of truth in ./test-plan-item-validator.yml - - name: Run Test Plan Item Validator - if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') - uses: ./actions/test-plan-item-validator - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - refLabel: on-testplan - label: testplan-item - invalidLabel: invalid-testplan-item - comment: Invalid test plan item. See errors below and the [test plan item spec](https://github.com/microsoft/vscode/wiki/Writing-Test-Plan-Items) for more information. This comment will go away when the issues are resolved. - - # source of truth in ./english-please.yml - - name: Run English Please - if: contains(github.event.issue.labels.*.name, '*english-please') - uses: ./actions/english-please - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - cognitiveServicesAPIKey: ${{secrets.AZURE_TEXT_TRANSLATOR_KEY}} - nonEnglishLabel: "*english-please" - needsMoreInfoLabel: "info-needed" - translatorRequestedLabelPrefix: "translation-required-" - translatorRequestedLabelColor: "c29cff" diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml deleted file mode 100644 index 2a26794c6b0..00000000000 --- a/.github/workflows/on-open.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: On Open -on: - issues: - types: [opened] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - - name: Check for Validity - uses: ./actions/validity-checker - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - - - name: Run CopyCat (VSCodeTriageBot/testissues) - if: github.event.issue.user.login != 'ghost' - uses: ./actions/copycat - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - owner: VSCodeTriageBot - repo: testissues - - - name: Run New Release - if: github.event.issue.user.login != 'ghost' - uses: ./actions/new-release - with: - label: new release - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - labelColor: "006b75" - labelDescription: Issues found in a recent release of VS Code - oldVersionMessage: "Thanks for creating this issue! It looks like you may be using an old version of VS Code, the latest stable release is {currentVersion}. Please try upgrading to the latest version and checking whether this issue remains.\n\nHappy Coding!" - days: 5 - - - name: Run Clipboard Labeler - if: github.event.issue.user.login != 'ghost' - uses: ./actions/regex-labeler - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - label: "invalid" - mustNotMatch: "^We have written the needed data into your clipboard because it was too large to send\\. Please paste\\.$" - comment: "It looks like you're using the VS Code Issue Reporter but did not paste the text generated into the created issue. We've closed this issue, please open a new one containing the text we placed in your clipboard.\n\nHappy Coding!" - - - name: Run Clipboard Labeler (Chinese) - if: github.event.issue.user.login != 'ghost' - uses: ./actions/regex-labeler - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - label: "invalid" - mustNotMatch: "^所需的数据太大,无法直接发送。我们已经将其写入剪贴板,请粘贴。$" - comment: "看起来您正在使用 VS Code 问题报告程序,但是没有将生成的文本粘贴到创建的问题中。我们将关闭这个问题,请使用剪贴板中的内容创建一个新的问题。\n\n祝您使用愉快!" - - # source of truth in ./english-please.yml - - name: Run English Please - if: github.event.issue.user.login != 'ghost' - uses: ./actions/english-please - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - cognitiveServicesAPIKey: ${{secrets.AZURE_TEXT_TRANSLATOR_KEY}} - nonEnglishLabel: "*english-please" - needsMoreInfoLabel: "info-needed" - translatorRequestedLabelPrefix: "translation-required-" - translatorRequestedLabelColor: "c29cff" - # source of truth in ./test-plan-item-validator.yml - - name: Run Test Plan Item Validator - if: github.event.issue.user.login != 'ghost' - uses: ./actions/test-plan-item-validator - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - refLabel: on-testplan - label: testplan-item - invalidLabel: invalid-testplan-item - comment: Invalid test plan item. See errors below and the [test plan item spec](https://github.com/microsoft/vscode/wiki/Writing-Test-Plan-Items) for more information. This comment will go away when the issues are resolved. - diff --git a/.github/workflows/on-reopen.yml b/.github/workflows/on-reopen.yml deleted file mode 100644 index d29de326c53..00000000000 --- a/.github/workflows/on-reopen.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: On Reopen -on: - issues: - types: [reopened] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - - name: Check for Validity - uses: ./actions/validity-checker - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/release-pipeline-labeler.yml b/.github/workflows/release-pipeline-labeler.yml deleted file mode 100644 index 87e188a02ab..00000000000 --- a/.github/workflows/release-pipeline-labeler.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: "Release Pipeline Labeler" -on: - issues: - types: [closed, reopened] - repository_dispatch: - types: [released-insider] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Checkout Repo - if: github.event_name != 'issues' - uses: actions/checkout@v4 - with: - path: ./repo - fetch-depth: 0 - - name: Install Actions - run: npm install --production --prefix ./actions - - name: "Run Release Pipeline Labeler" - uses: ./actions/release-pipeline - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - notYetReleasedLabel: unreleased - insidersReleasedLabel: insiders-released diff --git a/.github/workflows/telemetry.yml b/.github/workflows/telemetry.yml index d463a0e2eca..d29ea6c58da 100644 --- a/.github/workflows/telemetry.yml +++ b/.github/workflows/telemetry.yml @@ -16,4 +16,4 @@ jobs: - name: 'Run vscode-telemetry-extractor' run: 'npx --package=@vscode/telemetry-extractor --yes vscode-telemetry-extractor -s .' env: - GITHUB_TOKEN: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml deleted file mode 100644 index 117eaf6908a..00000000000 --- a/.github/workflows/test-plan-item-validator.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Test Plan Item Validator -on: - issues: - types: [edited] - -# also edit in ./on-label.yml and ./on-open.yml -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') - run: npm install --production --prefix ./actions - - name: Run Test Plan Item Validator - if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') - uses: ./actions/test-plan-item-validator - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - refLabel: on-testplan - label: testplan-item - invalidLabel: invalid-testplan-item - comment: Invalid test plan item. See errors below and the [test plan item spec](https://github.com/microsoft/vscode/wiki/Writing-Test-Plan-Items) for more information. This comment will go away when the issues are resolved. diff --git a/.nvmrc b/.nvmrc index bc78e9f2695..48b14e6b2b5 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.12.1 +20.14.0 diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index f472098cd14..3548b00ba81 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -4,6 +4,7 @@ "description": "Test provider for the VS Code project", "enabledApiProposals": [ "testObserver", + "testRelatedCode", "attributableCoverage" ], "engines": { @@ -83,6 +84,7 @@ "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "ansi-styles": "^5.2.0", + "cockatiel": "^3.1.3", "istanbul-to-vscode": "^2.0.1" } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index 491f67ee300..2732ef3b3f6 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -20,6 +20,7 @@ import { itemData, } from './testTree'; import { BrowserTestRunner, PlatformTestRunner, VSCodeTestRunner } from './vscodeTestRunner'; +import { ImportGraph } from './importGraph'; const TEST_FILE_PATTERN = 'src/vs/**/*.{test,integrationTest}.ts'; @@ -51,10 +52,15 @@ export async function activate(context: vscode.ExtensionContext) { }, })); - - ctrl.resolveHandler = async test => { + let initialWatchPromise: Promise | undefined; + const resolveHandler = async (test?: vscode.TestItem) => { if (!test) { - context.subscriptions.push(await startWatchingWorkspace(ctrl, fileChangedEmitter)); + if (!initialWatchPromise) { + initialWatchPromise = startWatchingWorkspace(ctrl, fileChangedEmitter); + context.subscriptions.push(await initialWatchPromise); + } else { + await initialWatchPromise; + } return; } @@ -66,10 +72,24 @@ export async function activate(context: vscode.ExtensionContext) { } }; + ctrl.resolveHandler = resolveHandler; + guessWorkspaceFolder().then(folder => { - if (folder) { - context.subscriptions.push(new FailureTracker(context, folder.uri.fsPath)); + if (!folder) { + return; } + + const graph = new ImportGraph( + folder.uri, async () => { + await resolveHandler(); + return [...ctrl.items].map(([, item]) => item); + }, uri => ctrl.items.get(uri.toString().toLowerCase())); + ctrl.relatedCodeProvider = graph; + + context.subscriptions.push( + new FailureTracker(context, folder.uri.fsPath), + fileChangedEmitter.event(e => graph.didChange(e.uri, e.removed)), + ); }); const createRunHandler = ( diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/importGraph.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/importGraph.ts new file mode 100644 index 00000000000..ab3c25720ac --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/importGraph.ts @@ -0,0 +1,239 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { join } from 'path'; +import * as vscode from 'vscode'; +import { bulkhead } from 'cockatiel'; +import { promises as fs } from 'fs'; + +const maxInt32 = 2 ** 31 - 1; + +// limit concurrency to avoid overwhelming the filesystem during discovery +const discoverLimiter = bulkhead(8, Infinity); + +// Max import distance when listing related code to improve relevancy. +const defaultMaxDistance = 3; + +/** + * Maintains a graph of imports in the codebase. This works lazily resolving + * imports and re-parsing files only on request. + * + * This is a rough, file-level graph derived from simple regex matching on + * source files to avoid having to parse the AST of every file in the codebase, + * which is possible but more intensive. (See: all the years of work from the + * TS language server.) + * + * A more advanced implementation could use references from the language server. + */ +export class ImportGraph implements vscode.TestRelatedCodeProvider { + private graph = new Map(); + + constructor( + private readonly root: vscode.Uri, + private readonly discoverWorkspaceTests: () => Thenable, + private readonly getTestNodeForDoc: (uri: vscode.Uri) => vscode.TestItem | undefined, + ) { } + + /** @inheritdoc */ + public async provideRelatedCode(test: vscode.TestItem, token: vscode.CancellationToken): Promise { + // this is kind of a stub for this implementation. Naive following imports + // isn't that useful for finding a test's related code. + const node = await this.discoverOutwards(test.uri, new Set(), defaultMaxDistance, token); + if (!node) { + return []; + } + + const imports = new Set(); + const queue = [{ distance: 0, next: node.imports }]; + while (queue.length) { + const { distance, next } = queue.shift()!; + for (const imp of next) { + if (imports.has(imp.path)) { + continue; + } + + imports.add(imp.path); + if (distance < defaultMaxDistance) { + queue.push({ next: imp.imports, distance: distance + 1 }); + } + } + } + + return [...imports].map(importPath => + new vscode.Location( + vscode.Uri.file(join(this.root.fsPath, 'src', `${importPath}.ts`)), + new vscode.Range(0, 0, maxInt32, 0), + ), + ); + } + + /** @inheritdoc */ + public async provideRelatedTests(document: vscode.TextDocument, _position: vscode.Position, token: vscode.CancellationToken): Promise { + // Expand all known tests to ensure imports of this file are realized. + const rootTests = await this.discoverWorkspaceTests(); + const seen = new Set(); + await Promise.all(rootTests.map(v => v.uri && this.discoverOutwards(v.uri, seen, defaultMaxDistance, token))); + + const node = this.getNode(document.uri); + if (!node) { + return []; + } + + const tests: vscode.TestItem[] = []; + const queue: { next: FileNode; distance: number }[] = [{ next: node, distance: 0 }]; + const visited = new Set(); + let maxDistance = Infinity; + + while (queue.length) { + const { next, distance } = queue.shift()!; + if (visited.has(next)) { + continue; + } + + visited.add(next); + const testForDoc = this.getTestNodeForDoc(next.uri); + if (testForDoc) { + tests.push(testForDoc); + // only look for tests half again as far away as the closest test to keep things relevant + if (!Number.isFinite(maxDistance)) { + maxDistance = distance * 3 / 2; + } + } + + if (distance < maxDistance) { + for (const importedByNode of next.importedBy) { + queue.push({ next: importedByNode, distance: distance + 1 }); + } + } + } + + return tests; + } + + public didChange(uri: vscode.Uri, deleted: boolean) { + const rel = this.uriToImportPath(uri); + const node = rel && this.graph.get(rel); + if (!node) { + return; + } + + if (deleted) { + this.graph.delete(rel); + for (const imp of node.imports) { + imp.importedBy.delete(node); + } + } else { + node.isSynced = false; + } + } + + private getNode(uri: vscode.Uri | undefined): FileNode | undefined { + const rel = this.uriToImportPath(uri); + return rel ? this.graph.get(rel) : undefined; + } + + /** Discover all nodes that import the file */ + private async discoverOutwards(uri: vscode.Uri | undefined, seen: Set, maxDistance: number, token: vscode.CancellationToken): Promise { + const rel = this.uriToImportPath(uri); + if (!rel) { + return undefined; + } + + let node = this.graph.get(rel); + if (!node) { + node = new FileNode(uri!, rel); + this.graph.set(rel, node); + } + + await this.discoverOutwardsInner(node, seen, maxDistance, token); + return node; + } + + private async discoverOutwardsInner(node: FileNode, seen: Set, maxDistance: number, token: vscode.CancellationToken) { + if (seen.has(node.path) || maxDistance === 0) { + return; + } + + seen.add(node.path); + if (node.isSynced === false) { + await this.syncNode(node); + } else if (node.isSynced instanceof Promise) { + await node.isSynced; + } + + if (token.isCancellationRequested) { + return; + } + await Promise.all([...node.imports].map(i => this.discoverOutwardsInner(i, seen, maxDistance - 1, token))); + } + + private async syncNode(node: FileNode) { + node.isSynced = discoverLimiter.execute(async () => { + const doc = vscode.workspace.textDocuments.find(d => d.uri.toString() === node.uri.toString()); + + let text: string; + if (doc) { + text = doc.getText(); + } else { + try { + text = await fs.readFile(node.uri.fsPath, 'utf8'); + } catch { + text = ''; + } + } + + for (const imp of node.imports) { + imp.importedBy.delete(node); + } + node.imports.clear(); + + for (const [, importPath] of text.matchAll(IMPORT_RE)) { + let imp = this.graph.get(importPath); + if (!imp) { + imp = new FileNode(this.importPathToUri(importPath), importPath); + this.graph.set(importPath, imp); + } + + imp.importedBy.add(node); + node.imports.add(imp); + } + + node.isSynced = true; + }); + + await node.isSynced; + } + + private uriToImportPath(uri: vscode.Uri | undefined) { + if (!uri) { + return undefined; + } + + const relativePath = vscode.workspace.asRelativePath(uri).replaceAll('\\', '/'); + if (!relativePath.startsWith('src/vs/') || !relativePath.endsWith('.ts')) { + return undefined; + } + + return relativePath.slice('src/'.length, -'.ts'.length); + } + + private importPathToUri(importPath: string) { + return vscode.Uri.file(join(this.root.fsPath, 'src', `${importPath}.ts`)); + } +} + +const IMPORT_RE = /import .*? from ["'](vs\/[^"']+)/g; + +class FileNode { + public imports = new Set(); + public importedBy = new Set(); + public isSynced: boolean | Promise = false; + + // Path is the *import path* starting with `vs/` + constructor( + public readonly uri: vscode.Uri, + public readonly path: string, + ) { } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/stackTraceParser.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/stackTraceParser.ts new file mode 100644 index 00000000000..ca3236ce96a --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/stackTraceParser.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Copied from https://github.com/microsoft/vscode-js-debug/blob/1d104b5184736677ab5cc280c70bbd227403850c/src/common/stackTraceParser.ts#L18 + +// Either match lines like +// " at fulfilled (/Users/roblou/code/testapp-node2/out/app.js:5:58)" +// or +// " at /Users/roblou/code/testapp-node2/out/app.js:60:23" +// and replace the path in them +const re1 = /^(\W*at .*\()(.*):(\d+):(\d+)(\))$/; +const re2 = /^(\W*at )(.*):(\d+):(\d+)$/; + +const getLabelRe = /^\W*at (.*) \($/; + +/** + * Parses a textual stack trace. + */ +export class StackTraceParser { + /** Gets whether the stacktrace has any locations in it. */ + public static isStackLike(str: string) { + return re1.test(str) || re2.test(str); + } + constructor(private readonly stack: string) { } + + /** Iterates over segments of text and locations in the stack. */ + *[Symbol.iterator]() { + for (const line of this.stack.split('\n')) { + const match = re1.exec(line) || re2.exec(line); + if (!match) { + yield line + '\n'; + continue; + } + + const [, prefix, url, lineNo, columnNo, suffix] = match; + if (prefix) { + yield prefix; + } + + yield new StackTraceLocation(getLabelRe.exec(prefix)?.[1], url, Number(lineNo), Number(columnNo)); + + if (suffix) { + yield suffix; + } + + yield '\n'; + } + } +} + +export class StackTraceLocation { + constructor( + public readonly label: string | undefined, + public readonly path: string, + public readonly lineBase1: number, + public readonly columnBase1: number, + ) { } +} \ No newline at end of file diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index b5a448aaba1..f8e70b4d9cd 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -16,6 +16,7 @@ import * as vscode from 'vscode'; import { istanbulCoverageContext, PerTestCoverageTracker } from './coverageProvider'; import { attachTestMessageMetadata } from './metadata'; import { snapshotComment } from './snapshot'; +import { StackTraceLocation, StackTraceParser } from './stackTraceParser'; import { StreamSplitter } from './streamSplitter'; import { getContentFromFilesystem } from './testTree'; import { IScriptCoverage } from './v8CoverageWrangling'; @@ -288,8 +289,8 @@ export async function scanTestOutput( enqueueExitBlocker( (async () => { - const location = await tryDeriveStackLocation(store, rawErr, tcase!); - let message: vscode.TestMessage; + const stackInfo = await deriveStackLocations(store, rawErr, tcase!); + let message: vscode.TestMessage2; if (hasDiff) { message = new vscode.TestMessage(tryMakeMarkdown(err)); @@ -310,7 +311,8 @@ export async function scanTestOutput( ); } - message.location = location ?? testFirstLine; + message.location = stackInfo.primary ?? testFirstLine; + message.stackTrace = stackInfo.stack; task.failed(tcase!, message, duration); })() ); @@ -608,44 +610,38 @@ async function replaceAllLocations(store: SourceMapStore, str: string) { return values.join(''); } -async function tryDeriveStackLocation( +async function deriveStackLocations( store: SourceMapStore, stack: string, tcase: vscode.TestItem ) { locationRe.lastIndex = 0; - return new Promise(resolve => { - const matches = [...stack.matchAll(locationRe)]; - let todo = matches.length; - if (todo === 0) { - return resolve(undefined); - } + const locationsRaw = [...new StackTraceParser(stack)].filter(t => t instanceof StackTraceLocation); + const locationsMapped = await Promise.all(locationsRaw.map(async location => { + const mapped = location.path.startsWith('file:') ? await store.getSourceLocation(location.path, location.lineBase1 - 1, location.columnBase1 - 1) : undefined; + const stack = new vscode.TestMessageStackFrame(location.label || '', mapped?.uri, mapped?.range.start || new vscode.Position(location.lineBase1 - 1, location.columnBase1 - 1)); + return { location: mapped, stack }; + })); - let best: undefined | { location: vscode.Location; i: number; score: number }; - for (const [i, match] of matches.entries()) { - deriveSourceLocation(store, match) - .catch(() => undefined) - .then(location => { - if (location) { - let score = 0; - if (tcase.uri && tcase.uri.toString() === location.uri.toString()) { - score = 1; - if (tcase.range && tcase.range.contains(location?.range)) { - score = 2; - } - } - if (!best || score > best.score || (score === best.score && i < best.i)) { - best = { location, i, score }; - } - } - - if (!--todo) { - resolve(best?.location); - } - }); + let best: undefined | { location: vscode.Location; score: number }; + for (const { location } of locationsMapped) { + if (!location) { + continue; } - }); + let score = 0; + if (tcase.uri && tcase.uri.toString() === location.uri.toString()) { + score = 1; + if (tcase.range && tcase.range.contains(location?.range)) { + score = 2; + } + } + if (!best || score > best.score) { + best = { location, score }; + } + } + + return { stack: locationsMapped.map(s => s.stack), primary: best?.location }; } async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArray) { @@ -661,4 +657,4 @@ function findLastIndex(arr: T[], predicate: (value: T) => boolean) { } return -1; -} \ No newline at end of file +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json index 9725e14041e..df98ed3e695 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json @@ -11,6 +11,8 @@ "src/**/*", "../../../src/vscode-dts/vscode.d.ts", "../../../src/vscode-dts/vscode.proposed.testObserver.d.ts", + "../../../src/vscode-dts/vscode.proposed.testMessageStackTrace.d.ts", + "../../../src/vscode-dts/vscode.proposed.testRelatedCode.d.ts", "../../../src/vscode-dts/vscode.proposed.attributableCoverage.d.ts" ] } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock index 50478f52c73..5dd9568bc9c 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock +++ b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock @@ -42,6 +42,11 @@ ansi-styles@^5.2.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +cockatiel@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/cockatiel/-/cockatiel-3.1.3.tgz#bb1774a498a17e739dd994d56610dc6538b02858" + integrity sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ== + istanbul-to-vscode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/istanbul-to-vscode/-/istanbul-to-vscode-2.0.1.tgz#84994d06e604b68cac7301840f338b1e74eb888b" diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 957ce5a9ee0..c402cca3836 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"June 2024\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"August 2024\"" }, { "kind": 1, diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index e1c0a9fce02..450ba41835f 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\r\n\r\n$MILESTONE=milestone:\"June 2024\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"August 2024\"" }, { "kind": 1, @@ -32,7 +32,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS -$MILESTONE is:issue is:closed reason:completed label:bug label:insiders-released -label:verified -label:*duplicate -label:*as-designed -label:z-author-verified -label:on-testplan" + "value": "$REPOS -$MILESTONE is:issue is:closed reason:completed label:bug label:insiders-released -label:verified -label:*duplicate -label:*as-designed -label:z-author-verified -label:on-testplan -label:error-telemetry" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 0b260270ed7..df0f8a92998 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"June 2024\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"August 2024\"\n\n$MINE=assignee:@me" }, { "kind": 1, @@ -157,7 +157,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:*out-of-scope -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:andreamah -author:bamurtaugh -author:bpasero -author:chrisdias -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:gregvanl -author:hediet -author:isidorn -author:joaomoreno -author:joyceerhl -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:tanhakabir -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:paulacamargo25 -author:ulugbekna -author:aiday-mar -author:daviddossett -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1" + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:*out-of-scope -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:andreamah -author:bamurtaugh -author:bpasero -author:chrisdias -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:gregvanl -author:hediet -author:isidorn -author:joaomoreno -author:joyceerhl -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:tanhakabir -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:paulacamargo25 -author:ulugbekna -author:aiday-mar -author:daviddossett -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 27fc3c2ecb5..979180e758a 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"June 2024\"\n" + "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"August 2024\"\n" }, { "kind": 1, diff --git a/.vscode/notebooks/vscode-dev.github-issues b/.vscode/notebooks/vscode-dev.github-issues index 53fad82e6d9..db480b64bd8 100644 --- a/.vscode/notebooks/vscode-dev.github-issues +++ b/.vscode/notebooks/vscode-dev.github-issues @@ -2,7 +2,7 @@ { "kind": 2, "language": "github-issues", - "value": "$milestone=milestone:\"November 2023\"" + "value": "$milestone=milestone:\"August 2024\"" }, { "kind": 1, diff --git a/.vscode/settings.json b/.vscode/settings.json index 9255a781bd4..7a4df1616c4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -166,7 +166,5 @@ "editor.wordWrap": "on" }, "css.format.spaceAroundSelectorSeparator": true, - "inlineChat.mode": "live", - "inlineChat.experimental.textButtons": true, "typescript.enablePromptUseWorkspaceTsdk": true } diff --git a/.yarnrc b/.yarnrc index b153fa4724f..dadeafb95be 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "29.4.0" -ms_build_id "9728852" +target "30.3.1" +ms_build_id "9960165" runtime "electron" build_from_source "true" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index fe3cf5c4a9b..0752f0a1f31 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -517,7 +517,7 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -go-syntax 0.6.8 - MIT +go-syntax 0.7.5 - MIT https://github.com/worlpaker/go-syntax MIT License @@ -1506,6 +1506,22 @@ SOFTWARE. --------------------------------------------------------- +RedCMD/YAML-Syntax-Highlighter 1.0.1 - MIT +https://github.com/RedCMD/YAML-Syntax-Highlighter + +MIT License + +Copyright 2024 RedCMD + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + redhat-developer/vscode-java 1.26.0 - MIT https://github.com/redhat-developer/vscode-java @@ -1928,31 +1944,6 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -textmate/yaml.tmbundle 0.0.0 - TextMate Bundle License -https://github.com/textmate/yaml.tmbundle - -Copyright (c) 2015 FichteFoll - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - trond-snekvik/vscode-rst 1.5.3 - MIT https://github.com/trond-snekvik/vscode-rst diff --git a/build/.cachesalt b/build/.cachesalt index d7d415d3213..af13febe516 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2024-07-04T16:31:11.121Z +2024-08-08T03:47:49.879Z diff --git a/build/.moduleignore b/build/.moduleignore index 32fb3bd21c5..97b504d8522 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -96,10 +96,13 @@ node-pty/lib/*.test.js node-pty/tools/** node-pty/deps/** node-pty/scripts/** +node-pty/third_party/** !node-pty/build/Release/spawn-helper !node-pty/build/Release/*.exe !node-pty/build/Release/*.dll !node-pty/build/Release/*.node +!node-pty/build/Release/conpty/conpty.dll +!node-pty/build/Release/conpty/OpenConsole.exe @parcel/watcher/binding.gyp @parcel/watcher/build/** diff --git a/build/.webignore b/build/.webignore index 15935edce8a..860ab59616b 100644 --- a/build/.webignore +++ b/build/.webignore @@ -38,6 +38,7 @@ vscode-textmate/webpack.config.js # This makes sure the model is included in the package !@vscode/vscode-languagedetection/model/** +!@vscode/tree-sitter-wasm/wasm/** # Ensure only the required telemetry pieces are loaded in web to reduce bundle size @microsoft/1ds-core-js/** diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index c990e3a7146..aa185ed8369 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -389,14 +389,8 @@ function getPlatform(product, os, arch, type, isLegacy) { } } case 'server': - if (arch === 'arm64') { - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } return `server-win32-${arch}`; case 'web': - if (arch === 'arm64') { - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } return `server-win32-${arch}-web`; case 'cli': return `cli-win32-${arch}`; diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index 75065ffa2d3..652cd168335 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -550,14 +550,8 @@ function getPlatform(product: string, os: string, arch: string, type: string, is } } case 'server': - if (arch === 'arm64') { - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } return `server-win32-${arch}`; case 'web': - if (arch === 'arm64') { - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } return `server-win32-${arch}-web`; case 'cli': return `cli-win32-${arch}`; diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index ed6d0236516..92fe6eb715e 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -59,7 +59,6 @@ steps: compile-extension:ipynb \ compile-extension:notebook-renderers \ compile-extension:json-language-features-server \ - compile-extension:markdown-language-features-server \ compile-extension:markdown-language-features \ compile-extension-media \ compile-extension:microsoft-authentication \ diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 11f69d735ac..734a15c82ba 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -101,6 +101,11 @@ steps: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" + # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries + # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 + # flipped the default to support legacy linux distros which shouldn't happen + # on macOS. + GYP_DEFINES: "kerberos_use_rtld=false" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml index 921bf2a9370..26f02657e10 100644 --- a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml +++ b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml @@ -84,16 +84,6 @@ steps: imageName: vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) containerCommand: uname - - ${{ if eq(parameters.VSCODE_ARCH, 'armhf') }}: - - task: Docker@1 - displayName: "Pull Docker image" - inputs: - azureSubscriptionEndpoint: "vscode-builds-subscription" - azureContainerRegistry: vscodehub.azurecr.io - command: "Run an image" - imageName: vscode-linux-build-agent:bionic-arm32v7 - containerCommand: uname - - script: | set -e # To workaround the issue of yarn not respecting the registry value from .npmrc @@ -129,8 +119,6 @@ steps: VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) - ${{ if eq(parameters.VSCODE_ARCH, 'armhf') }}: - VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-arm32v7 displayName: Install dependencies - script: node build/azure-pipelines/distro/mixin-npm @@ -199,7 +187,7 @@ steps: - ${{ else }}: - script: | set -e - EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBC_VERSION="2.17" \ EXPECTED_GLIBCXX_VERSION="3.4.22" \ ./build/azure-pipelines/linux/verify-glibc-requirements.sh env: diff --git a/build/azure-pipelines/linux/product-build-linux-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index f5c00aa0cf0..91cc411dd44 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -77,7 +77,6 @@ steps: compile-extension:ipynb \ compile-extension:notebook-renderers \ compile-extension:json-language-features-server \ - compile-extension:markdown-language-features-server \ compile-extension:markdown-language-features \ compile-extension-media \ compile-extension:microsoft-authentication \ @@ -151,7 +150,7 @@ steps: - script: yarn --cwd test/smoke compile displayName: Compile smoke tests - - script: yarn gulp compile-extension:markdown-language-features compile-extension-media compile-extension:vscode-test-resolver + - script: yarn gulp compile-extension:markdown-language-features compile-extension:ipynb compile-extension-media compile-extension:vscode-test-resolver displayName: Build extensions for smoke tests - script: yarn gulp node diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index 9bfbf9ab41a..949b5f371ba 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -13,7 +13,7 @@ SYSROOT_ARCH="$SYSROOT_ARCH" node -e '(async () => { const { getVSCodeSysroot } if [ "$npm_config_arch" == "x64" ]; then if [ "$(echo "$@" | grep -c -- "--only-remote")" -eq 0 ]; then # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/122.0.6261.156/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + curl -s https://raw.githubusercontent.com/chromium/chromium/124.0.6367.243/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux # Download libcxx headers and objects from upstream electron releases DEBUG=libcxx-fetcher \ @@ -25,9 +25,9 @@ if [ "$npm_config_arch" == "x64" ]; then # Set compiler toolchain # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:build/config/c++/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:build/config/c++/BUILD.gn export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" @@ -54,17 +54,15 @@ elif [ "$npm_config_arch" == "arm64" ]; then export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" fi elif [ "$npm_config_arch" == "arm" ]; then - if [ "$(echo "$@" | grep -c -- "--only-remote")" -eq 0 ]; then - # Set compiler toolchain for client native modules - export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc - export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ - export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" - export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" + # Set compiler toolchain for client native modules + export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" - # Set compiler toolchain for remote server - export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc - export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ - export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" - export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" - fi + # Set compiler toolchain for remote server + export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" fi diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 0c4f98aa511..8bd621388d4 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -148,6 +148,8 @@ variables: value: microsoft/vscode-distro - name: skipComponentGovernanceDetection value: true + - name: ComponentDetection.Timeout + value: 600 - name: Codeql.SkipTaskAutoInjection value: true - name: ARTIFACT_PREFIX @@ -316,17 +318,21 @@ extends: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} - - stage: CustomSDL - dependsOn: [] - pool: - name: 1es-windows-2019-x64 - os: windows - jobs: - - job: WindowsSDL - variables: - - group: 'API Scan' - steps: - - template: build/azure-pipelines/sdl-scan.yml@self + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false)) }}: + - stage: CustomSDL + dependsOn: [] + pool: + name: 1es-windows-2019-x64 + os: windows + jobs: + - job: WindowsSDL + variables: + - group: 'API Scan' + steps: + - template: build/azure-pipelines/win32/sdl-scan-win32.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true)) }}: - stage: Windows diff --git a/build/azure-pipelines/upload-configuration.js b/build/azure-pipelines/upload-configuration.js deleted file mode 100644 index 39a44dc5c41..00000000000 --- a/build/azure-pipelines/upload-configuration.js +++ /dev/null @@ -1,112 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getSettingsSearchBuildId = exports.shouldSetupSettingsSearch = void 0; -const path = require("path"); -const os = require("os"); -const cp = require("child_process"); -const vfs = require("vinyl-fs"); -const util = require("../lib/util"); -const identity_1 = require("@azure/identity"); -const azure = require('gulp-azure-storage'); -const packageJson = require("../../package.json"); -const commit = process.env['BUILD_SOURCEVERSION']; -function generateVSCodeConfigurationTask() { - return new Promise((resolve, reject) => { - const buildDir = process.env['AGENT_BUILDDIRECTORY']; - if (!buildDir) { - return reject(new Error('$AGENT_BUILDDIRECTORY not set')); - } - if (!shouldSetupSettingsSearch()) { - console.log(`Only runs on main and release branches, not ${process.env.BUILD_SOURCEBRANCH}`); - return resolve(undefined); - } - if (process.env.VSCODE_QUALITY !== 'insider' && process.env.VSCODE_QUALITY !== 'stable') { - console.log(`Only runs on insider and stable qualities, not ${process.env.VSCODE_QUALITY}`); - return resolve(undefined); - } - const result = path.join(os.tmpdir(), 'configuration.json'); - const userDataDir = path.join(os.tmpdir(), 'tmpuserdata'); - const extensionsDir = path.join(os.tmpdir(), 'tmpextdir'); - const arch = process.env['VSCODE_ARCH']; - const appRoot = path.join(buildDir, `VSCode-darwin-${arch}`); - const appName = process.env.VSCODE_QUALITY === 'insider' ? 'Visual\\ Studio\\ Code\\ -\\ Insiders.app' : 'Visual\\ Studio\\ Code.app'; - const appPath = path.join(appRoot, appName, 'Contents', 'Resources', 'app', 'bin', 'code'); - const codeProc = cp.exec(`${appPath} --export-default-configuration='${result}' --wait --user-data-dir='${userDataDir}' --extensions-dir='${extensionsDir}'`, (err, stdout, stderr) => { - clearTimeout(timer); - if (err) { - console.log(`err: ${err} ${err.message} ${err.toString()}`); - reject(err); - } - if (stdout) { - console.log(`stdout: ${stdout}`); - } - if (stderr) { - console.log(`stderr: ${stderr}`); - } - resolve(result); - }); - const timer = setTimeout(() => { - codeProc.kill(); - reject(new Error('export-default-configuration process timed out')); - }, 60 * 1000); - codeProc.on('error', err => { - clearTimeout(timer); - reject(err); - }); - }); -} -function shouldSetupSettingsSearch() { - const branch = process.env.BUILD_SOURCEBRANCH; - return !!(branch && (/\/main$/.test(branch) || branch.indexOf('/release/') >= 0)); -} -exports.shouldSetupSettingsSearch = shouldSetupSettingsSearch; -function getSettingsSearchBuildId(packageJson) { - try { - const branch = process.env.BUILD_SOURCEBRANCH; - const branchId = branch.indexOf('/release/') >= 0 ? 0 : - /\/main$/.test(branch) ? 1 : - 2; // Some unexpected branch - const out = cp.execSync(`git rev-list HEAD --count`); - const count = parseInt(out.toString()); - // - // 1.25.1, 1,234,567 commits, main = 1250112345671 - return util.versionStringToNumber(packageJson.version) * 1e8 + count * 10 + branchId; - } - catch (e) { - throw new Error('Could not determine build number: ' + e.toString()); - } -} -exports.getSettingsSearchBuildId = getSettingsSearchBuildId; -async function main() { - const configPath = await generateVSCodeConfigurationTask(); - if (!configPath) { - return; - } - const settingsSearchBuildId = getSettingsSearchBuildId(packageJson); - if (!settingsSearchBuildId) { - throw new Error('Failed to compute build number'); - } - const credential = new identity_1.ClientSecretCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_CLIENT_SECRET']); - return new Promise((c, e) => { - vfs.src(configPath) - .pipe(azure.upload({ - account: process.env.AZURE_STORAGE_ACCOUNT, - credential, - container: 'configuration', - prefix: `${settingsSearchBuildId}/${commit}/` - })) - .on('end', () => c()) - .on('error', (err) => e(err)); - }); -} -if (require.main === module) { - main().catch(err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXBsb2FkLWNvbmZpZ3VyYXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJ1cGxvYWQtY29uZmlndXJhdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7OztnR0FHZ0c7OztBQUVoRyw2QkFBNkI7QUFDN0IseUJBQXlCO0FBQ3pCLG9DQUFvQztBQUNwQyxnQ0FBZ0M7QUFDaEMsb0NBQW9DO0FBQ3BDLDhDQUF5RDtBQUN6RCxNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsb0JBQW9CLENBQUMsQ0FBQztBQUM1QyxrREFBa0Q7QUFFbEQsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUIsQ0FBQyxDQUFDO0FBRWxELFNBQVMsK0JBQStCO0lBQ3ZDLE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDdEMsTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxzQkFBc0IsQ0FBQyxDQUFDO1FBQ3JELElBQUksQ0FBQyxRQUFRLEVBQUU7WUFDZCxPQUFPLE1BQU0sQ0FBQyxJQUFJLEtBQUssQ0FBQywrQkFBK0IsQ0FBQyxDQUFDLENBQUM7U0FDMUQ7UUFFRCxJQUFJLENBQUMseUJBQXlCLEVBQUUsRUFBRTtZQUNqQyxPQUFPLENBQUMsR0FBRyxDQUFDLCtDQUErQyxPQUFPLENBQUMsR0FBRyxDQUFDLGtCQUFrQixFQUFFLENBQUMsQ0FBQztZQUM3RixPQUFPLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQztTQUMxQjtRQUVELElBQUksT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLEtBQUssU0FBUyxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxLQUFLLFFBQVEsRUFBRTtZQUN4RixPQUFPLENBQUMsR0FBRyxDQUFDLGtEQUFrRCxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsRUFBRSxDQUFDLENBQUM7WUFDNUYsT0FBTyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUM7U0FDMUI7UUFFRCxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxNQUFNLEVBQUUsRUFBRSxvQkFBb0IsQ0FBQyxDQUFDO1FBQzVELE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLGFBQWEsQ0FBQyxDQUFDO1FBQzFELE1BQU0sYUFBYSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLFdBQVcsQ0FBQyxDQUFDO1FBQzFELE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUM7UUFDeEMsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsaUJBQWlCLElBQUksRUFBRSxDQUFDLENBQUM7UUFDN0QsTUFBTSxPQUFPLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQywyQ0FBMkMsQ0FBQyxDQUFDLENBQUMsNEJBQTRCLENBQUM7UUFDdEksTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxNQUFNLENBQUMsQ0FBQztRQUMzRixNQUFNLFFBQVEsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUN2QixHQUFHLE9BQU8sb0NBQW9DLE1BQU0sNkJBQTZCLFdBQVcsdUJBQXVCLGFBQWEsR0FBRyxFQUNuSSxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDdkIsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO1lBQ3BCLElBQUksR0FBRyxFQUFFO2dCQUNSLE9BQU8sQ0FBQyxHQUFHLENBQUMsUUFBUSxHQUFHLElBQUksR0FBRyxDQUFDLE9BQU8sSUFBSSxHQUFHLENBQUMsUUFBUSxFQUFFLEVBQUUsQ0FBQyxDQUFDO2dCQUM1RCxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7YUFDWjtZQUVELElBQUksTUFBTSxFQUFFO2dCQUNYLE9BQU8sQ0FBQyxHQUFHLENBQUMsV0FBVyxNQUFNLEVBQUUsQ0FBQyxDQUFDO2FBQ2pDO1lBRUQsSUFBSSxNQUFNLEVBQUU7Z0JBQ1gsT0FBTyxDQUFDLEdBQUcsQ0FBQyxXQUFXLE1BQU0sRUFBRSxDQUFDLENBQUM7YUFDakM7WUFFRCxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDakIsQ0FBQyxDQUNELENBQUM7UUFDRixNQUFNLEtBQUssR0FBRyxVQUFVLENBQUMsR0FBRyxFQUFFO1lBQzdCLFFBQVEsQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUNoQixNQUFNLENBQUMsSUFBSSxLQUFLLENBQUMsZ0RBQWdELENBQUMsQ0FBQyxDQUFDO1FBQ3JFLENBQUMsRUFBRSxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUM7UUFFZCxRQUFRLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxHQUFHLENBQUMsRUFBRTtZQUMxQixZQUFZLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDcEIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ2IsQ0FBQyxDQUFDLENBQUM7SUFDSixDQUFDLENBQUMsQ0FBQztBQUNKLENBQUM7QUFFRCxTQUFnQix5QkFBeUI7SUFDeEMsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxrQkFBa0IsQ0FBQztJQUM5QyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLElBQUksTUFBTSxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ25GLENBQUM7QUFIRCw4REFHQztBQUVELFNBQWdCLHdCQUF3QixDQUFDLFdBQWdDO0lBQ3hFLElBQUk7UUFDSCxNQUFNLE1BQU0sR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLGtCQUFtQixDQUFDO1FBQy9DLE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUN0RCxTQUFTLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztnQkFDM0IsQ0FBQyxDQUFDLENBQUMseUJBQXlCO1FBRTlCLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQyxRQUFRLENBQUMsMkJBQTJCLENBQUMsQ0FBQztRQUNyRCxNQUFNLEtBQUssR0FBRyxRQUFRLENBQUMsR0FBRyxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUM7UUFFdkMsc0VBQXNFO1FBQ3RFLGtEQUFrRDtRQUNsRCxPQUFPLElBQUksQ0FBQyxxQkFBcUIsQ0FBQyxXQUFXLENBQUMsT0FBTyxDQUFDLEdBQUcsR0FBRyxHQUFHLEtBQUssR0FBRyxFQUFFLEdBQUcsUUFBUSxDQUFDO0tBQ3JGO0lBQUMsT0FBTyxDQUFDLEVBQUU7UUFDWCxNQUFNLElBQUksS0FBSyxDQUFDLG9DQUFvQyxHQUFHLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO0tBQ3JFO0FBQ0YsQ0FBQztBQWhCRCw0REFnQkM7QUFFRCxLQUFLLFVBQVUsSUFBSTtJQUNsQixNQUFNLFVBQVUsR0FBRyxNQUFNLCtCQUErQixFQUFFLENBQUM7SUFFM0QsSUFBSSxDQUFDLFVBQVUsRUFBRTtRQUNoQixPQUFPO0tBQ1A7SUFFRCxNQUFNLHFCQUFxQixHQUFHLHdCQUF3QixDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBRXBFLElBQUksQ0FBQyxxQkFBcUIsRUFBRTtRQUMzQixNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxDQUFDLENBQUM7S0FDbEQ7SUFFRCxNQUFNLFVBQVUsR0FBRyxJQUFJLGlDQUFzQixDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsaUJBQWlCLENBQUUsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLGlCQUFpQixDQUFFLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUIsQ0FBRSxDQUFDLENBQUM7SUFFckosT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsRUFBRTtRQUMzQixHQUFHLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQzthQUNqQixJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQztZQUNsQixPQUFPLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUI7WUFDMUMsVUFBVTtZQUNWLFNBQVMsRUFBRSxlQUFlO1lBQzFCLE1BQU0sRUFBRSxHQUFHLHFCQUFxQixJQUFJLE1BQU0sR0FBRztTQUM3QyxDQUFDLENBQUM7YUFDRixFQUFFLENBQUMsS0FBSyxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDO2FBQ3BCLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxHQUFRLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQ3JDLENBQUMsQ0FBQyxDQUFDO0FBQ0osQ0FBQztBQUVELElBQUksT0FBTyxDQUFDLElBQUksS0FBSyxNQUFNLEVBQUU7SUFDNUIsSUFBSSxFQUFFLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxFQUFFO1FBQ2xCLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDbkIsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNqQixDQUFDLENBQUMsQ0FBQztDQUNIIn0= \ No newline at end of file diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index ce791c094e6..c9b2d6cfa61 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -61,7 +61,6 @@ steps: compile-extension:ipynb ` compile-extension:notebook-renderers ` compile-extension:json-language-features-server ` - compile-extension:markdown-language-features-server ` compile-extension:markdown-language-features ` compile-extension-media ` compile-extension:microsoft-authentication ` diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index d3827b930f8..7fa2df0ddd1 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -160,7 +160,6 @@ steps: env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - powershell: | . build/azure-pipelines/win32/exec.ps1 @@ -171,7 +170,6 @@ steps: env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server (web) - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - template: product-build-win32-test.yml@self @@ -312,7 +310,7 @@ steps: sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH) sbomPackageName: "VS Code Windows $(VSCODE_ARCH) Server" sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], ''), ne(variables['VSCODE_ARCH'], 'arm64')) + condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], '')) displayName: Publish server archive - task: 1ES.PublishPipelineArtifact@1 @@ -322,7 +320,7 @@ steps: sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)-web sbomPackageName: "VS Code Windows $(VSCODE_ARCH) Web" sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], ''), ne(variables['VSCODE_ARCH'], 'arm64')) + condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], '')) displayName: Publish web server archive - task: 1ES.PublishPipelineArtifact@1 diff --git a/build/azure-pipelines/sdl-scan.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml similarity index 60% rename from build/azure-pipelines/sdl-scan.yml rename to build/azure-pipelines/win32/sdl-scan-win32.yml index af20a305d9c..81c0258f4ee 100644 --- a/build/azure-pipelines/sdl-scan.yml +++ b/build/azure-pipelines/win32/sdl-scan-win32.yml @@ -1,14 +1,8 @@ parameters: - - name: NPM_REGISTRY - displayName: "Custom NPM Registry" - type: string - default: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/npm/registry/" - - name: NPM_ARCH - type: string - default: x64 - name: VSCODE_ARCH type: string - default: x64 + - name: VSCODE_QUALITY + type: string steps: - task: NodeTool@0 @@ -17,7 +11,12 @@ steps: versionFilePath: .nvmrc nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - template: ./distro/download-distro.yml + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.x" + addToPath: true + + - template: ../distro/download-distro.yml@self - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -26,40 +25,34 @@ steps: KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" + - powershell: node build/setup-npm-registry.js $env:NPM_REGISTRY + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Registry + - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { npm config set registry "${{ parameters.NPM_REGISTRY }}" --location=project } + exec { npm config set registry "$env:NPM_REGISTRY" --location=project } # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb # following is a workaround for yarn to send authorization header # for GET requests to the registry. exec { Add-Content -Path .npmrc -Value "always-auth=true" } - exec { yarn config set registry "${{ parameters.NPM_REGISTRY }}" } - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + exec { yarn config set registry "$env:NPM_REGISTRY" } + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM & Yarn - task: npmAuthenticate@0 inputs: workingFile: .npmrc - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { node build/setup-npm-registry.js "${{ parameters.NPM_REGISTRY }}" } - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) - displayName: Setup NPM Registry - - pwsh: | $includes = @' { 'target_defaults': { 'conditions': [ ['OS=="win"', { - 'msvs_configuration_attributes': { - 'SpectreMitigation': 'Spectre' - }, 'msvs_settings': { 'VCCLCompilerTool': { 'AdditionalOptions': [ @@ -91,9 +84,11 @@ steps: $ErrorActionPreference = "Stop" retry { exec { yarn --frozen-lockfile --check-files } } env: + npm_config_arch: ${{ parameters.VSCODE_ARCH }} + CHILD_CONCURRENCY: 1 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" - CHILD_CONCURRENCY: 1 displayName: Install dependencies - script: node build/azure-pipelines/distro/mixin-npm @@ -102,46 +97,62 @@ steps: - script: node build/azure-pipelines/distro/mixin-quality displayName: Mixin distro quality env: - VSCODE_QUALITY: stable + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} - powershell: yarn compile displayName: Compile + - powershell: | + Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.exe" + Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.dll" + Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.node" + Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.pdb" + displayName: List files + - powershell: yarn gulp "vscode-symbols-win32-${{ parameters.VSCODE_ARCH }}" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Download Symbols + - powershell: | + Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.exe" + Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.dll" + Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.node" + Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.pdb" + displayName: List files again + - task: BinSkim@4 inputs: InputType: "Basic" Function: "analyze" TargetPattern: "guardianGlob" AnalyzeIgnorePdbLoadError: true - AnalyzeTargetGlob: '$(agent.builddirectory)\scanbin\**.dll;$(agent.builddirectory)\scanbin\**.exe;$(agent.builddirectory)\scanbin\**.node' - AnalyzeLocalSymbolDirectories: '$(agent.builddirectory)\scanbin\VSCode-win32-${{ parameters.VSCODE_ARCH }}\pdb' + AnalyzeTargetGlob: '$(Agent.BuildDirectory)\scanbin\**.dll;$(Agent.BuildDirectory)\scanbin\**.exe;$(Agent.BuildDirectory)\scanbin\**.node' + AnalyzeLocalSymbolDirectories: '$(Agent.BuildDirectory)\scanbin\VSCode-win32-${{ parameters.VSCODE_ARCH }}\pdb' - task: CopyFiles@2 displayName: 'Collect Symbols for API Scan' inputs: SourceFolder: $(Agent.BuildDirectory) Contents: 'scanbin\**\*.pdb' - TargetFolder: '$(agent.builddirectory)\symbols' + TargetFolder: '$(Agent.BuildDirectory)\symbols' flattenFolders: true condition: succeeded() - # - task: APIScan@2 - # inputs: - # softwareFolder: $(agent.builddirectory)\scanbin - # softwareName: 'vscode-client' - # softwareVersionNum: '1' - # symbolsFolder: 'SRV*http://symweb;$(agent.builddirectory)\symbols' - # isLargeApp: false - # toolVersion: 'Latest' - # displayName: Run ApiScan - # condition: succeeded() - # env: - # AzureServicesAuthConnectionString: $(apiscan-connectionstring) + - task: APIScan@2 + inputs: + softwareFolder: $(Agent.BuildDirectory)\scanbin + softwareName: 'vscode-client' + softwareVersionNum: '1' + symbolsFolder: 'srv*https://symweb.azurefd.net;$(Agent.BuildDirectory)\symbols' + isLargeApp: false + toolVersion: 'Latest' + azureSubscription: 'vscode-apiscan' + displayName: Run ApiScan + condition: succeeded() + env: + AzureServicesAuthConnectionString: $(apiscan-connectionstring) + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: PublishSecurityAnalysisLogs@3 inputs: diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index a80aa1531f1..60291393bb0 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -3d3d8bb185d7b63b0db910661fdd69d6381afb8c97742bbd2526a9c932e1f8ca *chromedriver-v29.4.0-darwin-arm64.zip -c3d075943d87604ffa50382cc8d5798485349544ca391cab88c892f889d3b14c *chromedriver-v29.4.0-darwin-x64.zip -6d62d2dba55e4419fa003d45f93dad1324ec29a4d3eb84fd9fd5fd7a64339389 *chromedriver-v29.4.0-linux-arm64.zip -81bb3d362331c7296f700b1b0e8f07c4c7739b1151f698cd56af927bedda59e7 *chromedriver-v29.4.0-linux-armv7l.zip -ab593cc39aefac8c5abd259e31f6add4b2b70c52231724a6c08ac1872b4a0edf *chromedriver-v29.4.0-linux-x64.zip -705d42ccc05b2c48b0673b9dcf63eb78772bb79dba078a523d384ed2481bc9c0 *chromedriver-v29.4.0-mas-arm64.zip -956a7caa28eeeb0c02eb7638a53215ffd89b4f12880f0893ff10f497ca1a8117 *chromedriver-v29.4.0-mas-x64.zip -1f070176aa33e0139d61a3d758fd2f015f09bb275577293fe93564749b6310ba *chromedriver-v29.4.0-win32-arm64.zip -38a71526d243bcb73c28cb648bd4816d70b5e643df52f9f86a83416014589744 *chromedriver-v29.4.0-win32-ia32.zip -f90750d3589cb3c9f6f0ebc70d5e025cf81c382e8c23fa47a54570696a478ef0 *chromedriver-v29.4.0-win32-x64.zip -05dffc90dd1341cc7a6b50127985e4e217fef7f50a173c7d0ff34039dd2d81b6 *electron-api.json -7f63f7cf675ba6dec3a5e4173d729bd53c75f81e612f809641d9d0c4d9791649 *electron-v29.4.0-darwin-arm64-dsym-snapshot.zip -aa29530fcafa4db364978d4f414a6ec2005ea695f7fee70ffbe5e114e9e453f0 *electron-v29.4.0-darwin-arm64-dsym.zip -8d12fb6d9bcdf5bbfc93dbcd1cac348735dc6f98aa450ee03ec7837a01a8a938 *electron-v29.4.0-darwin-arm64-symbols.zip -c16d05f1231bb3c77da05ab236b454b3a2b6a642403be51e7c9b16cd2c421a19 *electron-v29.4.0-darwin-arm64.zip -2dfc1017831ab2f6e9ddb575d3b9cff5a0d56f16a335a3c0df508e964e2db963 *electron-v29.4.0-darwin-x64-dsym-snapshot.zip -025de6aa39d98762928e1b700f46177e74be20101b27457659b938e2c69db326 *electron-v29.4.0-darwin-x64-dsym.zip -ec4eb0a618207233985ceaab297be34b3d4f0813d88801d5637295b238dd661a *electron-v29.4.0-darwin-x64-symbols.zip -8ed7924f77a5c43c137a57097c5c47c2e8e9a78197e18af11a767c98035c123e *electron-v29.4.0-darwin-x64.zip -bde1772fa8ac4850e108012a9edd3bd93472bad8f68ddd55fca355dad81dde4f *electron-v29.4.0-linux-arm64-debug.zip -dfe7852a7423196efb2205c788d942db3ffc9de6ce52577e173bcf7ca6973d48 *electron-v29.4.0-linux-arm64-symbols.zip -c3764d6c3799950e3418e8e5a5a5b2c41abe421dd8bcdebf054c7c85798d9860 *electron-v29.4.0-linux-arm64.zip -bde1772fa8ac4850e108012a9edd3bd93472bad8f68ddd55fca355dad81dde4f *electron-v29.4.0-linux-armv7l-debug.zip -360668ba669cb2c01c2f960cdee76c29670e6ce907ccc0718e971a04af594ce9 *electron-v29.4.0-linux-armv7l-symbols.zip -c5e92943ad78b4e41a32ae53c679e148ea2ae09f95f914b1834dbdbae578ba91 *electron-v29.4.0-linux-armv7l.zip -375be885426bcbd272bd068bfcef41a83296c2f8e61e633233d2a9e9a69242fc *electron-v29.4.0-linux-x64-debug.zip -847e0f75624616c2918b33de2eefeec63419bd250685610d3f52fa115527d2b9 *electron-v29.4.0-linux-x64-symbols.zip -91e5eb374c2c85a07c2d4e99a89eb18515ff0169a49c3fa75289800e1225729e *electron-v29.4.0-linux-x64.zip -098f973537c3d9679a69409d0b84bcc1a6113bb2002ee60068e2c22f335a3855 *electron-v29.4.0-mas-arm64-dsym-snapshot.zip -2724aa32eb441eea21680d95fc1efdd75ac473fa19623c7acf3d546419e96154 *electron-v29.4.0-mas-arm64-dsym.zip -98dd81914752a57da4cbaad1f0aa94b16335f9b8f997be9aa049be90b96b2886 *electron-v29.4.0-mas-arm64-symbols.zip -fd2663f65c1f995304e3eb65870b7146adfefef07cf82bf44de75855fd4f36e8 *electron-v29.4.0-mas-arm64.zip -237983b2169e69bb73aa0987e871e3e486755904b71ebe36c3e902377f92754a *electron-v29.4.0-mas-x64-dsym-snapshot.zip -a5d59599827d32ef322b99eee8416e39235f4c7a0ada78342a885665e0b732dd *electron-v29.4.0-mas-x64-dsym.zip -5182e7697ac0591e0b95c33f70316af24093c9100f442be2cee0039660e959ac *electron-v29.4.0-mas-x64-symbols.zip -e0ee7057aff0240a70b9ed75ff44d55aeae9af67fbc8915f741711a8bb6fe744 *electron-v29.4.0-mas-x64.zip -2802872dfc6de0f0e2e8cef9d2f4f384e3d82b20ad36fc981c4e725dd2f2abcd *electron-v29.4.0-win32-arm64-pdb.zip -d49c954dc25ae9e4c75e61af80b9718014c52f016f43a29071913f0e7100c7bd *electron-v29.4.0-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.4.0-win32-arm64-toolchain-profile.zip -483d692efbe4fb1231ff63afb8a236b2e22b486fbe5ac6abbc8b208abf94a4d3 *electron-v29.4.0-win32-arm64.zip -98458f49ba67a08e473d475a68a2818d9df076a5246fbc9b45403e8796f9d35b *electron-v29.4.0-win32-ia32-pdb.zip -69d505d4ae59d9dddf83c4e530e45dd7c5bc64d6da90cf4f851e523be9e51014 *electron-v29.4.0-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.4.0-win32-ia32-toolchain-profile.zip -d5a21a17a64e9638f49f057356af23b51f56bd6a7fea3c2e0a28ff3186a7bc41 *electron-v29.4.0-win32-ia32.zip -521ee7b3398c4dc395b43dac86cd099e86a6123de2b43636ee805b7da014ed3f *electron-v29.4.0-win32-x64-pdb.zip -e33848ebd6c6e4ce431aa367bef887050947a136e883677cfc524ca5cabc1e98 *electron-v29.4.0-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.4.0-win32-x64-toolchain-profile.zip -e4ef85aa3608221f8a3e011c1b1c2d2d36093ad19bda12d16b3816929fb6c99b *electron-v29.4.0-win32-x64.zip -707ee08593289ee83514b4fc55123611309f995788f38a5ec03e285741aac1c8 *electron.d.ts -281b5f4a49de55fdb86b1662530f07f2ced1252c878eb7a941c88ede545339e0 *ffmpeg-v29.4.0-darwin-arm64.zip -0b735912df9b2ff3d03eb23942e03bc0116d82f1291d0a45cbde14177c2f3066 *ffmpeg-v29.4.0-darwin-x64.zip -4e2ba537d7c131abbd34168bce2c28cc9ef6262b217d5f4085afccfdf9635da6 *ffmpeg-v29.4.0-linux-arm64.zip -4aa56ad5d849f4e61af22678a179346b68aec9100282e1b8a43df25d95721677 *ffmpeg-v29.4.0-linux-armv7l.zip -0558e6e1f78229d303e16d4d8c290794baa9adc619fdd2ddccadb3ea241a1df4 *ffmpeg-v29.4.0-linux-x64.zip -224f15d8f96c75348cd7f1b85c4eab63468fae1e50ff4b1381e08011cf76e4f7 *ffmpeg-v29.4.0-mas-arm64.zip -175ec79f0dc4c5966d9a0ca6ec1674106340ecc64503585c12c2f854249af06f *ffmpeg-v29.4.0-mas-x64.zip -5fa13744b87fef1bfd24a37513677f446143e085504541f8ce97466803bd1893 *ffmpeg-v29.4.0-win32-arm64.zip -d7ba316bb7e13025c9db29e0acafebb540b7716c9f111e469733615d8521186a *ffmpeg-v29.4.0-win32-ia32.zip -35c70a28bcfd4f0b1f8c985d3d1348936bd60767d231ce28ba38f3daeeef64bb *ffmpeg-v29.4.0-win32-x64.zip -8c7228ea0ecab25a1f7fcd1ba9680684d19f9671a497113d71a851a53867b048 *hunspell_dictionaries.zip -7552547c8d585b9bc43518d239d7ce3ad7c5cad0346b07cdcfc1eab638b2b794 *libcxx-objects-v29.4.0-linux-arm64.zip -76054a779d4845ad752b625213ce8990f08dcc5b89aa20660dd4f2e817ba30a8 *libcxx-objects-v29.4.0-linux-armv7l.zip -761c317a9c874bd3d1118d0ecad33c4be23727f538cfbb42a08dd87c68da6039 *libcxx-objects-v29.4.0-linux-x64.zip -f98f9972cc30200b8e05815f5a9cd5cec04bdeee0e48ae2143cdaeff5db9d71d *libcxx_headers.zip -f0b0dd2be579baaf97901322ef489d03fae69a0b8524ea77b24fb3c896f73dd9 *libcxxabi_headers.zip -5da864ea23d70538298a40e0d037a5a461a6b74984e72fd4f0cd20904bccaed1 *mksnapshot-v29.4.0-darwin-arm64.zip -bde97bd7c69209ed6bf4cf1cdf7de622e3a9f50fe6b4dc4b5618eee868f47c62 *mksnapshot-v29.4.0-darwin-x64.zip -a3df9b9e6ef14efe5827d0256d8ecaebe6d8be130cfc3faac0dea76eb53b9b11 *mksnapshot-v29.4.0-linux-arm64-x64.zip -648b9dbca21194d663ddb706e6086a166e691263c764c80f836ae02c27e3657a *mksnapshot-v29.4.0-linux-armv7l-x64.zip -e7a4201cda3956380facc2b5b9d0b1020cc5e654fba44129fc7429a982411cc1 *mksnapshot-v29.4.0-linux-x64.zip -ffb44c45733675e0378f45fce25dafa95697d0c86179f8e46742ada16bc11aa1 *mksnapshot-v29.4.0-mas-arm64.zip -0242da3ca193206e56b88eb108502244bae35dcc587210bd0a32d9fa4cb71041 *mksnapshot-v29.4.0-mas-x64.zip -1445806dca6effbc60072bbde7997cefb62bdb7a9e295a090d26f27c3882685f *mksnapshot-v29.4.0-win32-arm64-x64.zip -09599adc3afb0a13ae87fc4b8ab97c729fe3689faa6a4f5f7a4a3cf0d9cc49d3 *mksnapshot-v29.4.0-win32-ia32.zip -84f80683d95665d29284386509bb104e840ff0b797bfbbd19da86b84d370aa49 *mksnapshot-v29.4.0-win32-x64.zip +65621c6968832b604d1b81b65c2a4fdc56baafe167e76d22c55afa4c67c840f1 *chromedriver-v30.3.1-darwin-arm64.zip +28001491af5805d8975f59edcee644f93e425b7e59f1fe94915ce9a286f7cd9e *chromedriver-v30.3.1-darwin-x64.zip +c7767da210b8d4f084576e308570fcf15268c7f6a1182a621ce90788fe45d458 *chromedriver-v30.3.1-linux-arm64.zip +509197b47830f31b715f3ab8940279c5b2c7f473c2059148efd86dffb58b895d *chromedriver-v30.3.1-linux-armv7l.zip +f4f6a66323b1180045321c779eacb88b48c8969e3ddf95d1d0aab92eb26ff071 *chromedriver-v30.3.1-linux-x64.zip +b66e108c5c3ea7ed9c3cc1ec57f3e248222ebcbf3b44acea740e1b6bb142e088 *chromedriver-v30.3.1-mas-arm64.zip +7f70cd8e34081f5991d9270885d56dd2ec13ac7e2aada8c486cb6cdced1104ab *chromedriver-v30.3.1-mas-x64.zip +4b983f7a9daf85f792ea4a98889d2ae1fe865cd132e9cb383bccc9cc85a922c2 *chromedriver-v30.3.1-win32-arm64.zip +4ec1c750ff60ffd381bde850f9769d1b38c7d227f83e79f969d1e9808f453219 *chromedriver-v30.3.1-win32-ia32.zip +bfaa6754c0cb425c731c69f4a097ebab70b4384d94ec258a055d1e18c611952a *chromedriver-v30.3.1-win32-x64.zip +64b237fce7aba1ee0f74355cc37a5d88a4989de253d94161a20002ee276242ea *electron-api.json +16f1e22aabb19fb9551dd4c0eeb36789813decca31a0921cc62745d314ffcda0 *electron-v30.3.1-darwin-arm64-dsym-snapshot.zip +c6b220a63b9afc19b7875127f2496f2aa42d5c092ef85e4b8f4d2783c26dcf6e *electron-v30.3.1-darwin-arm64-dsym.zip +bf8b1f7c511b9b71d6ca277a3f4b49ff3d6a17740b66ce1ea3de86d5d6219bbc *electron-v30.3.1-darwin-arm64-symbols.zip +9d7b31185ef61628e6f3abda32517953518f68d9f71a8fa29f2a0a17b9d3b9bb *electron-v30.3.1-darwin-arm64.zip +185b6216dc5f8e8284150eaf6398d56dcf611a8cef9320f1754ac235f96f877e *electron-v30.3.1-darwin-x64-dsym-snapshot.zip +a148b28e52ed9a8aa0ef605038bb9c232143f795e448c053452a3e259e55c7f1 *electron-v30.3.1-darwin-x64-dsym.zip +8cfa27404ae829d61962211304e4a7b3de14488fa816bb68ef12d3a359ab64d3 *electron-v30.3.1-darwin-x64-symbols.zip +a66dbe01006fee9319f19c16ef08d4d72ecbb79bd35fffece32b36b8afe2ea47 *electron-v30.3.1-darwin-x64.zip +302f8f0ac2bfd6e5abcf6c6ab631b2d9f726332e3db17e53a5311cc831d0c784 *electron-v30.3.1-linux-arm64-debug.zip +85a9babff9a45804c5356159baa747ee6ec5f31c60f1c11ddf5ebf2cc7752be7 *electron-v30.3.1-linux-arm64-symbols.zip +5f2326631080cc4a5f6231a83e492d8d75d9ccdd3bd41f56f6a1090e790d2a2a *electron-v30.3.1-linux-arm64.zip +302f8f0ac2bfd6e5abcf6c6ab631b2d9f726332e3db17e53a5311cc831d0c784 *electron-v30.3.1-linux-armv7l-debug.zip +aaa80103e9bdf90b8524c40a75588816ac8f41c5c3e3d399cce1be559e0a9c90 *electron-v30.3.1-linux-armv7l-symbols.zip +ef7be229a610cee357cba2f790618b239aab82fc2b26e0645c4cf4600dffb1c4 *electron-v30.3.1-linux-armv7l.zip +78ebaf13163db5065e2bc649c2d7de9f70448ebd593c618bc4b14467587d4ea2 *electron-v30.3.1-linux-x64-debug.zip +082c614d78f81a817fa098f874f23662ad1da7a25cf0536f313ebde8f8d8ec9c *electron-v30.3.1-linux-x64-symbols.zip +94d7470d9dae2dd2376612c804068ea6514e5efe342de8946f49f93bf0ed50de *electron-v30.3.1-linux-x64.zip +4665bf2334ebec90a5c8757e78c9b49cb102603f25bf4091dd890603cc8656f0 *electron-v30.3.1-mas-arm64-dsym-snapshot.zip +d104a56931d1acda28ca990c9d422fc8b14bc0ad1b340b7a6968778b8004727f *electron-v30.3.1-mas-arm64-dsym.zip +dd8e7e4b0adc35258cf7ad2f7119a6f367bb67442c1c53d3197324d3459676cd *electron-v30.3.1-mas-arm64-symbols.zip +84c806219aeb9d51d041b5f51a87a046c6234c9a42fdaa2dd76376141c668390 *electron-v30.3.1-mas-arm64.zip +ab0c4bce223194ebf4a1ec690ba21124e0478dd8b5a445f4666780d93e56e084 *electron-v30.3.1-mas-x64-dsym-snapshot.zip +e60b71d65b2101798a633645e251f97b3357018ea06bbc54f980c963d49c588f *electron-v30.3.1-mas-x64-dsym.zip +875cc7a062edf1c83d0c45be073e3edf8a7820bec2be45940c8092029aaafab6 *electron-v30.3.1-mas-x64-symbols.zip +c674cd81a47b25b74a0827c2184e895b1a56b51c5cc926963b7d4f04aae102cf *electron-v30.3.1-mas-x64.zip +1dedd1067c9110297ef503eee5c3c46d6acd2cfd9beacc6b74c1ceee50589818 *electron-v30.3.1-win32-arm64-pdb.zip +6c6cd1cf07e065ca64b383df12f9a7db3a1019a703e4a901da95a54f2d9672c2 *electron-v30.3.1-win32-arm64-symbols.zip +7351fa5cb892853a7d4b67d8858d0f9cc6506c554a2e42c9ad7e8d5e29ae2743 *electron-v30.3.1-win32-arm64-toolchain-profile.zip +ad1cf249104bedf4f2f22025c43aef14e26ca8eb57e14ee8061cbe9ec89185d2 *electron-v30.3.1-win32-arm64.zip +b9904b4f6a922995ade1a65580e6333956ee7862dce289d29052a0e3aa4b59e5 *electron-v30.3.1-win32-ia32-pdb.zip +e1bc6a68a1ff6f84ee95343e74bbbb4a47ac801307398537fa6e0d26343f916d *electron-v30.3.1-win32-ia32-symbols.zip +7351fa5cb892853a7d4b67d8858d0f9cc6506c554a2e42c9ad7e8d5e29ae2743 *electron-v30.3.1-win32-ia32-toolchain-profile.zip +65b5dda5cb4fcd5eec6a8e7667d6461324abf321ff7287b1aa79cc885fec2711 *electron-v30.3.1-win32-ia32.zip +2597a12e9c03bfe52c991f3e51319ad2861d1931890b8d6ec94c44edcac73e79 *electron-v30.3.1-win32-x64-pdb.zip +66cf63327bb0fd2a0a0aadb39f949714ed80e3a80832d852658a54dd59cf502f *electron-v30.3.1-win32-x64-symbols.zip +7351fa5cb892853a7d4b67d8858d0f9cc6506c554a2e42c9ad7e8d5e29ae2743 *electron-v30.3.1-win32-x64-toolchain-profile.zip +5b2701e5b02a89f4a07b0d7a401c7daaa4cc26926cbdc88637982c2cb941f82f *electron-v30.3.1-win32-x64.zip +a61941eebd117ed89d6fbda8558f246558b95a6c4dd7d2d37d38f1e7631da221 *electron.d.ts +7bdcd58d36dd5e985a93112c9c690ce08409953affe676815fbf7829fa03cda8 *ffmpeg-v30.3.1-darwin-arm64.zip +9e64d80ccc6b1e35ac8035e0def8c849e2db3b073070dbcc2a2eedc9ad7004b3 *ffmpeg-v30.3.1-darwin-x64.zip +d8e8ac7d0da34655aa4276750bc13bbd43cc47c67f9e1330bdf1607ad4a4b50e *ffmpeg-v30.3.1-linux-arm64.zip +4f7583513d48b48c44a2cbc4430cbc9a33d8f9728622166db688e3de61190821 *ffmpeg-v30.3.1-linux-armv7l.zip +4d7514fba8524bbc3d0e541324bf9d702dfb61d102041d6ee8fbe3a428935292 *ffmpeg-v30.3.1-linux-x64.zip +7bdcd58d36dd5e985a93112c9c690ce08409953affe676815fbf7829fa03cda8 *ffmpeg-v30.3.1-mas-arm64.zip +9e64d80ccc6b1e35ac8035e0def8c849e2db3b073070dbcc2a2eedc9ad7004b3 *ffmpeg-v30.3.1-mas-x64.zip +584b0ae41a9b2cf5682fbe4260a40566e1d1f93bb104e5bed54212e64cb6ae5b *ffmpeg-v30.3.1-win32-arm64.zip +4987fa69493ef769077cab04d626a879e344b0da166048c34c8aedcf3b10f89c *ffmpeg-v30.3.1-win32-ia32.zip +190f265268d6fb948d93ec177b2ac504acea8bd5e254d5b34369da6674236bad *ffmpeg-v30.3.1-win32-x64.zip +0db5cfa86a97971eedc62a09baec388ca88a9ef5e607044f2e288c92cc04ed60 *hunspell_dictionaries.zip +421ff8bbe7d784d1d7ce794eae88e5d345ab9c9180e8c2aa643886440b4bbe97 *libcxx-objects-v30.3.1-linux-arm64.zip +aa6a2f00286b9c41d37426a839d64b6cbfaed7a7a5348e5390e3aadbce6937a4 *libcxx-objects-v30.3.1-linux-armv7l.zip +7f89023e904af0262b2806c05068e8a2f1a2152de9839ef52f235222045e8082 *libcxx-objects-v30.3.1-linux-x64.zip +c7b21096ffbdcdff5c0135260184388b842616c63201f45e0939ab425ac5a0aa *libcxx_headers.zip +5ff9e452351176c45ad43ab3c754ad25ec300872b96f583963d73d071ab349c6 *libcxxabi_headers.zip +fe69b74f92a7902dfee1cd9a91bd9dd4a13f75513083d0481ba65b27c8979473 *mksnapshot-v30.3.1-darwin-arm64.zip +eee44d512db09f72b57fb0decae6dd8ad3e67a04f263e27876ae4f99feb270ab *mksnapshot-v30.3.1-darwin-x64.zip +40fd5aaa90f697dfbd16f2577f409ea0349e2f9d7f9d4eca8aac313c10dc85fd *mksnapshot-v30.3.1-linux-arm64-x64.zip +9fd98963fae95b8fe8b9641e7ac1f29f06e6f91d944653d08e5c02bd2bd59010 *mksnapshot-v30.3.1-linux-armv7l-x64.zip +1cb47a00b4cf699a953751ebd6c3e09fbcf0a16780ecc0bce71605a9edce4673 *mksnapshot-v30.3.1-linux-x64.zip +5216e7e39618acb8815b26cea10fe91f8f1aac90bfb74c10bf997744eb98b781 *mksnapshot-v30.3.1-mas-arm64.zip +e3bd5ca0ff14b6e16b9ab73f9e4bb7c543c6b9fe45acef60191c1f1a99e74a95 *mksnapshot-v30.3.1-mas-x64.zip +93e882361573c53ff990bb633d3402e65027d871cf1417fc582a9fe126ca14c7 *mksnapshot-v30.3.1-win32-arm64-x64.zip +224eb2493a93f1ba47321edfbe6413583fe353ecf9c1773f7b2b212a1e293518 *mksnapshot-v30.3.1-win32-ia32.zip +ff3fa1c931f33c01d2b094f811f83ab0c370b400e8a2df4210648c46699ff4b1 *mksnapshot-v30.3.1-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index bcc9340406d..23a56cdb23b 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -e0065c61f340e85106a99c4b54746c5cee09d59b08c5712f67f99e92aa44995d node-v20.11.1-darwin-arm64.tar.gz -c52e7fb0709dbe63a4cbe08ac8af3479188692937a7bd8e776e0eedfa33bb848 node-v20.11.1-darwin-x64.tar.gz -e34ab2fc2726b4abd896bcbff0250e9b2da737cbd9d24267518a802ed0606f3b node-v20.11.1-linux-arm64.tar.gz -e42791f76ece283c7a4b97fbf716da72c5128c54a9779f10f03ae74a4bcfb8f6 node-v20.11.1-linux-armv7l.tar.gz -bf3a779bef19452da90fb88358ec2c57e0d2f882839b20dc6afc297b6aafc0d7 node-v20.11.1-linux-x64.tar.gz -a5a9d30a8f7d56e00ccb27c1a7d24c8d0bc96a2689ebba8eb7527698793496f1 win-arm64/node.exe -bc585910690318aaebe3c57669cb83ca9d1e5791efd63195e238f54686e6c2ec win-x64/node.exe +4743bc042f90ba5d9edf09403207290a9cdd2f6061bdccf7caaa0bbfd49f343e node-v20.15.1-darwin-arm64.tar.gz +f5379772ffae1404cfd1fcc8cf0c6c5971306b8fb2090d348019047306de39dc node-v20.15.1-darwin-x64.tar.gz +8554c91ccd32782351035d3a9b168ad01c6922480800a21870fc5d6d86c2bb70 node-v20.15.1-linux-arm64.tar.gz +2c16717da7d2d7b00f6af146cdf436a0297cbcee52c85b754e4c9ed7cee34b51 node-v20.15.1-linux-armv7l.tar.gz +a9db028c0a1c63e3aa0d97de24b0966bc507d8239b3aedc4e752eea6b0580665 node-v20.15.1-linux-x64.tar.gz +8e3f84e8ec7e41f98a048eb0c1365cfe54426a556ead98c4803df45d29e0335d win-arm64/node.exe +229fb64aeb10d3cc18eaaa2f5a4c3f1c81792dd3647c5c4350e142db528d0f89 win-x64/node.exe diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index 85d27273861..a3daf1878b0 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const path = require("path"); const fs = require("fs"); +const minimatch = require("minimatch"); const vscode_universal_bundler_1 = require("vscode-universal-bundler"); const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); const root = path.dirname(path.dirname(__dirname)); @@ -18,25 +19,29 @@ async function main(buildDir) { const appName = product.nameLong + '.app'; const x64AppPath = path.join(buildDir, 'VSCode-darwin-x64', appName); const arm64AppPath = path.join(buildDir, 'VSCode-darwin-arm64', appName); - const x64AsarPath = path.join(x64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); - const arm64AsarPath = path.join(arm64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); + const asarRelativePath = path.join('Contents', 'Resources', 'app', 'node_modules.asar'); const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + const filesToSkip = [ + '**/CodeResources', + '**/Credits.rtf', + ]; await (0, vscode_universal_bundler_1.makeUniversalApp)({ x64AppPath, arm64AppPath, - x64AsarPath, - arm64AsarPath, - filesToSkip: [ - 'Credits.rtf', - 'CodeResources', - 'fsevents.node', - 'Info.plist', // TODO@deepak1556: regressed with 11.4.2 internal builds - 'MainMenu.nib', // Generated sequence is not deterministic with Xcode 13 - '.npmrc' - ], + asarPath: asarRelativePath, outAppPath, - force: true + force: true, + mergeASARs: true, + x64ArchFiles: '*/kerberos.node', + filesToSkipComparison: (file) => { + for (const expected of filesToSkip) { + if (minimatch(file, expected)) { + return true; + } + } + return false; + } }); const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); Object.assign(productJson, { diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 04eb3a11e20..94b8a23b9e5 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import * as fs from 'fs'; +import * as minimatch from 'minimatch'; import { makeUniversalApp } from 'vscode-universal-bundler'; import { spawn } from '@malept/cross-spawn-promise'; @@ -21,26 +22,31 @@ async function main(buildDir?: string) { const appName = product.nameLong + '.app'; const x64AppPath = path.join(buildDir, 'VSCode-darwin-x64', appName); const arm64AppPath = path.join(buildDir, 'VSCode-darwin-arm64', appName); - const x64AsarPath = path.join(x64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); - const arm64AsarPath = path.join(arm64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); + const asarRelativePath = path.join('Contents', 'Resources', 'app', 'node_modules.asar'); const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + const filesToSkip = [ + '**/CodeResources', + '**/Credits.rtf', + ]; + await makeUniversalApp({ x64AppPath, arm64AppPath, - x64AsarPath, - arm64AsarPath, - filesToSkip: [ - 'Credits.rtf', - 'CodeResources', - 'fsevents.node', - 'Info.plist', // TODO@deepak1556: regressed with 11.4.2 internal builds - 'MainMenu.nib', // Generated sequence is not deterministic with Xcode 13 - '.npmrc' - ], + asarPath: asarRelativePath, outAppPath, - force: true + force: true, + mergeASARs: true, + x64ArchFiles: '*/kerberos.node', + filesToSkipComparison: (file: string) => { + for (const expected of filesToSkip) { + if (minimatch(file, expected)) { + return true; + } + } + return false; + } }); const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); diff --git a/build/filters.js b/build/filters.js index 915240f0f0b..e4d74a6cfb3 100644 --- a/build/filters.js +++ b/build/filters.js @@ -117,7 +117,7 @@ module.exports.indentationFilter = [ '!src/vs/*/**/*.d.ts', '!src/typings/**/*.d.ts', '!extensions/**/*.d.ts', - '!**/*.{svg,exe,png,bmp,jpg,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,template,yaml,yml,d.ts.recipe,ico,icns,plist,opus,admx,adml,wasm}', + '!**/*.{svg,exe,png,bmp,jpg,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,psm1,template,yaml,yml,d.ts.recipe,ico,icns,plist,opus,admx,adml,wasm}', '!build/{lib,download,linux,darwin}/**/*.js', '!build/**/*.sh', '!build/azure-pipelines/**/*.js', diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index b85425bccfc..4631b295ae4 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -48,7 +48,6 @@ const compilations = [ 'extensions/json-language-features/client/tsconfig.json', 'extensions/json-language-features/server/tsconfig.json', 'extensions/markdown-language-features/preview-src/tsconfig.json', - 'extensions/markdown-language-features/server/tsconfig.json', 'extensions/markdown-language-features/tsconfig.json', 'extensions/markdown-math/tsconfig.json', 'extensions/media-preview/tsconfig.json', diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index d7a814b9a1b..560bdc1f6b7 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -41,6 +41,7 @@ const REMOTE_FOLDER = path.join(REPO_ROOT, 'remote'); const BUILD_TARGETS = [ { platform: 'win32', arch: 'x64' }, + { platform: 'win32', arch: 'arm64' }, { platform: 'darwin', arch: 'x64' }, { platform: 'darwin', arch: 'arm64' }, { platform: 'linux', arch: 'x64' }, @@ -63,6 +64,8 @@ const serverResources = [ // Terminal shell integration 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1', + 'out-build/vs/workbench/contrib/terminal/browser/media/CodeTabExpansion.psm1', + 'out-build/vs/workbench/contrib/terminal/browser/media/GitTabExpansion.psm1', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh', @@ -127,21 +130,7 @@ function getNodeVersion() { return { nodeVersion, internalNodeVersion }; } -function getNodeChecksum(nodeVersion, platform, arch, glibcPrefix) { - let expectedName; - switch (platform) { - case 'win32': - expectedName = product.nodejsRepository !== 'https://nodejs.org' ? - `win-${arch}-node.exe` : `win-${arch}/node.exe`; - break; - - case 'darwin': - case 'alpine': - case 'linux': - expectedName = `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`; - break; - } - +function getNodeChecksum(expectedName) { const nodeJsChecksums = fs.readFileSync(path.join(REPO_ROOT, 'build', 'checksums', 'nodejs.txt'), 'utf8'); for (const line of nodeJsChecksums.split('\n')) { const [checksum, name] = line.split(/\s+/); @@ -196,7 +185,24 @@ function nodejs(platform, arch) { log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from ${product.nodejsRepository}...`); const glibcPrefix = process.env['VSCODE_NODE_GLIBC'] ?? ''; - const checksumSha256 = getNodeChecksum(nodeVersion, platform, arch, glibcPrefix); + let expectedName; + switch (platform) { + case 'win32': + expectedName = product.nodejsRepository !== 'https://nodejs.org' ? + `win-${arch}-node.exe` : `win-${arch}/node.exe`; + break; + + case 'darwin': + expectedName = `node-v${nodeVersion}-${platform}-${arch}.tar.gz`; + break; + case 'linux': + expectedName = `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`; + break; + case 'alpine': + expectedName = `node-v${nodeVersion}-linux-${arch}-musl.tar.gz`; + break; + } + const checksumSha256 = getNodeChecksum(expectedName); if (checksumSha256) { log(`Using SHA256 checksum for checking integrity: ${checksumSha256}`); @@ -207,13 +213,13 @@ function nodejs(platform, arch) { switch (platform) { case 'win32': return (product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: `win-${arch}-node.exe`, checksumSha256 }) : + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) : fetchUrls(`/dist/v${nodeVersion}/win-${arch}/node.exe`, { base: 'https://nodejs.org', checksumSha256 })) .pipe(rename('node.exe')); case 'darwin': case 'linux': return (product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`, checksumSha256 }) : + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) : fetchUrls(`/dist/v${nodeVersion}/node-v${nodeVersion}-${platform}-${arch}.tar.gz`, { base: 'https://nodejs.org', checksumSha256 }) ).pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) .pipe(filter('**/node')) @@ -221,7 +227,7 @@ function nodejs(platform, arch) { .pipe(rename('node')); case 'alpine': return product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: `node-v${nodeVersion}-${platform}-${arch}.tar.gz`, checksumSha256 }) + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) .pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) .pipe(filter('**/node')) .pipe(util.setExecutableBit('**')) diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index b3b35466af0..edca10c8420 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -65,6 +65,7 @@ const vscodeResources = [ 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', 'out-build/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/*.fish', 'out-build/vs/workbench/contrib/terminal/browser/media/*.ps1', + 'out-build/vs/workbench/contrib/terminal/browser/media/*.psm1', 'out-build/vs/workbench/contrib/terminal/browser/media/*.sh', 'out-build/vs/workbench/contrib/terminal/browser/media/*.zsh', 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', @@ -79,7 +80,6 @@ const vscodeResources = [ // be inlined into the target window file in this order // and they depend on each other in this way. const windowBootstrapFiles = [ - 'out-build/bootstrap.js', 'out-build/vs/loader.js', 'out-build/bootstrap-window.js' ]; @@ -288,10 +288,13 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op '**/*.node', '**/@vscode/ripgrep/bin/*', '**/node-pty/build/Release/*', + '**/node-pty/build/Release/conpty/*', '**/node-pty/lib/worker/conoutSocketWorker.js', '**/node-pty/lib/shared/conout.js', '**/*.wasm', '**/@vscode/vsce-sign/bin/*', + ], [ + '**/*.mk', ], 'node_modules.asar')); let all = es.merge( diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 28ddfb04c3d..79594543c3b 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -108,7 +108,11 @@ function prepareDebPackage(arch) { .pipe(replace('@@NAME@@', product.applicationName)) .pipe(rename('DEBIAN/postinst')); - const all = es.merge(control, postinst, postrm, prerm, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, code); + const templates = gulp.src('resources/linux/debian/templates.template', { base: '.' }) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(rename('DEBIAN/templates')); + + const all = es.merge(control, templates, postinst, postrm, prerm, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, code); return all.pipe(vfs.dest(destination)); }; diff --git a/build/lib/asar.js b/build/lib/asar.js index 31845f2f2dd..07b39bf79ff 100644 --- a/build/lib/asar.js +++ b/build/lib/asar.js @@ -11,7 +11,7 @@ const pickle = require('chromium-pickle-js'); const Filesystem = require('asar/lib/filesystem'); const VinylFile = require("vinyl"); const minimatch = require("minimatch"); -function createAsar(folderPath, unpackGlobs, destFilename) { +function createAsar(folderPath, unpackGlobs, skipGlobs, destFilename) { const shouldUnpackFile = (file) => { for (let i = 0; i < unpackGlobs.length; i++) { if (minimatch(file.relative, unpackGlobs[i])) { @@ -20,6 +20,14 @@ function createAsar(folderPath, unpackGlobs, destFilename) { } return false; }; + const shouldSkipFile = (file) => { + for (const skipGlob of skipGlobs) { + if (minimatch(file.relative, skipGlob)) { + return true; + } + } + return false; + }; const filesystem = new Filesystem(folderPath); const out = []; // Keep track of pending inserts @@ -64,6 +72,9 @@ function createAsar(folderPath, unpackGlobs, destFilename) { if (!file.stat.isFile()) { throw new Error(`unknown item in stream!`); } + if (shouldSkipFile(file)) { + return; + } const shouldUnpack = shouldUnpackFile(file); insertFile(file.relative, { size: file.contents.length, mode: file.stat.mode }, shouldUnpack); if (shouldUnpack) { diff --git a/build/lib/asar.ts b/build/lib/asar.ts index 44a6416bdfb..7dc1dd3b2e6 100644 --- a/build/lib/asar.ts +++ b/build/lib/asar.ts @@ -17,7 +17,7 @@ declare class AsarFilesystem { insertFile(path: string, shouldUnpack: boolean, file: { stat: { size: number; mode: number } }, options: {}): Promise; } -export function createAsar(folderPath: string, unpackGlobs: string[], destFilename: string): NodeJS.ReadWriteStream { +export function createAsar(folderPath: string, unpackGlobs: string[], skipGlobs: string[], destFilename: string): NodeJS.ReadWriteStream { const shouldUnpackFile = (file: VinylFile): boolean => { for (let i = 0; i < unpackGlobs.length; i++) { @@ -28,6 +28,15 @@ export function createAsar(folderPath: string, unpackGlobs: string[], destFilena return false; }; + const shouldSkipFile = (file: VinylFile): boolean => { + for (const skipGlob of skipGlobs) { + if (minimatch(file.relative, skipGlob)) { + return true; + } + } + return false; + }; + const filesystem = new Filesystem(folderPath); const out: Buffer[] = []; @@ -78,6 +87,9 @@ export function createAsar(folderPath: string, unpackGlobs: string[], destFilena if (!file.stat.isFile()) { throw new Error(`unknown item in stream!`); } + if (shouldSkipFile(file)) { + return; + } const shouldUnpack = shouldUnpackFile(file); insertFile(file.relative, { size: file.contents.length, mode: file.stat.mode }, shouldUnpack); diff --git a/build/lib/bundle.js b/build/lib/bundle.js index a0989638f7c..df7c10fa37e 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -5,6 +5,7 @@ *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); exports.bundle = bundle; +exports.removeDuplicateTSBoilerplate = removeDuplicateTSBoilerplate; const fs = require("fs"); const path = require("path"); const vm = require("vm"); @@ -127,7 +128,7 @@ function emitEntryPoints(modules, entryPoints) { }); return { // TODO@TS 2.1.2 - files: extractStrings(removeDuplicateTSBoilerplate(result)), + files: extractStrings(removeAllDuplicateTSBoilerplate(result)), bundleData: bundleData }; } @@ -216,7 +217,16 @@ function extractStrings(destFiles) { }); return destFiles; } -function removeDuplicateTSBoilerplate(destFiles) { +function removeAllDuplicateTSBoilerplate(destFiles) { + destFiles.forEach((destFile) => { + const SEEN_BOILERPLATE = []; + destFile.sources.forEach((source) => { + source.contents = removeDuplicateTSBoilerplate(source.contents, SEEN_BOILERPLATE); + }); + }); + return destFiles; +} +function removeDuplicateTSBoilerplate(source, SEEN_BOILERPLATE = []) { // Taken from typescript compiler => emitFiles const BOILERPLATE = [ { start: /^var __extends/, end: /^}\)\(\);$/ }, @@ -230,45 +240,39 @@ function removeDuplicateTSBoilerplate(destFiles) { { start: /^var __setModuleDefault/, end: /^}\);$/ }, { start: /^var __importStar/, end: /^};$/ }, ]; - destFiles.forEach((destFile) => { - const SEEN_BOILERPLATE = []; - destFile.sources.forEach((source) => { - const lines = source.contents.split(/\r\n|\n|\r/); - const newLines = []; - let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); - if (END_BOILERPLATE.test(line)) { - IS_REMOVING_BOILERPLATE = false; - } - } - else { - for (let j = 0; j < BOILERPLATE.length; j++) { - const boilerplate = BOILERPLATE[j]; - if (boilerplate.start.test(line)) { - if (SEEN_BOILERPLATE[j]) { - IS_REMOVING_BOILERPLATE = true; - END_BOILERPLATE = boilerplate.end; - } - else { - SEEN_BOILERPLATE[j] = true; - } - } - } - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); + const lines = source.split(/\r\n|\n|\r/); + const newLines = []; + let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + if (END_BOILERPLATE.test(line)) { + IS_REMOVING_BOILERPLATE = false; + } + } + else { + for (let j = 0; j < BOILERPLATE.length; j++) { + const boilerplate = BOILERPLATE[j]; + if (boilerplate.start.test(line)) { + if (SEEN_BOILERPLATE[j]) { + IS_REMOVING_BOILERPLATE = true; + END_BOILERPLATE = boilerplate.end; } else { - newLines.push(line); + SEEN_BOILERPLATE[j] = true; } } } - source.contents = newLines.join('\n'); - }); - }); - return destFiles; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + } + else { + newLines.push(line); + } + } + } + return newLines.join('\n'); } function emitEntryPoint(modulesMap, deps, entryPoint, includedModules, prepend, append, dest) { if (!dest) { diff --git a/build/lib/bundle.ts b/build/lib/bundle.ts index 692100ff515..74d37fcce55 100644 --- a/build/lib/bundle.ts +++ b/build/lib/bundle.ts @@ -251,7 +251,7 @@ function emitEntryPoints(modules: IBuildModuleInfo[], entryPoints: IEntryPointMa return { // TODO@TS 2.1.2 - files: extractStrings(removeDuplicateTSBoilerplate(result)), + files: extractStrings(removeAllDuplicateTSBoilerplate(result)), bundleData: bundleData }; } @@ -350,7 +350,19 @@ function extractStrings(destFiles: IConcatFile[]): IConcatFile[] { return destFiles; } -function removeDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { +function removeAllDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { + destFiles.forEach((destFile) => { + const SEEN_BOILERPLATE: boolean[] = []; + destFile.sources.forEach((source) => { + source.contents = removeDuplicateTSBoilerplate(source.contents, SEEN_BOILERPLATE); + }); + }); + + return destFiles; +} + +export function removeDuplicateTSBoilerplate(source: string, SEEN_BOILERPLATE: boolean[] = []): string { + // Taken from typescript compiler => emitFiles const BOILERPLATE = [ { start: /^var __extends/, end: /^}\)\(\);$/ }, @@ -365,44 +377,37 @@ function removeDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { { start: /^var __importStar/, end: /^};$/ }, ]; - destFiles.forEach((destFile) => { - const SEEN_BOILERPLATE: boolean[] = []; - destFile.sources.forEach((source) => { - const lines = source.contents.split(/\r\n|\n|\r/); - const newLines: string[] = []; - let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE: RegExp; + const lines = source.split(/\r\n|\n|\r/); + const newLines: string[] = []; + let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE: RegExp; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); - if (END_BOILERPLATE!.test(line)) { - IS_REMOVING_BOILERPLATE = false; - } - } else { - for (let j = 0; j < BOILERPLATE.length; j++) { - const boilerplate = BOILERPLATE[j]; - if (boilerplate.start.test(line)) { - if (SEEN_BOILERPLATE[j]) { - IS_REMOVING_BOILERPLATE = true; - END_BOILERPLATE = boilerplate.end; - } else { - SEEN_BOILERPLATE[j] = true; - } - } - } - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + if (END_BOILERPLATE!.test(line)) { + IS_REMOVING_BOILERPLATE = false; + } + } else { + for (let j = 0; j < BOILERPLATE.length; j++) { + const boilerplate = BOILERPLATE[j]; + if (boilerplate.start.test(line)) { + if (SEEN_BOILERPLATE[j]) { + IS_REMOVING_BOILERPLATE = true; + END_BOILERPLATE = boilerplate.end; } else { - newLines.push(line); + SEEN_BOILERPLATE[j] = true; } } } - source.contents = newLines.join('\n'); - }); - }); - - return destFiles; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + } else { + newLines.push(line); + } + } + } + return newLines.join('\n'); } interface IPluginMap { diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 58d4d3e9a7f..7bc9cb51cae 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -482,9 +482,6 @@ async function esbuildExtensions(taskName, isWatch, scripts) { return reject(error); } reporter(stderr, script); - if (stderr) { - return reject(); - } return resolve(); }); proc.stdout.on('data', (data) => { diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 0582e0cb11e..5c7fa45323c 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -563,9 +563,6 @@ async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { return reject(error); } reporter(stderr, script); - if (stderr) { - return reject(); - } return resolve(); }); diff --git a/build/lib/fetch.js b/build/lib/fetch.js index 2fed63bca0e..b7da65f4af2 100644 --- a/build/lib/fetch.js +++ b/build/lib/fetch.js @@ -33,7 +33,7 @@ function fetchUrls(urls, options) { })); } async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { - const verbose = !!options.verbose ?? (!!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']); + const verbose = !!options.verbose || !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']; try { let startTime = 0; if (verbose) { diff --git a/build/lib/fetch.ts b/build/lib/fetch.ts index dc1de777e04..0c44b8e567f 100644 --- a/build/lib/fetch.ts +++ b/build/lib/fetch.ts @@ -42,7 +42,7 @@ export function fetchUrls(urls: string[] | string, options: IFetchOptions): es.T } export async function fetchUrl(url: string, options: IFetchOptions, retries = 10, retryDelay = 1000): Promise { - const verbose = !!options.verbose ?? (!!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']); + const verbose = !!options.verbose || !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']; try { let startTime = 0; if (verbose) { diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index b080b05f102..fb732ae1eaf 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -561,6 +561,14 @@ { "name": "vs/workbench/contrib/authentication", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/replNotebook", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/list", + "project": "vscode-workbench" } ] } diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 7494b71bb66..7c7fd3bc5fd 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -69,7 +69,9 @@ const CORE_TYPES = [ 'RequestInit', 'Headers', 'Response', - '__global' + '__global', + 'PerformanceMark', + 'PerformanceObserver', ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 4861fa6d86e..60939fe2750 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -70,7 +70,9 @@ const CORE_TYPES = [ 'RequestInit', 'Headers', 'Response', - '__global' + '__global', + 'PerformanceMark', + 'PerformanceObserver', ]; // Types that are defined in a common layer but are known to be only diff --git a/build/lib/nls.js b/build/lib/nls.js index ae235a5a534..107a8d4162e 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -10,6 +10,7 @@ const event_stream_1 = require("event-stream"); const File = require("vinyl"); const sm = require("source-map"); const path = require("path"); +const sort = require("gulp-sort"); var CollectStepResult; (function (CollectStepResult) { CollectStepResult[CollectStepResult["Yes"] = 0] = "Yes"; @@ -44,7 +45,9 @@ function clone(object) { function nls(options) { let base; const input = (0, event_stream_1.through)(); - const output = input.pipe((0, event_stream_1.through)(function (f) { + const output = input + .pipe(sort()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files + .pipe((0, event_stream_1.through)(function (f) { if (!f.sourceMap) { return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); } diff --git a/build/lib/nls.ts b/build/lib/nls.ts index 066dc1440c2..1861eca6287 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -8,7 +8,8 @@ import * as lazy from 'lazy.js'; import { duplex, through } from 'event-stream'; import * as File from 'vinyl'; import * as sm from 'source-map'; -import * as path from 'path'; +import * as path from 'path'; +import * as sort from 'gulp-sort'; declare class FileSourceMap extends File { public sourceMap: sm.RawSourceMap; @@ -54,62 +55,64 @@ function clone(object: T): T { export function nls(options: { preserveEnglish: boolean }): NodeJS.ReadWriteStream { let base: string; const input = through(); - const output = input.pipe(through(function (f: FileSourceMap) { - if (!f.sourceMap) { - return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); - } + const output = input + .pipe(sort()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files + .pipe(through(function (f: FileSourceMap) { + if (!f.sourceMap) { + return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); + } - let source = f.sourceMap.sources[0]; - if (!source) { - return this.emit('error', new Error(`File ${f.relative} does not have a source in the source map.`)); - } + let source = f.sourceMap.sources[0]; + if (!source) { + return this.emit('error', new Error(`File ${f.relative} does not have a source in the source map.`)); + } - const root = f.sourceMap.sourceRoot; - if (root) { - source = path.join(root, source); - } + const root = f.sourceMap.sourceRoot; + if (root) { + source = path.join(root, source); + } - const typescript = f.sourceMap.sourcesContent![0]; - if (!typescript) { - return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); - } + const typescript = f.sourceMap.sourcesContent![0]; + if (!typescript) { + return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); + } - base = f.base; - this.emit('data', _nls.patchFile(f, typescript, options)); - }, function () { - for (const file of [ - new File({ - contents: Buffer.from(JSON.stringify({ - keys: _nls.moduleToNLSKeys, - messages: _nls.moduleToNLSMessages, - }, null, '\t')), - base, - path: `${base}/nls.metadata.json` - }), - new File({ - contents: Buffer.from(JSON.stringify(_nls.allNLSMessages)), - base, - path: `${base}/nls.messages.json` - }), - new File({ - contents: Buffer.from(JSON.stringify(_nls.allNLSModulesAndKeys)), - base, - path: `${base}/nls.keys.json` - }), - new File({ - contents: Buffer.from(`/*--------------------------------------------------------- + base = f.base; + this.emit('data', _nls.patchFile(f, typescript, options)); + }, function () { + for (const file of [ + new File({ + contents: Buffer.from(JSON.stringify({ + keys: _nls.moduleToNLSKeys, + messages: _nls.moduleToNLSMessages, + }, null, '\t')), + base, + path: `${base}/nls.metadata.json` + }), + new File({ + contents: Buffer.from(JSON.stringify(_nls.allNLSMessages)), + base, + path: `${base}/nls.messages.json` + }), + new File({ + contents: Buffer.from(JSON.stringify(_nls.allNLSModulesAndKeys)), + base, + path: `${base}/nls.keys.json` + }), + new File({ + contents: Buffer.from(`/*--------------------------------------------------------- * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(_nls.allNLSMessages)};`), - base, - path: `${base}/nls.messages.js` - }) - ]) { - this.emit('data', file); - } + base, + path: `${base}/nls.messages.js` + }) + ]) { + this.emit('data', file); + } - this.emit('end'); - })); + this.emit('end'); + })); return duplex(input, output); } diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 148aa2786dd..a17856c02f3 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -255,6 +255,10 @@ "--vscode-editorLightBulb-foreground", "--vscode-editorLightBulbAi-foreground", "--vscode-editorLightBulbAutoFix-foreground", + "--vscode-editorActionList-background", + "--vscode-editorActionList-foreground", + "--vscode-editorActionList-focusForeground", + "--vscode-editorActionList-focusBackground", "--vscode-editorLineNumber-activeForeground", "--vscode-editorLineNumber-dimmedForeground", "--vscode-editorLineNumber-foreground", @@ -339,7 +343,7 @@ "--vscode-icon-foreground", "--vscode-inlineChat-background", "--vscode-inlineChat-border", - "--vscode-inlineChat-regionHighlight", + "--vscode-inlineChat-foreground", "--vscode-inlineChat-shadow", "--vscode-inlineChatDiff-inserted", "--vscode-inlineChatDiff-removed", @@ -531,7 +535,21 @@ "--vscode-quickInputList-focusForeground", "--vscode-quickInputList-focusIconForeground", "--vscode-quickInputTitle-background", + "--vscode-radio-activeBackground", + "--vscode-radio-activeBorder", + "--vscode-radio-activeForeground", + "--vscode-radio-inactiveBackground", + "--vscode-radio-inactiveBorder", + "--vscode-radio-inactiveForeground", + "--vscode-radio-inactiveHoverBackground", "--vscode-sash-hoverBorder", + "--vscode-scm-historyGraph-green", + "--vscode-scm-historyGraph-historyItemGroupBase", + "--vscode-scm-historyGraph-historyItemGroupHoverLabelForeground", + "--vscode-scm-historyGraph-historyItemGroupLocal", + "--vscode-scm-historyGraph-historyItemGroupRemote", + "--vscode-scm-historyGraph-red", + "--vscode-scm-historyGraph-yellow", "--vscode-scm-historyItemAdditionsForeground", "--vscode-scm-historyItemDeletionsForeground", "--vscode-scm-historyItemSelectedStatisticsBorder", @@ -711,8 +729,10 @@ "--vscode-terminalCommandDecoration-defaultBackground", "--vscode-terminalCommandDecoration-errorBackground", "--vscode-terminalCommandDecoration-successBackground", + "--vscode-terminalCommandGuide-foreground", "--vscode-terminalCursor-background", "--vscode-terminalCursor-foreground", + "--vscode-terminalOverviewRuler-border", "--vscode-terminalOverviewRuler-cursorForeground", "--vscode-terminalOverviewRuler-findMatchForeground", "--vscode-terminalStickyScroll-background", @@ -835,6 +855,8 @@ "--vscode-editorStickyScroll-scrollableWidth", "--vscode-editorStickyScroll-foldingOpacityTransition", "--window-border-color", + "--vscode-parameterHintsWidget-editorFontFamily", + "--vscode-parameterHintsWidget-editorFontFamilyDefault", "--workspace-trust-check-color", "--workspace-trust-selected-color", "--workspace-trust-unselected-color", @@ -858,4 +880,4 @@ "--widget-color", "--text-link-decoration" ] -} +} \ No newline at end of file diff --git a/build/linux/debian/dep-lists.js b/build/linux/debian/dep-lists.js index d843c090063..3bb58fb1215 100644 --- a/build/linux/debian/dep-lists.js +++ b/build/linux/debian/dep-lists.js @@ -31,6 +31,7 @@ exports.referenceGeneratedDepsByArch = { 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', 'libc6 (>= 2.2.5)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', @@ -39,10 +40,8 @@ exports.referenceGeneratedDepsByArch = { 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.37.3)', - 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.30)', 'libnss3 (>= 3.26)', @@ -67,6 +66,7 @@ exports.referenceGeneratedDepsByArch = { 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libc6 (>= 2.4)', 'libc6 (>= 2.9)', @@ -77,10 +77,8 @@ exports.referenceGeneratedDepsByArch = { 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.37.3)', - 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.30)', 'libnss3 (>= 3.26)', @@ -108,6 +106,7 @@ exports.referenceGeneratedDepsByArch = { 'libatk1.0-0 (>= 2.2.0)', 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', @@ -116,10 +115,8 @@ exports.referenceGeneratedDepsByArch = { 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.37.3)', - 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.30)', 'libnss3 (>= 3.26)', diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 4028370cd02..e3d78d1139a 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -31,6 +31,7 @@ export const referenceGeneratedDepsByArch = { 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', 'libc6 (>= 2.2.5)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', @@ -39,10 +40,8 @@ export const referenceGeneratedDepsByArch = { 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.37.3)', - 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.30)', 'libnss3 (>= 3.26)', @@ -67,6 +66,7 @@ export const referenceGeneratedDepsByArch = { 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libc6 (>= 2.4)', 'libc6 (>= 2.9)', @@ -77,10 +77,8 @@ export const referenceGeneratedDepsByArch = { 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.37.3)', - 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.30)', 'libnss3 (>= 3.26)', @@ -108,6 +106,7 @@ export const referenceGeneratedDepsByArch = { 'libatk1.0-0 (>= 2.2.0)', 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', @@ -116,10 +115,8 @@ export const referenceGeneratedDepsByArch = { 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.37.3)', - 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.30)', 'libnss3 (>= 3.26)', diff --git a/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index bff0c9a25df..19adbeb0529 100644 --- a/build/linux/dependencies-generator.js +++ b/build/linux/dependencies-generator.js @@ -23,7 +23,7 @@ const product = require("../../product.json"); // The reference dependencies, which one has to update when the new dependencies // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index 226310e1258..5fe4ac5da64 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -25,7 +25,7 @@ import product = require('../../product.json'); // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index 8be477290bb..fa393808c53 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -45,12 +45,14 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', + 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', 'libc.so.6(GLIBC_2.3.2)(64bit)', 'libc.so.6(GLIBC_2.3.3)(64bit)', 'libc.so.6(GLIBC_2.3.4)(64bit)', 'libc.so.6(GLIBC_2.4)(64bit)', + 'libc.so.6(GLIBC_2.5)(64bit)', 'libc.so.6(GLIBC_2.6)(64bit)', 'libc.so.6(GLIBC_2.7)(64bit)', 'libc.so.6(GLIBC_2.8)(64bit)', @@ -71,11 +73,7 @@ exports.referenceGeneratedDepsByArch = { 'libgio-2.0.so.0()(64bit)', 'libglib-2.0.so.0()(64bit)', 'libgobject-2.0.so.0()(64bit)', - 'libgssapi_krb5.so.2()(64bit)', - 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)(64bit)', 'libgtk-3.so.0()(64bit)', - 'libkrb5.so.3()(64bit)', - 'libkrb5.so.3(krb5_3_MIT)(64bit)', 'libm.so.6()(64bit)', 'libm.so.6(GLIBC_2.2.5)(64bit)', 'libnspr4.so()(64bit)', @@ -140,8 +138,10 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.16)', 'libc.so.6(GLIBC_2.17)', 'libc.so.6(GLIBC_2.18)', + 'libc.so.6(GLIBC_2.25)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', + 'libc.so.6(GLIBC_2.5)', 'libc.so.6(GLIBC_2.6)', 'libc.so.6(GLIBC_2.7)', 'libc.so.6(GLIBC_2.8)', @@ -162,12 +162,8 @@ exports.referenceGeneratedDepsByArch = { 'libgio-2.0.so.0', 'libglib-2.0.so.0', 'libgobject-2.0.so.0', - 'libgssapi_krb5.so.2', - 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)', 'libgtk-3.so.0', 'libgtk-3.so.0()(64bit)', - 'libkrb5.so.3', - 'libkrb5.so.3(krb5_3_MIT)', 'libm.so.6', 'libm.so.6(GLIBC_2.4)', 'libnspr4.so', @@ -241,6 +237,7 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', + 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', @@ -259,11 +256,7 @@ exports.referenceGeneratedDepsByArch = { 'libgio-2.0.so.0()(64bit)', 'libglib-2.0.so.0()(64bit)', 'libgobject-2.0.so.0()(64bit)', - 'libgssapi_krb5.so.2()(64bit)', - 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)(64bit)', 'libgtk-3.so.0()(64bit)', - 'libkrb5.so.3()(64bit)', - 'libkrb5.so.3(krb5_3_MIT)(64bit)', 'libm.so.6()(64bit)', 'libm.so.6(GLIBC_2.17)(64bit)', 'libnspr4.so()(64bit)', diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 24b18d504c8..9eed3a79f7e 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -44,12 +44,14 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', + 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', 'libc.so.6(GLIBC_2.3.2)(64bit)', 'libc.so.6(GLIBC_2.3.3)(64bit)', 'libc.so.6(GLIBC_2.3.4)(64bit)', 'libc.so.6(GLIBC_2.4)(64bit)', + 'libc.so.6(GLIBC_2.5)(64bit)', 'libc.so.6(GLIBC_2.6)(64bit)', 'libc.so.6(GLIBC_2.7)(64bit)', 'libc.so.6(GLIBC_2.8)(64bit)', @@ -70,11 +72,7 @@ export const referenceGeneratedDepsByArch = { 'libgio-2.0.so.0()(64bit)', 'libglib-2.0.so.0()(64bit)', 'libgobject-2.0.so.0()(64bit)', - 'libgssapi_krb5.so.2()(64bit)', - 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)(64bit)', 'libgtk-3.so.0()(64bit)', - 'libkrb5.so.3()(64bit)', - 'libkrb5.so.3(krb5_3_MIT)(64bit)', 'libm.so.6()(64bit)', 'libm.so.6(GLIBC_2.2.5)(64bit)', 'libnspr4.so()(64bit)', @@ -139,8 +137,10 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.16)', 'libc.so.6(GLIBC_2.17)', 'libc.so.6(GLIBC_2.18)', + 'libc.so.6(GLIBC_2.25)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', + 'libc.so.6(GLIBC_2.5)', 'libc.so.6(GLIBC_2.6)', 'libc.so.6(GLIBC_2.7)', 'libc.so.6(GLIBC_2.8)', @@ -161,12 +161,8 @@ export const referenceGeneratedDepsByArch = { 'libgio-2.0.so.0', 'libglib-2.0.so.0', 'libgobject-2.0.so.0', - 'libgssapi_krb5.so.2', - 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)', 'libgtk-3.so.0', 'libgtk-3.so.0()(64bit)', - 'libkrb5.so.3', - 'libkrb5.so.3(krb5_3_MIT)', 'libm.so.6', 'libm.so.6(GLIBC_2.4)', 'libnspr4.so', @@ -240,6 +236,7 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', + 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', @@ -258,11 +255,7 @@ export const referenceGeneratedDepsByArch = { 'libgio-2.0.so.0()(64bit)', 'libglib-2.0.so.0()(64bit)', 'libgobject-2.0.so.0()(64bit)', - 'libgssapi_krb5.so.2()(64bit)', - 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)(64bit)', 'libgtk-3.so.0()(64bit)', - 'libkrb5.so.3()(64bit)', - 'libkrb5.so.3(krb5_3_MIT)(64bit)', 'libm.so.6()(64bit)', 'libm.so.6(GLIBC_2.17)(64bit)', 'libnspr4.so()(64bit)', diff --git a/build/npm/dirs.js b/build/npm/dirs.js index 372d546cd78..fbefd418b0a 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -29,7 +29,6 @@ const dirs = [ 'extensions/jake', 'extensions/json-language-features', 'extensions/json-language-features/server', - 'extensions/markdown-language-features/server', 'extensions/markdown-language-features', 'extensions/markdown-math', 'extensions/media-preview', diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index bcac781e265..d45d5bc8cbc 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -54,14 +54,10 @@ function yarnInstall(dir, opts) { console.log(`Installing dependencies in ${dir} inside container ${process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME']}...`); opts.cwd = root; - if (process.env['npm_config_arch'] === 'arm64' || process.env['npm_config_arch'] === 'arm') { + if (process.env['npm_config_arch'] === 'arm64') { run('sudo', ['docker', 'run', '--rm', '--privileged', 'multiarch/qemu-user-static', '--reset', '-p', 'yes'], opts); } - if (process.env['npm_config_arch'] === 'arm') { - run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/home/builduser`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/home/builduser/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); - } else { - run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); - } + run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); run('sudo', ['chown', '-R', `${userinfo.uid}:${userinfo.gid}`, `${dir}/node_modules`], opts); } else { console.log(`Installing dependencies in ${dir}...`); diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index fdb01f579d6..7c51e83ff9c 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -73,9 +73,9 @@ function hasSupportedVisualStudioVersion() { const programFiles86Path = process.env['ProgramFiles(x86)']; const programFiles64Path = process.env['ProgramFiles']; + const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools', 'IntPreview']; if (programFiles64Path) { vsPath = `${programFiles64Path}/Microsoft Visual Studio/${version}`; - const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools']; if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath, vsType)))) { availableVersions.push(version); break; @@ -84,7 +84,6 @@ function hasSupportedVisualStudioVersion() { if (programFiles86Path) { vsPath = `${programFiles86Path}/Microsoft Visual Studio/${version}`; - const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools']; if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath, vsType)))) { availableVersions.push(version); break; diff --git a/build/package.json b/build/package.json index 0bbeed3f136..4270709f2f8 100644 --- a/build/package.json +++ b/build/package.json @@ -20,6 +20,7 @@ "@types/gulp-gzip": "^0.0.31", "@types/gulp-json-editor": "^2.2.31", "@types/gulp-rename": "^0.0.33", + "@types/gulp-sort": "^2.0.4", "@types/gulp-sourcemaps": "^0.0.32", "@types/mime": "0.0.29", "@types/minimatch": "^3.0.3", @@ -41,9 +42,10 @@ "commander": "^7.0.0", "debug": "^4.3.2", "electron-osx-sign": "^0.4.16", - "esbuild": "0.20.0", + "esbuild": "0.23.0", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", + "gulp-sort": "^2.0.0", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", "mkdirp": "^1.0.4", @@ -51,10 +53,11 @@ "ternary-stream": "^3.0.0", "through2": "^4.0.2", "tmp": "^0.2.1", - "vscode-universal-bundler": "^0.0.2", + "vscode-universal-bundler": "^0.1.2", "workerpool": "^6.4.0", "yauzl": "^2.10.0" }, + "type": "commonjs", "scripts": { "compile": "../node_modules/.bin/tsc -p tsconfig.build.json", "watch": "../node_modules/.bin/tsc -p tsconfig.build.json --watch", diff --git a/build/yarn.lock b/build/yarn.lock index d99ceffaadf..d25994cedf0 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -199,6 +199,15 @@ events "^3.0.0" tslib "^2.2.0" +"@electron/asar@^3.2.7": + version "3.2.10" + resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.10.tgz#615cf346b734b23cafa4e0603551010bd0e50aa8" + integrity sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw== + dependencies: + commander "^5.0.0" + glob "^7.1.6" + minimatch "^3.0.4" + "@electron/get@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.3.tgz#fba552683d387aebd9f3fcadbcafc8e12ee4f960" @@ -214,125 +223,130 @@ optionalDependencies: global-agent "^3.0.0" -"@esbuild/aix-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" - integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== +"@esbuild/aix-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259" + integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ== -"@esbuild/android-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" - integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== +"@esbuild/android-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832" + integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ== -"@esbuild/android-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" - integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== +"@esbuild/android-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99" + integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g== -"@esbuild/android-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" - integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== +"@esbuild/android-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6" + integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ== -"@esbuild/darwin-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" - integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== +"@esbuild/darwin-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e" + integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow== -"@esbuild/darwin-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" - integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== +"@esbuild/darwin-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c" + integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ== -"@esbuild/freebsd-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" - integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== +"@esbuild/freebsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4" + integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw== -"@esbuild/freebsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" - integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== +"@esbuild/freebsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d" + integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ== -"@esbuild/linux-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" - integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== +"@esbuild/linux-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a" + integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw== -"@esbuild/linux-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" - integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== +"@esbuild/linux-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad" + integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw== -"@esbuild/linux-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" - integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== +"@esbuild/linux-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238" + integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA== -"@esbuild/linux-loong64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" - integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== +"@esbuild/linux-loong64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280" + integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A== -"@esbuild/linux-mips64el@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" - integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== +"@esbuild/linux-mips64el@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5" + integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w== -"@esbuild/linux-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" - integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== +"@esbuild/linux-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6" + integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw== -"@esbuild/linux-riscv64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" - integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== +"@esbuild/linux-riscv64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780" + integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw== -"@esbuild/linux-s390x@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" - integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== +"@esbuild/linux-s390x@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8" + integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg== -"@esbuild/linux-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" - integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== +"@esbuild/linux-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910" + integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ== -"@esbuild/netbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" - integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== +"@esbuild/netbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c" + integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw== -"@esbuild/openbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" - integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== +"@esbuild/openbsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db" + integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ== -"@esbuild/sunos-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" - integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== +"@esbuild/openbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8" + integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg== -"@esbuild/win32-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" - integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== +"@esbuild/sunos-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8" + integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA== -"@esbuild/win32-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" - integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== +"@esbuild/win32-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d" + integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ== -"@esbuild/win32-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" - integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== +"@esbuild/win32-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7" + integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA== -"@malept/cross-spawn-promise@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" - integrity sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ== +"@esbuild/win32-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" + integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== + +"@malept/cross-spawn-promise@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz#d0772de1aa680a0bfb9ba2f32b4c828c7857cb9d" + integrity sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg== dependencies: cross-spawn "^7.0.1" @@ -469,6 +483,14 @@ dependencies: "@types/node" "*" +"@types/gulp-sort@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/gulp-sort/-/gulp-sort-2.0.4.tgz#60625bf608dbac8f03644c6785d25c616f1b7d8c" + integrity sha512-HUHxH+oMox1ct0SnxPqCXBni0MSws1ygcSAoLO4ISRmR/UuvNIz40rgNveZxwxQk+p78kw09z/qKQkgKJmNUOQ== + dependencies: + "@types/gulp-util" "*" + "@types/node" "*" + "@types/gulp-sourcemaps@^0.0.32": version "0.0.32" resolved "https://registry.yarnpkg.com/@types/gulp-sourcemaps/-/gulp-sourcemaps-0.0.32.tgz#e79ee617e0cb15729874be4533fe59c07793a175" @@ -476,6 +498,16 @@ dependencies: "@types/node" "*" +"@types/gulp-util@*": + version "3.0.41" + resolved "https://registry.yarnpkg.com/@types/gulp-util/-/gulp-util-3.0.41.tgz#52868a6f8b6af55a099fe48ea20736833c02bed4" + integrity sha512-BK0kJZ8euQNlISsmD6mBr/1RZkB0mljdtBsz2usv+QHPV10alH2AJw5p05S9LU6S+VdTjbFmGU0OxpH++2W9/Q== + dependencies: + "@types/node" "*" + "@types/through2" "*" + "@types/vinyl" "*" + chalk "^2.2.0" + "@types/gulp@^4.0.17": version "4.0.17" resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.17.tgz#b314c3762d08d8d69b7c0b86f78d069bafd65009" @@ -577,6 +609,13 @@ "@types/glob" "*" "@types/node" "*" +"@types/through2@*": + version "2.0.41" + resolved "https://registry.yarnpkg.com/@types/through2/-/through2-2.0.41.tgz#3e5e1720d71ffdfa03c22f2aad6593d12a47034f" + integrity sha512-ryQ0tidWkb1O1JuYvWKyMLYEtOWDqF5mHerJzKz/gQpoAaJq2l/dsMPBF0B5BNVT34rbARYJ5/tsZwLfUi2kwQ== + dependencies: + "@types/node" "*" + "@types/through2@^2.0.36": version "2.0.36" resolved "https://registry.yarnpkg.com/@types/through2/-/through2-2.0.36.tgz#35fda0db635827d44c0e08e2c94653e647574a00" @@ -686,6 +725,11 @@ optionalDependencies: keytar "^7.7.0" +"@xmldom/xmldom@^0.8.8": + version "0.8.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" + integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -755,18 +799,6 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -asar@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/asar/-/asar-3.0.3.tgz#1fef03c2d6d2de0cbad138788e4f7ae03b129c7b" - integrity sha512-k7zd+KoR+n8pl71PvgElcoKHrVNiSXtw7odKbyNpmgKe7EGRF9Pnu3uLOukD37EvavKwVFxOUpqXTIZC5B5Pmw== - dependencies: - chromium-pickle-js "^0.2.0" - commander "^5.0.0" - glob "^7.1.6" - minimatch "^3.0.4" - optionalDependencies: - "@types/glob" "^7.1.1" - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -787,11 +819,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - azure-devops-node-api@^11.0.1: version "11.2.0" resolved "https://registry.yarnpkg.com/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz#bf04edbef60313117a0507415eed4790a420ad6b" @@ -847,6 +874,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -877,11 +911,6 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= -buffer-equal@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" - integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= - buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -926,7 +955,7 @@ call-bind@^1.0.0: function-bind "^1.1.1" get-intrinsic "^1.0.2" -chalk@^2.4.2: +chalk@^2.2.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -995,11 +1024,6 @@ chownr@^1.1.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== -chromium-pickle-js@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" - integrity sha1-BKEGZywYsIWrd02YPfo+oTjyIgU= - clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -1048,11 +1072,6 @@ color-support@^1.1.3: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -colors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= - colors@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -1065,13 +1084,6 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= - dependencies: - graceful-readlink ">= 1.0.0" - commander@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" @@ -1185,15 +1197,13 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -dir-compare@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-2.4.0.tgz#785c41dc5f645b34343a4eafc50b79bac7f11631" - integrity sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA== +dir-compare@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-4.2.0.tgz#d1d4999c14fbf55281071fdae4293b3b9ce86f19" + integrity sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ== dependencies: - buffer-equal "1.0.0" - colors "1.0.3" - commander "2.9.0" - minimatch "3.0.4" + minimatch "^3.0.5" + p-limit "^3.1.0 " dom-serializer@^2.0.0: version "2.0.0" @@ -1281,34 +1291,35 @@ es6-error@^4.1.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -esbuild@0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4" - integrity sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA== +esbuild@0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.0.tgz#de06002d48424d9fdb7eb52dbe8e95927f852599" + integrity sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA== optionalDependencies: - "@esbuild/aix-ppc64" "0.20.0" - "@esbuild/android-arm" "0.20.0" - "@esbuild/android-arm64" "0.20.0" - "@esbuild/android-x64" "0.20.0" - "@esbuild/darwin-arm64" "0.20.0" - "@esbuild/darwin-x64" "0.20.0" - "@esbuild/freebsd-arm64" "0.20.0" - "@esbuild/freebsd-x64" "0.20.0" - "@esbuild/linux-arm" "0.20.0" - "@esbuild/linux-arm64" "0.20.0" - "@esbuild/linux-ia32" "0.20.0" - "@esbuild/linux-loong64" "0.20.0" - "@esbuild/linux-mips64el" "0.20.0" - "@esbuild/linux-ppc64" "0.20.0" - "@esbuild/linux-riscv64" "0.20.0" - "@esbuild/linux-s390x" "0.20.0" - "@esbuild/linux-x64" "0.20.0" - "@esbuild/netbsd-x64" "0.20.0" - "@esbuild/openbsd-x64" "0.20.0" - "@esbuild/sunos-x64" "0.20.0" - "@esbuild/win32-arm64" "0.20.0" - "@esbuild/win32-ia32" "0.20.0" - "@esbuild/win32-x64" "0.20.0" + "@esbuild/aix-ppc64" "0.23.0" + "@esbuild/android-arm" "0.23.0" + "@esbuild/android-arm64" "0.23.0" + "@esbuild/android-x64" "0.23.0" + "@esbuild/darwin-arm64" "0.23.0" + "@esbuild/darwin-x64" "0.23.0" + "@esbuild/freebsd-arm64" "0.23.0" + "@esbuild/freebsd-x64" "0.23.0" + "@esbuild/linux-arm" "0.23.0" + "@esbuild/linux-arm64" "0.23.0" + "@esbuild/linux-ia32" "0.23.0" + "@esbuild/linux-loong64" "0.23.0" + "@esbuild/linux-mips64el" "0.23.0" + "@esbuild/linux-ppc64" "0.23.0" + "@esbuild/linux-riscv64" "0.23.0" + "@esbuild/linux-s390x" "0.23.0" + "@esbuild/linux-x64" "0.23.0" + "@esbuild/netbsd-x64" "0.23.0" + "@esbuild/openbsd-arm64" "0.23.0" + "@esbuild/openbsd-x64" "0.23.0" + "@esbuild/sunos-x64" "0.23.0" + "@esbuild/win32-arm64" "0.23.0" + "@esbuild/win32-ia32" "0.23.0" + "@esbuild/win32-x64" "0.23.0" escape-string-regexp@^1.0.5: version "1.0.5" @@ -1413,6 +1424,15 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^11.1.1: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -1422,16 +1442,6 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1550,11 +1560,6 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= - gulp-merge-json@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/gulp-merge-json/-/gulp-merge-json-2.1.1.tgz#cfb1d066467577545b8c1c289a278e6ef4b4e0de" @@ -1566,6 +1571,13 @@ gulp-merge-json@^2.1.1: through "^2.3.8" vinyl "^2.1.0" +gulp-sort@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/gulp-sort/-/gulp-sort-2.0.0.tgz#c6762a2f1f0de0a3fc595a21599d3fac8dba1aca" + integrity sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g== + dependencies: + through2 "^2.0.1" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -1926,19 +1938,26 @@ mimic-response@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@^3.0.3, minimatch@^3.0.5, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.3, minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== +minimatch@^9.0.3: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: - brace-expansion "^1.1.7" + brace-expansion "^2.0.1" minimist@^1.2.0, minimist@^1.2.3: version "1.2.6" @@ -2062,6 +2081,13 @@ p-cancelable@^2.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== +"p-limit@^3.1.0 ": + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + parse-node-version@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" @@ -2127,6 +2153,15 @@ plist@^3.0.1: base64-js "^1.5.1" xmlbuilder "^9.0.7" +plist@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" + integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== + dependencies: + "@xmldom/xmldom" "^0.8.8" + base64-js "^1.5.1" + xmlbuilder "^15.1.1" + plugin-error@1.0.1, plugin-error@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" @@ -2252,6 +2287,19 @@ readable-stream@^2.0.2, readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readdirp@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" @@ -2502,6 +2550,14 @@ ternary-stream@^3.0.0: merge-stream "^2.0.0" through2 "^3.0.1" +through2@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + through2@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" @@ -2678,16 +2734,18 @@ vscode-gulp-watch@^5.0.3: vinyl "^2.2.0" vinyl-file "^3.0.0" -vscode-universal-bundler@^0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/vscode-universal-bundler/-/vscode-universal-bundler-0.0.2.tgz#2c988dac681d3ffe6baec6defac0995cb833c55a" - integrity sha512-FPJcvKnQGBqFzy6M6Nm2yvAczNLUeXsfYM6GwCex/pUOkvIM2icIHmiSvtMJINlLW1iG+oEwE3/LVbABmcjEmQ== +vscode-universal-bundler@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/vscode-universal-bundler/-/vscode-universal-bundler-0.1.2.tgz#50ba7c637942dae6c0bc9bb37c0fcabf509cbca5" + integrity sha512-fboaE0933+r+qW4aRuuMwC8yle4DX6xyRDCM0otYMo5+4POZjEHbsqkPQ8H8xmhScHf1Fa7kZXYo1V2n0d2x/g== dependencies: - "@malept/cross-spawn-promise" "^1.1.0" - asar "^3.0.3" + "@electron/asar" "^3.2.7" + "@malept/cross-spawn-promise" "^2.0.0" debug "^4.3.1" - dir-compare "^2.4.0" - fs-extra "^9.0.1" + dir-compare "^4.2.0" + fs-extra "^11.1.1" + minimatch "^9.0.3" + plist "^3.1.0" webidl-conversions@^3.0.0: version "3.0.1" @@ -2727,6 +2785,11 @@ xml2js@^0.4.19, xml2js@^0.4.23: sax ">=0.6.0" xmlbuilder "~11.0.0" +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + xmlbuilder@^9.0.7: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" @@ -2737,6 +2800,11 @@ xmlbuilder@~11.0.0: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== +xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" @@ -2756,3 +2824,8 @@ yazl@^2.2.2: integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== dependencies: buffer-crc32 "~0.2.3" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/cglicenses.json b/cglicenses.json index d90d55d7315..268a7e1dd1b 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -567,6 +567,32 @@ "SOFTWARE" ] }, + { + "name":"vscode-markdown-languageserver", + "fullLicenseText": [ + "MIT License", + "", + "Copyright (c) Microsoft Corporation.", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE" + ] + }, { // Reason: mono-repo where the individual packages are also dual-licensed under MIT and Apache-2.0 "name": "system-configuration", diff --git a/cgmanifest.json b/cgmanifest.json index 61747342eef..b0d01095af2 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "f1a45d7ded05d64ca8136cc142ddc0c271b1dd43" + "commitHash": "7fa4f6e14e0707c0e604cf7c1da33566e78169ce" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "122.0.6261.156" + "version": "124.0.6367.243" }, { "component": { @@ -48,7 +48,7 @@ "git": { "name": "ffmpeg", "repositoryUrl": "https://chromium.googlesource.com/chromium/third_party/ffmpeg", - "commitHash": "17525de887d54b970ffdd421a0879c1db1952307" + "commitHash": "52d8ef3799b2f16b66351dd0972bb0bcee1648ac" } }, "isOnlyProductionDependency": true, @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "9b1bf44ea9e7785e38c93b7d22d32dbca262df6c" + "commitHash": "a407d1f0b3669cc82c755700f0d500fb27cc39ea" } }, "isOnlyProductionDependency": true, - "version": "20.11.1" + "version": "20.15.1" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "f9ed0eaee4b172733872c2f84e5061882dd08e5c" + "commitHash": "78279119e22fe4c01f47d9a5d4f00dde1bf0c21b" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "29.4.0" + "version": "30.3.1" }, { "component": { diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 3be3815a748..27fe79896a2 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -1571,9 +1571,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.5.0", "cfg-if", @@ -1603,9 +1603,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index a9630c12022..8d04738ba05 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -3396,7 +3396,7 @@ https://github.com/hyperium/http-body The MIT License (MIT) -Copyright (c) 2019 Hyper Contributors +Copyright (c) 2019-2024 Sean McArthur & Hyper Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -10791,7 +10791,33 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI zbus 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -10799,7 +10825,33 @@ LICENSE-MIT zbus_macros 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -10807,7 +10859,33 @@ LICENSE-MIT zbus_names 2.6.1 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -10900,7 +10978,33 @@ licences; see files named LICENSE.*.txt for details. zvariant 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -10908,7 +11012,33 @@ LICENSE-MIT zvariant_derive 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -10916,5 +11046,31 @@ LICENSE-MIT zvariant_utils 1.0.1 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- \ No newline at end of file diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 05e22e0cfb3..101f1eac29f 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -230,8 +230,8 @@ pub struct CommandShellArgs { #[clap(long)] pub on_socket: bool, /// Listen on a host/port instead of stdin/stdout. - #[clap(long, num_args = 0..=1, default_missing_value = "0")] - pub on_port: Option, + #[clap(long, num_args = 0..=2, default_missing_value = "0")] + pub on_port: Vec, /// Listen on a host/port instead of stdin/stdout. #[clap[long]] pub on_host: Option, diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index 12c0cdafec9..d8d2a49bb1a 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -71,13 +71,19 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result Resul code_server_args: (&ctx.args).into(), }; - let mut listener: Box = match (args.on_port, &args.on_host, args.on_socket) - { - (_, _, true) => { - let socket = get_socket_name(); - let listener = listen_socket_rw_stream(&socket) - .await - .map_err(|e| wrap(e, "error listening on socket"))?; + let mut listener: Box = + match (args.on_port.first(), &args.on_host, args.on_socket) { + (_, _, true) => { + let socket = get_socket_name(); + let listener = listen_socket_rw_stream(&socket) + .await + .map_err(|e| wrap(e, "error listening on socket"))?; - params - .log - .result(format!("Listening on {}", socket.display())); + params + .log + .result(format!("Listening on {}", socket.display())); - Box::new(listener) - } - (Some(_), _, _) | (_, Some(_), _) => { - let addr = SocketAddr::new( - args.on_host + Box::new(listener) + } + (Some(_), _, _) | (_, Some(_), _) => { + let host = args + .on_host .as_ref() .map(|h| h.parse().map_err(CodeError::InvalidHostAddress)) - .unwrap_or(Ok(IpAddr::V4(Ipv4Addr::LOCALHOST)))?, - args.on_port.unwrap_or_default(), - ); - let listener = tokio::net::TcpListener::bind(addr) - .await - .map_err(|e| wrap(e, "error listening on port"))?; + .unwrap_or(Ok(IpAddr::V4(Ipv4Addr::LOCALHOST)))?; - params - .log - .result(format!("Listening on {}", listener.local_addr().unwrap())); + let lower_port = args.on_port.first().copied().unwrap_or_default(); + let port_no = if let Some(upper) = args.on_port.get(1) { + find_unused_port(&host, lower_port, *upper) + .await + .unwrap_or_default() + } else { + lower_port + }; - Box::new(listener) - } - _ => { - serve_stream(tokio::io::stdin(), tokio::io::stderr(), params).await; - return Ok(0); - } - }; + let addr = SocketAddr::new(host, port_no); + let listener = tokio::net::TcpListener::bind(addr) + .await + .map_err(|e| wrap(e, "error listening on port"))?; + + params + .log + .result(format!("Listening on {}", listener.local_addr().unwrap())); + + Box::new(listener) + } + _ => { + serve_stream(tokio::io::stdin(), tokio::io::stderr(), params).await; + return Ok(0); + } + }; let mut servers = FuturesUnordered::new(); @@ -216,6 +225,21 @@ pub async fn command_shell(ctx: CommandContext, args: CommandShellArgs) -> Resul } } +async fn find_unused_port(host: &IpAddr, start_port: u16, end_port: u16) -> Option { + for port in start_port..=end_port { + if is_port_available(*host, port).await { + return Some(port); + } + } + None +} + +async fn is_port_available(host: IpAddr, port: u16) -> bool { + tokio::net::TcpListener::bind(SocketAddr::new(host, port)) + .await + .is_ok() +} + pub async fn service( ctx: CommandContext, service_args: TunnelServiceSubCommands, diff --git a/cli/src/update_service.rs b/cli/src/update_service.rs index 4bec13d6e86..90339148188 100644 --- a/cli/src/update_service.rs +++ b/cli/src/update_service.rs @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use std::{ffi::OsStr, fmt, path::Path}; +use std::{fmt, path::Path}; use serde::{Deserialize, Serialize}; @@ -11,10 +11,11 @@ use crate::{ constants::VSCODE_CLI_UPDATE_ENDPOINT, debug, log, options, spanf, util::{ - errors::{AnyError, CodeError, WrappedError}, + errors::{wrap, AnyError, CodeError, WrappedError}, http::{BoxedHttp, SimpleResponse}, io::ReportCopyProgress, - tar, zipper, + tar::{self, has_gzip_header}, + zipper, }, }; @@ -178,10 +179,10 @@ pub fn unzip_downloaded_release( where T: ReportCopyProgress, { - if compressed_file.extension() == Some(OsStr::new("zip")) { - zipper::unzip_file(compressed_file, target_dir, reporter) - } else { - tar::decompress_tarball(compressed_file, target_dir, reporter) + match has_gzip_header(compressed_file) { + Ok((f, true)) => tar::decompress_tarball(f, target_dir, reporter), + Ok((f, false)) => zipper::unzip_file(f, target_dir, reporter), + Err(e) => Err(wrap(e, "error checking for gzip header")), } } @@ -249,7 +250,7 @@ impl Platform { Platform::DarwinARM64 => "server-darwin-arm64", Platform::WindowsX64 => "server-win32-x64", Platform::WindowsX86 => "server-win32", - Platform::WindowsARM64 => "server-win32-x64", // we don't publish an arm64 server build yet + Platform::WindowsARM64 => "server-win32-arm64", } .to_owned() } diff --git a/cli/src/util/errors.rs b/cli/src/util/errors.rs index 7d28ce9f741..67519d5437e 100644 --- a/cli/src/util/errors.rs +++ b/cli/src/util/errors.rs @@ -512,6 +512,8 @@ pub enum CodeError { // todo: can be specialized when update service is moved to CodeErrors #[error("Could not check for update: {0}")] UpdateCheckFailed(String), + #[error("Could not read connection token file: {0}")] + CouldNotReadConnectionTokenFile(std::io::Error), #[error("Could not write connection token file: {0}")] CouldNotCreateConnectionTokenFile(std::io::Error), #[error("A tunnel with the name {0} exists and is in-use. Please pick a different name or stop the existing tunnel.")] diff --git a/cli/src/util/prereqs.rs b/cli/src/util/prereqs.rs index 5d4953acbb2..0f49ab20887 100644 --- a/cli/src/util/prereqs.rs +++ b/cli/src/util/prereqs.rs @@ -19,12 +19,22 @@ lazy_static! { static ref GENERIC_VERSION_RE: Regex = Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap(); static ref LIBSTD_CXX_VERSION_RE: BinRegex = BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap(); - static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 25); - static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 19); static ref MIN_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 28, 0); static ref MIN_LEGACY_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 17, 0); } +#[cfg(target_arch = "arm")] +lazy_static! { + static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 26); + static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 22); +} + +#[cfg(not(target_arch = "arm"))] +lazy_static! { + static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 25); + static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 19); +} + const NIXOS_TEST_PATH: &str = "/etc/NIXOS"; pub struct PreReqChecker {} diff --git a/cli/src/util/tar.rs b/cli/src/util/tar.rs index fe4d4269700..10577140325 100644 --- a/cli/src/util/tar.rs +++ b/cli/src/util/tar.rs @@ -5,8 +5,8 @@ use crate::util::errors::{wrap, WrappedError}; use flate2::read::GzDecoder; -use std::fs; -use std::io::Seek; +use std::fs::{self, File}; +use std::io::{Read, Seek}; use std::path::{Path, PathBuf}; use tar::Archive; @@ -57,16 +57,13 @@ fn should_skip_first_segment(file: &fs::File) -> Result<(bool, u64), WrappedErro } pub fn decompress_tarball( - path: &Path, + mut tar_gz: File, parent_path: &Path, mut reporter: T, ) -> Result<(), WrappedError> where T: ReportCopyProgress, { - let mut tar_gz = fs::File::open(path) - .map_err(|e| wrap(e, format!("error opening file {}", path.display())))?; - let (skip_first, num_entries) = should_skip_first_segment(&tar_gz)?; let report_progress_every = num_entries / 20; let mut entries_so_far = 0; @@ -81,7 +78,7 @@ where let mut archive = Archive::new(tar); archive .entries() - .map_err(|e| wrap(e, format!("error opening archive {}", path.display())))? + .map_err(|e| wrap(e, "error opening archive"))? .filter_map(|e| e.ok()) .try_for_each::<_, Result<_, WrappedError>>(|mut entry| { // approximate progress based on where we are in the archive: @@ -118,3 +115,13 @@ where Ok(()) } + +pub fn has_gzip_header(path: &Path) -> std::io::Result<(File, bool)> { + let mut file = fs::File::open(path)?; + let mut header = [0; 2]; + let _ = file.read_exact(&mut header); + + file.rewind()?; + + Ok((file, header[0] == 0x1f && header[1] == 0x8b)) +} diff --git a/cli/src/util/zipper.rs b/cli/src/util/zipper.rs index 69bcf2d23f6..5521fa42c54 100644 --- a/cli/src/util/zipper.rs +++ b/cli/src/util/zipper.rs @@ -44,15 +44,12 @@ fn should_skip_first_segment(archive: &mut ZipArchive) -> bool { archive.len() > 1 // prefix removal is invalid if there's only a single file } -pub fn unzip_file(path: &Path, parent_path: &Path, mut reporter: T) -> Result<(), WrappedError> +pub fn unzip_file(file: File, parent_path: &Path, mut reporter: T) -> Result<(), WrappedError> where T: ReportCopyProgress, { - let file = fs::File::open(path) - .map_err(|e| wrap(e, format!("unable to open file {}", path.display())))?; - - let mut archive = zip::ZipArchive::new(file) - .map_err(|e| wrap(e, format!("failed to open zip archive {}", path.display())))?; + let mut archive = + zip::ZipArchive::new(file).map_err(|e| wrap(e, "failed to open zip archive"))?; let skip_segments_no = usize::from(should_skip_first_segment(&mut archive)); let report_progress_every = archive.len() / 20; diff --git a/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json b/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json index 681ca6105cf..3f8400a7bd4 100644 --- a/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json @@ -180,6 +180,11 @@ "items": { "type": "string" } + }, + "disableAutomaticConfiguration": { + "type": "boolean", + "description": "Disables the setup that is automatically run in a codespace if no `postCreateCommand` is specified.", + "default": false } } } diff --git a/extensions/css-language-features/client/src/browser/cssClientMain.ts b/extensions/css-language-features/client/src/browser/cssClientMain.ts index c89997ffaa0..1d4153d9836 100644 --- a/extensions/css-language-features/client/src/browser/cssClientMain.ts +++ b/extensions/css-language-features/client/src/browser/cssClientMain.ts @@ -7,6 +7,7 @@ import { ExtensionContext, Uri, l10n } from 'vscode'; import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor } from '../cssClient'; import { LanguageClient } from 'vscode-languageclient/browser'; +import { registerDropOrPasteResourceSupport } from '../dropOrPaste/dropOrPasteResource'; let client: BaseLanguageClient | undefined; @@ -23,6 +24,7 @@ export async function activate(context: ExtensionContext) { client = await startClient(context, newLanguageClient, { TextDecoder }); + context.subscriptions.push(registerDropOrPasteResourceSupport({ language: 'css', scheme: '*' })); } catch (e) { console.log(e); } diff --git a/extensions/css-language-features/client/src/dropOrPaste/dropOrPasteResource.ts b/extensions/css-language-features/client/src/dropOrPaste/dropOrPasteResource.ts new file mode 100644 index 00000000000..6a4c38d2417 --- /dev/null +++ b/extensions/css-language-features/client/src/dropOrPaste/dropOrPasteResource.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as vscode from 'vscode'; +import { getDocumentDir, Mimes, Schemes } from './shared'; +import { UriList } from './uriList'; + +class DropOrPasteResourceProvider implements vscode.DocumentDropEditProvider, vscode.DocumentPasteEditProvider { + readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('css', 'url'); + + async provideDocumentDropEdits( + document: vscode.TextDocument, + position: vscode.Position, + dataTransfer: vscode.DataTransfer, + token: vscode.CancellationToken, + ): Promise { + const uriList = await this.getUriList(dataTransfer); + if (!uriList.entries.length || token.isCancellationRequested) { + return; + } + + const snippet = await this.createUriListSnippet(document.uri, uriList); + if (!snippet || token.isCancellationRequested) { + return; + } + + return { + kind: this.kind, + title: snippet.label, + insertText: snippet.snippet.value, + yieldTo: this.pasteAsCssUrlByDefault(document, position) ? [] : [vscode.DocumentDropOrPasteEditKind.Empty.append('uri')] + }; + } + + async provideDocumentPasteEdits( + document: vscode.TextDocument, + ranges: readonly vscode.Range[], + dataTransfer: vscode.DataTransfer, + _context: vscode.DocumentPasteEditContext, + token: vscode.CancellationToken + ): Promise { + const uriList = await this.getUriList(dataTransfer); + if (!uriList.entries.length || token.isCancellationRequested) { + return; + } + + const snippet = await this.createUriListSnippet(document.uri, uriList); + if (!snippet || token.isCancellationRequested) { + return; + } + + return [{ + kind: this.kind, + title: snippet.label, + insertText: snippet.snippet.value, + yieldTo: this.pasteAsCssUrlByDefault(document, ranges[0].start) ? [] : [vscode.DocumentDropOrPasteEditKind.Empty.append('uri')] + }]; + } + + private async getUriList(dataTransfer: vscode.DataTransfer): Promise { + const urlList = await dataTransfer.get(Mimes.uriList)?.asString(); + if (urlList) { + return UriList.from(urlList); + } + + // Find file entries + const uris: vscode.Uri[] = []; + for (const [_, entry] of dataTransfer) { + const file = entry.asFile(); + if (file?.uri) { + uris.push(file.uri); + } + } + + return new UriList(uris.map(uri => ({ uri, str: uri.toString(true) }))); + } + + private async createUriListSnippet(docUri: vscode.Uri, uriList: UriList): Promise<{ readonly snippet: vscode.SnippetString; readonly label: string } | undefined> { + if (!uriList.entries.length) { + return; + } + + const snippet = new vscode.SnippetString(); + for (let i = 0; i < uriList.entries.length; i++) { + const uri = uriList.entries[i]; + const relativePath = getRelativePath(getDocumentDir(docUri), uri.uri); + const urlText = relativePath ?? uri.str; + + snippet.appendText(`url(${urlText})`); + if (i !== uriList.entries.length - 1) { + snippet.appendText(' '); + } + } + + return { + snippet, + label: uriList.entries.length > 1 + ? vscode.l10n.t('Insert url() Functions') + : vscode.l10n.t('Insert url() Function') + }; + } + + private pasteAsCssUrlByDefault(document: vscode.TextDocument, position: vscode.Position): boolean { + const regex = /url\(.+?\)/gi; + for (const match of Array.from(document.lineAt(position.line).text.matchAll(regex))) { + if (position.character > match.index && position.character < match.index + match[0].length) { + return false; + } + } + return true; + } +} + +function getRelativePath(fromFile: vscode.Uri | undefined, toFile: vscode.Uri): string | undefined { + if (fromFile && fromFile.scheme === toFile.scheme && fromFile.authority === toFile.authority) { + if (toFile.scheme === Schemes.file) { + // On windows, we must use the native `path.relative` to generate the relative path + // so that drive-letters are resolved cast insensitively. However we then want to + // convert back to a posix path to insert in to the document + const relativePath = path.relative(fromFile.fsPath, toFile.fsPath); + return path.posix.normalize(relativePath.split(path.sep).join(path.posix.sep)); + } + + return path.posix.relative(fromFile.path, toFile.path); + } + + return undefined; +} + +export function registerDropOrPasteResourceSupport(selector: vscode.DocumentSelector): vscode.Disposable { + const provider = new DropOrPasteResourceProvider(); + + return vscode.Disposable.from( + vscode.languages.registerDocumentDropEditProvider(selector, provider, { + providedDropEditKinds: [provider.kind], + dropMimeTypes: [ + Mimes.uriList, + 'files' + ] + }), + vscode.languages.registerDocumentPasteEditProvider(selector, provider, { + providedPasteEditKinds: [provider.kind], + pasteMimeTypes: [ + Mimes.uriList, + 'files' + ] + }) + ); +} diff --git a/extensions/css-language-features/client/src/dropOrPaste/shared.ts b/extensions/css-language-features/client/src/dropOrPaste/shared.ts new file mode 100644 index 00000000000..548bccfec69 --- /dev/null +++ b/extensions/css-language-features/client/src/dropOrPaste/shared.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { Utils } from 'vscode-uri'; + +export const Schemes = Object.freeze({ + file: 'file', + notebookCell: 'vscode-notebook-cell', + untitled: 'untitled', +}); + +export const Mimes = Object.freeze({ + plain: 'text/plain', + uriList: 'text/uri-list', +}); + + +export function getDocumentDir(uri: vscode.Uri): vscode.Uri | undefined { + const docUri = getParentDocumentUri(uri); + if (docUri.scheme === Schemes.untitled) { + return vscode.workspace.workspaceFolders?.[0]?.uri; + } + return Utils.dirname(docUri); +} + +function getParentDocumentUri(uri: vscode.Uri): vscode.Uri { + if (uri.scheme === Schemes.notebookCell) { + // is notebook documents necessary? + for (const notebook of vscode.workspace.notebookDocuments) { + for (const cell of notebook.getCells()) { + if (cell.document.uri.toString() === uri.toString()) { + return notebook.uri; + } + } + } + } + + return uri; +} diff --git a/extensions/css-language-features/client/src/dropOrPaste/uriList.ts b/extensions/css-language-features/client/src/dropOrPaste/uriList.ts new file mode 100644 index 00000000000..ed20b1ee797 --- /dev/null +++ b/extensions/css-language-features/client/src/dropOrPaste/uriList.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +function splitUriList(str: string): string[] { + return str.split('\r\n'); +} + +function parseUriList(str: string): string[] { + return splitUriList(str) + .filter(value => !value.startsWith('#')) // Remove comments + .map(value => value.trim()); +} + +export class UriList { + + static from(str: string): UriList { + return new UriList(coalesce(parseUriList(str).map(line => { + try { + return { uri: vscode.Uri.parse(line), str: line }; + } catch { + // Uri parse failure + return undefined; + } + }))); + } + + constructor( + public readonly entries: ReadonlyArray<{ readonly uri: vscode.Uri; readonly str: string }> + ) { } +} + +function coalesce(array: ReadonlyArray): T[] { + return array.filter(e => !!e); +} diff --git a/extensions/css-language-features/client/src/node/cssClientMain.ts b/extensions/css-language-features/client/src/node/cssClientMain.ts index 802b58ab286..96926979b2a 100644 --- a/extensions/css-language-features/client/src/node/cssClientMain.ts +++ b/extensions/css-language-features/client/src/node/cssClientMain.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getNodeFSRequestService } from './nodeFs'; -import { ExtensionContext, extensions, l10n } from 'vscode'; -import { startClient, LanguageClientConstructor } from '../cssClient'; -import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node'; import { TextDecoder } from 'util'; - +import { ExtensionContext, extensions, l10n } from 'vscode'; +import { BaseLanguageClient, LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node'; +import { LanguageClientConstructor, startClient } from '../cssClient'; +import { getNodeFSRequestService } from './nodeFs'; +import { registerDropOrPasteResourceSupport } from '../dropOrPaste/dropOrPasteResource'; let client: BaseLanguageClient | undefined; @@ -37,6 +37,8 @@ export async function activate(context: ExtensionContext) { process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? ''; client = await startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder }); + + context.subscriptions.push(registerDropOrPasteResourceSupport({ language: 'css', scheme: '*' })); } export async function deactivate(): Promise { @@ -45,3 +47,4 @@ export async function deactivate(): Promise { client = undefined; } } + diff --git a/extensions/css-language-features/client/tsconfig.json b/extensions/css-language-features/client/tsconfig.json index 44b77895c10..17bf7e962a8 100644 --- a/extensions/css-language-features/client/tsconfig.json +++ b/extensions/css-language-features/client/tsconfig.json @@ -8,6 +8,7 @@ }, "include": [ "src/**/*", - "../../../src/vscode-dts/vscode.d.ts" + "../../../src/vscode-dts/vscode.d.ts", + "../../../src/vscode-dts/vscode.proposed.documentPaste.d.ts" ] } diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index 4ddfe8fce0d..57cbba323a4 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -23,6 +23,9 @@ "supported": true } }, + "enabledApiProposals": [ + "documentPaste" + ], "scripts": { "compile": "npx gulp compile-extension:css-language-features-client compile-extension:css-language-features-server", "watch": "npx gulp watch-extension:css-language-features-client watch-extension:css-language-features-server", diff --git a/extensions/css-language-features/schemas/package.schema.json b/extensions/css-language-features/schemas/package.schema.json index 831149caa9e..6c4b91faa27 100644 --- a/extensions/css-language-features/schemas/package.schema.json +++ b/extensions/css-language-features/schemas/package.schema.json @@ -1,6 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CSS contributions to package.json", "type": "object", "properties": { "contributes": { diff --git a/extensions/fsharp/cgmanifest.json b/extensions/fsharp/cgmanifest.json index 0b3c5d112e5..f10f9ca661a 100644 --- a/extensions/fsharp/cgmanifest.json +++ b/extensions/fsharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "ionide/ionide-fsgrammar", "repositoryUrl": "https://github.com/ionide/ionide-fsgrammar", - "commitHash": "0100f551f6c32598a58aba97344bf828673fec7a" + "commitHash": "7d1b695da917dc4c7a0f7fb4683f42da208f87a2" } }, "license": "MIT", diff --git a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json b/extensions/fsharp/syntaxes/fsharp.tmLanguage.json index 7806c100eae..41436335cdc 100644 --- a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json +++ b/extensions/fsharp/syntaxes/fsharp.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/ionide/ionide-fsgrammar/commit/0100f551f6c32598a58aba97344bf828673fec7a", + "version": "https://github.com/ionide/ionide-fsgrammar/commit/7d1b695da917dc4c7a0f7fb4683f42da208f87a2", "name": "fsharp", "scopeName": "source.fsharp", "patterns": [ @@ -841,7 +841,7 @@ "name": "keyword.symbol.fsharp" } }, - "end": "(\\)\\s*(([?[:alpha:]0-9'`^._ ]+))+)", + "end": "(\\)\\s*(([?[:alpha:]0-9'`^._ ]+))*)", "endCaptures": { "1": { "name": "keyword.symbol.fsharp" diff --git a/extensions/git/package.json b/extensions/git/package.json index f2445eb3978..a7c8e455c11 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -16,6 +16,7 @@ "contribMergeEditorMenus", "contribMultiDiffEditorMenus", "contribDiffEditorGutterToolBarMenus", + "contribSourceControlHistoryItemChangesMenu", "contribSourceControlHistoryItemGroupMenu", "contribSourceControlHistoryItemMenu", "contribSourceControlInputBoxMenu", @@ -24,6 +25,7 @@ "diffCommand", "editSessionIdentityProvider", "quickDiffProvider", + "quickInputButtonLocation", "quickPickSortByLabel", "scmActionButton", "scmHistoryProvider", @@ -891,6 +893,16 @@ "icon": "$(diff-multiple)", "category": "Git", "enablement": "!operationInProgress" + }, + { + "command": "git.copyCommitId", + "title": "%command.timelineCopyCommitId%", + "category": "Git" + }, + { + "command": "git.copyCommitMessage", + "title": "%command.timelineCopyCommitMessage%", + "category": "Git" } ], "continueEditSession": [ @@ -1426,6 +1438,14 @@ { "command": "git.pushRef", "when": "false" + }, + { + "command": "git.copyCommitId", + "when": "false" + }, + { + "command": "git.copyCommitMessage", + "when": "false" } ], "scm/title": [ @@ -1915,6 +1935,40 @@ "group": "1_modification@3" } ], + "scm/historyItemChanges/title": [ + { + "command": "git.fetchRef", + "group": "navigation@1", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote" + }, + { + "command": "git.pullRef", + "group": "navigation@2", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote" + }, + { + "command": "git.pushRef", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote", + "group": "navigation@3" + }, + { + "command": "git.publish", + "when": "scmProvider == git && !scmHistoryItemGroupHasRemote", + "group": "navigation@3" + } + ], + "scm/historyItem/context": [ + { + "command": "git.copyCommitId", + "when": "scmProvider == git && !listMultiSelection", + "group": "1_copy@1" + }, + { + "command": "git.copyCommitMessage", + "when": "scmProvider == git && !listMultiSelection", + "group": "1_copy@2" + } + ], "scm/incomingChanges": [ { "command": "git.fetchRef", diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index f94ecbab7b0..332c328b2d1 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -322,6 +322,10 @@ export class ApiImpl implements API { } async openRepository(root: Uri): Promise { + if (root.scheme !== 'file') { + return null; + } + await this._model.openRepository(root.fsPath); return this.getRepository(root) || null; } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index ce27e914244..2abbeaaa14e 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -146,6 +146,7 @@ export interface LogOptions { readonly shortStats?: boolean; readonly author?: string; readonly refNames?: string[]; + readonly maxParents?: number; } export interface CommitOptions { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 686ce366e29..cb13cca654e 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,7 +5,7 @@ import * as os from 'os'; import * as path from 'path'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import { ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote } from './api/git'; @@ -2703,6 +2703,7 @@ export class CommandCenter { { iconPath: new ThemeIcon('refresh'), tooltip: l10n.t('Regenerate Branch Name'), + location: QuickInputButtonLocation.Inline } ] : []; @@ -3051,7 +3052,8 @@ export class CommandCenter { } @command('git.fetchRef', { repository: true }) - async fetchRef(repository: Repository, ref: string): Promise { + async fetchRef(repository: Repository, ref?: string): Promise { + ref = ref ?? repository?.historyProvider.currentHistoryItemGroup?.remote?.id; if (!repository || !ref) { return; } @@ -3123,7 +3125,8 @@ export class CommandCenter { } @command('git.pullRef', { repository: true }) - async pullRef(repository: Repository, ref: string): Promise { + async pullRef(repository: Repository, ref?: string): Promise { + ref = ref ?? repository?.historyProvider.currentHistoryItemGroup?.remote?.id; if (!repository || !ref) { return; } @@ -3270,8 +3273,8 @@ export class CommandCenter { } @command('git.pushRef', { repository: true }) - async pushRef(repository: Repository, ref: string): Promise { - if (!repository || !ref) { + async pushRef(repository: Repository): Promise { + if (!repository) { return; } @@ -4189,17 +4192,36 @@ export class CommandCenter { } @command('git.viewCommit', { repository: true }) - async viewCommit(repository: Repository, historyItem: SourceControlHistoryItem): Promise { - if (!repository || !historyItem) { + async viewCommit(repository: Repository, historyItem1: SourceControlHistoryItem, historyItem2?: SourceControlHistoryItem): Promise { + if (!repository || !historyItem1) { return; } - const commit = await repository.getCommit(historyItem.id); - const title = `${historyItem.id.substring(0, 8)} - ${commit.message}`; + if (historyItem2) { + const mergeBase = await repository.getMergeBase(historyItem1.id, historyItem2.id); + if (!mergeBase || (mergeBase !== historyItem1.id && mergeBase !== historyItem2.id)) { + return; + } + } - const multiDiffSourceUri = toGitUri(Uri.file(repository.root), historyItem.id, { scheme: 'git-commit' }); + let title: string | undefined; + let historyItemParentId: string | undefined; - await this._viewChanges(repository, historyItem, multiDiffSourceUri, title); + // If historyItem2 is not provided, we are viewing a single commit. If historyItem2 is + // provided, we are viewing a range and we have to include both start and end commits. + // TODO@lszomoru - handle the case when historyItem2 is the first commit in the repository + if (!historyItem2) { + const commit = await repository.getCommit(historyItem1.id); + title = `${historyItem1.id.substring(0, 8)} - ${commit.message}`; + historyItemParentId = historyItem1.parentIds.length > 0 ? historyItem1.parentIds[0] : `${historyItem1.id}^`; + } else { + title = l10n.t('All Changes ({0} ↔ {1})', historyItem2.id.substring(0, 8), historyItem1.id.substring(0, 8)); + historyItemParentId = historyItem2.parentIds.length > 0 ? historyItem2.parentIds[0] : `${historyItem2.id}^`; + } + + const multiDiffSourceUri = toGitUri(Uri.file(repository.root), `${historyItemParentId}..${historyItem1.id}`, { scheme: 'git-commit', }); + + await this._viewChanges(repository, historyItem1.id, historyItemParentId, multiDiffSourceUri, title); } @command('git.viewAllChanges', { repository: true }) @@ -4214,20 +4236,32 @@ export class CommandCenter { const multiDiffSourceUri = toGitUri(Uri.file(repository.root), historyItem.id, { scheme: 'git-changes' }); - await this._viewChanges(repository, historyItem, multiDiffSourceUri, title); + await this._viewChanges(repository, modifiedShortRef, originalShortRef, multiDiffSourceUri, title); } - async _viewChanges(repository: Repository, historyItem: SourceControlHistoryItem, multiDiffSourceUri: Uri, title: string): Promise { - const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : `${historyItem.id}^`; - const changes = await repository.diffBetween(historyItemParentId, historyItem.id); + async _viewChanges(repository: Repository, historyItemId: string, historyItemParentId: string, multiDiffSourceUri: Uri, title: string): Promise { + const changes = await repository.diffBetween(historyItemParentId, historyItemId); + const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItemId)); - if (changes.length === 0) { + await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); + } + + @command('git.copyCommitId', { repository: true }) + async copyCommitId(repository: Repository, historyItem: SourceControlHistoryItem): Promise { + if (!repository || !historyItem) { return; } - const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItem.id)); + env.clipboard.writeText(historyItem.id); + } - await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); + @command('git.copyCommitMessage', { repository: true }) + async copyCommitMessage(repository: Repository, historyItem: SourceControlHistoryItem): Promise { + if (!repository || !historyItem) { + return; + } + + env.clipboard.writeText(historyItem.message); } private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index eb0007893e8..915210c4847 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1062,6 +1062,7 @@ export interface PullOptions { } export class Repository { + private _isUsingRefTable = false; constructor( private _git: Git, @@ -1165,6 +1166,10 @@ export class Repository { args.push(`--author="${options.author}"`); } + if (typeof options?.maxParents === 'number') { + args.push(`--max-parents=${options.maxParents}`); + } + if (options?.refNames) { args.push('--topo-order'); args.push('--decorate=full'); @@ -1502,6 +1507,21 @@ export class Repository { return parseGitChanges(this.repositoryRoot, gitResult.stdout); } + async diffTrees(treeish1: string, treeish2?: string): Promise { + const args = ['diff-tree', '-r', '--name-status', '-z', '--diff-filter=ADMR', treeish1]; + + if (treeish2) { + args.push(treeish2); + } + + const gitResult = await this.exec(args); + if (gitResult.exitCode) { + return []; + } + + return parseGitChanges(this.repositoryRoot, gitResult.stdout); + } + async getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { try { const args = ['merge-base']; @@ -2311,13 +2331,25 @@ export class Repository { } async getHEAD(): Promise { - try { - // Attempt to parse the HEAD file - const result = await this.getHEADFS(); - return result; - } - catch (err) { - this.logger.warn(`[Git][getHEAD] Failed to parse HEAD file: ${err.message}`); + if (!this._isUsingRefTable) { + try { + // Attempt to parse the HEAD file + const result = await this.getHEADFS(); + + // Git 2.45 adds support for a new reference storage backend called "reftable", promising + // faster lookups, reads, and writes for repositories with any number of references. For + // backwards compatibility the `.git/HEAD` file contains `ref: refs/heads/.invalid`. More + // details are available at https://git-scm.com/docs/reftable + if (result.name === '.invalid') { + this._isUsingRefTable = true; + this.logger.warn(`[Git][getHEAD] Failed to parse HEAD file: Repository is using reftable format.`); + } else { + return result; + } + } + catch (err) { + this.logger.warn(`[Git][getHEAD] Failed to parse HEAD file: ${err.message}`); + } } try { @@ -2670,7 +2702,7 @@ export class Repository { } async getCommit(ref: string): Promise { - const result = await this.exec(['show', '-s', `--format=${COMMIT_FORMAT}`, '-z', ref]); + const result = await this.exec(['show', '-s', '--decorate=full', '--shortstat', `--format=${COMMIT_FORMAT}`, '-z', ref]); const commits = parseGitCommits(result.stdout); if (commits.length === 0) { return Promise.reject('bad commit format'); diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 8bd8b70022d..1d9641474e9 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -83,13 +83,18 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.currentHistoryItemGroup = { id: `refs/heads/${this.repository.HEAD.name ?? ''}`, name: this.repository.HEAD.name ?? '', + revision: this.repository.HEAD.commit, remote: this.repository.HEAD.upstream ? { id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + revision: this.repository.HEAD.upstream.commit } : undefined, - base: mergeBase ? { + base: mergeBase && + (mergeBase.remote !== this.repository.HEAD.upstream?.remote || + mergeBase.name !== this.repository.HEAD.upstream?.name) ? { id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, name: `${mergeBase.remote}/${mergeBase.name}`, + revision: mergeBase.commit } : undefined }; @@ -132,65 +137,57 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { - if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) { + if (!this.currentHistoryItemGroup || !options.historyItemGroupIds || typeof options.limit === 'number' || !options.limit?.id) { return []; } // Deduplicate refNames const refNames = Array.from(new Set(options.historyItemGroupIds)); - // Get the merge base of the refNames - const refsMergeBase = await this.resolveHistoryItemGroupsMergeBase(refNames); - if (!refsMergeBase) { + try { + // Get the common ancestor commit, and commits + const commit = await this.repository.getCommit(options.limit.id); + const commitParentId = commit.parents.length > 0 ? commit.parents[0] : await this.repository.getEmptyTree(); + const commits = await this.repository.log({ range: `${commitParentId}..`, refNames, shortStats: true }); + + await ensureEmojis(); + + return commits.map(commit => { + const newLineIndex = commit.message.indexOf('\n'); + const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; + + const labels = this.resolveHistoryItemLabels(commit, refNames); + + return { + id: commit.hash, + parentIds: commit.parents, + message: emojify(subject), + author: commit.authorName, + icon: new ThemeIcon('git-commit'), + timestamp: commit.authorDate?.getTime(), + statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, + labels: labels.length !== 0 ? labels : undefined + }; + }); + } catch (err) { + this.logger.error(`[GitHistoryProvider][provideHistoryItems2] Failed to get history items '${options.limit.id}..': ${err}`); return []; } - - // Get the commits - const commits = await this.repository.log({ range: `${refsMergeBase}^..`, refNames, shortStats: true }); - - await ensureEmojis(); - - const historyItems: SourceControlHistoryItem[] = []; - historyItems.push(...commits.map(commit => { - const newLineIndex = commit.message.indexOf('\n'); - const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; - - const labels = this.resolveHistoryItemLabels(commit, refNames); - - return { - id: commit.hash, - parentIds: commit.parents, - message: emojify(subject), - author: commit.authorName, - icon: new ThemeIcon('git-commit'), - timestamp: commit.authorDate?.getTime(), - statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, - labels: labels.length !== 0 ? labels : undefined - }; - })); - - return historyItems; } async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { - if (!historyItemParentId) { - const commit = await this.repository.getCommit(historyItemId); - historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : `${historyItemId}^`; - } - + historyItemParentId = historyItemParentId ?? await this.repository.getEmptyTree(); const allChanges = await this.repository.diffBetweenShortStat(historyItemParentId, historyItemId); + return { id: historyItemId, parentIds: [historyItemParentId], message: '', statistics: allChanges }; } async provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise { - if (!historyItemParentId) { - const commit = await this.repository.getCommit(historyItemId); - historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : `${historyItemId}^`; - } + historyItemParentId = historyItemParentId ?? await this.repository.getEmptyTree(); const historyItemChangesUri: Uri[] = []; const historyItemChanges: SourceControlHistoryItemChange[] = []; - const changes = await this.repository.diffBetween(historyItemParentId, historyItemId); + const changes = await this.repository.diffTrees(historyItemParentId, historyItemId); for (const change of changes) { const historyItemUri = change.uri.with({ @@ -247,17 +244,43 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return undefined; } - provideFileDecoration(uri: Uri): FileDecoration | undefined { - return this.historyItemDecorations.get(uri.toString()); - } + async resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds: string[]): Promise { + try { + if (historyItemGroupIds.length === 0) { + // TODO@lszomoru - log + return undefined; + } else if (historyItemGroupIds.length === 1 && historyItemGroupIds[0] === this.currentHistoryItemGroup?.id) { + // Remote + if (this.currentHistoryItemGroup.remote) { + const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], this.currentHistoryItemGroup.remote.id); + return ancestor; + } - private async resolveHistoryItemGroupsMergeBase(refNames: string[]): Promise { - if (refNames.length < 2) { - return undefined; + // Base + if (this.currentHistoryItemGroup.base) { + const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], this.currentHistoryItemGroup.base.id); + return ancestor; + } + + // First commit + const commits = await this.repository.log({ maxParents: 0, refNames: ['HEAD'] }); + if (commits.length > 0) { + return commits[0].hash; + } + } else if (historyItemGroupIds.length > 1) { + const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], historyItemGroupIds[1], ...historyItemGroupIds.slice(2)); + return ancestor; + } + } + catch (err) { + this.logger.error(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor2] Failed to resolve common ancestor for ${historyItemGroupIds.join(',')}: ${err}`); } - const refsMergeBase = await this.repository.getMergeBase(refNames[0], refNames[1], ...refNames.slice(2)); - return refsMergeBase; + return undefined; + } + + provideFileDecoration(uri: Uri): FileDecoration | undefined { + return this.historyItemDecorations.get(uri.toString()); } private resolveHistoryItemLabels(commit: Commit, refNames: string[]): SourceControlHistoryItemLabel[] { diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index b3f6b6466fe..aa4d98adc8b 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -20,7 +20,7 @@ import * as fs from 'fs'; import * as os from 'os'; import { GitTimelineProvider } from './timelineProvider'; import { registerAPICommands } from './api/api1'; -import { TerminalEnvironmentManager } from './terminal'; +import { TerminalEnvironmentManager, TerminalShellExecutionManager } from './terminal'; import { createIPCServer, IPCServer } from './ipc/ipcServer'; import { GitEditor } from './gitEditor'; import { GitPostCommitCommandsProvider } from './postCommitCommands'; @@ -113,7 +113,8 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, new GitFileSystemProvider(model), new GitDecorations(model), new GitTimelineProvider(model, cc), - new GitEditSessionIdentityProvider(model) + new GitEditSessionIdentityProvider(model), + new TerminalShellExecutionManager(model, logger) ); const postCommitCommandsProvider = new GitPostCommitCommandsProvider(); diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index d4e00449e20..48e66d5e9ff 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -325,7 +325,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } */ this.telemetryReporter.sendTelemetryEvent('git.repositoryInitialScan', { autoRepositoryDetection: String(autoRepositoryDetection) }, { repositoryCount: this.openRepositories.length }); - this.logger.info(`[Model][doInitialScan] Initial repository scan completed - repositories(${this.repositories.length}), closed repositories (${this.closedRepositories.length}), parent repositories (${this.parentRepositories.length}), unsafe repositories (${this.unsafeRepositories.length})`); + this.logger.info(`[Model][doInitialScan] Initial repository scan completed - repositories (${this.repositories.length}), closed repositories (${this.closedRepositories.length}), parent repositories (${this.parentRepositories.length}), unsafe repositories (${this.unsafeRepositories.length})`); } /** diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 5c14345e61a..b7250d1036c 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -718,6 +718,8 @@ export class Repository implements Disposable { private _untrackedGroup: SourceControlResourceGroup; get untrackedGroup(): GitResourceGroup { return this._untrackedGroup as GitResourceGroup; } + private _EMPTY_TREE: string | undefined; + private _HEAD: Branch | undefined; get HEAD(): Branch | undefined { return this._HEAD; @@ -1112,6 +1114,10 @@ export class Repository implements Disposable { return this.run(Operation.Diff, () => this.repository.diffBetweenShortStat(ref1, ref2)); } + diffTrees(treeish1: string, treeish2?: string): Promise { + return this.run(Operation.Diff, () => this.repository.diffTrees(treeish1, treeish2)); + } + getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2, ...refs)); } @@ -1602,6 +1608,15 @@ export class Repository implements Disposable { return await this.repository.getCommit(ref); } + async getEmptyTree(): Promise { + if (!this._EMPTY_TREE) { + const result = await this.repository.exec(['hash-object', '-t', 'tree', '/dev/null']); + this._EMPTY_TREE = result.stdout.trim(); + } + + return this._EMPTY_TREE; + } + async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> { return await this.run(Operation.RevList, () => this.repository.getCommitCount(range)); } @@ -1943,7 +1958,8 @@ export class Repository implements Disposable { return await this.run(Operation.Ignore, async () => { const ignoreFile = `${this.repository.root}${path.sep}.gitignore`; const textToAppend = files - .map(uri => relativePath(this.repository.root, uri.fsPath).replace(/\\/g, '/')) + .map(uri => relativePath(this.repository.root, uri.fsPath) + .replace(/\\|\[/g, match => match === '\\' ? '/' : `\\${match}`)) .join('\n'); const document = await new Promise(c => fs.exists(ignoreFile, c)) diff --git a/extensions/git/src/terminal.ts b/extensions/git/src/terminal.ts index 4f6d95488bb..05a4366d0c8 100644 --- a/extensions/git/src/terminal.ts +++ b/extensions/git/src/terminal.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext, l10n, workspace } from 'vscode'; -import { filterEvent, IDisposable } from './util'; +import { ExtensionContext, l10n, LogOutputChannel, TerminalShellExecutionEndEvent, window, workspace } from 'vscode'; +import { dispose, filterEvent, IDisposable } from './util'; +import { Model } from './model'; export interface ITerminalEnvironmentProvider { featureDescription?: string; @@ -50,3 +51,42 @@ export class TerminalEnvironmentManager { this.disposable.dispose(); } } + +export class TerminalShellExecutionManager { + private readonly subcommands = new Set([ + 'add', 'branch', 'checkout', 'cherry-pick', 'clean', 'commit', 'fetch', 'merge', + 'mv', 'rebase', 'reset', 'restore', 'revert', 'rm', 'pull', 'push', 'stash', 'switch']); + + private readonly disposables: IDisposable[] = []; + + constructor( + private readonly model: Model, + private readonly logger: LogOutputChannel + ) { + window.onDidEndTerminalShellExecution(this.onDidEndTerminalShellExecution, this, this.disposables); + } + + private onDidEndTerminalShellExecution(e: TerminalShellExecutionEndEvent): void { + const { execution, exitCode, shellIntegration } = e; + const [executable, subcommand] = execution.commandLine.value.split(/\s+/); + const cwd = execution.cwd ?? shellIntegration.cwd; + + if (executable.toLowerCase() !== 'git' || !this.subcommands.has(subcommand.toLowerCase()) || !cwd || exitCode !== 0) { + return; + } + + this.logger.trace(`[TerminalShellExecutionManager][onDidEndTerminalShellExecution] Matched git subcommand: ${subcommand}`); + + const repository = this.model.getRepository(cwd); + if (!repository) { + this.logger.trace(`[TerminalShellExecutionManager][onDidEndTerminalShellExecution] Unable to find repository for current working directory: ${cwd.toString()}`); + return; + } + + repository.status(); + } + + dispose(): void { + dispose(this.disposables); + } +} diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 6ca99fecf1b..75fb8217df7 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -21,6 +21,7 @@ "../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", + "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", "../types/lib.textEncoder.d.ts" ] } diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 2d2bea56277..7fcbc7f151c 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -38,7 +38,7 @@ "id": "github-enterprise" } ], - "configuration": { + "configuration": [{ "title": "GitHub Enterprise Server Authentication Provider", "properties": { "github-enterprise.uri": { @@ -46,7 +46,17 @@ "description": "GitHub Enterprise Server URI" } } + }, + { + "title": "GitHub Authentication", + "properties": { + "github.experimental.multipleAccounts": { + "type": "boolean", + "description": "Experimental support for multiple GitHub accounts" + } + } } + ] }, "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "main": "./out/extension.js", diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 7498a2b2202..a2497b2b0b2 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -173,6 +173,8 @@ const allFlows: IFlow[] = [ ]); if (existingLogin) { searchParams.append('login', existingLogin); + } else { + searchParams.append('prompt', 'select_account'); } // The extra toString, parse is apparently needed for env.openExternal @@ -240,6 +242,8 @@ const allFlows: IFlow[] = [ ]); if (existingLogin) { searchParams.append('login', existingLogin); + } else { + searchParams.append('prompt', 'select_account'); } const loginUrl = baseUri.with({ diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 15fe2ef04f8..ed584c65f8b 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -97,6 +97,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid private readonly _keychain: Keychain; private readonly _accountsSeen = new Set(); private readonly _disposable: vscode.Disposable | undefined; + private _supportsMultipleAccounts = false; private _sessionsPromise: Promise; @@ -133,10 +134,24 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid return sessions; }); + this._supportsMultipleAccounts = vscode.workspace.getConfiguration('github.experimental').get('multipleAccounts', false); + this._disposable = vscode.Disposable.from( this._telemetryReporter, - vscode.authentication.registerAuthenticationProvider(type, this._githubServer.friendlyName, this, { supportsMultipleAccounts: false }), - this.context.secrets.onDidChange(() => this.checkForUpdates()) + vscode.authentication.registerAuthenticationProvider(type, this._githubServer.friendlyName, this, { supportsMultipleAccounts: this._supportsMultipleAccounts }), + this.context.secrets.onDidChange(() => this.checkForUpdates()), + vscode.workspace.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration('github.experimental.multipleAccounts')) { + const newValue = vscode.workspace.getConfiguration('github.experimental').get('multipleAccounts', false); + if (newValue === this._supportsMultipleAccounts) { + return; + } + const result = await vscode.window.showInformationMessage(vscode.l10n.t('Please reload the window to apply the new setting.'), { modal: true }, vscode.l10n.t('Reload Window')); + if (result) { + vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + } + }) ); } @@ -148,14 +163,17 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid return this._sessionChangeEmitter.event; } - async getSessions(scopes?: string[]): Promise { + async getSessions(scopes: string[] | undefined, options?: vscode.AuthenticationProviderSessionOptions): Promise { // For GitHub scope list, order doesn't matter so we immediately sort the scopes const sortedScopes = scopes?.sort() || []; this._logger.info(`Getting sessions for ${sortedScopes.length ? sortedScopes.join(',') : 'all scopes'}...`); const sessions = await this._sessionsPromise; - const finalSessions = sortedScopes.length - ? sessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes)) + const accountFilteredSessions = options?.account + ? sessions.filter(session => session.account.label === options.account?.label) : sessions; + const finalSessions = sortedScopes.length + ? accountFilteredSessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes)) + : accountFilteredSessions; this._logger.info(`Got ${finalSessions.length} sessions for ${sortedScopes?.join(',') ?? 'all scopes'}...`); return finalSessions; @@ -226,7 +244,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid const sessionPromises = sessionData.map(async (session: SessionData) => { // For GitHub scope list, order doesn't matter so we immediately sort the scopes const scopesStr = [...session.scopes].sort().join(' '); - if (scopesSeen.has(scopesStr)) { + if (!this._supportsMultipleAccounts && scopesSeen.has(scopesStr)) { return undefined; } let userInfo: { id: string; accountName: string } | undefined; @@ -279,7 +297,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid this._logger.info(`Stored ${sessions.length} sessions!`); } - public async createSession(scopes: string[]): Promise { + public async createSession(scopes: string[], options?: vscode.AuthenticationProviderSessionOptions): Promise { try { // For GitHub scope list, order doesn't matter so we use a sorted scope to determine // if we've got a session already. @@ -298,14 +316,20 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid const sessions = await this._sessionsPromise; - const accounts = new Set(sessions.map(session => session.account.label)); - const existingLogin = accounts.size <= 1 ? sessions[0]?.account.label : await vscode.window.showQuickPick([...accounts], { placeHolder: 'Choose an account that you would like to log in to' }); + // First we use the account specified in the options, otherwise we use the first account we have to seed auth. + const loginWith = options?.account?.label ?? sessions[0]?.account.label; + this._logger.info(`Logging in with '${loginWith ? loginWith : 'any'}' account...`); + const scopeString = sortedScopes.join(' '); - const token = await this._githubServer.login(scopeString, existingLogin); + const token = await this._githubServer.login(scopeString, loginWith); const session = await this.tokenToSession(token, scopes); this.afterSessionLoad(session); - const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes)); + const sessionIndex = sessions.findIndex( + this._supportsMultipleAccounts + ? s => s.account.id === session.account.id && arrayEquals([...s.scopes].sort(), sortedScopes) + : s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes) + ); const removed = new Array(); if (sessionIndex > -1) { removed.push(...sessions.splice(sessionIndex, 1, session)); diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index af2cf22724f..c9f0a8c07d5 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -197,7 +197,7 @@ export class GitHubServer implements IGitHubServer { throw new Error(`${result.status} ${result.statusText}`); } } catch (e) { - this._logger.warn('Failed to delete token from server.' + e.message ?? e); + this._logger.warn('Failed to delete token from server.' + (e.message ?? e)); } } diff --git a/extensions/grunt/src/main.ts b/extensions/grunt/src/main.ts index 886e7e27e71..fd99ba335c4 100644 --- a/extensions/grunt/src/main.ts +++ b/extensions/grunt/src/main.ts @@ -316,7 +316,7 @@ class TaskDetector { if (this.detectors.size === 0) { return Promise.resolve([]); } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTasks(); + return this.detectors.values().next().value!.getTasks(); } else { const promises: Promise[] = []; for (const detector of this.detectors.values()) { @@ -338,7 +338,7 @@ class TaskDetector { if (this.detectors.size === 0) { return undefined; } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTask(task); + return this.detectors.values().next().value!.getTask(task); } else { if ((task.scope === vscode.TaskScope.Workspace) || (task.scope === vscode.TaskScope.Global)) { return undefined; diff --git a/extensions/gulp/src/main.ts b/extensions/gulp/src/main.ts index 284175741a5..b0b85ca29b9 100644 --- a/extensions/gulp/src/main.ts +++ b/extensions/gulp/src/main.ts @@ -357,7 +357,7 @@ class TaskDetector { if (this.detectors.size === 0) { return Promise.resolve([]); } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTasks(); + return this.detectors.values().next().value!.getTasks(); } else { const promises: Promise[] = []; for (const detector of this.detectors.values()) { @@ -379,7 +379,7 @@ class TaskDetector { if (this.detectors.size === 0) { return undefined; } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTask(task); + return this.detectors.values().next().value!.getTask(task); } else { if ((task.scope === vscode.TaskScope.Workspace) || (task.scope === vscode.TaskScope.Global)) { // Not supported, we don't have enough info to create the task. diff --git a/extensions/html-language-features/schemas/package.schema.json b/extensions/html-language-features/schemas/package.schema.json index ef717dbd1d1..205143c33ca 100644 --- a/extensions/html-language-features/schemas/package.schema.json +++ b/extensions/html-language-features/schemas/package.schema.json @@ -1,6 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "HTML contributions to package.json", "type": "object", "properties": { "contributes": { diff --git a/extensions/jake/src/main.ts b/extensions/jake/src/main.ts index 33d39e288a4..a2511dc62df 100644 --- a/extensions/jake/src/main.ts +++ b/extensions/jake/src/main.ts @@ -290,7 +290,7 @@ class TaskDetector { if (this.detectors.size === 0) { return Promise.resolve([]); } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTasks(); + return this.detectors.values().next().value!.getTasks(); } else { const promises: Promise[] = []; for (const detector of this.detectors.values()) { @@ -312,7 +312,7 @@ class TaskDetector { if (this.detectors.size === 0) { return undefined; } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTask(task); + return this.detectors.values().next().value!.getTask(task); } else { if ((task.scope === vscode.TaskScope.Workspace) || (task.scope === vscode.TaskScope.Global)) { // Not supported, we don't have enough info to create the task. diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index f892664d917..90aafc89b84 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -8,7 +8,8 @@ export type JSONLanguageStatus = { schemas: string[] }; import { workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation, Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange, - ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n + ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n, + RelativePattern } from 'vscode'; import { LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions, DocumentDiagnosticReportKind, @@ -360,18 +361,29 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa const schemaDocuments: { [uri: string]: boolean } = {}; // handle content request - client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { + client.onRequest(VSCodeContentRequest.type, async (uriPath: string) => { const uri = Uri.parse(uriPath); + const uriString = uri.toString(); if (uri.scheme === 'untitled') { - return Promise.reject(new ResponseError(3, l10n.t('Unable to load {0}', uri.toString()))); + throw new ResponseError(3, l10n.t('Unable to load {0}', uriString)); } - if (uri.scheme !== 'http' && uri.scheme !== 'https') { - return workspace.openTextDocument(uri).then(doc => { - schemaDocuments[uri.toString()] = true; - return doc.getText(); - }, error => { - return Promise.reject(new ResponseError(2, error.toString())); - }); + if (uri.scheme === 'vscode') { + try { + runtime.logOutputChannel.info('read schema from vscode: ' + uriString); + ensureFilesystemWatcherInstalled(uri); + const content = await workspace.fs.readFile(uri); + return new TextDecoder().decode(content); + } catch (e) { + throw new ResponseError(5, e.toString(), e); + } + } else if (uri.scheme !== 'http' && uri.scheme !== 'https') { + try { + const document = await workspace.openTextDocument(uri); + schemaDocuments[uriString] = true; + return document.getText(); + } catch (e) { + throw new ResponseError(2, e.toString(), e); + } } else if (schemaDownloadEnabled) { if (runtime.telemetry && uri.authority === 'schema.management.azure.com') { /* __GDPR__ @@ -381,13 +393,15 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa "schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The azure schema URL that was requested." } } */ - runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriPath }); + runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriString }); + } + try { + return await runtime.schemaRequests.getContent(uriString); + } catch (e) { + throw new ResponseError(4, e.toString()); } - return runtime.schemaRequests.getContent(uriPath).catch(e => { - return Promise.reject(new ResponseError(4, e.toString())); - }); } else { - return Promise.reject(new ResponseError(1, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload))); + throw new ResponseError(1, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)); } }); @@ -415,15 +429,50 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa schemaResolutionErrorStatusBarItem.hide(); } }; - - toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); - toDispose.push(workspace.onDidCloseTextDocument(d => { - const uriString = d.uri.toString(); + const handleContentClosed = (uriString: string) => { if (handleContentChange(uriString)) { delete schemaDocuments[uriString]; } fileSchemaErrors.delete(uriString); + }; + + const watchers: Map = new Map(); + toDispose.push(new Disposable(() => { + for (const d of watchers.values()) { + d.dispose(); + } })); + + + const ensureFilesystemWatcherInstalled = (uri: Uri) => { + + const uriString = uri.toString(); + if (!watchers.has(uriString)) { + try { + const watcher = workspace.createFileSystemWatcher(new RelativePattern(uri, '*')); + const handleChange = (uri: Uri) => { + runtime.logOutputChannel.info('schema change detected ' + uri.toString()); + client.sendNotification(SchemaContentChangeNotification.type, uriString); + }; + const createListener = watcher.onDidCreate(handleChange); + const changeListener = watcher.onDidChange(handleChange); + const deleteListener = watcher.onDidDelete(() => { + const watcher = watchers.get(uriString); + if (watcher) { + watcher.dispose(); + watchers.delete(uriString); + } + }); + watchers.set(uriString, Disposable.from(watcher, createListener, changeListener, deleteListener)); + } catch { + runtime.logOutputChannel.info('Problem installing a file system watcher for ' + uriString); + } + } + }; + + toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); + toDispose.push(workspace.onDidCloseTextDocument(d => handleContentClosed(d.uri.toString()))); + toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange)); const handleRetryResolveSchemaCommand = () => { diff --git a/extensions/julia/cgmanifest.json b/extensions/julia/cgmanifest.json index 9daaee1fdd8..b5d8a03be09 100644 --- a/extensions/julia/cgmanifest.json +++ b/extensions/julia/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "JuliaEditorSupport/atom-language-julia", "repositoryUrl": "https://github.com/JuliaEditorSupport/atom-language-julia", - "commitHash": "663bf8d943fd8440f4ae7565f73327dd616bf191" + "commitHash": "c686684f18153687886e7d19c1bfc3a33076b1ab" } }, "license": "MIT", - "version": "0.22.1" + "version": "0.23.0" } ], "version": 1 diff --git a/extensions/julia/syntaxes/julia.tmLanguage.json b/extensions/julia/syntaxes/julia.tmLanguage.json index 35a4ea7d927..f66fda97f70 100644 --- a/extensions/julia/syntaxes/julia.tmLanguage.json +++ b/extensions/julia/syntaxes/julia.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/JuliaEditorSupport/atom-language-julia/commit/663bf8d943fd8440f4ae7565f73327dd616bf191", + "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/c686684f18153687886e7d19c1bfc3a33076b1ab", "name": "Julia", "scopeName": "source.julia", "comment": "This grammar is used by Atom (Oniguruma), GitHub (PCRE), and VSCode (Oniguruma),\nso all regexps must be compatible with both engines.\n\nSpecs:\n- https://github.com/kkos/oniguruma/blob/master/doc/RE\n- https://www.pcre.org/current/doc/html/", @@ -333,7 +333,7 @@ "name": "keyword.control.using.julia" }, { - "match": "(?<=\\w\\s)\\b(as)\\b(?=\\s\\w)", + "match": "(?<=\\S\\s+)\\b(as)\\b(?=\\s+\\S)", "name": "keyword.control.as.julia" }, { diff --git a/extensions/latex/cgmanifest.json b/extensions/latex/cgmanifest.json index b537c48ee8c..3c7203d5d2a 100644 --- a/extensions/latex/cgmanifest.json +++ b/extensions/latex/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "jlelong/vscode-latex-basics", "repositoryUrl": "https://github.com/jlelong/vscode-latex-basics", - "commitHash": "5d7c2a4e451a932b776f6d9342087be6a1e8c0a1" + "commitHash": "969429cb9230a63f9155987f069acd4234d10e1a" } }, "license": "MIT", diff --git a/extensions/latex/syntaxes/LaTeX.tmLanguage.json b/extensions/latex/syntaxes/LaTeX.tmLanguage.json index 76486732195..bc97a73bda8 100644 --- a/extensions/latex/syntaxes/LaTeX.tmLanguage.json +++ b/extensions/latex/syntaxes/LaTeX.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/jlelong/vscode-latex-basics/commit/9cd6bc151f4b9df5d9aeb1e39e30071018d3cb2a", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/969429cb9230a63f9155987f069acd4234d10e1a", "name": "LaTeX", "scopeName": "text.tex.latex", "patterns": [ @@ -94,7 +94,7 @@ "4": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -1900,7 +1900,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -1910,7 +1910,7 @@ "5": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2368,7 +2368,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2404,7 +2404,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2562,7 +2562,7 @@ "name": "meta.scope.item.latex" }, { - "begin": "((\\\\)(?:[aA]uto|foot|full|no|ref|short|[tT]ext|[pP]aren|[sS]mart)?[cC]ite(?:al)?(?:p|s|t|author|year(?:par)?|title)?[ANP]*\\*?)((?:(?:\\([^\\)]*\\)){0,2}(?:\\[[^\\]]*\\]){0,2}\\{[\\p{Alphabetic}:.]*\\})*)(?:([<\\[])[^\\]<>]*([>\\]]))?(?:(\\[)[^\\]]*(\\]))?(\\{)", + "begin": "((\\\\)(?:[aA]uto|foot|full|no|ref|short|[tT]ext|[pP]aren|[sS]mart)?[cC]ite(?:al)?(?:p|s|t|author|year(?:par)?|title)?[ANP]*\\*?)((?:(?:\\([^\\)]*\\)){0,2}(?:\\[[^\\]]*\\]){0,2}\\{[\\p{Alphabetic}\\p{Number}_:.-]*\\})*)(<[^\\]<>]*>)?((?:\\[[^\\]]*\\])*)(\\{)", "captures": { "1": { "name": "keyword.control.cite.latex" @@ -2578,18 +2578,20 @@ ] }, "4": { - "name": "punctuation.definition.arguments.optional.begin.latex" + "patterns": [ + { + "include": "#optional-arg-angle-no-highlight" + } + ] }, "5": { - "name": "punctuation.definition.arguments.optional.end.latex" + "patterns": [ + { + "include": "#optional-arg-bracket-no-highlight" + } + ] }, "6": { - "name": "punctuation.definition.arguments.optional.begin.latex" - }, - "7": { - "name": "punctuation.definition.arguments.optional.end.latex" - }, - "8": { "name": "punctuation.definition.arguments.begin.latex" } }, @@ -2602,6 +2604,7 @@ "name": "meta.citation.latex", "patterns": [ { + "match": "((%).*)$", "captures": { "1": { "name": "comment.line.percentage.tex" @@ -2609,8 +2612,7 @@ "2": { "name": "punctuation.definition.comment.tex" } - }, - "match": "((%).*)$" + } }, { "match": "[\\p{Alphabetic}\\p{Number}:.-]+", @@ -2740,7 +2742,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2783,7 +2785,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2820,7 +2822,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2867,7 +2869,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -3048,7 +3050,7 @@ "name": "punctuation.definition.variable.latex" } }, - "match": "(\\\\)[cgl](?:[_\\p{Alphabetic}@]+)+_[a-z]+", + "match": "(\\\\)(?:[cgl]_+[_\\p{Alphabetic}@]+_[a-z]+|[qs]_[_\\p{Alphabetic}@]+[\\p{Alphabetic}@])", "name": "variable.other.latex3.latex" }, { @@ -3073,27 +3075,29 @@ { "captures": { "1": { - "name": "punctuation.definition.arguments.optional.begin.latex" + "patterns": [ + { + "include": "#optional-arg-parenthesis-no-highlight" + } + ] }, "2": { - "name": "punctuation.definition.arguments.optional.end.latex" + "patterns": [ + { + "include": "#optional-arg-bracket-no-highlight" + } + ] }, "3": { - "name": "punctuation.definition.arguments.optional.begin.latex" - }, - "4": { - "name": "punctuation.definition.arguments.optional.end.latex" - }, - "5": { "name": "punctuation.definition.arguments.begin.latex" }, - "6": { + "4": { "name": "constant.other.reference.citation.latex" }, - "7": { + "5": { "name": "punctuation.definition.arguments.end.latex" }, - "8": { + "6": { "patterns": [ { "include": "#autocites-arg" @@ -3101,7 +3105,7 @@ ] } }, - "match": "(?:(\\()[^\\)]*(\\))){0,2}(?:(\\[)[^\\]]*(\\])){0,2}(\\{)([\\p{Alphabetic}\\p{Number}:.]+)(\\})(.*)" + "match": "((?:\\([^\\)]*\\)){0,2})((?:\\[[^\\]]*\\]){0,2})(\\{)([\\p{Alphabetic}\\p{Number}_:.-]+)(\\})(.*)" } ] }, @@ -3159,7 +3163,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -3222,7 +3226,7 @@ } ] }, - "optional-arg": { + "optional-arg-bracket": { "patterns": [ { "captures": { @@ -3240,6 +3244,73 @@ "name": "meta.parameter.optional.latex" } ] + }, + "optional-arg-parenthesis": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.arguments.optional.begin.latex" + }, + "2": { + "name": "variable.parameter.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "match": "(\\()([^\\(]*?)(\\))", + "name": "meta.parameter.optional.latex" + } + ] + }, + "optional-arg-bracket-no-highlight": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.arguments.optional.begin.latex" + }, + "2": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "match": "(\\[)[^\\[]*?(\\])", + "name": "meta.parameter.optional.latex" + } + ] + }, + "optional-arg-angle-no-highlight": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.arguments.optional.begin.latex" + }, + "2": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "match": "(<)[^<]*?(>)", + "name": "meta.parameter.optional.latex" + } + ] + }, + "optional-arg-parenthesis-no-highlight": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.arguments.optional.begin.latex" + }, + "2": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "match": "(\\()[^\\(]*?(\\))", + "name": "meta.parameter.optional.latex" + } + ] } } } \ No newline at end of file diff --git a/extensions/less/cgmanifest.json b/extensions/less/cgmanifest.json index a8d1702aa82..69a66b5d9b5 100644 --- a/extensions/less/cgmanifest.json +++ b/extensions/less/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "language-less", "repositoryUrl": "https://github.com/radium-v/Better-Less", - "commitHash": "b06a4555c711a6ef0d76cf2b4fc8b929a6ce551a" + "commitHash": "fb9c21917193746433743a7c971b70230b40bc2b" } }, "license": "MIT", diff --git a/extensions/less/syntaxes/less.tmLanguage.json b/extensions/less/syntaxes/less.tmLanguage.json index cea782810fb..6f57a48e02d 100644 --- a/extensions/less/syntaxes/less.tmLanguage.json +++ b/extensions/less/syntaxes/less.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/radium-v/Better-Less/commit/b06a4555c711a6ef0d76cf2b4fc8b929a6ce551a", + "version": "https://github.com/radium-v/Better-Less/commit/fb9c21917193746433743a7c971b70230b40bc2b", "name": "Less", "scopeName": "source.css.less", "patterns": [ @@ -40,6 +40,14 @@ "match": "(?i:[-+]?(?:(?:\\d*\\.\\d+(?:[eE](?:[-+]?\\d+))*)|(?:[-+]?\\d+))(deg|grad|rad|turn))\\b", "name": "constant.numeric.less" }, + "arbitrary-repetition": { + "captures": { + "1": { + "name": "punctuation.definition.arbitrary-repetition.less" + } + }, + "match": "\\s*(?:(,))" + }, "at-charset": { "begin": "\\s*((@)charset\\b)\\s*", "captures": { @@ -268,6 +276,9 @@ "patterns": [ { "include": "#keyframe-name" + }, + { + "include": "#arbitrary-repetition" } ] } @@ -700,6 +711,9 @@ }, { "include": "#less-math" + }, + { + "include": "#relative-color" } ] } @@ -718,6 +732,7 @@ "name": "support.function.color.less" } }, + "comment": "rgb(), rgba()", "end": "\\)", "endCaptures": { "0": { @@ -747,6 +762,9 @@ { "include": "#comma-delimiter" }, + { + "include": "#value-separator" + }, { "include": "#percentage-type" }, @@ -758,12 +776,13 @@ ] }, { - "begin": "\\b(hs(l|v)a?|hwb)(?=\\()", + "begin": "\\b(hsla|hsl|hwb|oklab|oklch|lab|lch)(?=\\()", "beginCaptures": { "1": { "name": "support.function.color.less" } }, + "comment": "hsla, hsl, hwb, oklab, oklch, lab, lch", "end": "\\)", "endCaptures": { "0": { @@ -781,6 +800,9 @@ }, "end": "(?=\\))", "patterns": [ + { + "include": "#color-values" + }, { "include": "#less-strings" }, @@ -801,6 +823,47 @@ }, { "include": "#number-type" + }, + { + "include": "#calc-function" + }, + { + "include": "#value-separator" + } + ] + } + ] + }, + { + "begin": "\\b(light-dark)(?=\\()", + "beginCaptures": { + "1": { + "name": "support.function.color.less" + } + }, + "comment": "light-dark()", + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", + "patterns": [ + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.group.begin.less" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "include": "#color-values" + }, + { + "include": "#comma-delimiter" } ] } @@ -845,6 +908,9 @@ }, "match": "(#)(\\h{3}|\\h{4}|\\h{6}|\\h{8})\\b", "name": "constant.other.color.rgb-value.less" + }, + { + "include": "#relative-color" } ] }, @@ -1048,12 +1114,16 @@ ] }, "cubic-bezier-function": { - "begin": "\\b(cubic-bezier)(?=\\()", + "begin": "\\b(cubic-bezier)(\\()", "beginCaptures": { - "0": { + "1": { "name": "support.function.timing.less" + }, + "2": { + "name": "punctuation.definition.group.begin.less" } }, + "contentName": "meta.group.less", "end": "\\)", "endCaptures": { "0": { @@ -1063,24 +1133,22 @@ "name": "meta.function-call.less", "patterns": [ { - "begin": "\\(", - "beginCaptures": { - "0": { - "name": "punctuation.definition.group.begin.less" - } - }, - "end": "(?=\\))", - "patterns": [ - { - "include": "#var-function" - }, - { - "include": "#comma-delimiter" - }, - { - "include": "#number-type" - } - ] + "include": "#less-functions" + }, + { + "include": "#calc-function" + }, + { + "include": "#less-variables" + }, + { + "include": "#var-function" + }, + { + "include": "#comma-delimiter" + }, + { + "include": "#number-type" } ] }, @@ -1104,14 +1172,14 @@ { "include": "#frequency-type" }, + { + "include": "#time-type" + }, { "include": "#length-type" }, { "include": "#resolution-type" - }, - { - "include": "#time-type" } ] }, @@ -1611,6 +1679,15 @@ } ] }, + "important": { + "captures": { + "1": { + "name": "punctuation.separator.less" + } + }, + "match": "(\\!)\\s*important", + "name": "keyword.other.important.less" + }, "integer-type": { "match": "(?:[-+]?\\d+)", "name": "constant.numeric.less" @@ -1757,6 +1834,7 @@ "name": "support.function.color-definition.less" } }, + "comment": "argb()", "end": "\\)", "endCaptures": { "0": { @@ -1786,6 +1864,59 @@ ] } ] + }, + { + "begin": "\\b(hsva?)(?=\\()", + "beginCaptures": { + "1": { + "name": "support.function.color.less" + } + }, + "comment": "hsva(), hsv()", + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", + "patterns": [ + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.group.begin.less" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "include": "#integer-type" + }, + { + "include": "#percentage-type" + }, + { + "include": "#number-type" + }, + { + "include": "#less-strings" + }, + { + "include": "#less-variables" + }, + { + "include": "#var-function" + }, + { + "include": "#calc-function" + }, + { + "include": "#comma-delimiter" + } + ] + } + ] } ] }, @@ -3515,7 +3646,48 @@ "name": "support.constant.property-value.less" }, { - "match": "(?x)\\b(\n absolute|active|add\n|all(-(petite|small)-caps|-scroll)?\n|alpha(betic)?\n|alternate(-reverse)?\n|always|annotation|antialiased|at\n|auto(hiding-scrollbar)?\n|avoid(-column|-page|-region)?\n|background(-color|-image|-position|-size)?\n|backwards|balance|baseline|below|bevel|bicubic|bidi-override|blink\n|block(-(line-height|start|end))?\n|blur\n|bold(er)?\n|border(-bottom|-left|-right|-top)?-(color|radius|width|style)\n|border-(bottom|top)-(left|right)-radius\n|border-image(-outset|-repeat|-slice|-source|-width)?\n|border(-bottom|-left|-right|-top|-collapse|-spacing|-box)?\n|both|bottom\n|box(-shadow)?\n|break-(all|word|spaces)\n|brightness\n|butt(on)?\n|capitalize\n|cent(er|ral)\n|char(acter-variant)?\n|cjk-ideographic|clip|clone|close-quote\n|closest-(corner|side)\n|col-resize|collapse\n|color(-stop|-burn|-dodge)?\n|column((-count|-gap|-reverse|-rule(-color|-width)?|-width)|s)?\n|common-ligatures|condensed|consider-shifts|contain\n|content(-box|s)?\n|contextual|contrast|cover\n|crisp(-e|E)dges\n|crop\n|cross(hair)?\n|da(rken|shed)\n|default|dense|diagonal-fractions|difference|disabled\n|discard|discretionary-ligatures|disregard-shifts\n|distribute(-all-lines|-letter|-space)?\n|dotted|double|drop-shadow\n|(nwse|nesw|ns|ew|sw|se|nw|ne|w|s|e|n)-resize\n|ease(-in-out|-in|-out)?\n|element|ellipsis|embed|end|EndColorStr|evenodd\n|exclu(de(-ruby)?|sion)\n|expanded\n|(extra|semi|ultra)-(condensed|expanded)\n|farthest-(corner|side)?\n|fill(-box|-opacity)?\n|filter\n|fit-content\n|fixed\n|flat\n|flex((-basis|-end|-grow|-shrink|-start)|box)?\n|flip|flood-color\n|font(-size(-adjust)?|-stretch|-weight)?\n|forwards\n|from(-image)?\n|full-width|gap|geometricPrecision|glyphs|gradient|grayscale\n|grid((-column|-row)?-gap|-height)?\n|groove|hand|hanging|hard-light|height|help|hidden|hide\n|historical-(forms|ligatures)\n|horizontal(-tb)?\n|hue\n|ideograph(-alpha|-numeric|-parenthesis|-space|ic)\n|inactive|include-ruby|infinite|inherit|initial\n|inline(-(block|box|flex(box)?|line-height|table|start|end))?\n|inset|inside\n|inter(-ideograph|-word|sect)\n|invert|isolat(e|ion)|italic\n|jis(04|78|83|90)\n|justify(-all)?\n|keep-all\n|large[r]?\n|last|layout|left|letter-spacing\n|light(e[nr]|ing-color)\n|line(-edge|-height|-through)?\n|linear(-gradient|RGB)?\n|lining-nums|list-item|local|loose|lowercase|lr-tb|ltr\n|lumin(osity|ance)|manual\n|manipulation\n|margin(-bottom|-box|-left|-right|-top)?\n|marker(-offset|s)?\n|match-parent\n|mathematical\n|max-(content|height|lines|size|width)\n|medium|middle\n|min-(content|height|width)\n|miter|mixed|move|multiply|newspaper\n|no-(change|clip|(close|open)-quote|(common|discretionary|historical)-ligatures|contextual|drop|repeat)\n|none|nonzero|normal|not-allowed|nowrap|oblique\n|offset(-after|-before|-end|-start)?\n|oldstyle-nums|opacity|open-quote\n|optimize(Legibility|Precision|Quality|Speed)\n|order|ordinal|ornaments\n|outline(-color|-offset|-width)?\n|outset|outside|over(line|-edge|lay)\n|padding(-bottom|-box|-left|-right|-top|-box)?\n|page|paint(ed)?|paused\n|pan-(x|left|right|y|up|down)\n|perspective-origin\n|petite-caps|pixelated|pointer\n|pinch-zoom\n|pretty\n|pre(-line|-wrap)?\n|preserve(-3d|-breaks|-spaces)?\n|progid:DXImageTransform.Microsoft.(Alpha|Blur|dropshadow|gradient|Shadow)\n|progress\n|proportional-(nums|width)\n|radial-gradient|recto|region|relative\n|repeat(-[xy])?\n|repeating-(linear|radial)-gradient\n|replaced|reset-size|reverse|revert(-layer)?|ridge|right\n|round\n|row(-gap|-resize|-reverse)?\n|rtl|ruby|running|saturat(e|ion)|screen\n|scroll(-position|bar)?\n|separate|sepia\n|scale-down\n|shape-(image-threshold|margin|outside)\n|show\n|sideways(-lr|-rl)?\n|simplified\n|size\n|slashed-zero|slice\n|small(-caps|er)?\n|smooth|snap|solid|soft-light\n|space(-around|-between)?\n|span|sRGB\n|stable\n|stack(ed-fractions)?\n|start(ColorStr)?\n|static\n|step-(end|start)\n|sticky\n|stop-(color|opacity)\n|stretch|strict\n|stroke(-box|-dash(array|offset)|-miterlimit|-opacity|-width)?\n|style(set)?\n|stylistic\n|sub(grid|pixel-antialiased|tract)?\n|super|swash\n|table(-caption|-cell|(-column|-footer|-header|-row)-group|-column|-row)?\n|tabular-nums|tb-rl\n|text((-bottom|-(decoration|emphasis)-color|-indent|-(over|under)-edge|-shadow|-size(-adjust)?|-top)|field)?\n|thi(ck|n)\n|titling-ca(ps|se)\n|to[p]?\n|touch|traditional\n|transform(-origin)?\n|under(-edge|line)?\n|unicase|unset|uppercase|upright\n|use-(glyph-orientation|script)\n|verso\n|vertical(-align|-ideographic|-lr|-rl|-text)?\n|view-box\n|viewport-fill(-opacity)?\n|visibility\n|visible(Fill|Painted|Stroke)?\n|wait|wavy|weight|whitespace|(device-)?width|word-spacing\n|wrap(-reverse)?\n|x{1,2}-(large|small)\n|z-index|zero\n|zoom(-in|-out)?\n|((?xi:arabic-indic|armenian|bengali|cambodian|circle|cjk-decimal|cjk-earthly-branch|cjk-heavenly-stem|decimal-leading-zero|decimal|devanagari|disclosure-closed|disclosure-open|disc|ethiopic-numeric|georgian|gujarati|gurmukhi|hebrew|hiragana-iroha|hiragana|japanese-formal|japanese-informal|kannada|katakana-iroha|katakana|khmer|korean-hangul-formal|korean-hanja-formal|korean-hanja-informal|lao|lower-alpha|lower-armenian|lower-greek|lower-latin|lower-roman|malayalam|mongolian|myanmar|oriya|persian|simp-chinese-formal|simp-chinese-informal|square|tamil|telugu|thai|tibetan|trad-chinese-formal|trad-chinese-informal|upper-alpha|upper-armenian|upper-latin|upper-roman)))\\b", + "include": "#cubic-bezier-function" + }, + { + "include": "#steps-function" + }, + { + "comment": "animation-composition", + "match": "\\b(?:replace|add|accumulate)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-direction", + "match": "\\b(?:normal|alternate-reverse|alternate|reverse)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-fill-mode", + "match": "\\b(?:forwards|backwards|both)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-iteration-count", + "match": "\\b(?:infinite)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-play-state", + "match": "\\b(?:running|paused)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-range, animation-range-start, animation-range-end", + "match": "\\b(?:entry-crossing|exit-crossing|entry|exit)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-timing-function", + "match": "\\b(?:linear|ease-in-out|ease-in|ease-out|ease|step-start|step-end)\\b", + "name": "support.constant.property-value.less" + }, + { + "match": "(?x)\\b(\n absolute|active|add\n|all(-(petite|small)-caps|-scroll)?\n|alpha(betic)?\n|alternate(-reverse)?\n|always|annotation|antialiased|at\n|auto(hiding-scrollbar)?\n|avoid(-column|-page|-region)?\n|background(-color|-image|-position|-size)?\n|backwards|balance|baseline|below|bevel|bicubic|bidi-override|blink\n|block(-(line-height|start|end))?\n|blur\n|bold(er)?\n|border-top-left-radius\n|border-top-right-radius\n|border-bottom-left-radius\n|border-bottom-right-radius\n|border-end-end-radius\n|border-end-start-radius\n|border-start-end-radius\n|border-start-start-radius\n|border-block-start-color\n|border-block-start-style\n|border-block-start-width\n|border-block-start\n|border-block-end-color\n|border-block-end-style\n|border-block-end-width\n|border-block-end\n|border-block-color\n|border-block-style\n|border-block-width\n|border-block\n|border-inline-start-color\n|border-inline-start-style\n|border-inline-start-width\n|border-inline-start\n|border-inline-end-color\n|border-inline-end-style\n|border-inline-end-width\n|border-inline-end\n|border-inline-color\n|border-inline-style\n|border-inline-width\n|border-inline\n|border-top-color\n|border-top-style\n|border-top-width\n|border-top\n|border-right-color\n|border-right-style\n|border-right-width\n|border-right\n|border-bottom-color\n|border-bottom-style\n|border-bottom-width\n|border-bottom\n|border-left-color\n|border-left-style\n|border-left-width\n|border-left\n|border-image-outset\n|border-image-repeat\n|border-image-slice\n|border-image-source\n|border-image-width\n|border-image\n|border-color\n|border-style\n|border-width\n|border-radius\n|border-collapse\n|border-spacing\n|border\n|both\n|bottom\n|box(-shadow)?\n|break-(all|word|spaces)\n|brightness\n|butt(on)?\n|capitalize\n|cent(er|ral)\n|char(acter-variant)?\n|cjk-ideographic|clip|clone|close-quote\n|closest-(corner|side)\n|col-resize|collapse\n|color(-stop|-burn|-dodge)?\n|column((-count|-gap|-reverse|-rule(-color|-width)?|-width)|s)?\n|common-ligatures|condensed|consider-shifts|contain\n|content(-box|s)?\n|contextual|contrast|cover\n|crisp(-e|E)dges\n|crop\n|cross(hair)?\n|da(rken|shed)\n|default|dense|diagonal-fractions|difference|disabled\n|discard|discretionary-ligatures|disregard-shifts\n|distribute(-all-lines|-letter|-space)?\n|dotted|double|drop-shadow\n|(nwse|nesw|ns|ew|sw|se|nw|ne|w|s|e|n)-resize\n|ease(-in-out|-in|-out)?\n|element|ellipsis|embed|end|EndColorStr|evenodd\n|exclu(de(-ruby)?|sion)\n|expanded\n|(extra|semi|ultra)-(condensed|expanded)\n|farthest-(corner|side)?\n|fill(-box|-opacity)?\n|filter\n|fit-content\n|fixed\n|flat\n|flex((-basis|-end|-grow|-shrink|-start)|box)?\n|flip|flood-color\n|font(-size(-adjust)?|-stretch|-weight)?\n|forwards\n|from(-image)?\n|full-width|gap|geometricPrecision|glyphs|gradient|grayscale\n|grid((-column|-row)?-gap|-height)?\n|groove|hand|hanging|hard-light|height|help|hidden|hide\n|historical-(forms|ligatures)\n|horizontal(-tb)?\n|hue\n|ideograph(-alpha|-numeric|-parenthesis|-space|ic)\n|inactive|include-ruby|infinite|inherit|initial\n|inline(-(block|box|flex(box)?|line-height|table|start|end))?\n|inset|inside\n|inter(-ideograph|-word|sect)\n|invert|isolat(e|ion)|italic\n|jis(04|78|83|90)\n|justify(-all)?\n|keep-all\n|large[r]?\n|last|layout|left|letter-spacing\n|light(e[nr]|ing-color)\n|line(-edge|-height|-through)?\n|linear(-gradient|RGB)?\n|lining-nums|list-item|local|loose|lowercase|lr-tb|ltr\n|lumin(osity|ance)|manual\n|manipulation\n|margin(-bottom|-box|-left|-right|-top)?\n|marker(-offset|s)?\n|match-parent\n|mathematical\n|max-(content|height|lines|size|width)\n|medium|middle\n|min-(content|height|width)\n|miter|mixed|move|multiply|newspaper\n|no-(change|clip|(close|open)-quote|(common|discretionary|historical)-ligatures|contextual|drop|repeat)\n|none|nonzero|normal|not-allowed|nowrap|oblique\n|offset(-after|-before|-end|-start)?\n|oldstyle-nums|opacity|open-quote\n|optimize(Legibility|Precision|Quality|Speed)\n|order|ordinal|ornaments\n|outline(-color|-offset|-width)?\n|outset|outside|over(line|-edge|lay)\n|padding(-bottom|-box|-left|-right|-top|-box)?\n|page|paint(ed)?|paused\n|pan-(x|left|right|y|up|down)\n|perspective-origin\n|petite-caps|pixelated|pointer\n|pinch-zoom\n|pretty\n|pre(-line|-wrap)?\n|preserve(-3d|-breaks|-spaces)?\n|progid:DXImageTransform.Microsoft.(Alpha|Blur|dropshadow|gradient|Shadow)\n|progress\n|proportional-(nums|width)\n|radial-gradient|recto|region|relative\n|repeat(-[xy])?\n|repeating-(linear|radial)-gradient\n|replaced|reset-size|reverse|revert(-layer)?|ridge|right\n|round\n|row(-gap|-resize|-reverse)?\n|rtl|ruby|running|saturat(e|ion)|screen\n|scroll(-position|bar)?\n|separate|sepia\n|scale-down\n|shape-(image-threshold|margin|outside)\n|show\n|sideways(-lr|-rl)?\n|simplified\n|size\n|slashed-zero|slice\n|small(-caps|er)?\n|smooth|snap|solid|soft-light\n|space(-around|-between)?\n|span|sRGB\n|stable\n|stack(ed-fractions)?\n|start(ColorStr)?\n|static\n|step-(end|start)\n|sticky\n|stop-(color|opacity)\n|stretch|strict\n|stroke(-box|-dash(array|offset)|-miterlimit|-opacity|-width)?\n|style(set)?\n|stylistic\n|sub(grid|pixel-antialiased|tract)?\n|super|swash\n|table(-caption|-cell|(-column|-footer|-header|-row)-group|-column|-row)?\n|tabular-nums|tb-rl\n|text((-bottom|-(decoration|emphasis)-color|-indent|-(over|under)-edge|-shadow|-size(-adjust)?|-top)|field)?\n|thi(ck|n)\n|titling-ca(ps|se)\n|to[p]?\n|touch|traditional\n|transform(-origin)?\n|under(-edge|line)?\n|unicase|unset|uppercase|upright\n|use-(glyph-orientation|script)\n|verso\n|vertical(-align|-ideographic|-lr|-rl|-text)?\n|view-box\n|viewport-fill(-opacity)?\n|visibility\n|visible(Fill|Painted|Stroke)?\n|wait|wavy|weight|whitespace|(device-)?width|word-spacing\n|wrap(-reverse)?\n|x{1,2}-(large|small)\n|z-index|zero\n|zoom(-in|-out)?\n|((?xi:arabic-indic|armenian|bengali|cambodian|circle|cjk-decimal|cjk-earthly-branch|cjk-heavenly-stem|decimal-leading-zero|decimal|devanagari|disclosure-closed|disclosure-open|disc|ethiopic-numeric|georgian|gujarati|gurmukhi|hebrew|hiragana-iroha|hiragana|japanese-formal|japanese-informal|kannada|katakana-iroha|katakana|khmer|korean-hangul-formal|korean-hanja-formal|korean-hanja-informal|lao|lower-alpha|lower-armenian|lower-greek|lower-latin|lower-roman|malayalam|mongolian|myanmar|oriya|persian|simp-chinese-formal|simp-chinese-informal|square|tamil|telugu|thai|tibetan|trad-chinese-formal|trad-chinese-informal|upper-alpha|upper-armenian|upper-latin|upper-roman)))\\b", "name": "support.constant.property-value.less" }, { @@ -3566,13 +3738,7 @@ "include": "#comma-delimiter" }, { - "captures": { - "1": { - "name": "punctuation.separator.less" - } - }, - "match": "(\\!)\\s*important", - "name": "keyword.other.important.less" + "include": "#important" } ] }, @@ -3830,6 +3996,18 @@ } ] }, + "relative-color": { + "patterns": [ + { + "match": "from", + "name": "keyword.other.less" + }, + { + "match": "\\b[hslawbch]\\b", + "name": "keyword.other.less" + } + ] + }, "resolution-type": { "captures": { "1": { @@ -3899,6 +4077,41 @@ { "include": "#filter-function" }, + { + "begin": "\\b(border((-(bottom|top)-(left|right))|((-(start|end)){2}))?-radius|(border-image(?!-)))\\b", + "beginCaptures": { + "0": { + "name": "support.type.property-name.less" + } + }, + "comment": "border-radius and border-image properties utilize a slash as a separator", + "end": "\\s*(;)|(?=[})])", + "endCaptures": { + "1": { + "name": "punctuation.terminator.rule.less" + } + }, + "patterns": [ + { + "begin": "(((\\+_?)?):)(?=[\\s\\t]*)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.key-value.less" + } + }, + "contentName": "meta.property-value.less", + "end": "(?=\\s*(;)|(?=[})]))", + "patterns": [ + { + "include": "#value-separator" + }, + { + "include": "#property-values" + } + ] + } + ] + }, { "captures": { "1": { @@ -3951,7 +4164,7 @@ ] }, { - "begin": "\\banimation(-(delay|direction|duration|fill-mode|iteration-count|name|play-state|timing-function))?\\b", + "begin": "\\banimation-timeline\\b", "beginCaptures": { "0": { "name": "support.type.property-name.less" @@ -3971,47 +4184,91 @@ "name": "punctuation.separator.key-value.less" } }, - "captures": { + "contentName": "meta.property-value.less", + "end": "(?=\\s*(;)|(?=[})]))", + "patterns": [ + { + "include": "#comment-block" + }, + { + "include": "#custom-property-name" + }, + { + "include": "#scroll-function" + }, + { + "include": "#view-function" + }, + { + "include": "#property-values" + }, + { + "include": "#less-variables" + }, + { + "include": "#arbitrary-repetition" + }, + { + "include": "#important" + } + ] + } + ] + }, + { + "begin": "\\banimation(?:-name)?(?=(?:\\+_?)?:)\\b", + "beginCaptures": { + "0": { + "name": "support.type.property-name.less" + } + }, + "end": "\\s*(;)|(?=[})])", + "endCaptures": { + "1": { + "name": "punctuation.terminator.rule.less" + } + }, + "patterns": [ + { + "begin": "(((\\+_?)?):)(?=[\\s\\t]*)", + "beginCaptures": { "1": { - "name": "punctuation.definition.arbitrary-repetition.less" + "name": "punctuation.separator.key-value.less" } }, "contentName": "meta.property-value.less", "end": "(?=\\s*(;)|(?=[})]))", "patterns": [ { - "match": "\\b(linear|ease(-in)?(-out)?|step-(start|end)|none|forwards|backwards|both|normal|alternate(-reverse)?|reverse|running|paused)\\b", - "name": "support.constant.property-value.less" + "include": "#comment-block" }, { - "include": "#cubic-bezier-function" + "include": "#builtin-functions" }, { - "include": "#steps-function" + "include": "#less-functions" }, { - "include": "#time-type" + "include": "#less-variables" }, { - "include": "#number-type" + "include": "#numeric-values" + }, + { + "include": "#property-value-constants" }, { "match": "-?(?:[_a-zA-Z]|[^\\x{00}-\\x{7F}]|(?:(:?\\\\[0-9a-f]{1,6}(\\r\\n|[\\s\\t\\r\\n\\f])?)|\\\\[^\\r\\n\\f0-9a-f]))(?:[-_a-zA-Z0-9]|[^\\x{00}-\\x{7F}]|(?:(:?\\\\[0-9a-f]{1,6}(\\r\\n|[\\t\\r\\n\\f])?)|\\\\[^\\r\\n\\f0-9a-f]))*", - "name": "variable.other.constant.animation-name.less" + "name": "variable.other.constant.animation-name.less string.unquoted.less" }, { - "include": "#literal-string" + "include": "#less-math" }, { - "include": "#property-values" + "include": "#arbitrary-repetition" }, { - "captures": { - "1": { - "name": "punctuation.definition.arbitrary-repetition.less" - } - }, - "match": "\\s*(?:(,))" + "include": "#important" } ] } @@ -4038,11 +4295,6 @@ "name": "punctuation.separator.key-value.less" } }, - "captures": { - "1": { - "name": "punctuation.definition.arbitrary-repetition.less" - } - }, "contentName": "meta.property-value.less", "end": "(?=\\s*(;)|(?=[})]))", "patterns": [ @@ -4059,12 +4311,7 @@ "include": "#steps-function" }, { - "captures": { - "1": { - "name": "punctuation.definition.arbitrary-repetition.less" - } - }, - "match": "\\s*(?:(,))" + "include": "#arbitrary-repetition" } ] } @@ -4140,12 +4387,7 @@ "name": "support.constant.property-value.less" }, { - "captures": { - "1": { - "name": "punctuation.definition.arbitrary-repetition.less" - } - }, - "match": "\\s*(?:(,))" + "include": "#arbitrary-repetition" } ] }, @@ -4189,7 +4431,7 @@ ] }, { - "match": "(?x)\\b( accent-height | align-content | align-items | align-self | alignment-baseline | all | animation-timing-function | animation-play-state | animation-name | animation-iteration-count | animation-fill-mode | animation-duration | animation-direction | animation-delay | animation | appearance | ascent | azimuth | backface-visibility | background-size | background-repeat-y | background-repeat-x | background-repeat | background-position-y | background-position-x | background-position | background-origin | background-image | background-color | background-clip | background-blend-mode | background-attachment | background | baseline-shift | begin | bias | blend-mode | border-((top|right|bottom|left|((block|inline)(-(start|end))?))-)?(width|style|color) | border-((top|bottom)-(right|left)|((start|end)-?){1,2})-radius | border-image-(width|source|slice|repeat|outset) | border-(top|right|bottom|left|collapse|image|radius|spacing|((block|inline)(-(start|end))?)) | border | bottom | box-(align|decoration-break|direction|flex|ordinal-group|orient|pack|shadow|sizing) | break-(after|before|inside) | caption-side | clear | clip-path | clip-rule | clip | color(-(interpolation(-filters)?|profile|rendering))? | columns | column-(break-before|count|fill|gap|(rule(-(color|style|width))?)|span|width) | contain(-intrinsic-((((block|inline)-)?size)|height|width))? | content | counter-(increment|reset) | cursor | (c|d|f)(x|y) | direction | display | divisor | dominant-baseline | dur | elevation | empty-cells | enable-background | end | fallback | fill(-(opacity|rule))? | filter | flex(-(align|basis|direction|flow|grow|item-align|line-pack|negative|order|pack|positive|preferred-size|shrink|wrap))? | float | flood-(color|opacity) | font-display | font-family | font-feature-settings | font-kerning | font-language-override | font-size(-adjust)? | font-smoothing | font-stretch | font-style | font-synthesis | font-variant(-(alternates|caps|east-asian|ligatures|numeric|position))? | font-weight | font | fr | ((column|row)-)?gap | glyph-orientation-(horizontal|vertical) | grid-(area|gap) | grid-auto-(columns|flow|rows) | grid-(column|row)(-(end|gap|start))? | grid-template(-(areas|columns|rows))? | height | hyphens | image-(orientation|rendering|resolution) | inset(-(block|inline))?(-(start|end))? | isolation | justify-content | justify-items | justify-self | kerning | left | letter-spacing | lighting-color | line-(box-contain|break|clamp|height) | list-style(-(image|position|type))? | (margin|padding)(-(bottom|left|right|top)|(-(block|inline)?(-(end|start))?))? | marker(-(end|mid|start))? | mask(-(clip||composite|image|origin|position|repeat|size|type))? | (max|min)-(height|width) | mix-blend-mode | nbsp-mode | negative | object-(fit|position) | opacity | operator | order | orphans | outline(-(color|offset|style|width))? | overflow(-((inline|block)|scrolling|wrap|x|y))? | overscroll-behavior(-block|-(inline|x|y))? | pad(ding(-(bottom|left|right|top))?)? | page(-break-(after|before|inside))? | paint-order | pause(-(after|before))? | perspective(-origin(-(x|y))?)? | pitch(-range)? | place-content | place-self | pointer-events | position | prefix | quotes | range | resize | right | rotate | scale | scroll-behavior | shape-(image-threshold|margin|outside|rendering) | size | speak(-as)? | src | stop-(color|opacity) | stroke(-(dash(array|offset)|line(cap|join)|miterlimit|opacity|width))? | suffix | symbols | system | tab-size | table-layout | tap-highlight-color | text-align(-last)? | text-decoration(-(color|line|style))? | text-emphasis(-(color|position|style))? | text-(anchor|fill-color|height|indent|justify|orientation|overflow|rendering|size-adjust|shadow|transform|underline-position|wrap) | top | touch-action | transform(-origin(-(x|y))?) | transform(-style)? | transition(-(delay|duration|property|timing-function))? | translate | unicode-(bidi|range) | user-(drag|select) | vertical-align | visibility | white-space(-collapse)? | widows | width | will-change | word-(break|spacing|wrap) | writing-mode | z-index | zoom )\\b", + "match": "(?x)\\b( accent-height | align-content | align-items | align-self | alignment-baseline | all | animation-timing-function | animation-range-start | animation-range-end | animation-range | animation-play-state | animation-name | animation-iteration-count | animation-fill-mode | animation-duration | animation-direction | animation-delay | animation-composition | animation | appearance | ascent | azimuth | backface-visibility | background-size | background-repeat-y | background-repeat-x | background-repeat | background-position-y | background-position-x | background-position | background-origin | background-image | background-color | background-clip | background-blend-mode | background-attachment | background | baseline-shift | begin | bias | blend-mode | border-top-left-radius | border-top-right-radius | border-bottom-left-radius | border-bottom-right-radius | border-end-end-radius | border-end-start-radius | border-start-end-radius | border-start-start-radius | border-block-start-color | border-block-start-style | border-block-start-width | border-block-start | border-block-end-color | border-block-end-style | border-block-end-width | border-block-end | border-block-color | border-block-style | border-block-width | border-block | border-inline-start-color | border-inline-start-style | border-inline-start-width | border-inline-start | border-inline-end-color | border-inline-end-style | border-inline-end-width | border-inline-end | border-inline-color | border-inline-style | border-inline-width | border-inline | border-top-color | border-top-style | border-top-width | border-top | border-right-color | border-right-style | border-right-width | border-right | border-bottom-color | border-bottom-style | border-bottom-width | border-bottom | border-left-color | border-left-style | border-left-width | border-left | border-image-outset | border-image-repeat | border-image-slice | border-image-source | border-image-width | border-image | border-color | border-style | border-width | border-radius | border-collapse | border-spacing | border | bottom | box-(align|decoration-break|direction|flex|ordinal-group|orient|pack|shadow|sizing) | break-(after|before|inside) | caption-side | clear | clip-path | clip-rule | clip | color(-(interpolation(-filters)?|profile|rendering))? | columns | column-(break-before|count|fill|gap|(rule(-(color|style|width))?)|span|width) | contain(-intrinsic-((((block|inline)-)?size)|height|width))? | content | counter-(increment|reset) | cursor | (c|d|f)(x|y) | direction | display | divisor | dominant-baseline | dur | elevation | empty-cells | enable-background | end | fallback | fill(-(opacity|rule))? | filter | flex(-(align|basis|direction|flow|grow|item-align|line-pack|negative|order|pack|positive|preferred-size|shrink|wrap))? | float | flood-(color|opacity) | font-display | font-family | font-feature-settings | font-kerning | font-language-override | font-size(-adjust)? | font-smoothing | font-stretch | font-style | font-synthesis | font-variant(-(alternates|caps|east-asian|ligatures|numeric|position))? | font-weight | font | fr | ((column|row)-)?gap | glyph-orientation-(horizontal|vertical) | grid-(area|gap) | grid-auto-(columns|flow|rows) | grid-(column|row)(-(end|gap|start))? | grid-template(-(areas|columns|rows))? | height | hyphens | image-(orientation|rendering|resolution) | inset(-(block|inline))?(-(start|end))? | isolation | justify-content | justify-items | justify-self | kerning | left | letter-spacing | lighting-color | line-(box-contain|break|clamp|height) | list-style(-(image|position|type))? | (margin|padding)(-(bottom|left|right|top)|(-(block|inline)?(-(end|start))?))? | marker(-(end|mid|start))? | mask(-(clip||composite|image|origin|position|repeat|size|type))? | (max|min)-(height|width) | mix-blend-mode | nbsp-mode | negative | object-(fit|position) | opacity | operator | order | orphans | outline(-(color|offset|style|width))? | overflow(-((inline|block)|scrolling|wrap|x|y))? | overscroll-behavior(-block|-(inline|x|y))? | pad(ding(-(bottom|left|right|top))?)? | page(-break-(after|before|inside))? | paint-order | pause(-(after|before))? | perspective(-origin(-(x|y))?)? | pitch(-range)? | place-content | place-self | pointer-events | position | prefix | quotes | range | resize | right | rotate | scale | scroll-behavior | shape-(image-threshold|margin|outside|rendering) | size | speak(-as)? | src | stop-(color|opacity) | stroke(-(dash(array|offset)|line(cap|join)|miterlimit|opacity|width))? | suffix | symbols | system | tab-size | table-layout | tap-highlight-color | text-align(-last)? | text-decoration(-(color|line|style))? | text-emphasis(-(color|position|style))? | text-(anchor|fill-color|height|indent|justify|orientation|overflow|rendering|size-adjust|shadow|transform|underline-position|wrap) | top | touch-action | transform(-origin(-(x|y))?) | transform(-style)? | transition(-(delay|duration|property|timing-function))? | translate | unicode-(bidi|range) | user-(drag|select) | vertical-align | visibility | white-space(-collapse)? | widows | width | will-change | word-(break|spacing|wrap) | writing-mode | z-index | zoom )\\b", "name": "support.type.property-name.less" }, { @@ -4237,6 +4479,40 @@ } ] }, + "scroll-function": { + "begin": "\\b(scroll)(\\()", + "beginCaptures": { + "1": { + "name": "support.function.scroll.less" + }, + "2": { + "name": "punctuation.definition.group.begin.less" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", + "patterns": [ + { + "match": "root|nearest|self", + "name": "support.constant.scroller.less" + }, + { + "match": "block|inline|x|y", + "name": "support.constant.axis.less" + }, + { + "include": "#less-variables" + }, + { + "include": "#var-function" + } + ] + }, "selector": { "patterns": [ { @@ -4257,13 +4533,7 @@ "include": "#less-variable-interpolation" }, { - "captures": { - "1": { - "name": "punctuation.separator.less" - } - }, - "match": "(\\!)\\s*important", - "name": "keyword.other.important.less" + "include": "#important" } ] } @@ -4405,12 +4675,7 @@ ] }, { - "captures": { - "1": { - "name": "punctuation.definition.arbitrary-repetition.less" - } - }, - "match": "\\s*(?:(,))" + "include": "#arbitrary-repetition" }, { "match": "\\*", @@ -4579,12 +4844,16 @@ ] }, "steps-function": { - "begin": "\\b(steps)(?=\\()", + "begin": "\\b(steps)(\\()", "beginCaptures": { - "0": { + "1": { "name": "support.function.timing.less" + }, + "2": { + "name": "punctuation.definition.group.begin.less" } }, + "contentName": "meta.group.less", "end": "\\)", "endCaptures": { "0": { @@ -4594,25 +4863,23 @@ "name": "meta.function-call.less", "patterns": [ { - "begin": "\\(", - "beginCaptures": { - "0": { - "name": "punctuation.definition.group.begin.less" - } - }, - "end": "(?=\\))", - "patterns": [ - { - "include": "#comma-delimiter" - }, - { - "include": "#integer-type" - }, - { - "match": "(end|middle|start)", - "name": "support.keyword.timing-direction.less" - } - ] + "match": "jump-start|jump-end|jump-none|jump-both|start|end", + "name": "support.constant.step-position.less" + }, + { + "include": "#comma-delimiter" + }, + { + "include": "#integer-type" + }, + { + "include": "#less-variables" + }, + { + "include": "#var-function" + }, + { + "include": "#calc-function" } ] }, @@ -5085,45 +5352,49 @@ } ] }, + "value-separator": { + "captures": { + "1": { + "name": "punctuation.separator.less" + } + }, + "match": "\\s*(/)\\s*" + }, "var-function": { + "begin": "\\b(var)(?=\\()", + "beginCaptures": { + "1": { + "name": "support.function.var.less" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", "patterns": [ { - "begin": "\\b(var)(?=\\()", + "begin": "\\(", "beginCaptures": { - "1": { - "name": "support.function.var.less" - } - }, - "end": "\\)", - "endCaptures": { "0": { - "name": "punctuation.definition.group.end.less" + "name": "punctuation.definition.group.begin.less" } }, - "name": "meta.function-call.less", + "end": "(?=\\))", "patterns": [ { - "begin": "\\(", - "beginCaptures": { - "0": { - "name": "punctuation.definition.group.begin.less" - } - }, - "end": "(?=\\))", - "patterns": [ - { - "include": "#comma-delimiter" - }, - { - "include": "#custom-property-name" - }, - { - "include": "#less-variables" - }, - { - "include": "#property-values" - } - ] + "include": "#comma-delimiter" + }, + { + "include": "#custom-property-name" + }, + { + "include": "#less-variables" + }, + { + "include": "#property-values" } ] } @@ -5132,6 +5403,56 @@ "vendor-prefix": { "match": "-(?:webkit|moz(-osx)?|ms|o)-", "name": "support.type.vendor-prefix.less" + }, + "view-function": { + "begin": "\\b(view)(?=\\()", + "beginCaptures": { + "1": { + "name": "support.function.view.less" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", + "patterns": [ + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.group.begin.less" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "match": "block|inline|x|y|auto", + "name": "support.constant.property-value.less" + }, + { + "include": "#length-type" + }, + { + "include": "#percentage-type" + }, + { + "include": "#less-variables" + }, + { + "include": "#var-function" + }, + { + "include": "#calc-function" + }, + { + "include": "#arbitrary-repetition" + } + ] + } + ] } } } \ No newline at end of file diff --git a/extensions/markdown-basics/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index 60c6b192bed..380b0c74ac6 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "f75d5f55730e72ee7ff386841949048b2395e440" + "commitHash": "7418dd20d76c72e82fadee2909e03239e9973b35" } }, "license": "MIT", diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index c84c468b80c..9761ca716ab 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/extensions/markdown-basics/syntaxes/markdown.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/vscode-markdown-tm-grammar/commit/f75d5f55730e72ee7ff386841949048b2395e440", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/7418dd20d76c72e82fadee2909e03239e9973b35", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -2480,14 +2480,34 @@ "name": "meta.separator.markdown" }, "frontMatter": { - "begin": "\\A-{3}\\s*$", - "contentName": "meta.embedded.block.frontmatter", + "begin": "\\A(?=(-{3,}))", + "end": "^ {,3}\\1-*[ \\t]*$|^[ \\t]*\\.{3}$", + "applyEndPatternLast": 1, + "endCaptures": { + "0": { + "name": "punctuation.definition.end.frontmatter" + } + }, "patterns": [ { - "include": "source.yaml" + "begin": "\\A(-{3,})(.*)$", + "while": "^(?! {,3}\\1-*[ \\t]*$|[ \\t]*\\.{3}$)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.begin.frontmatter" + }, + "2": { + "name": "comment.frontmatter" + } + }, + "contentName": "meta.embedded.block.frontmatter", + "patterns": [ + { + "include": "source.yaml" + } + ] } - ], - "end": "(^|\\G)-{3}|\\.{3}\\s*$" + ] }, "table": { "name": "markup.table.markdown", diff --git a/extensions/markdown-language-features/.vscodeignore b/extensions/markdown-language-features/.vscodeignore index 588916d37c3..fdbb001a79a 100644 --- a/extensions/markdown-language-features/.vscodeignore +++ b/extensions/markdown-language-features/.vscodeignore @@ -15,11 +15,4 @@ webpack.config.js esbuild-notebook.js esbuild-preview.js .gitignore -server/src/** -server/extension.webpack.config.js -server/extension-browser.webpack.config.js -server/tsconfig.json -server/.vscode/** -server/node_modules/** -server/yarn.lock -server/.npmignore +**/*.d.ts diff --git a/extensions/markdown-language-features/extension-browser.webpack.config.js b/extensions/markdown-language-features/extension-browser.webpack.config.js index bad27aab02e..47f50aa5e8f 100644 --- a/extensions/markdown-language-features/extension-browser.webpack.config.js +++ b/extensions/markdown-language-features/extension-browser.webpack.config.js @@ -7,13 +7,25 @@ 'use strict'; -const withBrowserDefaults = require('../shared.webpack.config').browser; +const CopyPlugin = require('copy-webpack-plugin'); +const { browserPlugins, browser } = require('../shared.webpack.config'); -module.exports = withBrowserDefaults({ +module.exports = browser({ context: __dirname, entry: { extension: './src/extension.browser.ts' - } + }, + plugins: [ + ...browserPlugins(__dirname), // add plugins, don't replace inherited + new CopyPlugin({ + patterns: [ + { + from: './node_modules/vscode-markdown-languageserver/dist/browser/workerMain.js', + to: 'serverWorkerMain.js', + } + ], + }), + ], }, { configFile: 'tsconfig.browser.json' }); diff --git a/extensions/markdown-language-features/extension.webpack.config.js b/extensions/markdown-language-features/extension.webpack.config.js index de88398eca0..588d0632fd2 100644 --- a/extensions/markdown-language-features/extension.webpack.config.js +++ b/extensions/markdown-language-features/extension.webpack.config.js @@ -7,6 +7,7 @@ 'use strict'; +const CopyPlugin = require('copy-webpack-plugin'); const withDefaults = require('../shared.webpack.config'); module.exports = withDefaults({ @@ -16,5 +17,16 @@ module.exports = withDefaults({ }, entry: { extension: './src/extension.ts', - } + }, + plugins: [ + ...withDefaults.nodePlugins(__dirname), // add plugins, don't replace inherited + new CopyPlugin({ + patterns: [ + { + from: './node_modules/vscode-markdown-languageserver/dist/node/workerMain.js', + to: 'serverWorkerMain.js', + } + ], + }), + ], }); diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index e238a24b0fe..894c1035c39 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -708,14 +708,11 @@ "%configuration.markdown.preferredMdPathExtensionStyle.removeExtension%" ] }, - "markdown.experimental.updateLinksOnPaste": { + "markdown.editor.updateLinksOnPaste.enabled": { "type": "boolean", - "default": false, - "markdownDescription": "%configuration.markdown.experimental.updateLinksOnPaste%", + "markdownDescription": "%configuration.markdown.editor.updateLinksOnPaste.enabled%", "scope": "resource", - "tags": [ - "experimental" - ] + "default": true } } }, @@ -756,8 +753,8 @@ ] }, "scripts": { - "compile": "gulp compile-extension:markdown-language-features-languageService && gulp compile-extension:markdown-language-features-server && gulp compile-extension:markdown-language-features && npm run build-preview && npm run build-notebook", - "watch": "npm run build-preview && gulp watch-extension:markdown-language-features watch-extension:markdown-language-features-languageService watch-extension:markdown-language-features-server", + "compile": "gulp compile-extension:markdown-language-features-languageService && gulp compile-extension:markdown-language-features && npm run build-preview && npm run build-notebook", + "watch": "npm run build-preview && gulp watch-extension:markdown-language-features watch-extension:markdown-language-features-languageService", "vscode:prepublish": "npm run build-ext && npm run build-preview", "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:markdown-language-features ./tsconfig.json", "build-notebook": "node ./esbuild-notebook", @@ -775,6 +772,7 @@ "picomatch": "^2.3.1", "vscode-languageclient": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.11", + "vscode-markdown-languageserver": "^0.5.0-alpha.8", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index 3549ec58c51..da567b46665 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -91,6 +91,6 @@ "configuration.markdown.preferredMdPathExtensionStyle.removeExtension": "Prefer removing the file extension. For example, path completions to a file named `file.md` will insert `file` without the `.md`.", "configuration.markdown.editor.filePaste.videoSnippet": "Snippet used when adding videos to Markdown. This snippet can use the following variables:\n- `${src}` — The resolved path of the video file.\n- `${title}` — The title used for the video. A snippet placeholder will automatically be created for this variable.", "configuration.markdown.editor.filePaste.audioSnippet": "Snippet used when adding audio to Markdown. This snippet can use the following variables:\n- `${src}` — The resolved path of the audio file.\n- `${title}` — The title used for the audio. A snippet placeholder will automatically be created for this variable.", - "configuration.markdown.experimental.updateLinksOnPaste": "Enable/disable automatic updating of links in text that is copied and pasted from one Markdown editor to another.", + "configuration.markdown.editor.updateLinksOnPaste.enabled": "Enable/disable a paste option that updates links and reference in text that is copied and pasted between Markdown editors.\n\nTo use this feature, after pasting text that contains updatable links, just click on the Paste Widget and select `Paste and update pasted links`.", "workspaceTrust": "Required for loading styles configured in the workspace." } diff --git a/extensions/markdown-language-features/schemas/package.schema.json b/extensions/markdown-language-features/schemas/package.schema.json index 5591d0b0032..8dea48f757f 100644 --- a/extensions/markdown-language-features/schemas/package.schema.json +++ b/extensions/markdown-language-features/schemas/package.schema.json @@ -1,6 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Markdown contributions to package.json", "type": "object", "properties": { "contributes": { diff --git a/extensions/markdown-language-features/server/.npmignore b/extensions/markdown-language-features/server/.npmignore deleted file mode 100644 index bfd4215998c..00000000000 --- a/extensions/markdown-language-features/server/.npmignore +++ /dev/null @@ -1,12 +0,0 @@ -.vscode/ -.github/ -out/test/ -src/ -.eslintrc.js -.gitignore -tsconfig*.json -*.tsbuildinfo -*.map -example.cjs -CODE_OF_CONDUCT.md -SECURITY.md \ No newline at end of file diff --git a/extensions/markdown-language-features/server/.vscode/launch.json b/extensions/markdown-language-features/server/.vscode/launch.json deleted file mode 100644 index fd9033bffaa..00000000000 --- a/extensions/markdown-language-features/server/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "0.1.0", - // List of configurations. Add new configurations or edit existing ones. - "configurations": [ - { - "name": "Attach", - "type": "node", - "request": "attach", - "port": 7997, - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ] - } - ] -} \ No newline at end of file diff --git a/extensions/markdown-language-features/server/.vscode/settings.json b/extensions/markdown-language-features/server/.vscode/settings.json deleted file mode 100644 index 7a73a41bfdf..00000000000 --- a/extensions/markdown-language-features/server/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/extensions/markdown-language-features/server/.vscode/tasks.json b/extensions/markdown-language-features/server/.vscode/tasks.json deleted file mode 100644 index ecc951a7baf..00000000000 --- a/extensions/markdown-language-features/server/.vscode/tasks.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": "2.0.0", - "command": "npm", - "args": [ - "run", - "watch" - ], - "isBackground": true, - "problemMatcher": "$tsc-watch", - "tasks": [ - { - "label": "npm", - "type": "shell", - "command": "npm", - "args": [ - "run", - "watch" - ], - "isBackground": true, - "problemMatcher": "$tsc-watch", - "group": { - "_id": "build", - "isDefault": false - } - } - ] -} \ No newline at end of file diff --git a/extensions/markdown-language-features/server/CHANGELOG.md b/extensions/markdown-language-features/server/CHANGELOG.md deleted file mode 100644 index a5cc9d15cf1..00000000000 --- a/extensions/markdown-language-features/server/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -# 0.4.0-alpha.3 — June 2, 2023 -- Pick up [Markdown Language Service](https://github.com/microsoft/vscode-markdown-languageservice) 0.4.0-alpha.3. See [CHANGELOG](https://github.com/microsoft/vscode-markdown-languageservice/blob/main/CHANGELOG.md#040-alpha3--may-30-2023) for details. - -## 0.3.0 - March 28, 2023 -- Pick up [Markdown Language Service](https://github.com/microsoft/vscode-markdown-languageservice) 0.3.0. See [CHANGELOG](https://github.com/microsoft/vscode-markdown-languageservice/blob/main/CHANGELOG.md#030--march-16-2023) for details. diff --git a/extensions/markdown-language-features/server/README.md b/extensions/markdown-language-features/server/README.md deleted file mode 100644 index 4114d2698bf..00000000000 --- a/extensions/markdown-language-features/server/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# Markdown Language Server - -> **❗ Import** This is still in development. While the language server is being used by VS Code, it has not yet been tested with other clients. - -The Markdown language server powers VS Code's built-in markdown support, providing tools for writing and browsing Markdown files. It runs as a separate executable and implements the [language server protocol](https://microsoft.github.io/language-server-protocol/overview). - -This server uses the [Markdown Language Service](https://github.com/microsoft/vscode-markdown-languageservice) to implement almost all of the language features. You can use that library if you need a library for working with Markdown instead of a full language server. - -## Server capabilities - -- [Completions](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) for Markdown links. - -- [Folding](https://microsoft.github.io/language-server-protocol/specification#textDocument_foldingRange) of Markdown regions, block elements, and header sections. - -- [Smart selection](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange) for inline elements, block elements, and header sections. - -- [Document Symbols](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol) for quick navigation to headers in a document. - -- [Workspace Symbols](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) for quick navigation to headers in the workspace - -- [Document links](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentLink) for making Markdown links in a document clickable. - -- [Find all references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) to headers and links across all Markdown files in the workspace. - -- [Go to definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) from links to headers or link definitions. - -- [Rename](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename) of headers and links across all Markdown files in the workspace. - -- Find all references to a file. Uses a custom `markdown/getReferencesToFileInWorkspace` message. - -- [Code Actions](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction) - - - Organize link definitions source action. - - Extract link to definition refactoring. - -- Updating links when a file is moved / renamed. Uses a custom `markdown/getEditForFileRenames` message. - -- [Pull diagnostics (validation)](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics) for links. - -## Client requirements - -### Initialization options - -The client can send the following initialization options to the server: - -- `markdownFileExtensions` Array file extensions that should be considered as Markdown. These should not include the leading `.`. For example: `['md', 'mdown', 'markdown']`. - -### Settings - -Clients may send a `workspace/didChangeConfiguration` notification to notify the server of settings changes. -The server supports the following settings: - -- `markdown` - - `suggest` - - `paths` - - `enabled` — Enable/disable path suggestions. - - - `occurrencesHighlight` - - `enabled` — Enable/disable highlighting of link occurrences. - - - `validate` - - `enabled` — Enable/disable all validation. - - `referenceLinks` - - `enabled` — Enable/disable validation of reference links: `[text][ref]` - - `fragmentLinks` - - `enabled` — Enable/disable validation of links to fragments in the current files: `[text](#head)` - - `fileLinks` - - `enabled` — Enable/disable validation of links to file in the workspace. - - `markdownFragmentLinks` — Enable/disable validation of links to headers in other Markdown files. Use `inherit` to inherit the `fragmentLinks` setting. - - `ignoredLinks` — Array of glob patterns for files that should not be validated. - - `unusedLinkDefinitions` - - `enabled` — Enable/disable validation of unused link definitions. - - `duplicateLinkDefinitions` - - `enabled` — Enable/disable validation of duplicated link definitions. - -### Custom requests - -To support all of the features of the language server, the client needs to implement a few custom request types. The definitions of these request types can be found in [`protocol.ts`](./src/protocol.ts) - -#### `markdown/parse` - -Get the tokens for a Markdown file. Clients are expected to use [Markdown-it](https://github.com/markdown-it/markdown-it) for this. - -We require that clients bring their own version of Markdown-it so that they can customize/extend Markdown-it. - -#### `markdown/fs/readFile` - -Read the contents of a file in the workspace. - -#### `markdown/fs/readDirectory` - -Read the contents of a directory in the workspace. - -#### `markdown/fs/stat` - -Check if a given file/directory exists in the workspace. - -#### `markdown/fs/watcher/create` - -Create a file watcher. This is needed for diagnostics support. - -#### `markdown/fs/watcher/delete` - -Delete a previously created file watcher. - -#### `markdown/findMarkdownFilesInWorkspace` - -Get a list of all markdown files in the workspace. - -## Contribute - -The source code of the Markdown language server can be found in the [VSCode repository](https://github.com/microsoft/vscode) at [extensions/markdown-language-features/server](https://github.com/microsoft/vscode/tree/master/extensions/markdown-language-features/server). - -File issues and pull requests in the [VSCode GitHub Issues](https://github.com/microsoft/vscode/issues). See the document [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) on how to build and run from source. - -Most of the functionality of the server is located in libraries: - -- [vscode-markdown-languageservice](https://github.com/microsoft/vscode-markdown-languageservice) contains the implementation of all features as a reusable library. -- [vscode-languageserver-node](https://github.com/microsoft/vscode-languageserver-node) contains the implementation of language server for NodeJS. - -Help on any of these projects is very welcome. - -## Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -## License - -Copyright (c) Microsoft Corporation. All rights reserved. - -Licensed under the [MIT](https://github.com/microsoft/vscode/blob/master/LICENSE.txt) License. diff --git a/extensions/markdown-language-features/server/build/pipeline.yml b/extensions/markdown-language-features/server/build/pipeline.yml deleted file mode 100644 index 0c9e3bdbd11..00000000000 --- a/extensions/markdown-language-features/server/build/pipeline.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: $(Date:yyyyMMdd)$(Rev:.r) - -trigger: none -pr: none - -resources: - repositories: - - repository: templates - type: github - name: microsoft/vscode-engineering - ref: main - endpoint: Monaco - -parameters: - - name: publishPackage - displayName: Publish vscode-markdown-languageserver - type: boolean - default: false - -extends: - template: azure-pipelines/npm-package/pipeline.yml@templates - parameters: - npmPackages: - - name: vscode-markdown-languageserver - workingDirectory: extensions/markdown-language-features/server - - buildSteps: - - script: yarn install - displayName: Install dependencies - - - script: gulp compile-extension:markdown-language-features-server - displayName: Compile - - publishPackage: ${{ parameters.publishPackage }} - packagePlatform: 'Windows' diff --git a/extensions/markdown-language-features/server/extension-browser.webpack.config.js b/extensions/markdown-language-features/server/extension-browser.webpack.config.js deleted file mode 100644 index 2a9de70bc01..00000000000 --- a/extensions/markdown-language-features/server/extension-browser.webpack.config.js +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check - -'use strict'; - -const withBrowserDefaults = require('../../shared.webpack.config').browser; -const path = require('path'); - -module.exports = withBrowserDefaults({ - context: __dirname, - entry: { - extension: './src/browser/workerMain.ts', - }, - output: { - filename: 'workerMain.js', - path: path.join(__dirname, 'dist', 'browser'), - libraryTarget: 'var', - library: 'serverExportVar' - } -}); diff --git a/extensions/markdown-language-features/server/extension.webpack.config.js b/extensions/markdown-language-features/server/extension.webpack.config.js deleted file mode 100644 index aafc9c1fd96..00000000000 --- a/extensions/markdown-language-features/server/extension.webpack.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check - -'use strict'; - -const withDefaults = require('../../shared.webpack.config'); -const path = require('path'); - -module.exports = withDefaults({ - context: path.join(__dirname), - entry: { - extension: './src/node/workerMain.ts', - }, - output: { - filename: 'workerMain.js', - path: path.join(__dirname, 'dist', 'node'), - } -}); diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json deleted file mode 100644 index 532c2dec843..00000000000 --- a/extensions/markdown-language-features/server/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "vscode-markdown-languageserver", - "description": "Markdown language server", - "version": "0.5.0-alpha.6", - "author": "Microsoft Corporation", - "license": "MIT", - "engines": { - "node": "*" - }, - "main": "./out/node/main", - "browser": "./dist/browser/main", - "files": [ - "dist/**/*.js", - "out/**/*.js" - ], - "dependencies": { - "@vscode/l10n": "^0.0.11", - "vscode-languageserver": "^8.1.0", - "vscode-languageserver-textdocument": "^1.0.8", - "vscode-languageserver-types": "^3.17.3", - "vscode-markdown-languageservice": "^0.5.0-alpha.6", - "vscode-uri": "^3.0.7" - }, - "devDependencies": { - "@types/node": "20.x" - }, - "scripts": { - "compile": "gulp compile-extension:markdown-language-features-server", - "prepublishOnly": "npm run compile", - "watch": "gulp watch-extension:markdown-language-features-server", - "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", - "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" - } -} diff --git a/extensions/markdown-language-features/server/src/browser/main.ts b/extensions/markdown-language-features/server/src/browser/main.ts deleted file mode 100644 index 126121080a8..00000000000 --- a/extensions/markdown-language-features/server/src/browser/main.ts +++ /dev/null @@ -1,14 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { BrowserMessageReader, BrowserMessageWriter, createConnection } from 'vscode-languageserver/browser'; -import { startVsCodeServer } from '../server'; - -const messageReader = new BrowserMessageReader(self); -const messageWriter = new BrowserMessageWriter(self); - -const connection = createConnection(messageReader, messageWriter); - -startVsCodeServer(connection); diff --git a/extensions/markdown-language-features/server/src/browser/workerMain.ts b/extensions/markdown-language-features/server/src/browser/workerMain.ts deleted file mode 100644 index e653751af40..00000000000 --- a/extensions/markdown-language-features/server/src/browser/workerMain.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as l10n from '@vscode/l10n'; - -let initialized = false; -const pendingMessages: any[] = []; - -const messageHandler = async (e: any) => { - if (!initialized) { - const l10nLog: string[] = []; - initialized = true; - const i10lLocation = e.data.i10lLocation; - if (i10lLocation) { - try { - await l10n.config({ uri: i10lLocation }); - l10nLog.push(`l10n: Configured to ${i10lLocation.toString()}.`); - } catch (e) { - l10nLog.push(`l10n: Problems loading ${i10lLocation.toString()} : ${e}.`); - } - } else { - l10nLog.push(`l10n: No bundle configured.`); - } - - await import('./main'); - - if (self.onmessage !== messageHandler) { - pendingMessages.forEach(msg => self.onmessage?.(msg)); - pendingMessages.length = 0; - } - - l10nLog.forEach(console.log); - } else { - pendingMessages.push(e); - } -}; -self.onmessage = messageHandler; diff --git a/extensions/markdown-language-features/server/src/config.ts b/extensions/markdown-language-features/server/src/config.ts deleted file mode 100644 index 5992258b0b6..00000000000 --- a/extensions/markdown-language-features/server/src/config.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { LsConfiguration } from 'vscode-markdown-languageservice'; - -export { LsConfiguration }; - -const defaultConfig: LsConfiguration = { - markdownFileExtensions: ['md'], - knownLinkedToFileExtensions: [ - 'jpg', - 'jpeg', - 'png', - 'gif', - 'webp', - 'bmp', - 'tiff', - ], - excludePaths: [ - '**/.*', - '**/node_modules/**', - ] -}; - -export function getLsConfiguration(overrides: Partial): LsConfiguration { - return { - ...defaultConfig, - ...overrides, - }; -} diff --git a/extensions/markdown-language-features/server/src/configuration.ts b/extensions/markdown-language-features/server/src/configuration.ts deleted file mode 100644 index 949573cfaf5..00000000000 --- a/extensions/markdown-language-features/server/src/configuration.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Connection, Emitter } from 'vscode-languageserver'; -import { Disposable } from './util/dispose'; - -export type ValidateEnabled = 'ignore' | 'warning' | 'error' | 'hint'; - -export interface Settings { - readonly markdown: { - readonly server: { - readonly log: 'off' | 'debug' | 'trace'; - }; - - readonly preferredMdPathExtensionStyle: 'auto' | 'includeExtension' | 'removeExtension'; - - readonly occurrencesHighlight: { - readonly enabled: boolean; - }; - - readonly suggest: { - readonly paths: { - readonly enabled: boolean; - readonly includeWorkspaceHeaderCompletions: 'never' | 'onSingleOrDoubleHash' | 'onDoubleHash'; - }; - }; - - readonly validate: { - readonly enabled: true; - readonly referenceLinks: { - readonly enabled: ValidateEnabled; - }; - readonly fragmentLinks: { - readonly enabled: ValidateEnabled; - }; - readonly fileLinks: { - readonly enabled: ValidateEnabled; - readonly markdownFragmentLinks: ValidateEnabled | 'inherit'; - }; - readonly ignoredLinks: readonly string[]; - readonly unusedLinkDefinitions: { - readonly enabled: ValidateEnabled; - }; - readonly duplicateLinkDefinitions: { - readonly enabled: ValidateEnabled; - }; - }; - }; -} - - -export class ConfigurationManager extends Disposable { - - private readonly _onDidChangeConfiguration = this._register(new Emitter()); - public readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; - - private _settings?: Settings; - - constructor(connection: Connection) { - super(); - - // The settings have changed. Is send on server activation as well. - this._register(connection.onDidChangeConfiguration((change) => { - this._settings = change.settings; - this._onDidChangeConfiguration.fire(this._settings!); - })); - } - - public getSettings(): Settings | undefined { - return this._settings; - } -} diff --git a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts deleted file mode 100644 index d21a6fdbfb6..00000000000 --- a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts +++ /dev/null @@ -1,114 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Connection, FullDocumentDiagnosticReport, TextDocuments, UnchangedDocumentDiagnosticReport } from 'vscode-languageserver'; -import * as md from 'vscode-markdown-languageservice'; -import { Disposable } from 'vscode-notebook-renderer/events'; -import { URI } from 'vscode-uri'; -import { ConfigurationManager, ValidateEnabled } from '../configuration'; -import { disposeAll } from '../util/dispose'; - -const defaultDiagnosticOptions: md.DiagnosticOptions = { - validateFileLinks: md.DiagnosticLevel.ignore, - validateReferences: md.DiagnosticLevel.ignore, - validateFragmentLinks: md.DiagnosticLevel.ignore, - validateMarkdownFileLinkFragments: md.DiagnosticLevel.ignore, - validateUnusedLinkDefinitions: md.DiagnosticLevel.ignore, - validateDuplicateLinkDefinitions: md.DiagnosticLevel.ignore, - ignoreLinks: [], -}; - -function convertDiagnosticLevel(enabled: ValidateEnabled): md.DiagnosticLevel | undefined { - switch (enabled) { - case 'error': return md.DiagnosticLevel.error; - case 'warning': return md.DiagnosticLevel.warning; - case 'ignore': return md.DiagnosticLevel.ignore; - case 'hint': return md.DiagnosticLevel.hint; - default: return md.DiagnosticLevel.ignore; - } -} - -function getDiagnosticsOptions(config: ConfigurationManager): md.DiagnosticOptions { - const settings = config.getSettings(); - if (!settings) { - return defaultDiagnosticOptions; - } - - const validateFragmentLinks = convertDiagnosticLevel(settings.markdown.validate.fragmentLinks.enabled); - return { - validateFileLinks: convertDiagnosticLevel(settings.markdown.validate.fileLinks.enabled), - validateReferences: convertDiagnosticLevel(settings.markdown.validate.referenceLinks.enabled), - validateFragmentLinks: convertDiagnosticLevel(settings.markdown.validate.fragmentLinks.enabled), - validateMarkdownFileLinkFragments: settings.markdown.validate.fileLinks.markdownFragmentLinks === 'inherit' ? validateFragmentLinks : convertDiagnosticLevel(settings.markdown.validate.fileLinks.markdownFragmentLinks), - validateUnusedLinkDefinitions: convertDiagnosticLevel(settings.markdown.validate.unusedLinkDefinitions.enabled), - validateDuplicateLinkDefinitions: convertDiagnosticLevel(settings.markdown.validate.duplicateLinkDefinitions.enabled), - ignoreLinks: settings.markdown.validate.ignoredLinks, - }; -} - -export function registerValidateSupport( - connection: Connection, - workspace: md.IWorkspace, - documents: TextDocuments, - ls: md.IMdLanguageService, - config: ConfigurationManager, - logger: md.ILogger, -): Disposable { - let diagnosticOptions: md.DiagnosticOptions = defaultDiagnosticOptions; - function updateDiagnosticsSetting(): void { - diagnosticOptions = getDiagnosticsOptions(config); - } - - const subs: Disposable[] = []; - const manager = ls.createPullDiagnosticsManager(); - subs.push(manager); - - subs.push(manager.onLinkedToFileChanged(() => { - // TODO: We only need to refresh certain files - connection.languages.diagnostics.refresh(); - })); - - const emptyDiagnosticsResponse = Object.freeze({ kind: 'full', items: [] }); - - connection.languages.diagnostics.on(async (params, token): Promise => { - logger.log(md.LogLevel.Debug, 'connection.languages.diagnostics.on', { document: params.textDocument.uri }); - - if (!config.getSettings()?.markdown.validate.enabled) { - return emptyDiagnosticsResponse; - } - - const uri = URI.parse(params.textDocument.uri); - if (!workspace.hasMarkdownDocument(uri)) { - return emptyDiagnosticsResponse; - } - - const document = await workspace.openMarkdownDocument(uri); - if (!document) { - return emptyDiagnosticsResponse; - } - - const diagnostics = await manager.computeDiagnostics(document, diagnosticOptions, token); - return { - kind: 'full', - items: diagnostics, - }; - }); - - updateDiagnosticsSetting(); - subs.push(config.onDidChangeConfiguration(() => { - updateDiagnosticsSetting(); - connection.languages.diagnostics.refresh(); - })); - - subs.push(documents.onDidClose(e => { - manager.disposeDocumentResources(URI.parse(e.document.uri)); - })); - - return { - dispose: () => { - disposeAll(subs); - } - }; -} diff --git a/extensions/markdown-language-features/server/src/logging.ts b/extensions/markdown-language-features/server/src/logging.ts deleted file mode 100644 index 0df6b8e0cc5..00000000000 --- a/extensions/markdown-language-features/server/src/logging.ts +++ /dev/null @@ -1,81 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as md from 'vscode-markdown-languageservice'; -import { ConfigurationManager } from './configuration'; -import { Disposable } from './util/dispose'; - -export class LogFunctionLogger extends Disposable implements md.ILogger { - - private static now(): string { - const now = new Date(); - return String(now.getUTCHours()).padStart(2, '0') - + ':' + String(now.getMinutes()).padStart(2, '0') - + ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + String(now.getMilliseconds()).padStart(3, '0'); - } - - private static data2String(data: any): string { - if (data instanceof Error) { - if (typeof data.stack === 'string') { - return data.stack; - } - return data.message; - } - if (typeof data === 'string') { - return data; - } - return JSON.stringify(data, undefined, 2); - } - - private _logLevel: md.LogLevel; - - constructor( - private readonly _logFn: typeof console.log, - private readonly _config: ConfigurationManager, - ) { - super(); - - this._register(this._config.onDidChangeConfiguration(() => { - this._logLevel = LogFunctionLogger.readLogLevel(this._config); - })); - - this._logLevel = LogFunctionLogger.readLogLevel(this._config); - } - - private static readLogLevel(config: ConfigurationManager): md.LogLevel { - switch (config.getSettings()?.markdown.server.log) { - case 'trace': return md.LogLevel.Trace; - case 'debug': return md.LogLevel.Debug; - case 'off': - default: - return md.LogLevel.Off; - } - } - - get level(): md.LogLevel { return this._logLevel; } - - public log(level: md.LogLevel, message: string, data?: any): void { - if (this.level < level) { - return; - } - - this.appendLine(`[${this.toLevelLabel(level)} ${LogFunctionLogger.now()}] ${message}`); - if (data) { - this.appendLine(LogFunctionLogger.data2String(data)); - } - } - - private toLevelLabel(level: md.LogLevel): string { - switch (level) { - case md.LogLevel.Off: return 'Off'; - case md.LogLevel.Debug: return 'Debug'; - case md.LogLevel.Trace: return 'Trace'; - } - } - - private appendLine(value: string): void { - this._logFn(value); - } -} diff --git a/extensions/markdown-language-features/server/src/node/main.ts b/extensions/markdown-language-features/server/src/node/main.ts deleted file mode 100644 index 7945d44acf6..00000000000 --- a/extensions/markdown-language-features/server/src/node/main.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Connection, createConnection } from 'vscode-languageserver/node'; -import { startVsCodeServer } from '../server'; - -// Create a connection for the server. -const connection: Connection = createConnection(); - -console.log = connection.console.log.bind(connection.console); -console.error = connection.console.error.bind(connection.console); - -process.on('unhandledRejection', (e: any) => { - connection.console.error(`Unhandled exception ${e}`); -}); - -startVsCodeServer(connection); diff --git a/extensions/markdown-language-features/server/src/node/workerMain.ts b/extensions/markdown-language-features/server/src/node/workerMain.ts deleted file mode 100644 index f3369768012..00000000000 --- a/extensions/markdown-language-features/server/src/node/workerMain.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as l10n from '@vscode/l10n'; - -async function setupMain() { - const l10nLog: string[] = []; - - const i10lLocation = process.env['VSCODE_L10N_BUNDLE_LOCATION']; - if (i10lLocation) { - try { - await l10n.config({ uri: i10lLocation }); - l10nLog.push(`l10n: Configured to ${i10lLocation.toString()}`); - } catch (e) { - l10nLog.push(`l10n: Problems loading ${i10lLocation.toString()} : ${e}`); - } - } - await import('./main'); - - l10nLog.forEach(console.log); -} -setupMain(); diff --git a/extensions/markdown-language-features/server/src/protocol.ts b/extensions/markdown-language-features/server/src/protocol.ts deleted file mode 100644 index d06edbd4303..00000000000 --- a/extensions/markdown-language-features/server/src/protocol.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { FileRename, RequestType } from 'vscode-languageserver'; -import type * as lsp from 'vscode-languageserver-types'; -import type * as md from 'vscode-markdown-languageservice'; - -//#region From server -export const parse = new RequestType<{ uri: string; text?: string }, md.Token[], any>('markdown/parse'); - -export const fs_readFile = new RequestType<{ uri: string }, number[], any>('markdown/fs/readFile'); -export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory'); -export const fs_stat = new RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any>('markdown/fs/stat'); - -export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions; watchParentDirs: boolean }, void, any>('markdown/fs/watcher/create'); -export const fs_watcher_delete = new RequestType<{ id: number }, void, any>('markdown/fs/watcher/delete'); - -export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('markdown/findMarkdownFilesInWorkspace'); -//#endregion - -//#region To server -export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace'); -export const getEditForFileRenames = new RequestType('markdown/getEditForFileRenames'); - -export const prepareUpdatePastedLinks = new RequestType<{ uri: string; ranges: lsp.Range[] }, string, any>('markdown/prepareUpdatePastedLinks'); -export const getUpdatePastedLinksEdit = new RequestType<{ pasteIntoDoc: string; metadata: string; edits: lsp.TextEdit[] }, lsp.TextEdit[] | undefined, any>('markdown/getUpdatePastedLinksEdit'); - -export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange'); - -export const resolveLinkTarget = new RequestType<{ linkText: string; uri: string }, md.ResolvedDocumentLinkTarget, any>('markdown/resolveLinkTarget'); -//#endregion diff --git a/extensions/markdown-language-features/server/src/server.ts b/extensions/markdown-language-features/server/src/server.ts deleted file mode 100644 index f1df5494f3b..00000000000 --- a/extensions/markdown-language-features/server/src/server.ts +++ /dev/null @@ -1,399 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as l10n from '@vscode/l10n'; -import { CancellationToken, CompletionRegistrationOptions, CompletionRequest, Connection, Disposable, DocumentHighlightRegistrationOptions, DocumentHighlightRequest, InitializeParams, InitializeResult, NotebookDocuments, ResponseError, TextDocuments } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import * as lsp from 'vscode-languageserver-types'; -import * as md from 'vscode-markdown-languageservice'; -import { URI } from 'vscode-uri'; -import { LsConfiguration, getLsConfiguration } from './config'; -import { ConfigurationManager, Settings } from './configuration'; -import { registerValidateSupport } from './languageFeatures/diagnostics'; -import { LogFunctionLogger } from './logging'; -import * as protocol from './protocol'; -import { IDisposable } from './util/dispose'; -import { VsCodeClientWorkspace } from './workspace'; - -interface MdServerInitializationOptions extends LsConfiguration { } - -const organizeLinkDefKind = 'source.organizeLinkDefinitions'; - -export async function startVsCodeServer(connection: Connection) { - const configurationManager = new ConfigurationManager(connection); - const logger = new LogFunctionLogger(connection.console.log.bind(connection.console), configurationManager); - - const parser = new class implements md.IMdParser { - slugifier = md.githubSlugifier; - - tokenize(document: md.ITextDocument): Promise { - return connection.sendRequest(protocol.parse, { - uri: document.uri, - - // Clients won't be able to read temp documents. - // Send along the full text for parsing. - text: document.version < 0 ? document.getText() : undefined - }); - } - }; - - const documents = new TextDocuments(TextDocument); - const notebooks = new NotebookDocuments(documents); - - const workspaceFactory: WorkspaceFactory = ({ connection, config, workspaceFolders }) => { - const workspace = new VsCodeClientWorkspace(connection, config, documents, notebooks, logger); - workspace.workspaceFolders = (workspaceFolders ?? []).map(x => URI.parse(x.uri)); - return workspace; - }; - - return startServer(connection, { documents, notebooks, configurationManager, logger, parser, workspaceFactory }); -} - -type WorkspaceFactory = (config: { - connection: Connection; - config: LsConfiguration; - workspaceFolders?: lsp.WorkspaceFolder[] | null; -}) => md.IWorkspace; - -export async function startServer(connection: Connection, serverConfig: { - documents: TextDocuments; - notebooks?: NotebookDocuments; - configurationManager: ConfigurationManager; - logger: md.ILogger; - parser: md.IMdParser; - workspaceFactory: WorkspaceFactory; -}) { - const { documents, notebooks } = serverConfig; - - let mdLs: md.IMdLanguageService | undefined; - - connection.onInitialize((params: InitializeParams): InitializeResult => { - const initOptions = params.initializationOptions as MdServerInitializationOptions | undefined; - - const mdConfig = getLsConfiguration(initOptions ?? {}); - - const workspace = serverConfig.workspaceFactory({ connection, config: mdConfig, workspaceFolders: params.workspaceFolders }); - mdLs = md.createLanguageService({ - workspace, - parser: serverConfig.parser, - logger: serverConfig.logger, - ...mdConfig, - get preferredMdPathExtensionStyle() { - switch (serverConfig.configurationManager.getSettings()?.markdown.preferredMdPathExtensionStyle) { - case 'includeExtension': return md.PreferredMdPathExtensionStyle.includeExtension; - case 'removeExtension': return md.PreferredMdPathExtensionStyle.removeExtension; - case 'auto': - default: - return md.PreferredMdPathExtensionStyle.auto; - } - } - }); - - registerCompletionsSupport(connection, documents, mdLs, serverConfig.configurationManager); - registerDocumentHighlightSupport(connection, documents, mdLs, serverConfig.configurationManager); - registerValidateSupport(connection, workspace, documents, mdLs, serverConfig.configurationManager, serverConfig.logger); - - return { - capabilities: { - diagnosticProvider: { - documentSelector: null, - identifier: 'markdown', - interFileDependencies: true, - workspaceDiagnostics: false, - }, - codeActionProvider: { - resolveProvider: true, - codeActionKinds: [ - organizeLinkDefKind, - 'quickfix', - 'refactor', - ] - }, - definitionProvider: true, - documentLinkProvider: { resolveProvider: true }, - documentSymbolProvider: true, - foldingRangeProvider: true, - hoverProvider: true, - referencesProvider: true, - renameProvider: { prepareProvider: true, }, - selectionRangeProvider: true, - workspaceSymbolProvider: true, - workspace: { - workspaceFolders: { - supported: true, - changeNotifications: true, - }, - } - } - }; - }); - - connection.onDocumentLinks(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return []; - } - return mdLs!.getDocumentLinks(document, token); - }); - - connection.onDocumentLinkResolve(async (link, token): Promise => { - return mdLs!.resolveDocumentLink(link, token); - }); - - connection.onDocumentSymbol(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return []; - } - return mdLs!.getDocumentSymbols(document, { includeLinkDefinitions: true }, token); - }); - - connection.onFoldingRanges(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return []; - } - return mdLs!.getFoldingRanges(document, token); - }); - - connection.onSelectionRanges(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return []; - } - return mdLs!.getSelectionRanges(document, params.positions, token); - }); - - connection.onWorkspaceSymbol(async (params, token): Promise => { - return mdLs!.getWorkspaceSymbols(params.query, token); - }); - - connection.onReferences(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return []; - } - return mdLs!.getReferences(document, params.position, params.context, token); - }); - - connection.onDefinition(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return undefined; - } - return mdLs!.getDefinition(document, params.position, token); - }); - - connection.onPrepareRename(async (params, token) => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return undefined; - } - - try { - return await mdLs!.prepareRename(document, params.position, token); - } catch (e) { - if (e instanceof md.RenameNotSupportedAtLocationError) { - throw new ResponseError(0, e.message); - } else { - throw e; - } - } - }); - - connection.onRenameRequest(async (params, token) => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return undefined; - } - return mdLs!.getRenameEdit(document, params.position, params.newName, token); - }); - - interface OrganizeLinkActionData { - readonly uri: string; - } - - connection.onCodeAction(async (params, token) => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return undefined; - } - - if (params.context.only?.some(kind => kind === 'source' || kind.startsWith('source.'))) { - const action: lsp.CodeAction = { - title: l10n.t("Organize link definitions"), - kind: organizeLinkDefKind, - data: { uri: document.uri } satisfies OrganizeLinkActionData, - }; - return [action]; - } - - return mdLs!.getCodeActions(document, params.range, params.context, token); - }); - - connection.onCodeActionResolve(async (codeAction, token) => { - if (codeAction.kind === organizeLinkDefKind) { - const data = codeAction.data as OrganizeLinkActionData; - const document = documents.get(data.uri); - if (!document) { - return codeAction; - } - - const edits = (await mdLs?.organizeLinkDefinitions(document, { removeUnused: true }, token)) || []; - codeAction.edit = { - changes: { - [data.uri]: edits - } - }; - return codeAction; - } - - return codeAction; - }); - - connection.onHover(async (params, token) => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return null; - } - - return mdLs!.getHover(document, params.position, token); - }); - - connection.onRequest(protocol.getReferencesToFileInWorkspace, (async (params: { uri: string }, token: CancellationToken) => { - return mdLs!.getFileReferences(URI.parse(params.uri), token); - })); - - connection.onRequest(protocol.getEditForFileRenames, (async (params, token: CancellationToken) => { - const result = await mdLs!.getRenameFilesInWorkspaceEdit(params.map(x => ({ oldUri: URI.parse(x.oldUri), newUri: URI.parse(x.newUri) })), token); - if (!result) { - return result; - } - - return { - edit: result.edit, - participatingRenames: result.participatingRenames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })) - }; - })); - - connection.onRequest(protocol.prepareUpdatePastedLinks, (async (params, token: CancellationToken) => { - const document = documents.get(params.uri); - if (!document) { - return undefined; - } - - return mdLs!.prepareUpdatePastedLinks(document, params.ranges, token); - })); - - connection.onRequest(protocol.getUpdatePastedLinksEdit, (async (params, token: CancellationToken) => { - const document = documents.get(params.pasteIntoDoc); - if (!document) { - return undefined; - } - - // TODO: Figure out why range types are lying - const edits = params.edits.map((edit: any) => lsp.TextEdit.replace(lsp.Range.create(edit.range[0].line, edit.range[0].character, edit.range[1].line, edit.range[1].character), edit.newText)); - return mdLs!.getUpdatePastedLinksEdit(document, edits, params.metadata, token); - })); - - connection.onRequest(protocol.resolveLinkTarget, (async (params, token: CancellationToken) => { - return mdLs!.resolveLinkTarget(params.linkText, URI.parse(params.uri), token); - })); - - documents.listen(connection); - notebooks?.listen(connection); - connection.listen(); -} - -function registerDynamicClientFeature( - config: ConfigurationManager, - isEnabled: (settings: Settings | undefined) => boolean, - register: () => Promise, -) { - let registration: Promise | undefined; - function update() { - const settings = config.getSettings(); - if (isEnabled(settings)) { - if (!registration) { - registration = register(); - } - } else { - registration?.then(x => x.dispose()); - registration = undefined; - } - } - - update(); - return config.onDidChangeConfiguration(() => update()); -} - -function registerCompletionsSupport( - connection: Connection, - documents: TextDocuments, - ls: md.IMdLanguageService, - config: ConfigurationManager, -): IDisposable { - function getIncludeWorkspaceHeaderCompletions(): md.IncludeWorkspaceHeaderCompletions { - switch (config.getSettings()?.markdown.suggest.paths.includeWorkspaceHeaderCompletions) { - case 'onSingleOrDoubleHash': return md.IncludeWorkspaceHeaderCompletions.onSingleOrDoubleHash; - case 'onDoubleHash': return md.IncludeWorkspaceHeaderCompletions.onDoubleHash; - case 'never': - default: return md.IncludeWorkspaceHeaderCompletions.never; - } - } - - connection.onCompletion(async (params, token): Promise => { - const settings = config.getSettings(); - if (!settings?.markdown.suggest.paths.enabled) { - return []; - } - - const document = documents.get(params.textDocument.uri); - if (document) { - // TODO: remove any type after picking up new release with correct types - return ls.getCompletionItems(document, params.position, { - ...(params.context || {}), - includeWorkspaceHeaderCompletions: getIncludeWorkspaceHeaderCompletions(), - } as any, token); - } - return []; - }); - - return registerDynamicClientFeature(config, (settings) => !!settings?.markdown.suggest.paths.enabled, () => { - const registrationOptions: CompletionRegistrationOptions = { - documentSelector: null, - triggerCharacters: ['.', '/', '#'], - }; - return connection.client.register(CompletionRequest.type, registrationOptions); - }); -} - -function registerDocumentHighlightSupport( - connection: Connection, - documents: TextDocuments, - mdLs: md.IMdLanguageService, - configurationManager: ConfigurationManager -) { - connection.onDocumentHighlight(async (params, token) => { - const settings = configurationManager.getSettings(); - if (!settings?.markdown.occurrencesHighlight.enabled) { - return undefined; - } - - const document = documents.get(params.textDocument.uri); - if (!document) { - return undefined; - } - - return mdLs!.getDocumentHighlights(document, params.position, token); - }); - - return registerDynamicClientFeature(configurationManager, (settings) => !!settings?.markdown.occurrencesHighlight.enabled, () => { - const registrationOptions: DocumentHighlightRegistrationOptions = { - documentSelector: null, - }; - return connection.client.register(DocumentHighlightRequest.type, registrationOptions); - }); -} diff --git a/extensions/markdown-language-features/server/src/util/dispose.ts b/extensions/markdown-language-features/server/src/util/dispose.ts deleted file mode 100644 index 2e9d8dc6a14..00000000000 --- a/extensions/markdown-language-features/server/src/util/dispose.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export function disposeAll(disposables: Iterable) { - const errors: any[] = []; - - for (const disposable of disposables) { - try { - disposable.dispose(); - } catch (e) { - errors.push(e); - } - } - - if (errors.length === 1) { - throw errors[0]; - } else if (errors.length > 1) { - throw new AggregateError(errors, 'Encountered errors while disposing of store'); - } -} - -export interface IDisposable { - dispose(): void; -} - -export abstract class Disposable { - private _isDisposed = false; - - protected _disposables: IDisposable[] = []; - - public dispose(): any { - if (this._isDisposed) { - return; - } - this._isDisposed = true; - disposeAll(this._disposables); - } - - protected _register(value: T): T { - if (this._isDisposed) { - value.dispose(); - } else { - this._disposables.push(value); - } - return value; - } - - protected get isDisposed() { - return this._isDisposed; - } -} - diff --git a/extensions/markdown-language-features/server/src/util/file.ts b/extensions/markdown-language-features/server/src/util/file.ts deleted file mode 100644 index 10e95bf5dcf..00000000000 --- a/extensions/markdown-language-features/server/src/util/file.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { URI, Utils } from 'vscode-uri'; -import { LsConfiguration } from '../config'; - -export function looksLikeMarkdownPath(config: LsConfiguration, resolvedHrefPath: URI) { - return config.markdownFileExtensions.includes(Utils.extname(resolvedHrefPath).toLowerCase().replace('.', '')); -} - -export function isMarkdownFile(document: TextDocument) { - return document.languageId === 'markdown'; -} diff --git a/extensions/markdown-language-features/server/src/util/limiter.ts b/extensions/markdown-language-features/server/src/util/limiter.ts deleted file mode 100644 index bd4153cd08b..00000000000 --- a/extensions/markdown-language-features/server/src/util/limiter.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -interface ILimitedTaskFactory { - factory: ITask>; - c: (value: T | Promise) => void; - e: (error?: unknown) => void; -} - -interface ITask { - (): T; -} - -/** - * A helper to queue N promises and run them all with a max degree of parallelism. The helper - * ensures that at any time no more than M promises are running at the same time. - * - * Taken from 'src/vs/base/common/async.ts' - */ -export class Limiter { - - private _size = 0; - private runningPromises: number; - private readonly maxDegreeOfParalellism: number; - private readonly outstandingPromises: ILimitedTaskFactory[]; - - constructor(maxDegreeOfParalellism: number) { - this.maxDegreeOfParalellism = maxDegreeOfParalellism; - this.outstandingPromises = []; - this.runningPromises = 0; - } - - get size(): number { - return this._size; - } - - queue(factory: ITask>): Promise { - this._size++; - - return new Promise((c, e) => { - this.outstandingPromises.push({ factory, c, e }); - this.consume(); - }); - } - - private consume(): void { - while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) { - const iLimitedTask = this.outstandingPromises.shift()!; - this.runningPromises++; - - const promise = iLimitedTask.factory(); - promise.then(iLimitedTask.c, iLimitedTask.e); - promise.then(() => this.consumed(), () => this.consumed()); - } - } - - private consumed(): void { - this._size--; - this.runningPromises--; - - if (this.outstandingPromises.length > 0) { - this.consume(); - } - } -} diff --git a/extensions/markdown-language-features/server/src/util/resourceMap.ts b/extensions/markdown-language-features/server/src/util/resourceMap.ts deleted file mode 100644 index 7cec9d661d3..00000000000 --- a/extensions/markdown-language-features/server/src/util/resourceMap.ts +++ /dev/null @@ -1,69 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from 'vscode-uri'; - - -type ResourceToKey = (uri: URI) => string; - -const defaultResourceToKey = (resource: URI): string => resource.toString(); - -export class ResourceMap { - - private readonly map = new Map(); - - private readonly toKey: ResourceToKey; - - constructor(toKey: ResourceToKey = defaultResourceToKey) { - this.toKey = toKey; - } - - public set(uri: URI, value: T): this { - this.map.set(this.toKey(uri), { uri, value }); - return this; - } - - public get(resource: URI): T | undefined { - return this.map.get(this.toKey(resource))?.value; - } - - public has(resource: URI): boolean { - return this.map.has(this.toKey(resource)); - } - - public get size(): number { - return this.map.size; - } - - public clear(): void { - this.map.clear(); - } - - public delete(resource: URI): boolean { - return this.map.delete(this.toKey(resource)); - } - - public *values(): IterableIterator { - for (const entry of this.map.values()) { - yield entry.value; - } - } - - public *keys(): IterableIterator { - for (const entry of this.map.values()) { - yield entry.uri; - } - } - - public *entries(): IterableIterator<[URI, T]> { - for (const entry of this.map.values()) { - yield [entry.uri, entry.value]; - } - } - - public [Symbol.iterator](): IterableIterator<[URI, T]> { - return this.entries(); - } -} diff --git a/extensions/markdown-language-features/server/src/util/schemes.ts b/extensions/markdown-language-features/server/src/util/schemes.ts deleted file mode 100644 index 67b75e0a0d6..00000000000 --- a/extensions/markdown-language-features/server/src/util/schemes.ts +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export const Schemes = Object.freeze({ - notebookCell: 'vscode-notebook-cell', -}); diff --git a/extensions/markdown-language-features/server/src/workspace.ts b/extensions/markdown-language-features/server/src/workspace.ts deleted file mode 100644 index 13e5c6b4472..00000000000 --- a/extensions/markdown-language-features/server/src/workspace.ts +++ /dev/null @@ -1,433 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Connection, Emitter, FileChangeType, NotebookDocuments, Position, Range, TextDocuments } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import * as md from 'vscode-markdown-languageservice'; -import { URI } from 'vscode-uri'; -import { LsConfiguration } from './config'; -import * as protocol from './protocol'; -import { isMarkdownFile, looksLikeMarkdownPath } from './util/file'; -import { Limiter } from './util/limiter'; -import { ResourceMap } from './util/resourceMap'; -import { Schemes } from './util/schemes'; - -declare const TextDecoder: any; - -class VsCodeDocument implements md.ITextDocument { - - private inMemoryDoc?: TextDocument; - private onDiskDoc?: TextDocument; - - readonly uri: string; - - constructor(uri: string, init: { inMemoryDoc: TextDocument }); - constructor(uri: string, init: { onDiskDoc: TextDocument }); - constructor(uri: string, init: { inMemoryDoc?: TextDocument; onDiskDoc?: TextDocument }) { - this.uri = uri; - this.inMemoryDoc = init?.inMemoryDoc; - this.onDiskDoc = init?.onDiskDoc; - } - - get version(): number { - return this.inMemoryDoc?.version ?? this.onDiskDoc?.version ?? 0; - } - - get lineCount(): number { - return this.inMemoryDoc?.lineCount ?? this.onDiskDoc?.lineCount ?? 0; - } - - getText(range?: Range): string { - if (this.inMemoryDoc) { - return this.inMemoryDoc.getText(range); - } - - if (this.onDiskDoc) { - return this.onDiskDoc.getText(range); - } - - throw new Error('Document has been closed'); - } - - positionAt(offset: number): Position { - if (this.inMemoryDoc) { - return this.inMemoryDoc.positionAt(offset); - } - - if (this.onDiskDoc) { - return this.onDiskDoc.positionAt(offset); - } - - throw new Error('Document has been closed'); - } - - offsetAt(position: Position): number { - if (this.inMemoryDoc) { - return this.inMemoryDoc.offsetAt(position); - } - - if (this.onDiskDoc) { - return this.onDiskDoc.offsetAt(position); - } - - throw new Error('Document has been closed'); - } - - hasInMemoryDoc(): boolean { - return !!this.inMemoryDoc; - } - - isDetached(): boolean { - return !this.onDiskDoc && !this.inMemoryDoc; - } - - setInMemoryDoc(doc: TextDocument | undefined) { - this.inMemoryDoc = doc; - } - - setOnDiskDoc(doc: TextDocument | undefined) { - this.onDiskDoc = doc; - } -} - -export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { - - private readonly _onDidCreateMarkdownDocument = new Emitter(); - public readonly onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocument.event; - - private readonly _onDidChangeMarkdownDocument = new Emitter(); - public readonly onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocument.event; - - private readonly _onDidDeleteMarkdownDocument = new Emitter(); - public readonly onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocument.event; - - private readonly _documentCache = new ResourceMap(); - - private readonly _utf8Decoder = new TextDecoder('utf-8'); - - private _watcherPool = 0; - private readonly _watchers = new Map; - readonly onDidCreate: Emitter; - readonly onDidDelete: Emitter; - }>(); - - constructor( - private readonly connection: Connection, - private readonly config: LsConfiguration, - private readonly documents: TextDocuments, - private readonly notebooks: NotebookDocuments, - private readonly logger: md.ILogger, - ) { - documents.onDidOpen(e => { - if (!this.isRelevantMarkdownDocument(e.document)) { - return; - } - - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidOpen', { document: e.document.uri }); - - const uri = URI.parse(e.document.uri); - const doc = this._documentCache.get(uri); - - if (doc) { - // File already existed on disk - doc.setInMemoryDoc(e.document); - - // The content visible to the language service may have changed since the in-memory doc - // may differ from the one on-disk. To be safe we always fire a change event. - this._onDidChangeMarkdownDocument.fire(doc); - } else { - // We're creating the file for the first time - const doc = new VsCodeDocument(e.document.uri, { inMemoryDoc: e.document }); - this._documentCache.set(uri, doc); - this._onDidCreateMarkdownDocument.fire(doc); - } - }); - - documents.onDidChangeContent(e => { - if (!this.isRelevantMarkdownDocument(e.document)) { - return; - } - - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidChanceContent', { document: e.document.uri }); - - const uri = URI.parse(e.document.uri); - const entry = this._documentCache.get(uri); - if (entry) { - entry.setInMemoryDoc(e.document); - this._onDidChangeMarkdownDocument.fire(entry); - } - }); - - documents.onDidClose(async e => { - if (!this.isRelevantMarkdownDocument(e.document)) { - return; - } - - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidClose', { document: e.document.uri }); - - const uri = URI.parse(e.document.uri); - const doc = this._documentCache.get(uri); - if (!doc) { - // Document was never opened - return; - } - - doc.setInMemoryDoc(undefined); - if (doc.isDetached()) { - // The document has been fully closed - this.doDeleteDocument(uri); - return; - } - - // Check that if file has been deleted on disk. - // This can happen when directories are renamed / moved. VS Code's file system watcher does not - // notify us when this happens. - if (!(await this.statBypassingCache(uri))) { - if (this._documentCache.get(uri) === doc && !doc.hasInMemoryDoc()) { - this.doDeleteDocument(uri); - return; - } - } - - // The document still exists on disk - // To be safe, tell the service that the document has changed because the - // in-memory doc contents may be different than the disk doc contents. - this._onDidChangeMarkdownDocument.fire(doc); - }); - - connection.onDidChangeWatchedFiles(async ({ changes }) => { - for (const change of changes) { - const resource = URI.parse(change.uri); - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.onDidChangeWatchedFiles', { type: change.type, resource: resource.toString() }); - switch (change.type) { - case FileChangeType.Changed: { - const entry = this._documentCache.get(resource); - if (entry) { - // Refresh the on-disk state - const document = await this.openMarkdownDocumentFromFs(resource); - if (document) { - this._onDidChangeMarkdownDocument.fire(document); - } - } - break; - } - case FileChangeType.Created: { - const entry = this._documentCache.get(resource); - if (entry) { - // Create or update the on-disk state - const document = await this.openMarkdownDocumentFromFs(resource); - if (document) { - this._onDidCreateMarkdownDocument.fire(document); - } - } - break; - } - case FileChangeType.Deleted: { - const entry = this._documentCache.get(resource); - if (entry) { - entry.setOnDiskDoc(undefined); - if (entry.isDetached()) { - this.doDeleteDocument(resource); - } - } - break; - } - } - } - }); - - connection.onRequest(protocol.fs_watcher_onChange, params => { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.fs_watcher_onChange', { kind: params.kind, uri: params.uri }); - - const watcher = this._watchers.get(params.id); - if (!watcher) { - return; - } - - switch (params.kind) { - case 'create': watcher.onDidCreate.fire(URI.parse(params.uri)); return; - case 'change': watcher.onDidChange.fire(URI.parse(params.uri)); return; - case 'delete': watcher.onDidDelete.fire(URI.parse(params.uri)); return; - } - }); - } - - public listen() { - this.connection.workspace.onDidChangeWorkspaceFolders(async () => { - this.workspaceFolders = (await this.connection.workspace.getWorkspaceFolders() ?? []).map(x => URI.parse(x.uri)); - }); - } - - private _workspaceFolders: readonly URI[] = []; - - get workspaceFolders(): readonly URI[] { - return this._workspaceFolders; - } - - set workspaceFolders(value: readonly URI[]) { - this._workspaceFolders = value; - } - - async getAllMarkdownDocuments(): Promise> { - // Add opened files (such as untitled files) - const openTextDocumentResults = this.documents.all() - .filter(doc => this.isRelevantMarkdownDocument(doc)); - - const allDocs = new ResourceMap(); - for (const doc of openTextDocumentResults) { - allDocs.set(URI.parse(doc.uri), doc); - } - - // And then add files on disk - const maxConcurrent = 20; - const limiter = new Limiter(maxConcurrent); - const resources = await this.connection.sendRequest(protocol.findMarkdownFilesInWorkspace, {}); - await Promise.all(resources.map(strResource => { - return limiter.queue(async () => { - const resource = URI.parse(strResource); - if (allDocs.has(resource)) { - return; - } - - const doc = await this.openMarkdownDocument(resource); - if (doc) { - allDocs.set(resource, doc); - } - return doc; - }); - })); - - return allDocs.values(); - } - - hasMarkdownDocument(resource: URI): boolean { - return !!this.documents.get(resource.toString()); - } - - async openMarkdownDocument(resource: URI): Promise { - const existing = this._documentCache.get(resource); - if (existing) { - return existing; - } - - const matchingDocument = this.documents.get(resource.toString()); - if (matchingDocument) { - let entry = this._documentCache.get(resource); - if (entry) { - entry.setInMemoryDoc(matchingDocument); - } else { - entry = new VsCodeDocument(resource.toString(), { inMemoryDoc: matchingDocument }); - this._documentCache.set(resource, entry); - } - - return entry; - } - - return this.openMarkdownDocumentFromFs(resource); - } - - private async openMarkdownDocumentFromFs(resource: URI): Promise { - if (!looksLikeMarkdownPath(this.config, resource)) { - return undefined; - } - - try { - const response = await this.connection.sendRequest(protocol.fs_readFile, { uri: resource.toString() }); - // TODO: LSP doesn't seem to handle Array buffers well - const bytes = new Uint8Array(response); - - // We assume that markdown is in UTF-8 - const text = this._utf8Decoder.decode(bytes); - const doc = new VsCodeDocument(resource.toString(), { - onDiskDoc: TextDocument.create(resource.toString(), 'markdown', 0, text) - }); - this._documentCache.set(resource, doc); - return doc; - } catch (e) { - return undefined; - } - } - - async stat(resource: URI): Promise { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.stat', { resource: resource.toString() }); - if (this._documentCache.has(resource)) { - return { isDirectory: false }; - } - return this.statBypassingCache(resource); - } - - private async statBypassingCache(resource: URI): Promise { - const uri = resource.toString(); - if (this.documents.get(uri)) { - return { isDirectory: false }; - } - const fsResult = await this.connection.sendRequest(protocol.fs_stat, { uri }); - return fsResult ?? undefined; // Force convert null to undefined - } - - async readDirectory(resource: URI): Promise<[string, md.FileStat][]> { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.readDir', { resource: resource.toString() }); - return this.connection.sendRequest(protocol.fs_readDirectory, { uri: resource.toString() }); - } - - getContainingDocument(resource: URI): md.ContainingDocumentContext | undefined { - if (resource.scheme === Schemes.notebookCell) { - const nb = this.notebooks.findNotebookDocumentForCell(resource.toString()); - if (nb) { - return { - uri: URI.parse(nb.uri), - children: nb.cells.map(cell => ({ uri: URI.parse(cell.document) })), - }; - } - } - return undefined; - } - - watchFile(resource: URI, options: md.FileWatcherOptions): md.IFileSystemWatcher { - const id = this._watcherPool++; - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.watchFile', { id, resource: resource.toString() }); - - const entry = { - resource, - options, - onDidCreate: new Emitter(), - onDidChange: new Emitter(), - onDidDelete: new Emitter(), - }; - this._watchers.set(id, entry); - - this.connection.sendRequest(protocol.fs_watcher_create, { - id, - uri: resource.toString(), - options, - watchParentDirs: true, - }); - - return { - onDidCreate: entry.onDidCreate.event, - onDidChange: entry.onDidChange.event, - onDidDelete: entry.onDidDelete.event, - dispose: () => { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.disposeWatcher', { id, resource: resource.toString() }); - this.connection.sendRequest(protocol.fs_watcher_delete, { id }); - this._watchers.delete(id); - } - }; - } - - private isRelevantMarkdownDocument(doc: TextDocument) { - return isMarkdownFile(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview'; - } - - private doDeleteDocument(uri: URI) { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.deleteDocument', { document: uri.toString() }); - - this._documentCache.delete(uri); - this._onDidDeleteMarkdownDocument.fire(uri); - } -} diff --git a/extensions/markdown-language-features/server/tsconfig.json b/extensions/markdown-language-features/server/tsconfig.json deleted file mode 100644 index 0a73af08ed8..00000000000 --- a/extensions/markdown-language-features/server/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./out", - "lib": [ - "ES2020", - "ES2021.Promise", - "WebWorker" - ] - }, - "include": [ - "src/**/*" - ] -} diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock deleted file mode 100644 index 148783435bd..00000000000 --- a/extensions/markdown-language-features/server/yarn.lock +++ /dev/null @@ -1,176 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/node@20.x": - version "20.11.24" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" - integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== - dependencies: - undici-types "~5.26.4" - -"@vscode/l10n@^0.0.10": - version "0.0.10" - resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.10.tgz#9c513107c690c0dd16e3ec61e453743de15ebdb0" - integrity sha512-E1OCmDcDWa0Ya7vtSjp/XfHFGqYJfh+YPC1RkATU71fTac+j1JjCcB3qwSzmlKAighx2WxhLlfhS0RwAN++PFQ== - -"@vscode/l10n@^0.0.11": - version "0.0.11" - resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.11.tgz#325d7beb2cfb87162bc624d16c4d546de6a73b72" - integrity sha512-ukOMWnCg1tCvT7WnDfsUKQOFDQGsyR5tNgRpwmqi+5/vzU3ghdDXzvIM4IOPdSb3OeSsBNvmSL8nxIVOqi2WXA== - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== - dependencies: - boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" - nth-check "^2.0.1" - -css-what@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" - -domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - dependencies: - domelementtype "^2.3.0" - -domutils@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - -entities@^4.2.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - -he@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -node-html-parser@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.5.tgz#c819dceb13a10a7642ff92f94f870b4f77968097" - integrity sha512-fAaM511feX++/Chnhe475a0NHD8M7AxDInsqQpz6x63GRF7xYNdS8Vo5dKsIVPgsOvG7eioRRTZQnWBrhDHBSg== - dependencies: - css-select "^5.1.0" - he "1.2.0" - -nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== - dependencies: - boolbase "^1.0.0" - -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -vscode-jsonrpc@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94" - integrity sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw== - -vscode-jsonrpc@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" - integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== - -vscode-languageserver-protocol@3.17.3: - version "3.17.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz#6d0d54da093f0c0ee3060b81612cce0f11060d57" - integrity sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA== - dependencies: - vscode-jsonrpc "8.1.0" - vscode-languageserver-types "3.17.3" - -vscode-languageserver-protocol@^3.17.1: - version "3.17.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" - integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== - dependencies: - vscode-jsonrpc "8.2.0" - vscode-languageserver-types "3.17.5" - -vscode-languageserver-textdocument@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" - integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== - -vscode-languageserver-textdocument@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0" - integrity sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q== - -vscode-languageserver-types@3.17.3, vscode-languageserver-types@^3.17.3: - version "3.17.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64" - integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA== - -vscode-languageserver-types@3.17.5: - version "3.17.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" - integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== - -vscode-languageserver@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz#5024253718915d84576ce6662dd46a791498d827" - integrity sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw== - dependencies: - vscode-languageserver-protocol "3.17.3" - -vscode-markdown-languageservice@^0.5.0-alpha.6: - version "0.5.0-alpha.6" - resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0-alpha.6.tgz#3aa5fc94fea3d5d7f0cd970e64348e2791643dc0" - integrity sha512-mA1JCA7aHHSek5gr8Yv7C3esEPo2hRrgxmoZUDRro+pnwbdsJuRaWOKWtCWxejRUVVVhc/5yTK2X64Jx9OCmFQ== - dependencies: - "@vscode/l10n" "^0.0.10" - node-html-parser "^6.1.5" - picomatch "^2.3.1" - vscode-languageserver-protocol "^3.17.1" - vscode-languageserver-textdocument "^1.0.11" - vscode-uri "^3.0.7" - -vscode-uri@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8" - integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA== diff --git a/extensions/markdown-language-features/src/extension.browser.ts b/extensions/markdown-language-features/src/extension.browser.ts index 30639672490..2bfc63fc857 100644 --- a/extensions/markdown-language-features/src/extension.browser.ts +++ b/extensions/markdown-language-features/src/extension.browser.ts @@ -27,7 +27,7 @@ export async function activate(context: vscode.ExtensionContext) { } function startServer(context: vscode.ExtensionContext, parser: IMdParser): Promise { - const serverMain = vscode.Uri.joinPath(context.extensionUri, 'server/dist/browser/workerMain.js'); + const serverMain = vscode.Uri.joinPath(context.extensionUri, 'dist', 'browser', 'serverWorkerMain.js'); const worker = new Worker(serverMain.toString()); worker.postMessage({ i10lLocation: vscode.l10n.uri?.toString() ?? '' }); diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index b14ab6d0e7e..98ea87df069 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -27,10 +27,15 @@ export async function activate(context: vscode.ExtensionContext) { } function startServer(context: vscode.ExtensionContext, parser: IMdParser): Promise { - const clientMain = vscode.extensions.getExtension('vscode.markdown-language-features')?.packageJSON?.main || ''; + const isDebugBuild = context.extension.packageJSON.main.includes('/out/'); - const serverMain = `./server/${clientMain.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/workerMain`; - const serverModule = context.asAbsolutePath(serverMain); + const serverModule = context.asAbsolutePath( + isDebugBuild + // For local non bundled version of vscode-markdown-languageserver + // ? './node_modules/vscode-markdown-languageserver/out/node/workerMain' + ? './node_modules/vscode-markdown-languageserver/dist/node/workerMain' + : './dist/serverWorkerMain' + ); // The debug options for the server const debugOptions = { execArgv: ['--nolazy', '--inspect=' + (7000 + Math.round(Math.random() * 999))] }; diff --git a/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts b/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts index 36b6eacfcfd..c8ad4c722fd 100644 --- a/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts +++ b/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts @@ -9,7 +9,7 @@ import { Mime } from '../util/mimes'; class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider { - public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('text', 'markdown', 'updateLinks'); + public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'updateLinks'); public static readonly metadataMime = 'vnd.vscode.markdown.updateLinksMetadata'; @@ -26,6 +26,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider if (token.isCancellationRequested) { return; } + dataTransfer.set(UpdatePastedLinksEditProvider.metadataMime, new vscode.DataTransferItem(metadata)); } @@ -33,7 +34,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, - _context: vscode.DocumentPasteEditContext, + context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, ): Promise { if (!this._isEnabled(document)) { @@ -56,7 +57,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider // - Copy with multiple cursors and paste into multiple locations // - ... const edits = await this._client.getUpdatePastedLinksEdit(document.uri, ranges.map(x => new vscode.TextEdit(x, text)), metadata, token); - if (!edits || !edits.length || token.isCancellationRequested) { + if (!edits?.length || token.isCancellationRequested) { return; } @@ -64,11 +65,16 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider const workspaceEdit = new vscode.WorkspaceEdit(); workspaceEdit.set(document.uri, edits.map(x => new vscode.TextEdit(new vscode.Range(x.range.start.line, x.range.start.character, x.range.end.line, x.range.end.character,), x.newText))); pasteEdit.additionalEdit = workspaceEdit; + + if (!context.only || !UpdatePastedLinksEditProvider.kind.contains(context.only)) { + pasteEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Empty.append('text')]; + } + return [pasteEdit]; } private _isEnabled(document: vscode.TextDocument): boolean { - return vscode.workspace.getConfiguration('markdown', document.uri).get('experimental.updateLinksOnPaste', false); + return vscode.workspace.getConfiguration('markdown', document.uri).get('editor.updateLinksOnPaste.enabled', true); } } diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 103cbc191e4..5f6e746a82d 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -313,7 +313,7 @@ export class MarkdownItEngine implements IMdParser { private _addNamedHeaders(md: MarkdownIt): void { const original = md.renderer.rules.heading_open; md.renderer.rules.heading_open = (tokens: Token[], idx: number, options, env, self) => { - const title = tokens[idx + 1].children!.reduce((acc, t) => acc + t.content, ''); + const title = this._tokenToPlainText(tokens[idx + 1]); let slug = this.slugifier.fromHeading(title); if (this._slugCount.has(slug.value)) { @@ -334,6 +334,21 @@ export class MarkdownItEngine implements IMdParser { }; } + private _tokenToPlainText(token: Token): string { + if (token.children) { + return token.children.map(x => this._tokenToPlainText(x)).join(''); + } + + switch (token.type) { + case 'text': + case 'emoji': + case 'code_inline': + return token.content; + default: + return ''; + } + } + private _addLinkRenderer(md: MarkdownIt): void { const original = md.renderer.rules.link_open; diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 2b688e40592..525795036ab 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -166,6 +166,11 @@ resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.10.tgz#9c513107c690c0dd16e3ec61e453743de15ebdb0" integrity sha512-E1OCmDcDWa0Ya7vtSjp/XfHFGqYJfh+YPC1RkATU71fTac+j1JjCcB3qwSzmlKAighx2WxhLlfhS0RwAN++PFQ== +"@vscode/l10n@^0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.11.tgz#325d7beb2cfb87162bc624d16c4d546de6a73b72" + integrity sha512-ukOMWnCg1tCvT7WnDfsUKQOFDQGsyR5tNgRpwmqi+5/vzU3ghdDXzvIM4IOPdSb3OeSsBNvmSL8nxIVOqi2WXA== + "@vscode/markdown-it-katex@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@vscode/markdown-it-katex/-/markdown-it-katex-1.0.2.tgz#27ba579fa3896b2944b71209dd30d0f983983f11" @@ -183,6 +188,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -201,16 +211,72 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + dompurify@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.5.tgz#eb3d9cfa10037b6e73f32c586682c4b2ab01fbed" integrity sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A== +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + entities@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + highlight.js@^11.8.0: version "11.8.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.8.0.tgz#966518ea83257bae2e7c9a48596231856555bb65" @@ -275,6 +341,21 @@ morphdom@^2.6.1: resolved "https://registry.yarnpkg.com/morphdom/-/morphdom-2.6.1.tgz#e868e24f989fa3183004b159aed643e628b4306e" integrity sha512-Y8YRbAEP3eKykroIBWrjcfMw7mmwJfjhqdpSvoqinu8Y702nAwikpXcNFDiIkyvfCLxLM9Wu95RZqo4a9jFBaA== +node-html-parser@^6.1.5: + version "6.1.13" + resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.13.tgz#a1df799b83df5c6743fcd92740ba14682083b7e4" + integrity sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg== + dependencies: + css-select "^5.1.0" + he "1.2.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -297,6 +378,16 @@ vscode-jsonrpc@8.0.2: resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz#f239ed2cd6004021b6550af9fd9d3e47eee3cac9" integrity sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ== +vscode-jsonrpc@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94" + integrity sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw== + +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== + vscode-languageclient@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz#f1f23ce8c8484aa11e4b7dfb24437d3e59bb61c6" @@ -314,7 +405,23 @@ vscode-languageserver-protocol@3.17.2: vscode-jsonrpc "8.0.2" vscode-languageserver-types "3.17.2" -vscode-languageserver-textdocument@^1.0.11: +vscode-languageserver-protocol@3.17.3: + version "3.17.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz#6d0d54da093f0c0ee3060b81612cce0f11060d57" + integrity sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA== + dependencies: + vscode-jsonrpc "8.1.0" + vscode-languageserver-types "3.17.3" + +vscode-languageserver-protocol@^3.17.1: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== + dependencies: + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-textdocument@^1.0.11, vscode-languageserver-textdocument@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== @@ -329,6 +436,35 @@ vscode-languageserver-types@3.17.2, vscode-languageserver-types@^3.17.1, vscode- resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz#b2c2e7de405ad3d73a883e91989b850170ffc4f2" integrity sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA== +vscode-languageserver-types@3.17.3: + version "3.17.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64" + integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA== + +vscode-languageserver-types@3.17.5, vscode-languageserver-types@^3.17.3: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-languageserver@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz#5024253718915d84576ce6662dd46a791498d827" + integrity sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw== + dependencies: + vscode-languageserver-protocol "3.17.3" + +vscode-markdown-languageserver@^0.5.0-alpha.8: + version "0.5.0-alpha.8" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageserver/-/vscode-markdown-languageserver-0.5.0-alpha.8.tgz#87ced4b241636b6aeda7aacc41badced0c2cc992" + integrity sha512-Bp6YXHy4EMQ8JpsmXpQHa78byLvv83wVnLmhVfTaJsZTBwF8IOJ56NBUasepg9L6QVffUA9T2H/PfBqEOGpeOQ== + dependencies: + "@vscode/l10n" "^0.0.11" + vscode-languageserver "^8.1.0" + vscode-languageserver-textdocument "^1.0.8" + vscode-languageserver-types "^3.17.3" + vscode-markdown-languageservice "^0.5.0-alpha.7" + vscode-uri "^3.0.7" + vscode-markdown-languageservice@^0.3.0-alpha.3: version "0.3.0-alpha.3" resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.3.0-alpha.3.tgz#219a4880cfc0ea037b5a1833bc0b0039bfd1e2db" @@ -340,11 +476,28 @@ vscode-markdown-languageservice@^0.3.0-alpha.3: vscode-languageserver-types "^3.17.1" vscode-uri "^3.0.3" +vscode-markdown-languageservice@^0.5.0-alpha.7: + version "0.5.0-alpha.7" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0-alpha.7.tgz#0cc0939ea803d2afcb7a6e99b55feec664ec1b37" + integrity sha512-Iq9S5YGHm3D/UG9Usm8a/O5tYCo9FwaMF7nJsDQCxKgVZu5OzwOj3ixDkhoM+c8GKXiwt23DxhhWRuvI4odkTg== + dependencies: + "@vscode/l10n" "^0.0.10" + node-html-parser "^6.1.5" + picomatch "^2.3.1" + vscode-languageserver-protocol "^3.17.1" + vscode-languageserver-textdocument "^1.0.11" + vscode-uri "^3.0.7" + vscode-uri@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA== +vscode-uri@^3.0.7: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" diff --git a/extensions/markdown-math/notebook/katex.ts b/extensions/markdown-math/notebook/katex.ts index 94aad4f3c3b..ccc43046b32 100644 --- a/extensions/markdown-math/notebook/katex.ts +++ b/extensions/markdown-math/notebook/katex.ts @@ -51,7 +51,8 @@ export async function activate(ctx: RendererContext) { return md.use(katex, { globalGroup: true, enableBareBlocks: true, - macros + enableFencedBlocks: true, + macros, }); }); } diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json index 44b442b3df4..9669efc2435 100644 --- a/extensions/markdown-math/package.json +++ b/extensions/markdown-math/package.json @@ -56,6 +56,16 @@ "meta.embedded.math.markdown": "latex", "punctuation.definition.math.end.markdown": "latex" } + }, + { + "scopeName": "markdown.math.codeblock", + "path": "./syntaxes/md-math-fence.tmLanguage.json", + "injectTo": [ + "text.html.markdown" + ], + "embeddedLanguages": { + "meta.embedded.math.markdown": "latex" + } } ], "notebookRenderer": [ @@ -99,7 +109,7 @@ "build-notebook": "node ./esbuild" }, "dependencies": { - "@vscode/markdown-it-katex": "^1.0.3" + "@vscode/markdown-it-katex": "^1.1.0" }, "devDependencies": { "@types/markdown-it": "^0.0.0", diff --git a/extensions/markdown-math/src/extension.ts b/extensions/markdown-math/src/extension.ts index 1c27036b2fc..6491b0c1459 100644 --- a/extensions/markdown-math/src/extension.ts +++ b/extensions/markdown-math/src/extension.ts @@ -30,7 +30,11 @@ export function activate(context: vscode.ExtensionContext) { if (isEnabled()) { const katex = require('@vscode/markdown-it-katex').default; const settingsMacros = getMacros(); - const options = { globalGroup: true, macros: { ...settingsMacros } }; + const options = { + enableFencedBlocks: true, + globalGroup: true, + macros: { ...settingsMacros } + }; md.core.ruler.push('reset-katex-macros', () => { options.macros = { ...settingsMacros }; }); @@ -39,4 +43,4 @@ export function activate(context: vscode.ExtensionContext) { return md; } }; -} +} \ No newline at end of file diff --git a/extensions/markdown-math/syntaxes/md-math-block.tmLanguage.json b/extensions/markdown-math/syntaxes/md-math-block.tmLanguage.json index 43fd1bda5db..543568bf83e 100644 --- a/extensions/markdown-math/syntaxes/md-math-block.tmLanguage.json +++ b/extensions/markdown-math/syntaxes/md-math-block.tmLanguage.json @@ -82,4 +82,4 @@ } }, "scopeName": "markdown.math.block" -} +} \ No newline at end of file diff --git a/extensions/markdown-math/syntaxes/md-math-fence.tmLanguage.json b/extensions/markdown-math/syntaxes/md-math-fence.tmLanguage.json new file mode 100644 index 00000000000..556c579d3e7 --- /dev/null +++ b/extensions/markdown-math/syntaxes/md-math-fence.tmLanguage.json @@ -0,0 +1,28 @@ +{ + "fileTypes": [], + "injectionSelector": "L:markup.fenced_code.block.markdown", + "patterns": [ + { + "include": "#math-code-block" + } + ], + "repository": { + "math-code-block": { + "begin": "(?<=[`~])math(\\s+[^`~]*)?$", + "end": "(^|\\G)(?=\\s*[`~]{3,}\\s*$)", + "patterns": [ + { + "begin": "(^|\\G)(\\s*)(.*)", + "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", + "contentName": "meta.embedded.math.markdown", + "patterns": [ + { + "include": "text.html.markdown.math#math" + } + ] + } + ] + } + }, + "scopeName": "markdown.math.codeblock" +} diff --git a/extensions/markdown-math/yarn.lock b/extensions/markdown-math/yarn.lock index f6b6729fce6..52f3ff9545e 100644 --- a/extensions/markdown-math/yarn.lock +++ b/extensions/markdown-math/yarn.lock @@ -12,10 +12,10 @@ resolved "https://registry.yarnpkg.com/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.72.0.tgz#8943dc3cef0ced2dfb1e04c0a933bd289e7d5199" integrity sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw== -"@vscode/markdown-it-katex@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@vscode/markdown-it-katex/-/markdown-it-katex-1.0.3.tgz#5364e4dbcb0f7e7fd2fdab3847ba5d6b0c3ce9d9" - integrity sha512-a8ppdac0CG2lAQC6E6lT8dxmXkUk9gRtYNtILx31FyrPEwj875AAHc6tpRGeJBpWMpiMtcvz7ymWYBwYgxuFmw== +"@vscode/markdown-it-katex@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@vscode/markdown-it-katex/-/markdown-it-katex-1.1.0.tgz#e991b58f6eb7cf56aef74b98e1a5edc1494649bb" + integrity sha512-9cF2eJpsJOEs2V1cCAoJW/boKz9GQQLvZhNvI030K90z6ZE9lRGc9hDVvKut8zdFO2ObjwylPXXXVYvTdP2O2Q== dependencies: katex "^0.16.4" diff --git a/extensions/microsoft-authentication/extension-browser.webpack.config.js b/extensions/microsoft-authentication/extension-browser.webpack.config.js index 2513c7d0f9c..0d395fc0f96 100644 --- a/extensions/microsoft-authentication/extension-browser.webpack.config.js +++ b/extensions/microsoft-authentication/extension-browser.webpack.config.js @@ -22,10 +22,9 @@ module.exports = withBrowserDefaults({ }, resolve: { alias: { - './node/crypto': path.resolve(__dirname, 'src/browser/crypto'), './node/authServer': path.resolve(__dirname, 'src/browser/authServer'), './node/buffer': path.resolve(__dirname, 'src/browser/buffer'), - './node/fetch': path.resolve(__dirname, 'src/browser/fetch'), + './node/authProvider': path.resolve(__dirname, 'src/browser/authProvider'), } } }); diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index fd3ba077028..202a7badd7e 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -14,8 +14,7 @@ ], "activationEvents": [], "enabledApiProposals": [ - "idToken", - "authGetSessions" + "idToken" ], "capabilities": { "virtualWorkspaces": true, @@ -96,6 +95,16 @@ ] } } + }, + { + "title": "Microsoft", + "properties": { + "microsoft.useMsal": { + "type": "boolean", + "default": false, + "description": "%useMsal.description%" + } + } } ] }, @@ -117,8 +126,8 @@ "@types/uuid": "8.0.0" }, "dependencies": { - "node-fetch": "2.6.7", "@azure/ms-rest-azure-env": "^2.0.0", + "@azure/msal-node": "^2.12.0", "@vscode/extension-telemetry": "^0.9.0" }, "repository": { diff --git a/extensions/microsoft-authentication/package.nls.json b/extensions/microsoft-authentication/package.nls.json index 14c625dc762..80cbb32d4ab 100644 --- a/extensions/microsoft-authentication/package.nls.json +++ b/extensions/microsoft-authentication/package.nls.json @@ -3,6 +3,7 @@ "description": "Microsoft authentication provider", "signIn": "Sign In", "signOut": "Sign Out", + "useMsal.description": "Use the Microsoft Authentication Library (MSAL) to sign in with a Microsoft account.", "microsoft-sovereign-cloud.environment.description": { "message": "The Sovereign Cloud to use for authentication. If you select `custom`, you must also set the `#microsoft-sovereign-cloud.customEnvironment#` setting.", "comment": [ diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index bc4d71e56d6..713f5f12e9a 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -11,7 +11,6 @@ import { generateCodeChallenge, generateCodeVerifier, randomUUID } from './crypt import { BetterTokenStorage, IDidChangeInOtherWindowEvent } from './betterSecretStorage'; import { LoopbackAuthServer } from './node/authServer'; import { base64Decode } from './node/buffer'; -import { fetching } from './node/fetch'; import { UriEventHandler } from './UriEventHandler'; import TelemetryReporter from '@vscode/extension-telemetry'; import { Environment } from '@azure/ms-rest-azure-env'; @@ -806,7 +805,7 @@ export class AzureActiveDirectoryService { let result; let errorMessage: string | undefined; try { - result = await fetching(endpoint, { + result = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/extensions/microsoft-authentication/src/UriEventHandler.ts b/extensions/microsoft-authentication/src/UriEventHandler.ts index 3dc753af835..f525912fa51 100644 --- a/extensions/microsoft-authentication/src/UriEventHandler.ts +++ b/extensions/microsoft-authentication/src/UriEventHandler.ts @@ -6,7 +6,14 @@ import * as vscode from 'vscode'; export class UriEventHandler extends vscode.EventEmitter implements vscode.UriHandler { - public handleUri(uri: vscode.Uri) { + private _disposable = vscode.window.registerUriHandler(this); + + handleUri(uri: vscode.Uri) { this.fire(uri); } + + override dispose(): void { + super.dispose(); + this._disposable.dispose(); + } } diff --git a/extensions/microsoft-authentication/src/browser/authProvider.ts b/extensions/microsoft-authentication/src/browser/authProvider.ts new file mode 100644 index 00000000000..3b4da5b18fa --- /dev/null +++ b/extensions/microsoft-authentication/src/browser/authProvider.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationSession, EventEmitter } from 'vscode'; + +export class MsalAuthProvider implements AuthenticationProvider { + private _onDidChangeSessions = new EventEmitter(); + onDidChangeSessions = this._onDidChangeSessions.event; + + initialize(): Thenable { + throw new Error('Method not implemented.'); + } + + getSessions(): Thenable { + throw new Error('Method not implemented.'); + } + createSession(): Thenable { + throw new Error('Method not implemented.'); + } + removeSession(): Thenable { + throw new Error('Method not implemented.'); + } + + dispose() { + this._onDidChangeSessions.dispose(); + } +} diff --git a/extensions/microsoft-authentication/src/browser/crypto.ts b/extensions/microsoft-authentication/src/browser/crypto.ts deleted file mode 100644 index 37a1b43361f..00000000000 --- a/extensions/microsoft-authentication/src/browser/crypto.ts +++ /dev/null @@ -1,6 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export const crypto = globalThis.crypto; diff --git a/extensions/microsoft-authentication/src/common/async.ts b/extensions/microsoft-authentication/src/common/async.ts index 641faaff0dd..094861518fc 100644 --- a/extensions/microsoft-authentication/src/common/async.ts +++ b/extensions/microsoft-authentication/src/common/async.ts @@ -3,7 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationError, CancellationToken, Disposable } from 'vscode'; +import { CancellationError, CancellationToken, Disposable, Event, EventEmitter } from 'vscode'; + +/** + * Can be passed into the Delayed to defer using a microtask + */ +export const MicrotaskDelay = Symbol('MicrotaskDelay'); export class SequencerByKey { @@ -80,3 +85,473 @@ export function raceTimeoutError(promise: Promise, timeout: number): Promi export function raceCancellationAndTimeoutError(promise: Promise, token: CancellationToken, timeout: number): Promise { return raceCancellationError(raceTimeoutError(promise, timeout), token); } + +interface ILimitedTaskFactory { + factory: () => Promise; + c: (value: T | Promise) => void; + e: (error?: unknown) => void; +} + +export interface ILimiter { + + readonly size: number; + + queue(factory: () => Promise): Promise; + + clear(): void; +} + +/** + * A helper to queue N promises and run them all with a max degree of parallelism. The helper + * ensures that at any time no more than M promises are running at the same time. + */ +export class Limiter implements ILimiter { + + private _size = 0; + private _isDisposed = false; + private runningPromises: number; + private readonly maxDegreeOfParalellism: number; + private readonly outstandingPromises: ILimitedTaskFactory[]; + private readonly _onDrained: EventEmitter; + + constructor(maxDegreeOfParalellism: number) { + this.maxDegreeOfParalellism = maxDegreeOfParalellism; + this.outstandingPromises = []; + this.runningPromises = 0; + this._onDrained = new EventEmitter(); + } + + /** + * + * @returns A promise that resolved when all work is done (onDrained) or when + * there is nothing to do + */ + whenIdle(): Promise { + return this.size > 0 + ? toPromise(this.onDrained) + : Promise.resolve(); + } + + get onDrained(): Event { + return this._onDrained.event; + } + + get size(): number { + return this._size; + } + + queue(factory: () => Promise): Promise { + if (this._isDisposed) { + throw new Error('Object has been disposed'); + } + this._size++; + + return new Promise((c, e) => { + this.outstandingPromises.push({ factory, c, e }); + this.consume(); + }); + } + + private consume(): void { + while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) { + const iLimitedTask = this.outstandingPromises.shift()!; + this.runningPromises++; + + const promise = iLimitedTask.factory(); + promise.then(iLimitedTask.c, iLimitedTask.e); + promise.then(() => this.consumed(), () => this.consumed()); + } + } + + private consumed(): void { + if (this._isDisposed) { + return; + } + this.runningPromises--; + if (--this._size === 0) { + this._onDrained.fire(); + } + + if (this.outstandingPromises.length > 0) { + this.consume(); + } + } + + clear(): void { + if (this._isDisposed) { + throw new Error('Object has been disposed'); + } + this.outstandingPromises.length = 0; + this._size = this.runningPromises; + } + + dispose(): void { + this._isDisposed = true; + this.outstandingPromises.length = 0; // stop further processing + this._size = 0; + this._onDrained.dispose(); + } +} + + +interface IScheduledLater extends Disposable { + isTriggered(): boolean; +} + +const timeoutDeferred = (timeout: number, fn: () => void): IScheduledLater => { + let scheduled = true; + const handle = setTimeout(() => { + scheduled = false; + fn(); + }, timeout); + return { + isTriggered: () => scheduled, + dispose: () => { + clearTimeout(handle); + scheduled = false; + }, + }; +}; + +const microtaskDeferred = (fn: () => void): IScheduledLater => { + let scheduled = true; + queueMicrotask(() => { + if (scheduled) { + scheduled = false; + fn(); + } + }); + + return { + isTriggered: () => scheduled, + dispose: () => { scheduled = false; }, + }; +}; + +/** + * A helper to delay (debounce) execution of a task that is being requested often. + * + * Following the throttler, now imagine the mail man wants to optimize the number of + * trips proactively. The trip itself can be long, so he decides not to make the trip + * as soon as a letter is submitted. Instead he waits a while, in case more + * letters are submitted. After said waiting period, if no letters were submitted, he + * decides to make the trip. Imagine that N more letters were submitted after the first + * one, all within a short period of time between each other. Even though N+1 + * submissions occurred, only 1 delivery was made. + * + * The delayer offers this behavior via the trigger() method, into which both the task + * to be executed and the waiting period (delay) must be passed in as arguments. Following + * the example: + * + * const delayer = new Delayer(WAITING_PERIOD); + * const letters = []; + * + * function letterReceived(l) { + * letters.push(l); + * delayer.trigger(() => { return makeTheTrip(); }); + * } + */ +export class Delayer implements Disposable { + + private deferred: IScheduledLater | null; + private completionPromise: Promise | null; + private doResolve: ((value?: any | Promise) => void) | null; + private doReject: ((err: any) => void) | null; + private task: (() => T | Promise) | null; + + constructor(public defaultDelay: number | typeof MicrotaskDelay) { + this.deferred = null; + this.completionPromise = null; + this.doResolve = null; + this.doReject = null; + this.task = null; + } + + trigger(task: () => T | Promise, delay = this.defaultDelay): Promise { + this.task = task; + this.cancelTimeout(); + + if (!this.completionPromise) { + this.completionPromise = new Promise((resolve, reject) => { + this.doResolve = resolve; + this.doReject = reject; + }).then(() => { + this.completionPromise = null; + this.doResolve = null; + if (this.task) { + const task = this.task; + this.task = null; + return task(); + } + return undefined; + }); + } + + const fn = () => { + this.deferred = null; + this.doResolve?.(null); + }; + + this.deferred = delay === MicrotaskDelay ? microtaskDeferred(fn) : timeoutDeferred(delay, fn); + + return this.completionPromise; + } + + isTriggered(): boolean { + return !!this.deferred?.isTriggered(); + } + + cancel(): void { + this.cancelTimeout(); + + if (this.completionPromise) { + this.doReject?.(new CancellationError()); + this.completionPromise = null; + } + } + + private cancelTimeout(): void { + this.deferred?.dispose(); + this.deferred = null; + } + + dispose(): void { + this.cancel(); + } +} + +/** + * A helper to prevent accumulation of sequential async tasks. + * + * Imagine a mail man with the sole task of delivering letters. As soon as + * a letter submitted for delivery, he drives to the destination, delivers it + * and returns to his base. Imagine that during the trip, N more letters were submitted. + * When the mail man returns, he picks those N letters and delivers them all in a + * single trip. Even though N+1 submissions occurred, only 2 deliveries were made. + * + * The throttler implements this via the queue() method, by providing it a task + * factory. Following the example: + * + * const throttler = new Throttler(); + * const letters = []; + * + * function deliver() { + * const lettersToDeliver = letters; + * letters = []; + * return makeTheTrip(lettersToDeliver); + * } + * + * function onLetterReceived(l) { + * letters.push(l); + * throttler.queue(deliver); + * } + */ +export class Throttler implements Disposable { + + private activePromise: Promise | null; + private queuedPromise: Promise | null; + private queuedPromiseFactory: (() => Promise) | null; + + private isDisposed = false; + + constructor() { + this.activePromise = null; + this.queuedPromise = null; + this.queuedPromiseFactory = null; + } + + queue(promiseFactory: () => Promise): Promise { + if (this.isDisposed) { + return Promise.reject(new Error('Throttler is disposed')); + } + + if (this.activePromise) { + this.queuedPromiseFactory = promiseFactory; + + if (!this.queuedPromise) { + const onComplete = () => { + this.queuedPromise = null; + + if (this.isDisposed) { + return; + } + + const result = this.queue(this.queuedPromiseFactory!); + this.queuedPromiseFactory = null; + + return result; + }; + + this.queuedPromise = new Promise(resolve => { + this.activePromise!.then(onComplete, onComplete).then(resolve); + }); + } + + return new Promise((resolve, reject) => { + this.queuedPromise!.then(resolve, reject); + }); + } + + this.activePromise = promiseFactory(); + + return new Promise((resolve, reject) => { + this.activePromise!.then((result: T) => { + this.activePromise = null; + resolve(result); + }, (err: unknown) => { + this.activePromise = null; + reject(err); + }); + }); + } + + dispose(): void { + this.isDisposed = true; + } +} + +/** + * A helper to delay execution of a task that is being requested often, while + * preventing accumulation of consecutive executions, while the task runs. + * + * The mail man is clever and waits for a certain amount of time, before going + * out to deliver letters. While the mail man is going out, more letters arrive + * and can only be delivered once he is back. Once he is back the mail man will + * do one more trip to deliver the letters that have accumulated while he was out. + */ +export class ThrottledDelayer { + + private delayer: Delayer>; + private throttler: Throttler; + + constructor(defaultDelay: number) { + this.delayer = new Delayer(defaultDelay); + this.throttler = new Throttler(); + } + + trigger(promiseFactory: () => Promise, delay?: number): Promise { + return this.delayer.trigger(() => this.throttler.queue(promiseFactory), delay) as unknown as Promise; + } + + isTriggered(): boolean { + return this.delayer.isTriggered(); + } + + cancel(): void { + this.delayer.cancel(); + } + + dispose(): void { + this.delayer.dispose(); + this.throttler.dispose(); + } +} + +/** + * A queue is handles one promise at a time and guarantees that at any time only one promise is executing. + */ +export class Queue extends Limiter { + + constructor() { + super(1); + } +} + +/** + * Given an event, returns another event which only fires once. + * + * @param event The event source for the new event. + */ +export function once(event: Event): Event { + return (listener, thisArgs = null, disposables?) => { + // we need this, in case the event fires during the listener call + let didFire = false; + let result: Disposable | undefined = undefined; + result = event(e => { + if (didFire) { + return; + } else if (result) { + result.dispose(); + } else { + didFire = true; + } + + return listener.call(thisArgs, e); + }, null, disposables); + + if (didFire) { + result.dispose(); + } + + return result; + }; +} + +/** + * Creates a promise out of an event, using the {@link Event.once} helper. + */ +export function toPromise(event: Event): Promise { + return new Promise(resolve => once(event)(resolve)); +} + +export type ValueCallback = (value: T | Promise) => void; + +const enum DeferredOutcome { + Resolved, + Rejected +} + +/** + * Creates a promise whose resolution or rejection can be controlled imperatively. + */ +export class DeferredPromise { + + private completeCallback!: ValueCallback; + private errorCallback!: (err: unknown) => void; + private outcome?: { outcome: DeferredOutcome.Rejected; value: any } | { outcome: DeferredOutcome.Resolved; value: T }; + + public get isRejected() { + return this.outcome?.outcome === DeferredOutcome.Rejected; + } + + public get isResolved() { + return this.outcome?.outcome === DeferredOutcome.Resolved; + } + + public get isSettled() { + return !!this.outcome; + } + + public get value() { + return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined; + } + + public readonly p: Promise; + + constructor() { + this.p = new Promise((c, e) => { + this.completeCallback = c; + this.errorCallback = e; + }); + } + + public complete(value: T) { + return new Promise(resolve => { + this.completeCallback(value); + this.outcome = { outcome: DeferredOutcome.Resolved, value }; + resolve(); + }); + } + + public error(err: unknown) { + return new Promise(resolve => { + this.errorCallback(err); + this.outcome = { outcome: DeferredOutcome.Rejected, value: err }; + resolve(); + }); + } + + public cancel() { + return this.error(new CancellationError()); + } +} diff --git a/extensions/microsoft-authentication/src/common/cachePlugin.ts b/extensions/microsoft-authentication/src/common/cachePlugin.ts new file mode 100644 index 00000000000..91b4f0ee6a8 --- /dev/null +++ b/extensions/microsoft-authentication/src/common/cachePlugin.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICachePlugin, TokenCacheContext } from '@azure/msal-node'; +import { Disposable, EventEmitter, SecretStorage } from 'vscode'; + +export class SecretStorageCachePlugin implements ICachePlugin { + private readonly _onDidChange: EventEmitter = new EventEmitter(); + readonly onDidChange = this._onDidChange.event; + + private _disposable: Disposable; + + private _value: string | undefined; + + constructor( + private readonly _secretStorage: SecretStorage, + private readonly _key: string + ) { + this._disposable = Disposable.from( + this._onDidChange, + this._registerChangeHandler() + ); + } + + private _registerChangeHandler() { + return this._secretStorage.onDidChange(e => { + if (e.key === this._key) { + this._onDidChange.fire(); + } + }); + } + + async beforeCacheAccess(tokenCacheContext: TokenCacheContext): Promise { + const data = await this._secretStorage.get(this._key); + this._value = data; + if (data) { + tokenCacheContext.tokenCache.deserialize(data); + } + } + + async afterCacheAccess(tokenCacheContext: TokenCacheContext): Promise { + if (tokenCacheContext.cacheHasChanged) { + const value = tokenCacheContext.tokenCache.serialize(); + if (value !== this._value) { + await this._secretStorage.store(this._key, value); + } + } + } + + dispose() { + this._disposable.dispose(); + } +} diff --git a/extensions/microsoft-authentication/src/common/loggerOptions.ts b/extensions/microsoft-authentication/src/common/loggerOptions.ts new file mode 100644 index 00000000000..86443c0281f --- /dev/null +++ b/extensions/microsoft-authentication/src/common/loggerOptions.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { LogLevel as MsalLogLevel } from '@azure/msal-node'; +import { env, LogLevel, LogOutputChannel } from 'vscode'; + +export class MsalLoggerOptions { + piiLoggingEnabled = false; + + constructor(private readonly _output: LogOutputChannel) { } + + get logLevel(): MsalLogLevel { + return this._toMsalLogLevel(env.logLevel); + } + + loggerCallback(level: MsalLogLevel, message: string, containsPii: boolean): void { + if (containsPii) { + return; + } + + switch (level) { + case MsalLogLevel.Error: + this._output.error(message); + return; + case MsalLogLevel.Warning: + this._output.warn(message); + return; + case MsalLogLevel.Info: + this._output.info(message); + return; + case MsalLogLevel.Verbose: + this._output.debug(message); + return; + case MsalLogLevel.Trace: + this._output.trace(message); + return; + default: + this._output.info(message); + return; + } + } + + private _toMsalLogLevel(logLevel: LogLevel): MsalLogLevel { + switch (logLevel) { + case LogLevel.Trace: + return MsalLogLevel.Trace; + case LogLevel.Debug: + return MsalLogLevel.Verbose; + case LogLevel.Info: + return MsalLogLevel.Info; + case LogLevel.Warning: + return MsalLogLevel.Warning; + case LogLevel.Error: + return MsalLogLevel.Error; + default: + return MsalLogLevel.Info; + } + } +} diff --git a/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts b/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts new file mode 100644 index 00000000000..4a455ea50f7 --- /dev/null +++ b/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { ILoopbackClient, ServerAuthorizationCodeResponse } from '@azure/msal-node'; +import type { UriEventHandler } from '../UriEventHandler'; +import { env, Uri } from 'vscode'; +import { toPromise } from './async'; + +export interface ILoopbackClientAndOpener extends ILoopbackClient { + openBrowser(url: string): Promise; +} + +export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener { + constructor( + private readonly _uriHandler: UriEventHandler, + private readonly _redirectUri: string + ) { } + + async listenForAuthCode(successTemplate?: string, errorTemplate?: string): Promise { + console.log(successTemplate, errorTemplate); + const url = await toPromise(this._uriHandler.event); + const result = new URL(url.toString(true)); + + return { + code: result.searchParams.get('code') ?? undefined, + state: result.searchParams.get('state') ?? undefined, + error: result.searchParams.get('error') ?? undefined, + error_description: result.searchParams.get('error_description') ?? undefined, + error_uri: result.searchParams.get('error_uri') ?? undefined, + }; + } + + getRedirectUri(): string { + // We always return the constant redirect URL because + // it will handle redirecting back to the extension + return this._redirectUri; + } + + closeServer(): void { + // No-op + } + + async openBrowser(url: string): Promise { + const callbackUri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://vscode.microsoft-authentication`)); + + const uri = Uri.parse(url + `&state=${encodeURI(callbackUri.toString(true))}`); + await env.openExternal(uri); + } +} diff --git a/extensions/microsoft-authentication/src/common/publicClientCache.ts b/extensions/microsoft-authentication/src/common/publicClientCache.ts new file mode 100644 index 00000000000..cb9339f926d --- /dev/null +++ b/extensions/microsoft-authentication/src/common/publicClientCache.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import type { AccountInfo, AuthenticationResult, InteractiveRequest, SilentFlowRequest } from '@azure/msal-node'; +import type { Disposable, Event } from 'vscode'; + +export interface ICachedPublicClientApplication extends Disposable { + initialize(): Promise; + acquireTokenSilent(request: SilentFlowRequest): Promise; + acquireTokenInteractive(request: InteractiveRequest): Promise; + removeAccount(account: AccountInfo): Promise; + accounts: AccountInfo[]; + clientId: string; + authority: string; +} + +export interface ICachedPublicClientApplicationManager { + getOrCreate(clientId: string, authority: string): Promise; + getAll(): ICachedPublicClientApplication[]; +} diff --git a/extensions/microsoft-authentication/src/common/telemetryReporter.ts b/extensions/microsoft-authentication/src/common/telemetryReporter.ts new file mode 100644 index 00000000000..52da2557a4d --- /dev/null +++ b/extensions/microsoft-authentication/src/common/telemetryReporter.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import TelemetryReporter from '@vscode/extension-telemetry'; + +export const enum MicrosoftAccountType { + AAD = 'aad', + MSA = 'msa', + Unknown = 'unknown' +} + +export class MicrosoftAuthenticationTelemetryReporter { + protected _telemetryReporter: TelemetryReporter; + constructor(aiKey: string) { + this._telemetryReporter = new TelemetryReporter(aiKey); + } + + sendLoginEvent(scopes: readonly string[]): void { + /* __GDPR__ + "login" : { + "owner": "TylerLeonhardt", + "comment": "Used to determine the usage of the Microsoft Auth Provider.", + "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } + } + */ + this._telemetryReporter.sendTelemetryEvent('login', { + // Get rid of guids from telemetry. + scopes: JSON.stringify(this._scrubGuids(scopes)), + }); + } + sendLoginFailedEvent(): void { + /* __GDPR__ + "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } + */ + this._telemetryReporter.sendTelemetryEvent('loginFailed'); + } + sendLogoutEvent(): void { + /* __GDPR__ + "logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } + */ + this._telemetryReporter.sendTelemetryEvent('logout'); + } + sendLogoutFailedEvent(): void { + /* __GDPR__ + "logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } + */ + this._telemetryReporter.sendTelemetryEvent('logoutFailed'); + } + /** + * Sends an event for an account type available at startup. + * @param scopes The scopes for the session + * @param accountType The account type for the session + * @todo Remove the scopes since we really don't care about them. + */ + sendAccountEvent(scopes: string[], accountType: MicrosoftAccountType): void { + /* __GDPR__ + "login" : { + "owner": "TylerLeonhardt", + "comment": "Used to determine the usage of the Microsoft Auth Provider.", + "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." }, + "accountType": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what account types are being used." } + } + */ + this._telemetryReporter.sendTelemetryEvent('account', { + // Get rid of guids from telemetry. + scopes: JSON.stringify(this._scrubGuids(scopes)), + accountType + }); + } + + protected _scrubGuids(scopes: readonly string[]): string[] { + return scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}')); + } +} + +export class MicrosoftSovereignCloudAuthenticationTelemetryReporter extends MicrosoftAuthenticationTelemetryReporter { + override sendLoginEvent(scopes: string[]): void { + /* __GDPR__ + "loginMicrosoftSovereignCloud" : { + "owner": "TylerLeonhardt", + "comment": "Used to determine the usage of the Microsoft Auth Provider.", + "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } + } + */ + this._telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloud', { + // Get rid of guids from telemetry. + scopes: JSON.stringify(this._scrubGuids(scopes)), + }); + } + override sendLoginFailedEvent(): void { + /* __GDPR__ + "loginMicrosoftSovereignCloudFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } + */ + this._telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloudFailed'); + } + override sendLogoutEvent(): void { + /* __GDPR__ + "logoutMicrosoftSovereignCloud" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } + */ + this._telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloud'); + } + override sendLogoutFailedEvent(): void { + /* __GDPR__ + "logoutMicrosoftSovereignCloudFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } + */ + this._telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloudFailed'); + } +} diff --git a/extensions/microsoft-authentication/src/cryptoUtils.ts b/extensions/microsoft-authentication/src/cryptoUtils.ts index 582dae74c3c..e608a81fc99 100644 --- a/extensions/microsoft-authentication/src/cryptoUtils.ts +++ b/extensions/microsoft-authentication/src/cryptoUtils.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { base64Encode } from './node/buffer'; -import { crypto } from './node/crypto'; export function randomUUID() { return crypto.randomUUID(); diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 87dc94e4c25..e06f2a0400a 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -3,179 +3,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; -import { Environment, EnvironmentParameters } from '@azure/ms-rest-azure-env'; -import { AzureActiveDirectoryService, IStoredSession } from './AADHelper'; -import { BetterTokenStorage } from './betterSecretStorage'; -import { UriEventHandler } from './UriEventHandler'; -import TelemetryReporter from '@vscode/extension-telemetry'; +import { commands, ExtensionContext, l10n, window, workspace } from 'vscode'; +import * as extensionV1 from './extensionV1'; +import * as extensionV2 from './extensionV2'; -async function initMicrosoftSovereignCloudAuthProvider(context: vscode.ExtensionContext, telemetryReporter: TelemetryReporter, uriHandler: UriEventHandler, tokenStorage: BetterTokenStorage): Promise { - const environment = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get('environment'); - let authProviderName: string | undefined; - if (!environment) { - return undefined; - } +const config = workspace.getConfiguration('microsoft'); +const useMsal = config.get('useMsal', false); - if (environment === 'custom') { - const customEnv = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get('customEnvironment'); - if (!customEnv) { - const res = await vscode.window.showErrorMessage(vscode.l10n.t('You must also specify a custom environment in order to use the custom environment auth provider.'), vscode.l10n.t('Open settings')); - if (res) { - await vscode.commands.executeCommand('workbench.action.openSettingsJson', 'microsoft-sovereign-cloud.customEnvironment'); - } - return undefined; +export async function activate(context: ExtensionContext) { + context.subscriptions.push(workspace.onDidChangeConfiguration(async e => { + if (!e.affectsConfiguration('microsoft.useMsal') && useMsal === config.get('useMsal', false)) { + return; } - try { - Environment.add(customEnv); - } catch (e) { - const res = await vscode.window.showErrorMessage(vscode.l10n.t('Error validating custom environment setting: {0}', e.message), vscode.l10n.t('Open settings')); - if (res) { - await vscode.commands.executeCommand('workbench.action.openSettings', 'microsoft-sovereign-cloud.customEnvironment'); - } - return undefined; - } - authProviderName = customEnv.name; - } else { - authProviderName = environment; - } - const env = Environment.get(authProviderName); - if (!env) { - const res = await vscode.window.showErrorMessage(vscode.l10n.t('The environment `{0}` is not a valid environment.', authProviderName), vscode.l10n.t('Open settings')); - return undefined; - } + const reload = l10n.t('Reload'); + const result = await window.showInformationMessage( + 'Reload required', + { + modal: true, + detail: l10n.t('Microsoft Account configuration has been changed.'), + }, + reload + ); - const aadService = new AzureActiveDirectoryService( - vscode.window.createOutputChannel(vscode.l10n.t('Microsoft Sovereign Cloud Authentication'), { log: true }), - context, - uriHandler, - tokenStorage, - telemetryReporter, - env); - await aadService.initialize(); - - const disposable = vscode.authentication.registerAuthenticationProvider('microsoft-sovereign-cloud', authProviderName, { - onDidChangeSessions: aadService.onDidChangeSessions, - getSessions: (scopes: string[]) => aadService.getSessions(scopes), - createSession: async (scopes: string[]) => { - try { - /* __GDPR__ - "login" : { - "owner": "TylerLeonhardt", - "comment": "Used to determine the usage of the Microsoft Sovereign Cloud Auth Provider.", - "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } - } - */ - telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloud', { - // Get rid of guids from telemetry. - scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), - }); - - return await aadService.createSession(scopes); - } catch (e) { - /* __GDPR__ - "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } - */ - telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloudFailed'); - - throw e; - } - }, - removeSession: async (id: string) => { - try { - /* __GDPR__ - "logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } - */ - telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloud'); - - await aadService.removeSessionById(id); - } catch (e) { - /* __GDPR__ - "logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } - */ - telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloudFailed'); - } - } - }, { supportsMultipleAccounts: true }); - - context.subscriptions.push(disposable); - return disposable; -} - -export async function activate(context: vscode.ExtensionContext) { - const aiKey: string = context.extension.packageJSON.aiKey; - const telemetryReporter = new TelemetryReporter(aiKey); - - const uriHandler = new UriEventHandler(); - context.subscriptions.push(uriHandler); - context.subscriptions.push(vscode.window.registerUriHandler(uriHandler)); - const betterSecretStorage = new BetterTokenStorage('microsoft.login.keylist', context); - - const loginService = new AzureActiveDirectoryService( - vscode.window.createOutputChannel(vscode.l10n.t('Microsoft Authentication'), { log: true }), - context, - uriHandler, - betterSecretStorage, - telemetryReporter, - Environment.AzureCloud); - await loginService.initialize(); - - context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', { - onDidChangeSessions: loginService.onDidChangeSessions, - getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options?.account), - createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => { - try { - /* __GDPR__ - "login" : { - "owner": "TylerLeonhardt", - "comment": "Used to determine the usage of the Microsoft Auth Provider.", - "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } - } - */ - telemetryReporter.sendTelemetryEvent('login', { - // Get rid of guids from telemetry. - scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), - }); - - return await loginService.createSession(scopes, options?.account); - } catch (e) { - /* __GDPR__ - "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } - */ - telemetryReporter.sendTelemetryEvent('loginFailed'); - - throw e; - } - }, - removeSession: async (id: string) => { - try { - /* __GDPR__ - "logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } - */ - telemetryReporter.sendTelemetryEvent('logout'); - - await loginService.removeSessionById(id); - } catch (e) { - /* __GDPR__ - "logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } - */ - telemetryReporter.sendTelemetryEvent('logoutFailed'); - } - } - }, { supportsMultipleAccounts: true })); - - let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage); - - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => { - if (e.affectsConfiguration('microsoft-sovereign-cloud')) { - microsoftSovereignCloudAuthProviderDisposable?.dispose(); - microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage); + if (result === reload) { + commands.executeCommand('workbench.action.reloadWindow'); } })); - - return; + // Only activate the new extension if we are not running in a browser environment + if (useMsal && typeof navigator === 'undefined') { + await extensionV2.activate(context); + } else { + await extensionV1.activate(context); + } } -// this method is called when your extension is deactivated -export function deactivate() { } +export function deactivate() { + if (useMsal) { + extensionV2.deactivate(); + } else { + extensionV1.deactivate(); + } +} diff --git a/extensions/microsoft-authentication/src/extensionV1.ts b/extensions/microsoft-authentication/src/extensionV1.ts new file mode 100644 index 00000000000..f63eedb2410 --- /dev/null +++ b/extensions/microsoft-authentication/src/extensionV1.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { Environment, EnvironmentParameters } from '@azure/ms-rest-azure-env'; +import { AzureActiveDirectoryService, IStoredSession } from './AADHelper'; +import { BetterTokenStorage } from './betterSecretStorage'; +import { UriEventHandler } from './UriEventHandler'; +import TelemetryReporter from '@vscode/extension-telemetry'; + +async function initMicrosoftSovereignCloudAuthProvider(context: vscode.ExtensionContext, telemetryReporter: TelemetryReporter, uriHandler: UriEventHandler, tokenStorage: BetterTokenStorage): Promise { + const environment = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get('environment'); + let authProviderName: string | undefined; + if (!environment) { + return undefined; + } + + if (environment === 'custom') { + const customEnv = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get('customEnvironment'); + if (!customEnv) { + const res = await vscode.window.showErrorMessage(vscode.l10n.t('You must also specify a custom environment in order to use the custom environment auth provider.'), vscode.l10n.t('Open settings')); + if (res) { + await vscode.commands.executeCommand('workbench.action.openSettingsJson', 'microsoft-sovereign-cloud.customEnvironment'); + } + return undefined; + } + try { + Environment.add(customEnv); + } catch (e) { + const res = await vscode.window.showErrorMessage(vscode.l10n.t('Error validating custom environment setting: {0}', e.message), vscode.l10n.t('Open settings')); + if (res) { + await vscode.commands.executeCommand('workbench.action.openSettings', 'microsoft-sovereign-cloud.customEnvironment'); + } + return undefined; + } + authProviderName = customEnv.name; + } else { + authProviderName = environment; + } + + const env = Environment.get(authProviderName); + if (!env) { + const res = await vscode.window.showErrorMessage(vscode.l10n.t('The environment `{0}` is not a valid environment.', authProviderName), vscode.l10n.t('Open settings')); + return undefined; + } + + const aadService = new AzureActiveDirectoryService( + vscode.window.createOutputChannel(vscode.l10n.t('Microsoft Sovereign Cloud Authentication'), { log: true }), + context, + uriHandler, + tokenStorage, + telemetryReporter, + env); + await aadService.initialize(); + + const disposable = vscode.authentication.registerAuthenticationProvider('microsoft-sovereign-cloud', authProviderName, { + onDidChangeSessions: aadService.onDidChangeSessions, + getSessions: (scopes: string[]) => aadService.getSessions(scopes), + createSession: async (scopes: string[]) => { + try { + /* __GDPR__ + "login" : { + "owner": "TylerLeonhardt", + "comment": "Used to determine the usage of the Microsoft Sovereign Cloud Auth Provider.", + "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } + } + */ + telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloud', { + // Get rid of guids from telemetry. + scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), + }); + + return await aadService.createSession(scopes); + } catch (e) { + /* __GDPR__ + "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } + */ + telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloudFailed'); + + throw e; + } + }, + removeSession: async (id: string) => { + try { + /* __GDPR__ + "logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } + */ + telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloud'); + + await aadService.removeSessionById(id); + } catch (e) { + /* __GDPR__ + "logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } + */ + telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloudFailed'); + } + } + }, { supportsMultipleAccounts: true }); + + context.subscriptions.push(disposable); + return disposable; +} + +export async function activate(context: vscode.ExtensionContext) { + const aiKey: string = context.extension.packageJSON.aiKey; + const telemetryReporter = new TelemetryReporter(aiKey); + + const uriHandler = new UriEventHandler(); + context.subscriptions.push(uriHandler); + const betterSecretStorage = new BetterTokenStorage('microsoft.login.keylist', context); + + const loginService = new AzureActiveDirectoryService( + vscode.window.createOutputChannel(vscode.l10n.t('Microsoft Authentication'), { log: true }), + context, + uriHandler, + betterSecretStorage, + telemetryReporter, + Environment.AzureCloud); + await loginService.initialize(); + + context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', { + onDidChangeSessions: loginService.onDidChangeSessions, + getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options?.account), + createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => { + try { + /* __GDPR__ + "login" : { + "owner": "TylerLeonhardt", + "comment": "Used to determine the usage of the Microsoft Auth Provider.", + "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } + } + */ + telemetryReporter.sendTelemetryEvent('login', { + // Get rid of guids from telemetry. + scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), + }); + + return await loginService.createSession(scopes, options?.account); + } catch (e) { + /* __GDPR__ + "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } + */ + telemetryReporter.sendTelemetryEvent('loginFailed'); + + throw e; + } + }, + removeSession: async (id: string) => { + try { + /* __GDPR__ + "logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } + */ + telemetryReporter.sendTelemetryEvent('logout'); + + await loginService.removeSessionById(id); + } catch (e) { + /* __GDPR__ + "logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } + */ + telemetryReporter.sendTelemetryEvent('logoutFailed'); + } + } + }, { supportsMultipleAccounts: true })); + + let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage); + + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration('microsoft-sovereign-cloud')) { + microsoftSovereignCloudAuthProviderDisposable?.dispose(); + microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage); + } + })); + + return; +} + +// this method is called when your extension is deactivated +export function deactivate() { } diff --git a/extensions/microsoft-authentication/src/extensionV2.ts b/extensions/microsoft-authentication/src/extensionV2.ts new file mode 100644 index 00000000000..571172eda5e --- /dev/null +++ b/extensions/microsoft-authentication/src/extensionV2.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Environment, EnvironmentParameters } from '@azure/ms-rest-azure-env'; +import Logger from './logger'; +import { MsalAuthProvider } from './node/authProvider'; +import { UriEventHandler } from './UriEventHandler'; +import { authentication, commands, ExtensionContext, l10n, window, workspace, Disposable } from 'vscode'; +import { MicrosoftAuthenticationTelemetryReporter, MicrosoftSovereignCloudAuthenticationTelemetryReporter } from './common/telemetryReporter'; + +async function initMicrosoftSovereignCloudAuthProvider( + context: ExtensionContext, + uriHandler: UriEventHandler +): Promise { + const environment = workspace.getConfiguration('microsoft-sovereign-cloud').get('environment'); + let authProviderName: string | undefined; + if (!environment) { + return undefined; + } + + if (environment === 'custom') { + const customEnv = workspace.getConfiguration('microsoft-sovereign-cloud').get('customEnvironment'); + if (!customEnv) { + const res = await window.showErrorMessage(l10n.t('You must also specify a custom environment in order to use the custom environment auth provider.'), l10n.t('Open settings')); + if (res) { + await commands.executeCommand('workbench.action.openSettingsJson', 'microsoft-sovereign-cloud.customEnvironment'); + } + return undefined; + } + try { + Environment.add(customEnv); + } catch (e) { + const res = await window.showErrorMessage(l10n.t('Error validating custom environment setting: {0}', e.message), l10n.t('Open settings')); + if (res) { + await commands.executeCommand('workbench.action.openSettings', 'microsoft-sovereign-cloud.customEnvironment'); + } + return undefined; + } + authProviderName = customEnv.name; + } else { + authProviderName = environment; + } + + const env = Environment.get(authProviderName); + if (!env) { + await window.showErrorMessage(l10n.t('The environment `{0}` is not a valid environment.', authProviderName), l10n.t('Open settings')); + return undefined; + } + + const authProvider = new MsalAuthProvider( + context, + new MicrosoftSovereignCloudAuthenticationTelemetryReporter(context.extension.packageJSON.aiKey), + window.createOutputChannel(l10n.t('Microsoft Sovereign Cloud Authentication'), { log: true }), + uriHandler, + env + ); + await authProvider.initialize(); + const disposable = authentication.registerAuthenticationProvider( + 'microsoft-sovereign-cloud', + authProviderName, + authProvider, + { supportsMultipleAccounts: true } + ); + context.subscriptions.push(disposable); + return disposable; +} + +export async function activate(context: ExtensionContext) { + const uriHandler = new UriEventHandler(); + context.subscriptions.push(uriHandler); + const authProvider = new MsalAuthProvider( + context, + new MicrosoftAuthenticationTelemetryReporter(context.extension.packageJSON.aiKey), + Logger, + uriHandler + ); + await authProvider.initialize(); + context.subscriptions.push(authentication.registerAuthenticationProvider( + 'microsoft', + 'Microsoft', + authProvider, + { supportsMultipleAccounts: true } + )); + + let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, uriHandler); + + context.subscriptions.push(workspace.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration('microsoft-sovereign-cloud')) { + microsoftSovereignCloudAuthProviderDisposable?.dispose(); + microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, uriHandler); + } + })); +} + +export function deactivate() { } diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts new file mode 100644 index 00000000000..0175b2981b8 --- /dev/null +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -0,0 +1,301 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { AccountInfo, AuthenticationResult } from '@azure/msal-node'; +import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, env, EventEmitter, ExtensionContext, l10n, LogOutputChannel, Memento, SecretStorage, Uri, window } from 'vscode'; +import { Environment } from '@azure/ms-rest-azure-env'; +import { CachedPublicClientApplicationManager } from './publicClientCache'; +import { UriHandlerLoopbackClient } from '../common/loopbackClientAndOpener'; +import { UriEventHandler } from '../UriEventHandler'; +import { ICachedPublicClientApplication } from '../common/publicClientCache'; +import { MicrosoftAccountType, MicrosoftAuthenticationTelemetryReporter } from '../common/telemetryReporter'; +import { loopbackTemplate } from './loopbackTemplate'; +import { Delayer } from '../common/async'; + +const redirectUri = 'https://vscode.dev/redirect'; +const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56'; +const DEFAULT_TENANT = 'organizations'; +const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad'; +const MSA_PASSTHRU_TID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a'; + +export class MsalAuthProvider implements AuthenticationProvider { + + private readonly _disposables: { dispose(): void }[]; + private readonly _publicClientManager: CachedPublicClientApplicationManager; + private readonly _refreshDelayer = new DelayerByKey(); + + /** + * Event to signal a change in authentication sessions for this provider. + */ + private readonly _onDidChangeSessionsEmitter = new EventEmitter(); + + /** + * Event to signal a change in authentication sessions for this provider. + * + * NOTE: This event is handled differently in the Microsoft auth provider than "typical" auth providers. Normally, + * this event would fire when the provider's sessions change... which are tied to a specific list of scopes. However, + * since Microsoft identity doesn't care too much about scopes (you can mint a new token from an existing token), + * we just fire this event whenever the account list changes... so essentially there is one session per account. + * + * This is not quite how the API should be used... but this event really is just for signaling that the account list + * has changed. + */ + onDidChangeSessions = this._onDidChangeSessionsEmitter.event; + + constructor( + context: ExtensionContext, + private readonly _telemetryReporter: MicrosoftAuthenticationTelemetryReporter, + private readonly _logger: LogOutputChannel, + private readonly _uriHandler: UriEventHandler, + private readonly _env: Environment = Environment.AzureCloud + ) { + this._disposables = context.subscriptions; + this._publicClientManager = new CachedPublicClientApplicationManager( + context.globalState, + context.secrets, + this._logger, + (e) => this._handleAccountChange(e) + ); + this._disposables.push(this._publicClientManager); + this._disposables.push(this._onDidChangeSessionsEmitter); + + } + + async initialize(): Promise { + await this._publicClientManager.initialize(); + + // Send telemetry for existing accounts + for (const cachedPca of this._publicClientManager.getAll()) { + for (const account of cachedPca.accounts) { + if (!account.idTokenClaims?.tid) { + continue; + } + const tid = account.idTokenClaims.tid; + const type = tid === MSA_TID || tid === MSA_PASSTHRU_TID ? MicrosoftAccountType.MSA : MicrosoftAccountType.AAD; + this._telemetryReporter.sendAccountEvent([], type); + } + } + } + + /** + * See {@link onDidChangeSessions} for more information on how this is used. + * @param param0 Event that contains the added and removed accounts + */ + private _handleAccountChange({ added, deleted }: { added: AccountInfo[]; deleted: AccountInfo[] }) { + const process = (a: AccountInfo) => ({ + // This shouldn't be needed + accessToken: '1234', + id: a.homeAccountId, + scopes: [], + account: { + id: a.homeAccountId, + label: a.username + }, + idToken: a.idToken, + }); + this._onDidChangeSessionsEmitter.fire({ added: added.map(process), changed: [], removed: deleted.map(process) }); + } + + async getSessions(scopes: string[] | undefined, options?: AuthenticationGetSessionOptions): Promise { + this._logger.info('[getSessions]', scopes ?? 'all', 'starting'); + const modifiedScopes = scopes ? [...scopes] : []; + const clientId = this.getClientId(modifiedScopes); + const tenant = this.getTenantId(modifiedScopes); + this._addCommonScopes(modifiedScopes); + if (!scopes) { + const allSessions: AuthenticationSession[] = []; + for (const cachedPca of this._publicClientManager.getAll()) { + const sessions = await this.getAllSessionsForPca(cachedPca, modifiedScopes, modifiedScopes, options?.account); + allSessions.push(...sessions); + } + return allSessions; + } + + const cachedPca = await this.getOrCreatePublicClientApplication(clientId, tenant); + const sessions = await this.getAllSessionsForPca(cachedPca, scopes, modifiedScopes.filter(s => !s.startsWith('VSCODE_'), options?.account)); + this._logger.info(`[getSessions] returned ${sessions.length} sessions`); + return sessions; + + } + + async createSession(scopes: readonly string[]): Promise { + this._logger.info('[createSession]', scopes, 'starting'); + const modifiedScopes = scopes ? [...scopes] : []; + const clientId = this.getClientId(modifiedScopes); + const tenant = this.getTenantId(modifiedScopes); + this._addCommonScopes(modifiedScopes); + + const cachedPca = await this.getOrCreatePublicClientApplication(clientId, tenant); + let result: AuthenticationResult; + try { + result = await cachedPca.acquireTokenInteractive({ + openBrowser: async (url: string) => { await env.openExternal(Uri.parse(url)); }, + scopes: modifiedScopes, + successTemplate: loopbackTemplate, + // TODO: This is currently not working (the loopback client closes before this is rendered). + // There is an issue opened on MSAL Node to fix this. We should workaround this by re-implementing the Loopback client. + // errorTemplate: loopbackTemplate + }); + this.setupRefresh(cachedPca, result, scopes); + } catch (e) { + if (e instanceof CancellationError) { + const yes = l10n.t('Yes'); + const result = await window.showErrorMessage( + l10n.t('Having trouble logging in?'), + { + modal: true, + detail: l10n.t('Would you like to try a different way to sign in to your Microsoft account? ({0})', 'protocol handler') + }, + yes + ); + if (!result) { + this._telemetryReporter.sendLoginFailedEvent(); + throw e; + } + } + const loopbackClient = new UriHandlerLoopbackClient(this._uriHandler, redirectUri); + try { + result = await cachedPca.acquireTokenInteractive({ + openBrowser: (url: string) => loopbackClient.openBrowser(url), + scopes: modifiedScopes, + loopbackClient + }); + this.setupRefresh(cachedPca, result, scopes); + } catch (e) { + this._telemetryReporter.sendLoginFailedEvent(); + throw e; + } + } + + const session = this.toAuthenticationSession(result, scopes); + this._telemetryReporter.sendLoginEvent(session.scopes); + this._logger.info('[createSession]', scopes, 'returned session'); + return session; + } + + async removeSession(sessionId: string): Promise { + this._logger.info('[removeSession]', sessionId, 'starting'); + for (const cachedPca of this._publicClientManager.getAll()) { + const accounts = cachedPca.accounts; + for (const account of accounts) { + if (account.homeAccountId === sessionId) { + this._telemetryReporter.sendLogoutEvent(); + try { + await cachedPca.removeAccount(account); + } catch (e) { + this._telemetryReporter.sendLogoutFailedEvent(); + throw e; + } + this._logger.info('[removeSession]', sessionId, 'removed session'); + return; + } + } + } + this._logger.info('[removeSession]', sessionId, 'session not found'); + } + + private async getOrCreatePublicClientApplication(clientId: string, tenant: string): Promise { + const authority = new URL(tenant, this._env.activeDirectoryEndpointUrl).toString(); + return await this._publicClientManager.getOrCreate(clientId, authority); + } + + private _addCommonScopes(scopes: string[]) { + if (!scopes.includes('openid')) { + scopes.push('openid'); + } + if (!scopes.includes('email')) { + scopes.push('email'); + } + if (!scopes.includes('profile')) { + scopes.push('profile'); + } + if (!scopes.includes('offline_access')) { + scopes.push('offline_access'); + } + return scopes; + } + + private async getAllSessionsForPca( + cachedPca: ICachedPublicClientApplication, + originalScopes: readonly string[], + scopesToSend: string[], + accountFilter?: AuthenticationSessionAccountInformation + ): Promise { + const accounts = accountFilter + ? cachedPca.accounts.filter(a => a.homeAccountId === accountFilter.id) + : cachedPca.accounts; + const sessions: AuthenticationSession[] = []; + for (const account of accounts) { + const result = await cachedPca.acquireTokenSilent({ account, scopes: scopesToSend, redirectUri }); + this.setupRefresh(cachedPca, result, originalScopes); + sessions.push(this.toAuthenticationSession(result, originalScopes)); + } + return sessions; + } + + private setupRefresh(cachedPca: ICachedPublicClientApplication, result: AuthenticationResult, originalScopes: readonly string[]) { + const on = result.refreshOn || result.expiresOn; + if (!result.account || !on) { + return; + } + + const account = result.account; + const scopes = result.scopes; + const timeToRefresh = on.getTime() - Date.now() - 5 * 60 * 1000; // 5 minutes before expiry + const key = JSON.stringify({ accountId: account.homeAccountId, scopes }); + this._refreshDelayer.trigger(key, async () => { + const result = await cachedPca.acquireTokenSilent({ account, scopes, redirectUri, forceRefresh: true }); + this._onDidChangeSessionsEmitter.fire({ added: [], changed: [this.toAuthenticationSession(result, originalScopes)], removed: [] }); + }, timeToRefresh > 0 ? timeToRefresh : 0); + } + + //#region scope parsers + + private getClientId(scopes: string[]) { + return scopes.reduce((prev, current) => { + if (current.startsWith('VSCODE_CLIENT_ID:')) { + return current.split('VSCODE_CLIENT_ID:')[1]; + } + return prev; + }, undefined) ?? DEFAULT_CLIENT_ID; + } + + private getTenantId(scopes: string[]) { + return scopes.reduce((prev, current) => { + if (current.startsWith('VSCODE_TENANT:')) { + return current.split('VSCODE_TENANT:')[1]; + } + return prev; + }, undefined) ?? DEFAULT_TENANT; + } + + //#endregion + + private toAuthenticationSession(result: AuthenticationResult, scopes: readonly string[]): AuthenticationSession & { idToken: string } { + return { + accessToken: result.accessToken, + idToken: result.idToken, + id: result.account?.homeAccountId ?? result.uniqueId, + account: { + id: result.account?.homeAccountId ?? result.uniqueId, + label: result.account?.username ?? 'Unknown', + }, + scopes + }; + } +} + +class DelayerByKey { + private _delayers = new Map>(); + + trigger(key: string, fn: () => Promise, delay: number): Promise { + let delayer = this._delayers.get(key); + if (!delayer) { + delayer = new Delayer(delay); + this._delayers.set(key, delayer); + } + + return delayer.trigger(fn, delay); + } +} diff --git a/extensions/microsoft-authentication/src/node/authServer.ts b/extensions/microsoft-authentication/src/node/authServer.ts index de08c6fca0f..2d6a8d03861 100644 --- a/extensions/microsoft-authentication/src/node/authServer.ts +++ b/extensions/microsoft-authentication/src/node/authServer.ts @@ -110,20 +110,29 @@ export class LoopbackAuthServer implements ILoopbackServer { const code = reqUrl.searchParams.get('code') ?? undefined; const state = reqUrl.searchParams.get('state') ?? undefined; const nonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+'); + const error = reqUrl.searchParams.get('error') ?? undefined; + if (error) { + res.writeHead(302, { location: `/?error=${reqUrl.searchParams.get('error_description')}` }); + res.end(); + deferred.reject(new Error(error)); + break; + } if (!code || !state || !nonce) { res.writeHead(400); res.end(); - return; + break; } if (this.state !== state) { res.writeHead(302, { location: `/?error=${encodeURIComponent('State does not match.')}` }); res.end(); - throw new Error('State does not match.'); + deferred.reject(new Error('State does not match.')); + break; } if (this.nonce !== nonce) { res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` }); res.end(); - throw new Error('Nonce does not match.'); + deferred.reject(new Error('Nonce does not match.')); + break; } deferred.resolve({ code, state }); res.writeHead(302, { location: '/' }); diff --git a/extensions/microsoft-authentication/src/node/fetch.ts b/extensions/microsoft-authentication/src/node/fetch.ts deleted file mode 100644 index 58718078e69..00000000000 --- a/extensions/microsoft-authentication/src/node/fetch.ts +++ /dev/null @@ -1,7 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import fetch from 'node-fetch'; - -export const fetching = fetch; diff --git a/extensions/microsoft-authentication/src/node/loopbackTemplate.ts b/extensions/microsoft-authentication/src/node/loopbackTemplate.ts new file mode 100644 index 00000000000..df41b86d17c --- /dev/null +++ b/extensions/microsoft-authentication/src/node/loopbackTemplate.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export const loopbackTemplate = ` + + + + + + Microsoft Account - Sign In + + + + + + + Visual Studio Code + +
+
+ You are signed in now and can close this page. +
+
+ An error occurred while signing in: +
+
+
+ + + + +`; diff --git a/extensions/microsoft-authentication/src/node/publicClientCache.ts b/extensions/microsoft-authentication/src/node/publicClientCache.ts new file mode 100644 index 00000000000..8ee95ba5a0c --- /dev/null +++ b/extensions/microsoft-authentication/src/node/publicClientCache.ts @@ -0,0 +1,241 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AccountInfo, AuthenticationResult, Configuration, InteractiveRequest, PublicClientApplication, SilentFlowRequest } from '@azure/msal-node'; +import { SecretStorageCachePlugin } from '../common/cachePlugin'; +import { SecretStorage, LogOutputChannel, Disposable, SecretStorageChangeEvent, EventEmitter, Memento, window, ProgressLocation, l10n } from 'vscode'; +import { MsalLoggerOptions } from '../common/loggerOptions'; +import { ICachedPublicClientApplication, ICachedPublicClientApplicationManager } from '../common/publicClientCache'; +import { raceCancellationAndTimeoutError } from '../common/async'; + +export interface IPublicClientApplicationInfo { + clientId: string; + authority: string; +} + +const _keyPrefix = 'pca:'; + +export class CachedPublicClientApplicationManager implements ICachedPublicClientApplicationManager { + // The key is the clientId and authority stringified + private readonly _pcas = new Map(); + + private _initialized = false; + private _disposable: Disposable; + + constructor( + private readonly _globalMemento: Memento, + private readonly _secretStorage: SecretStorage, + private readonly _logger: LogOutputChannel, + private readonly _accountChangeHandler: (e: { added: AccountInfo[]; deleted: AccountInfo[] }) => void + ) { + this._disposable = _secretStorage.onDidChange(e => this._handleSecretStorageChange(e)); + } + + async initialize() { + this._logger.debug('Initializing PublicClientApplicationManager'); + const keys = await this._secretStorage.get('publicClientApplications'); + if (!keys) { + this._initialized = true; + return; + } + + const promises = new Array>(); + try { + for (const key of JSON.parse(keys) as string[]) { + try { + const { clientId, authority } = JSON.parse(key) as IPublicClientApplicationInfo; + // Load the PCA in memory + promises.push(this.getOrCreate(clientId, authority)); + } catch (e) { + // ignore + } + } + } catch (e) { + // data is corrupted + this._logger.error('Error initializing PublicClientApplicationManager:', e); + await this._secretStorage.delete('publicClientApplications'); + } + + // TODO: should we do anything for when this fails? + await Promise.allSettled(promises); + this._logger.debug('PublicClientApplicationManager initialized'); + this._initialized = true; + } + + dispose() { + this._disposable.dispose(); + Disposable.from(...this._pcas.values()).dispose(); + } + + async getOrCreate(clientId: string, authority: string): Promise { + if (!this._initialized) { + throw new Error('PublicClientApplicationManager not initialized'); + } + + // Use the clientId and authority as the key + const pcasKey = JSON.stringify({ clientId, authority }); + let pca = this._pcas.get(pcasKey); + if (pca) { + this._logger.debug(clientId, authority, 'PublicClientApplicationManager cache hit'); + return pca; + } + + this._logger.debug(clientId, authority, 'PublicClientApplicationManager cache miss, creating new PCA...'); + pca = new CachedPublicClientApplication(clientId, authority, this._globalMemento, this._secretStorage, this._accountChangeHandler, this._logger); + this._pcas.set(pcasKey, pca); + await pca.initialize(); + await this._storePublicClientApplications(); + this._logger.debug(clientId, authority, 'PublicClientApplicationManager PCA created'); + return pca; + } + + getAll(): ICachedPublicClientApplication[] { + if (!this._initialized) { + throw new Error('PublicClientApplicationManager not initialized'); + } + return Array.from(this._pcas.values()); + } + + private async _handleSecretStorageChange(e: SecretStorageChangeEvent) { + if (!e.key.startsWith(_keyPrefix)) { + return; + } + + this._logger.debug('PublicClientApplicationManager secret storage change:', e.key); + const result = await this._secretStorage.get(e.key); + const pcasKey = e.key.split(_keyPrefix)[1]; + + // If the cache was deleted, or the PCA has zero accounts left, remove the PCA + if (!result || this._pcas.get(pcasKey)?.accounts.length === 0) { + this._logger.debug('PublicClientApplicationManager removing PCA:', pcasKey); + this._pcas.delete(pcasKey); + await this._storePublicClientApplications(); + this._logger.debug('PublicClientApplicationManager PCA removed:', pcasKey); + return; + } + + // Load the PCA in memory if it's not already loaded + const { clientId, authority } = JSON.parse(pcasKey) as IPublicClientApplicationInfo; + this._logger.debug('PublicClientApplicationManager loading PCA:', pcasKey); + await this.getOrCreate(clientId, authority); + this._logger.debug('PublicClientApplicationManager PCA loaded:', pcasKey); + } + + private async _storePublicClientApplications() { + await this._secretStorage.store( + 'publicClientApplications', + JSON.stringify(Array.from(this._pcas.keys())) + ); + } +} + +class CachedPublicClientApplication implements ICachedPublicClientApplication { + private _pca: PublicClientApplication; + + private _accounts: AccountInfo[] = []; + private readonly _disposable: Disposable; + + private readonly _loggerOptions = new MsalLoggerOptions(this._logger); + private readonly _secretStorageCachePlugin = new SecretStorageCachePlugin( + this._secretStorage, + // Include the prefix in the key so we can easily identify it later + `${_keyPrefix}${JSON.stringify({ clientId: this._clientId, authority: this._authority })}` + ); + private readonly _config: Configuration = { + auth: { clientId: this._clientId, authority: this._authority }, + system: { + loggerOptions: { + loggerCallback: (level, message, containsPii) => this._loggerOptions.loggerCallback(level, message, containsPii), + } + }, + cache: { + cachePlugin: this._secretStorageCachePlugin + } + }; + + /** + * We keep track of the last time an account was removed so we can recreate the PCA if we detect that an account was removed. + * This is due to MSAL-node not providing a way to detect when an account is removed from the cache. An internal issue has been + * filed to track this. If MSAL-node ever provides a way to detect this or handle this better in the Persistant Cache Plugin, + * we can remove this logic. + */ + private _lastCreated: Date; + + constructor( + private readonly _clientId: string, + private readonly _authority: string, + private readonly _globalMemento: Memento, + private readonly _secretStorage: SecretStorage, + private readonly _accountChangeHandler: (e: { added: AccountInfo[]; deleted: AccountInfo[] }) => void, + private readonly _logger: LogOutputChannel + ) { + this._pca = new PublicClientApplication(this._config); + this._lastCreated = new Date(); + this._disposable = this._registerOnSecretStorageChanged(); + } + + get accounts(): AccountInfo[] { return this._accounts; } + get clientId(): string { return this._clientId; } + get authority(): string { return this._authority; } + + initialize(): Promise { + return this._update(); + } + + dispose(): void { + this._disposable.dispose(); + } + + acquireTokenSilent(request: SilentFlowRequest): Promise { + return this._pca.acquireTokenSilent(request); + } + + async acquireTokenInteractive(request: InteractiveRequest): Promise { + return await window.withProgress( + { + location: ProgressLocation.Notification, + cancellable: true, + title: l10n.t('Signing in to Microsoft...') + }, + (_process, token) => raceCancellationAndTimeoutError(this._pca.acquireTokenInteractive(request), token, 1000 * 60 * 5), // 5 minutes + ); + } + + removeAccount(account: AccountInfo): Promise { + this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, new Date()); + return this._pca.getTokenCache().removeAccount(account); + } + + private _registerOnSecretStorageChanged() { + return this._secretStorageCachePlugin.onDidChange(() => this._update()); + } + + private async _update() { + const before = this._accounts; + this._logger.debug(this._clientId, this._authority, 'CachedPublicClientApplication update before:', before.length); + // Dates are stored as strings in the memento + const lastRemovalDate = this._globalMemento.get(`lastRemoval:${this._clientId}:${this._authority}`); + if (lastRemovalDate && this._lastCreated && Date.parse(lastRemovalDate) > this._lastCreated.getTime()) { + this._logger.debug(this._clientId, this._authority, 'CachedPublicClientApplication removal detected... recreating PCA...'); + this._pca = new PublicClientApplication(this._config); + this._lastCreated = new Date(); + } + + const after = await this._pca.getAllAccounts(); + this._accounts = after; + this._logger.debug(this._clientId, this._authority, 'CachedPublicClientApplication update after:', after.length); + + const beforeSet = new Set(before.map(b => b.homeAccountId)); + const afterSet = new Set(after.map(a => a.homeAccountId)); + + const added = after.filter(a => !beforeSet.has(a.homeAccountId)); + const deleted = before.filter(b => !afterSet.has(b.homeAccountId)); + if (added.length > 0 || deleted.length > 0) { + this._accountChangeHandler({ added, deleted }); + this._logger.debug(this._clientId, this._authority, 'CachedPublicClientApplication accounts changed. added, deleted:', added.length, deleted.length); + } + this._logger.debug(this._clientId, this._authority, 'CachedPublicClientApplication update complete'); + } +} diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json index cad76d078bd..4b9d06d1847 100644 --- a/extensions/microsoft-authentication/tsconfig.json +++ b/extensions/microsoft-authentication/tsconfig.json @@ -22,7 +22,6 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.idToken.d.ts", - "../../src/vscode-dts/vscode.proposed.authGetSessions.d.ts" + "../../src/vscode-dts/vscode.proposed.idToken.d.ts" ] } diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index 6f277110a56..7c5d34e02a8 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -7,6 +7,20 @@ resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-env/-/ms-rest-azure-env-2.0.0.tgz#45809f89763a480924e21d3c620cd40866771625" integrity sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw== +"@azure/msal-common@14.14.0": + version "14.14.0" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.14.0.tgz#31a015070d5864ebcf9ebb988fcbc5c5536f22d1" + integrity sha512-OxcOk9H1/1fktHh6//VCORgSNJc2dCQObTm6JNmL824Z6iZSO6eFo/Bttxe0hETn9B+cr7gDouTQtsRq3YPuSQ== + +"@azure/msal-node@^2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.12.0.tgz#57ee6b6011a320046d72dc0828fec46278f2ab2c" + integrity sha512-jmk5Im5KujRA2AcyCb0awA3buV8niSrwXZs+NBJWIvxOz76RvNlusGIqi43A0h45BPUy93Qb+CPdpJn82NFTIg== + dependencies: + "@azure/msal-common" "14.14.0" + jsonwebtoken "^9.0.0" + uuid "^8.3.0" + "@microsoft/1ds-core-js@4.0.3", "@microsoft/1ds-core-js@^4.0.3": version "4.0.3" resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz#c8a92c623745a9595e06558a866658480c33bdf9" @@ -153,6 +167,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -165,6 +184,13 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + form-data@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" @@ -174,6 +200,74 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" @@ -186,32 +280,27 @@ mime-types@^2.1.12: dependencies: mime-db "1.44.0" -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +semver@^7.5.4: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" +uuid@^8.3.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== diff --git a/extensions/npm/src/features/packageJSONContribution.ts b/extensions/npm/src/features/packageJSONContribution.ts index a2f4fabcfe3..999f39664f1 100644 --- a/extensions/npm/src/features/packageJSONContribution.ts +++ b/extensions/npm/src/features/packageJSONContribution.ts @@ -293,7 +293,13 @@ export class PackageJSONContribution implements IJSONContribution { // COREPACK_ENABLE_PROJECT_SPEC makes the npm view command succeed // even if packageManager specified a package manager other than npm. const env = { ...process.env, COREPACK_ENABLE_AUTO_PIN: '0', COREPACK_ENABLE_PROJECT_SPEC: '0' }; - cp.execFile(npmCommandPath, args, { cwd, env }, (error, stdout) => { + let options: cp.ExecFileOptions = { cwd, env }; + let commandPath: string = npmCommandPath; + if (process.platform === 'win32') { + options = { cwd, env, shell: true }; + commandPath = `"${npmCommandPath}"`; + } + cp.execFile(commandPath, args, options, (error, stdout) => { if (!error) { try { const content = JSON.parse(stdout); diff --git a/extensions/package.json b/extensions/package.json index 940bbe9b8a2..c918c19c535 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,14 +4,14 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^5.5.2" + "typescript": "^5.5.4" }, "scripts": { "postinstall": "node ./postinstall.mjs" }, "devDependencies": { "@parcel/watcher": "2.1.0", - "esbuild": "0.20.0", + "esbuild": "0.23.0", "vscode-grammar-updater": "^1.1.0" }, "resolutions": { diff --git a/extensions/shellscript/cgmanifest.json b/extensions/shellscript/cgmanifest.json index 48f939ecc45..73c65b96f2c 100644 --- a/extensions/shellscript/cgmanifest.json +++ b/extensions/shellscript/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "jeff-hykin/better-shell-syntax", "repositoryUrl": "https://github.com/jeff-hykin/better-shell-syntax", - "commitHash": "6d0bc37a6b8023a5fddf75bd2b4eb1e1f962e4c2" + "commitHash": "35020b0bd79a90d3b262b4c13a8bb0b33adc1f45" } }, "license": "MIT", diff --git a/extensions/shellscript/package.json b/extensions/shellscript/package.json index 6f9e7072ab8..93333abd313 100644 --- a/extensions/shellscript/package.json +++ b/extensions/shellscript/package.json @@ -34,6 +34,7 @@ ".bash_profile", ".bash_login", ".ebuild", + ".eclass", ".profile", ".bash_logout", ".xprofile", diff --git a/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json b/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json index 7aae970d227..255638d7db7 100644 --- a/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json +++ b/extensions/shellscript/syntaxes/shell-unix-bash.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/jeff-hykin/better-shell-syntax/commit/6d0bc37a6b8023a5fddf75bd2b4eb1e1f962e4c2", + "version": "https://github.com/jeff-hykin/better-shell-syntax/commit/35020b0bd79a90d3b262b4c13a8bb0b33adc1f45", "name": "Shell Script", "scopeName": "source.shell", "patterns": [ @@ -1547,7 +1547,7 @@ "include": "#subshell_dollar" }, { - "begin": "(? { new TypeScriptVersion( TypeScriptVersionSource.Bundled, vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript/tsserver.web.js').toString(), - API.fromSimpleString('5.4.5'))); + API.fromSimpleString('5.5.4'))); let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined; const packageInfo = getPackageInfo(context); @@ -102,16 +101,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { await startPreloadWorkspaceContentsIfNeeded(context, logger); })); - if (supportsReadableByteStreams()) { - context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs(), { - isCaseSensitive: true, - isReadonly: false - })); - context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(), { - isCaseSensitive: true, - isReadonly: false - })); - } + context.subscriptions.push(registerAtaSupport(logger)); return getExtensionApi(onCompletionAccepted.event, pluginManager); } diff --git a/extensions/typescript-language-features/src/filesystems/ata.ts b/extensions/typescript-language-features/src/filesystems/ata.ts new file mode 100644 index 00000000000..b5e43244e1b --- /dev/null +++ b/extensions/typescript-language-features/src/filesystems/ata.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { conditionalRegistration, requireGlobalConfiguration } from '../languageFeatures/util/dependentRegistration'; +import { supportsReadableByteStreams } from '../utils/platform'; +import { AutoInstallerFs } from './autoInstallerFs'; +import { MemFs } from './memFs'; +import { Logger } from '../logging/logger'; + +export function registerAtaSupport(logger: Logger): vscode.Disposable { + if (!supportsReadableByteStreams()) { + return vscode.Disposable.from(); + } + + return conditionalRegistration([ + requireGlobalConfiguration('typescript', 'tsserver.web.typeAcquisition.enabled'), + ], () => { + return vscode.Disposable.from( + // Ata + vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs('global-typings', logger), { + isCaseSensitive: true, + isReadonly: false, + }), + + // Read accesses to node_modules + vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(logger), { + isCaseSensitive: true, + isReadonly: false + })); + }); +} diff --git a/extensions/typescript-language-features/src/filesystems/autoInstallerFs.ts b/extensions/typescript-language-features/src/filesystems/autoInstallerFs.ts index 4e69fce8cda..c26476a983d 100644 --- a/extensions/typescript-language-features/src/filesystems/autoInstallerFs.ts +++ b/extensions/typescript-language-features/src/filesystems/autoInstallerFs.ts @@ -3,50 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { PackageManager } from '@vscode/ts-package-manager'; +import { basename, join } from 'path'; import * as vscode from 'vscode'; -import { MemFs } from './memFs'; import { URI } from 'vscode-uri'; -import { PackageManager, FileSystem, packagePath } from '@vscode/ts-package-manager'; -import { join, basename, dirname } from 'path'; +import { Throttler } from '../utils/async'; +import { Disposable } from '../utils/dispose'; +import { MemFs } from './memFs'; +import { Logger } from '../logging/logger'; const TEXT_DECODER = new TextDecoder('utf-8'); const TEXT_ENCODER = new TextEncoder(); -export class AutoInstallerFs implements vscode.FileSystemProvider { +export class AutoInstallerFs extends Disposable implements vscode.FileSystemProvider { - private readonly memfs = new MemFs(); - private readonly fs: FileSystem; - private readonly projectCache = new Map>(); - private readonly watcher: vscode.FileSystemWatcher; - private readonly _emitter = new vscode.EventEmitter(); + private readonly memfs: MemFs; + private readonly packageManager: PackageManager; + private readonly _projectCache = new Map(); - readonly onDidChangeFile: vscode.Event = this._emitter.event; + private readonly _emitter = this._register(new vscode.EventEmitter()); + readonly onDidChangeFile = this._emitter.event; - constructor() { - this.watcher = vscode.workspace.createFileSystemWatcher('**/{package.json,package-lock.json,package-lock.kdl}'); - const handler = (uri: URI) => { - const root = dirname(uri.path); - if (this.projectCache.delete(root)) { - (async () => { - const pm = new PackageManager(this.fs); - const opts = await this.getInstallOpts(uri, root); - const proj = await pm.resolveProject(root, opts); - proj.pruneExtraneous(); - // TODO: should this fire on vscode-node-modules instead? - // NB(kmarchan): This should tell TSServer that there's - // been changes inside node_modules and it needs to - // re-evaluate things. - this._emitter.fire([{ - type: vscode.FileChangeType.Changed, - uri: uri.with({ path: join(root, 'node_modules') }) - }]); - })(); - } - }; - this.watcher.onDidChange(handler); - this.watcher.onDidCreate(handler); - this.watcher.onDidDelete(handler); - const memfs = this.memfs; + constructor( + private readonly logger: Logger + ) { + super(); + + const memfs = new MemFs('auto-installer', logger); + this.memfs = memfs; memfs.onDidChangeFile((e) => { this._emitter.fire(e.map(ev => ({ type: ev.type, @@ -54,7 +40,8 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { uri: ev.uri.with({ scheme: 'memfs' }) }))); }); - this.fs = { + + this.packageManager = new PackageManager({ readDirectory(path: string, _extensions?: readonly string[], _exclude?: readonly string[], _include?: readonly string[], _depth?: number): string[] { return memfs.readDirectory(URI.file(path)).map(([name, _]) => name); }, @@ -87,17 +74,18 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { return undefined; } } - }; + }); } watch(resource: vscode.Uri): vscode.Disposable { const mapped = URI.file(new MappedUri(resource).path); - console.log('watching', mapped); + this.logger.trace(`AutoInstallerFs.watch. Original: ${resource.toString()}, Mapped: ${mapped.toString()}`); return this.memfs.watch(mapped); } async stat(uri: vscode.Uri): Promise { - // console.log('stat', uri.toString()); + this.logger.trace(`AutoInstallerFs.stat: ${uri}`); + const mapped = new MappedUri(uri); // TODO: case sensitivity configuration @@ -119,7 +107,8 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { } async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { - // console.log('readDirectory', uri.toString()); + this.logger.trace(`AutoInstallerFs.readDirectory: ${uri}`); + const mapped = new MappedUri(uri); await this.ensurePackageContents(mapped); @@ -127,7 +116,8 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { } async readFile(uri: vscode.Uri): Promise { - // console.log('readFile', uri.toString()); + this.logger.trace(`AutoInstallerFs.readFile: ${uri}`); + const mapped = new MappedUri(uri); await this.ensurePackageContents(mapped); @@ -151,8 +141,6 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { } private async ensurePackageContents(incomingUri: MappedUri): Promise { - // console.log('ensurePackageContents', incomingUri.path); - // If we're not looking for something inside node_modules, bail early. if (!incomingUri.path.includes('node_modules')) { throw vscode.FileSystemError.FileNotFound(); @@ -164,25 +152,27 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { } const root = this.getProjectRoot(incomingUri.path); - - const pkgPath = packagePath(incomingUri.path); - if (!root || this.projectCache.get(root)?.has(pkgPath)) { + if (!root) { return; } - const proj = await (new PackageManager(this.fs)).resolveProject(root, await this.getInstallOpts(incomingUri.original, root)); + this.logger.trace(`AutoInstallerFs.ensurePackageContents. Path: ${incomingUri.path}, Root: ${root}`); - const restore = proj.restorePackageAt(incomingUri.path); - try { - await restore; - } catch (e) { - console.error(`failed to restore package at ${incomingUri.path}: `, e); - throw e; + let projectEntry = this._projectCache.get(root); + if (!projectEntry) { + projectEntry = { throttler: new Throttler() }; + this._projectCache.set(root, projectEntry); } - if (!this.projectCache.has(root)) { - this.projectCache.set(root, new Set()); - } - this.projectCache.get(root)!.add(pkgPath); + + projectEntry.throttler.queue(async () => { + const proj = await this.packageManager.resolveProject(root, await this.getInstallOpts(incomingUri.original, root)); + try { + await proj.restore(); + } catch (e) { + console.error(`failed to restore package at ${incomingUri.path}: `, e); + throw e; + } + }); } private async getInstallOpts(originalUri: URI, root: string) { @@ -213,9 +203,6 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { const pkgPath = path.match(/(^.*)\/node_modules/); return pkgPath?.[1]; } - - // --- manage file events - } class MappedUri { diff --git a/extensions/typescript-language-features/src/filesystems/memFs.ts b/extensions/typescript-language-features/src/filesystems/memFs.ts index eeeb60e957d..05c4e7c3db7 100644 --- a/extensions/typescript-language-features/src/filesystems/memFs.ts +++ b/extensions/typescript-language-features/src/filesystems/memFs.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import { basename, dirname } from 'path'; +import * as vscode from 'vscode'; +import { Logger } from '../logging/logger'; export class MemFs implements vscode.FileSystemProvider { @@ -14,8 +15,13 @@ export class MemFs implements vscode.FileSystemProvider { 0, ); + constructor( + private readonly id: string, + private readonly logger: Logger, + ) { } + stat(uri: vscode.Uri): vscode.FileStat { - // console.log('stat', uri.toString()); + this.logger.trace(`MemFs.stat ${this.id}. uri: ${uri}`); const entry = this.getEntry(uri); if (!entry) { throw vscode.FileSystemError.FileNotFound(); @@ -25,7 +31,7 @@ export class MemFs implements vscode.FileSystemProvider { } readDirectory(uri: vscode.Uri): [string, vscode.FileType][] { - // console.log('readDirectory', uri.toString()); + this.logger.trace(`MemFs.readDirectory ${this.id}. uri: ${uri}`); const entry = this.getEntry(uri); if (!entry) { @@ -39,7 +45,7 @@ export class MemFs implements vscode.FileSystemProvider { } readFile(uri: vscode.Uri): Uint8Array { - // console.log('readFile', uri.toString()); + this.logger.trace(`MemFs.readFile ${this.id}. uri: ${uri}`); const entry = this.getEntry(uri); if (!entry) { @@ -54,7 +60,7 @@ export class MemFs implements vscode.FileSystemProvider { } writeFile(uri: vscode.Uri, content: Uint8Array, { create, overwrite }: { create: boolean; overwrite: boolean }): void { - // console.log('writeFile', uri.toString()); + this.logger.trace(`MemFs.writeFile ${this.id}. uri: ${uri}`); const dir = this.getParent(uri); @@ -98,7 +104,8 @@ export class MemFs implements vscode.FileSystemProvider { } createDirectory(uri: vscode.Uri): void { - // console.log('createDirectory', uri.toString()); + this.logger.trace(`MemFs.createDirectory ${this.id}. uri: ${uri}`); + const dir = this.getParent(uri); const now = Date.now() / 1000; dir.contents.set(basename(uri.path), new FsDirectoryEntry(new Map(), now, now)); diff --git a/extensions/typescript-language-features/src/languageFeatures/completions.ts b/extensions/typescript-language-features/src/languageFeatures/completions.ts index 708d7e028dd..038fb447da9 100644 --- a/extensions/typescript-language-features/src/languageFeatures/completions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/completions.ts @@ -58,6 +58,7 @@ class MyCompletionItem extends vscode.CompletionItem { private readonly completionContext: CompletionContext, public readonly metadata: any | undefined, client: ITypeScriptServiceClient, + defaultCommitCharacters: readonly string[] | undefined, ) { const label = tsEntry.name || (tsEntry.insertText ?? ''); super(label, MyCompletionItem.convertKind(tsEntry.kind)); @@ -93,7 +94,7 @@ class MyCompletionItem extends vscode.CompletionItem { this.useCodeSnippet = completionContext.completeFunctionCalls && (this.kind === vscode.CompletionItemKind.Function || this.kind === vscode.CompletionItemKind.Method); this.range = this.getRangeFromReplacementSpan(tsEntry, completionContext); - this.commitCharacters = MyCompletionItem.getCommitCharacters(completionContext, tsEntry); + this.commitCharacters = MyCompletionItem.getCommitCharacters(completionContext, tsEntry, defaultCommitCharacters); this.insertText = isSnippet && tsEntry.insertText ? new vscode.SnippetString(tsEntry.insertText) : tsEntry.insertText; this.filterText = tsEntry.filterText || this.getFilterText(completionContext.line, tsEntry.insertText); @@ -500,7 +501,22 @@ class MyCompletionItem extends vscode.CompletionItem { } } - private static getCommitCharacters(context: CompletionContext, entry: Proto.CompletionEntry): string[] | undefined { + private static getCommitCharacters( + context: CompletionContext, + entry: Proto.CompletionEntry, + defaultCommitCharacters: readonly string[] | undefined): string[] | undefined { + // @ts-expect-error until TS 5.6 + let commitCharacters = entry.commitCharacters ?? defaultCommitCharacters; + if (commitCharacters) { + if (context.enableCallCompletions + && !context.isNewIdentifierLocation + && entry.kind !== PConst.Kind.warning + && entry.kind !== PConst.Kind.string) { + commitCharacters.push('('); + } + return commitCharacters; + } + if (entry.kind === PConst.Kind.warning || entry.kind === PConst.Kind.string) { // Ambient JS word based suggestion, strings return undefined; } @@ -509,7 +525,7 @@ class MyCompletionItem extends vscode.CompletionItem { return undefined; } - const commitCharacters: string[] = ['.', ',', ';']; + commitCharacters = ['.', ',', ';']; if (context.enableCallCompletions) { commitCharacters.push('('); } @@ -744,6 +760,7 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< let response: ServerResponse.Response | undefined; let duration: number | undefined; let optionalReplacementRange: vscode.Range | undefined; + let defaultCommitCharacters: string[] | undefined; if (this.client.apiVersion.gte(API.v300)) { const startTime = Date.now(); try { @@ -769,6 +786,8 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< isIncomplete = !!response.body.isIncomplete || (response.metadata as any)?.isIncomplete; entries = response.body.entries; metadata = response.metadata; + // @ts-expect-error until TS 5.6 + defaultCommitCharacters = response.body.defaultCommitCharacters; if (response.body.optionalReplacementSpan) { optionalReplacementRange = typeConverters.Range.fromTextSpan(response.body.optionalReplacementSpan); @@ -799,7 +818,14 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< const items: MyCompletionItem[] = []; for (const entry of entries) { if (!shouldExcludeCompletionEntry(entry, completionConfiguration)) { - const item = new MyCompletionItem(position, document, entry, completionContext, metadata, this.client); + const item = new MyCompletionItem( + position, + document, + entry, + completionContext, + metadata, + this.client, + defaultCommitCharacters); item.command = { command: ApplyCompletionCommand.ID, title: '', diff --git a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts index 83a7bb38639..3e2e61a8a52 100644 --- a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts +++ b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts @@ -106,7 +106,7 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { pasteLocations: ranges.map(typeConverters.Range.toTextSpan), copiedFrom }, token)); - if (response.type !== 'response' || !response.body || token.isCancellationRequested) { + if (response.type !== 'response' || !response.body?.edits.length || token.isCancellationRequested) { return; } diff --git a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts index 990aefdfa56..032b2467674 100644 --- a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts @@ -11,6 +11,8 @@ import { ResourceMap } from '../utils/resourceMap'; import { TelemetryReporter } from '../logging/telemetry'; import { TypeScriptServiceConfiguration } from '../configuration/configuration'; import { equals } from '../utils/objects'; +// @ts-expect-error until ts 5.6 +import { DiagnosticPerformanceData as TsDiagnosticPerformanceData } from '../tsServer/protocol/protocol'; function diagnosticsEquals(a: vscode.Diagnostic, b: vscode.Diagnostic): boolean { if (a === b) { @@ -173,6 +175,10 @@ class DiagnosticSettings { } } +interface DiagnosticPerformanceData extends TsDiagnosticPerformanceData { + fileLineCount?: number; +} + class DiagnosticsTelemetryManager extends Disposable { private readonly _diagnosticCodesMap = new Map(); @@ -194,6 +200,37 @@ class DiagnosticsTelemetryManager extends Disposable { this._registerTelemetryEventEmitter(); } + public logDiagnosticsPerformanceTelemetry(performanceData: DiagnosticPerformanceData[]): void { + for (const data of performanceData) { + /* __GDPR__ + "diagnostics.performance" : { + "owner": "mjbvz", + "syntaxDiagDuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "semanticDiagDuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "suggestionDiagDuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "regionSemanticDiagDuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "fileLineCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "${include}": [ + "${TypeScriptCommonProperties}" + ] + } + */ + this._telemetryReporter.logTelemetry('diagnostics.performance', + { + // @ts-expect-error until ts 5.6 + syntaxDiagDuration: data.syntaxDiag, + // @ts-expect-error until ts 5.6 + semanticDiagDuration: data.semanticDiag, + // @ts-expect-error until ts 5.6 + suggestionDiagDuration: data.suggestionDiag, + // @ts-expect-error until ts 5.6 + regionSemanticDiagDuration: data.regionSemanticDiag, + fileLineCount: data.fileLineCount, + }, + ); + } + } + private _updateAllDiagnosticCodesAfterTimeout() { clearTimeout(this._timeout); this._timeout = setTimeout(() => this._updateDiagnosticCodes(), 5000); @@ -257,6 +294,8 @@ export class DiagnosticsManager extends Disposable { private readonly _updateDelay = 50; + private readonly _diagnosticsTelemetryManager: DiagnosticsTelemetryManager | undefined; + constructor( owner: string, configuration: TypeScriptServiceConfiguration, @@ -270,7 +309,7 @@ export class DiagnosticsManager extends Disposable { this._currentDiagnostics = this._register(vscode.languages.createDiagnosticCollection(owner)); // Here we are selecting only 1 user out of 1000 to send telemetry diagnostics if (Math.random() * 1000 <= 1 || configuration.enableDiagnosticsTelemetry) { - this._register(new DiagnosticsTelemetryManager(telemetryReporter, this._currentDiagnostics)); + this._diagnosticsTelemetryManager = this._register(new DiagnosticsTelemetryManager(telemetryReporter, this._currentDiagnostics)); } } @@ -349,6 +388,10 @@ export class DiagnosticsManager extends Disposable { return this._currentDiagnostics.get(file) || []; } + public logDiagnosticsPerformanceTelemetry(performanceData: DiagnosticPerformanceData[]): void { + this._diagnosticsTelemetryManager?.logDiagnosticsPerformanceTelemetry(performanceData); + } + private scheduleDiagnosticsUpdate(file: vscode.Uri) { if (!this._pendingUpdates.has(file)) { this._pendingUpdates.set(file, setTimeout(() => this.updateCurrentDiagnostics(file), this._updateDelay)); diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index 32707f1c049..2bb4d39ef26 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -662,6 +662,10 @@ export default class BufferSyncSupport extends Disposable { this.synchronizer.beforeCommand(command); } + public lineCount(resource: vscode.Uri): number | undefined { + return this.syncedBuffers.get(resource)?.lineCount; + } + private onDidCloseTextDocument(document: vscode.TextDocument): void { this.closeResource(document.uri); } diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts index f1b0cca26a4..ed4806e6fdd 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts @@ -92,6 +92,7 @@ export enum EventName { createFileWatcher = 'createFileWatcher', createDirectoryWatcher = 'createDirectoryWatcher', closeFileWatcher = 'closeFileWatcher', + requestCompleted = 'requestCompleted', } export enum OrganizeImportsMode { diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index 883aa6830bd..095295030b5 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -166,6 +166,10 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { this._tracer.traceRequestCompleted(this._serverId, 'requestCompleted', seq, callback); callback.onSuccess(undefined); } + // @ts-expect-error until ts 5.6 + if ((event as Proto.RequestCompletedEvent).body.performanceData) { + this._onEvent.fire(event); + } } else { this._tracer.traceEvent(this._serverId, event); this._onEvent.fire(event); diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts index 71daf1fb0b6..5adf1866112 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts @@ -40,7 +40,7 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory { version: TypeScriptVersion, args: readonly string[], kind: TsServerProcessKind, - _configuration: TypeScriptServiceConfiguration, + configuration: TypeScriptServiceConfiguration, _versionManager: TypeScriptVersionManager, _nodeVersionManager: NodeVersionManager, tsServerLog: TsServerLog | undefined, @@ -50,10 +50,10 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory { ...args, // Explicitly give TS Server its path so it can load local resources '--executingFilePath', tsServerPath, + // Enable/disable web type acquisition + (configuration.webTypeAcquisitionEnabled && supportsReadableByteStreams() ? '--experimentalTypeAcquisition' : '--disableAutomaticTypingAcquisition'), ]; - if (_configuration.webTypeAcquisitionEnabled && supportsReadableByteStreams()) { - launchArgs.push('--experimentalTypeAcquisition'); - } + return new WorkerServerProcess(kind, tsServerPath, this._extensionUri, launchArgs, tsServerLog, this._logger); } } diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 2435f7725a9..1ed70f1cdc7 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -30,6 +30,7 @@ import { TypeScriptVersionManager } from './tsServer/versionManager'; import { ITypeScriptVersionProvider, TypeScriptVersion } from './tsServer/versionProvider'; import { ClientCapabilities, ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService'; import { Disposable, DisposableStore, disposeAll } from './utils/dispose'; +import { hash } from './utils/hash'; import { isWeb, isWebAndHasSharedArrayBuffers } from './utils/platform'; @@ -141,7 +142,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType constructor( private readonly context: vscode.ExtensionContext, - onCaseInsenitiveFileSystem: boolean, + onCaseInsensitiveFileSystem: boolean, services: { pluginManager: PluginManager; logDirectoryProvider: ILogDirectoryProvider; @@ -191,7 +192,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.restartTsServer(); })); - this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds, onCaseInsenitiveFileSystem); + this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds, onCaseInsensitiveFileSystem); this.onReady(() => { this.bufferSyncSupport.listen(); }); this.bufferSyncSupport.onDelete(resource => { @@ -232,7 +233,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType return this.apiVersion.fullVersionString; }); - this.diagnosticsManager = new DiagnosticsManager('typescript', this._configuration, this.telemetryReporter, onCaseInsenitiveFileSystem); + this.diagnosticsManager = new DiagnosticsManager('typescript', this._configuration, this.telemetryReporter, onCaseInsensitiveFileSystem); this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this._versionManager, this._nodeVersionManager, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer, this.processFactory); this._register(this.pluginManager.onDidUpdateConfig(update => { @@ -424,17 +425,31 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.serverState = new ServerState.Running(handle, apiVersion, undefined, true); this.lastStart = Date.now(); + + /* __GDPR__FRAGMENT__ + "TypeScriptServerEnvCommonProperties" : { + "hasGlobalPlugins": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "globalPluginNameHashes": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + const typeScriptServerEnvCommonProperties = { + hasGlobalPlugins: this.pluginManager.plugins.length > 0, + globalPluginNameHashes: JSON.stringify(this.pluginManager.plugins.map(plugin => hash(plugin.name))), + }; + /* __GDPR__ "tsserver.spawned" : { "owner": "mjbvz", "${include}": [ - "${TypeScriptCommonProperties}" + "${TypeScriptCommonProperties}", + "${TypeScriptServerEnvCommonProperties}" ], "localTypeScriptVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "typeScriptVersionSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ this.logTelemetry('tsserver.spawned', { + ...typeScriptServerEnvCommonProperties, localTypeScriptVersion: this.versionProvider.localVersion ? this.versionProvider.localVersion.displayName : '', typeScriptVersionSource: version.source, }); @@ -459,11 +474,14 @@ export default class TypeScriptServiceClient extends Disposable implements IType "tsserver.error" : { "owner": "mjbvz", "${include}": [ - "${TypeScriptCommonProperties}" + "${TypeScriptCommonProperties}", + "${TypeScriptServerEnvCommonProperties}" ] } */ - this.logTelemetry('tsserver.error'); + this.logTelemetry('tsserver.error', { + ...typeScriptServerEnvCommonProperties + }); this.serviceExited(false, apiVersion); }); @@ -476,14 +494,19 @@ export default class TypeScriptServiceClient extends Disposable implements IType /* __GDPR__ "tsserver.exitWithCode" : { "owner": "mjbvz", - "code" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "signal" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "${include}": [ - "${TypeScriptCommonProperties}" - ] + "${TypeScriptCommonProperties}", + "${TypeScriptServerEnvCommonProperties}" + ], + "code" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "signal" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } */ - this.logTelemetry('tsserver.exitWithCode', { code: code ?? undefined, signal: signal ?? undefined }); + this.logTelemetry('tsserver.exitWithCode', { + ...typeScriptServerEnvCommonProperties, + code: code ?? undefined, + signal: signal ?? undefined, + }); if (this.token !== mytoken) { // this is coming from an old process @@ -963,16 +986,16 @@ export default class TypeScriptServiceClient extends Disposable implements IType spans: diagnosticEvent.body.spans, }); } - break; + return; } case EventName.configFileDiag: this._onConfigDiagnosticsReceived.fire(event as Proto.ConfigFileDiagnosticEvent); - break; + return; case EventName.telemetry: { const body = (event as Proto.TelemetryEvent).body; this.dispatchTelemetryEvent(body); - break; + return; } case EventName.projectLanguageServiceState: { const body = (event as Proto.ProjectLanguageServiceStateEvent).body!; @@ -980,7 +1003,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.serverState.updateLanguageServiceEnabled(body.languageServiceEnabled); } this._onProjectLanguageServiceStateChanged.fire(body); - break; + return; } case EventName.projectsUpdatedInBackground: { this.loadingIndicator.reset(); @@ -988,56 +1011,84 @@ export default class TypeScriptServiceClient extends Disposable implements IType const body = (event as Proto.ProjectsUpdatedInBackgroundEvent).body; const resources = body.openFiles.map(file => this.toResource(file)); this.bufferSyncSupport.getErr(resources); - break; + return; } case EventName.beginInstallTypes: this._onDidBeginInstallTypings.fire((event as Proto.BeginInstallTypesEvent).body); - break; + return; case EventName.endInstallTypes: this._onDidEndInstallTypings.fire((event as Proto.EndInstallTypesEvent).body); - break; + return; case EventName.typesInstallerInitializationFailed: this._onTypesInstallerInitializationFailed.fire((event as Proto.TypesInstallerInitializationFailedEvent).body); - break; + return; case EventName.surveyReady: this._onSurveyReady.fire((event as Proto.SurveyReadyEvent).body); - break; + return; case EventName.projectLoadingStart: this.loadingIndicator.startedLoadingProject((event as Proto.ProjectLoadingStartEvent).body.projectName); - break; + return; case EventName.projectLoadingFinish: this.loadingIndicator.finishedLoadingProject((event as Proto.ProjectLoadingFinishEvent).body.projectName); - break; + return; + + case EventName.createDirectoryWatcher: { + const path = (event.body as Proto.CreateDirectoryWatcherEventBody).path; + if (path.startsWith(inMemoryResourcePrefix)) { + return; + } - case EventName.createDirectoryWatcher: this.createFileSystemWatcher( (event.body as Proto.CreateDirectoryWatcherEventBody).id, new vscode.RelativePattern( - vscode.Uri.file((event.body as Proto.CreateDirectoryWatcherEventBody).path), + vscode.Uri.file(path), (event.body as Proto.CreateDirectoryWatcherEventBody).recursive ? '**' : '*' ), (event.body as Proto.CreateDirectoryWatcherEventBody).ignoreUpdate ); - break; + return; + } + case EventName.createFileWatcher: { + const path = (event.body as Proto.CreateFileWatcherEventBody).path; + if (path.startsWith(inMemoryResourcePrefix)) { + return; + } - case EventName.createFileWatcher: this.createFileSystemWatcher( (event.body as Proto.CreateFileWatcherEventBody).id, new vscode.RelativePattern( - vscode.Uri.file((event.body as Proto.CreateFileWatcherEventBody).path), + vscode.Uri.file(path), '*' ) ); - break; - + return; + } case EventName.closeFileWatcher: this.closeFileSystemWatcher(event.body.id); - break; + return; + + case EventName.requestCompleted: { + // @ts-expect-error until ts 5.6 + const diagnosticsDuration = (event.body as Proto.RequestCompletedEventBody).performanceData?.diagnosticsDuration; + if (diagnosticsDuration) { + this.diagnosticsManager.logDiagnosticsPerformanceTelemetry( + // @ts-expect-error until ts 5.6 + diagnosticsDuration.map(fileData => { + const resource = this.toResource(fileData.file); + return { + ...fileData, + fileLineCount: this.bufferSyncSupport.lineCount(resource), + }; + }) + ); + } + return; + } } } @@ -1121,13 +1172,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.watches.set(id, disposable); } - private closeFileSystemWatcher( - id: number, - ) { + private closeFileSystemWatcher(id: number) { const existing = this.watches.get(id); - if (existing) { - existing.dispose(); - } + existing?.dispose(); } private dispatchTelemetryEvent(telemetryData: Proto.TelemetryEventBody): void { @@ -1161,6 +1208,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType break; } } + + // Add plugin data here if (telemetryData.telemetryEventName === 'projectInfo') { if (this.serverState.type === ServerState.Type.Running) { this.serverState.updateTsserverVersion(properties['version']); diff --git a/extensions/typescript-language-features/src/ui/activeJsTsEditorTracker.ts b/extensions/typescript-language-features/src/ui/activeJsTsEditorTracker.ts index 3da1f5a8570..c3cae6321ff 100644 --- a/extensions/typescript-language-features/src/ui/activeJsTsEditorTracker.ts +++ b/extensions/typescript-language-features/src/ui/activeJsTsEditorTracker.ts @@ -28,6 +28,7 @@ export class ActiveJsTsEditorTracker extends Disposable { this._register(vscode.window.onDidChangeActiveTextEditor(_ => this.update())); this._register(vscode.window.onDidChangeVisibleTextEditors(_ => this.update())); + this._register(vscode.window.tabGroups.onDidChangeTabGroups(_ => this.update())); this.update(); } diff --git a/extensions/typescript-language-features/src/ui/intellisenseStatus.ts b/extensions/typescript-language-features/src/ui/intellisenseStatus.ts index 1a6ea63f427..e26e2b3719f 100644 --- a/extensions/typescript-language-features/src/ui/intellisenseStatus.ts +++ b/extensions/typescript-language-features/src/ui/intellisenseStatus.ts @@ -43,6 +43,8 @@ namespace IntellisenseState { export type State = typeof None | Pending | Resolved | typeof SyntaxOnly; } +type CreateOrOpenConfigCommandArgs = [root: vscode.Uri, projectType: ProjectType]; + export class IntellisenseStatus extends Disposable { public readonly openOpenConfigCommandId = '_typescript.openConfig'; @@ -62,7 +64,7 @@ export class IntellisenseStatus extends Disposable { commandManager.register({ id: this.openOpenConfigCommandId, - execute: async (root: vscode.Uri, projectType: ProjectType) => { + execute: async (...[root, projectType]: CreateOrOpenConfigCommandArgs) => { if (this._state.type === IntellisenseState.Type.Resolved) { await openProjectConfigOrPromptToCreate(projectType, this._client, root, this._state.configFile); } else if (this._state.type === IntellisenseState.Type.Pending) { @@ -72,7 +74,7 @@ export class IntellisenseStatus extends Disposable { }); commandManager.register({ id: this.createOrOpenConfigCommandId, - execute: async (root: vscode.Uri, projectType: ProjectType) => { + execute: async (...[root, projectType]: CreateOrOpenConfigCommandArgs) => { await openOrCreateConfig(this._client.apiVersion, projectType, root, this._client.configuration); }, }); @@ -182,7 +184,7 @@ export class IntellisenseStatus extends Disposable { title: this._state.projectType === ProjectType.TypeScript ? vscode.l10n.t("Configure tsconfig") : vscode.l10n.t("Configure jsconfig"), - arguments: [rootPath], + arguments: [rootPath, this._state.projectType] satisfies CreateOrOpenConfigCommandArgs, }; } else { statusItem.text = vscode.workspace.asRelativePath(this._state.configFile); @@ -190,7 +192,7 @@ export class IntellisenseStatus extends Disposable { statusItem.command = { command: this.openOpenConfigCommandId, title: vscode.l10n.t("Open config file"), - arguments: [rootPath], + arguments: [rootPath, this._state.projectType] satisfies CreateOrOpenConfigCommandArgs, }; } break; diff --git a/extensions/typescript-language-features/src/ui/managedFileContext.ts b/extensions/typescript-language-features/src/ui/managedFileContext.ts index 0b929f85277..1da4588a334 100644 --- a/extensions/typescript-language-features/src/ui/managedFileContext.ts +++ b/extensions/typescript-language-features/src/ui/managedFileContext.ts @@ -10,7 +10,7 @@ import { isSupportedLanguageMode } from '../configuration/languageIds'; import { Disposable } from '../utils/dispose'; import { ActiveJsTsEditorTracker } from './activeJsTsEditorTracker'; -/**E +/** * When clause context set when the current file is managed by vscode's built-in typescript extension. */ export default class ManagedFileContextManager extends Disposable { diff --git a/extensions/typescript-language-features/src/utils/async.ts b/extensions/typescript-language-features/src/utils/async.ts index db92754fd2e..9523d7fe67a 100644 --- a/extensions/typescript-language-features/src/utils/async.ts +++ b/extensions/typescript-language-features/src/utils/async.ts @@ -70,3 +70,94 @@ export function setImmediate(callback: (...args: any[]) => void, ...args: any[]) return { dispose: () => clearTimeout(handle) }; } } + + +/** + * A helper to prevent accumulation of sequential async tasks. + * + * Imagine a mail man with the sole task of delivering letters. As soon as + * a letter submitted for delivery, he drives to the destination, delivers it + * and returns to his base. Imagine that during the trip, N more letters were submitted. + * When the mail man returns, he picks those N letters and delivers them all in a + * single trip. Even though N+1 submissions occurred, only 2 deliveries were made. + * + * The throttler implements this via the queue() method, by providing it a task + * factory. Following the example: + * + * const throttler = new Throttler(); + * const letters = []; + * + * function deliver() { + * const lettersToDeliver = letters; + * letters = []; + * return makeTheTrip(lettersToDeliver); + * } + * + * function onLetterReceived(l) { + * letters.push(l); + * throttler.queue(deliver); + * } + */ +export class Throttler { + + private activePromise: Promise | null; + private queuedPromise: Promise | null; + private queuedPromiseFactory: ITask> | null; + + private isDisposed = false; + + constructor() { + this.activePromise = null; + this.queuedPromise = null; + this.queuedPromiseFactory = null; + } + + queue(promiseFactory: ITask>): Promise { + if (this.isDisposed) { + return Promise.reject(new Error('Throttler is disposed')); + } + + if (this.activePromise) { + this.queuedPromiseFactory = promiseFactory; + + if (!this.queuedPromise) { + const onComplete = () => { + this.queuedPromise = null; + + if (this.isDisposed) { + return; + } + + const result = this.queue(this.queuedPromiseFactory!); + this.queuedPromiseFactory = null; + + return result; + }; + + this.queuedPromise = new Promise(resolve => { + this.activePromise!.then(onComplete, onComplete).then(resolve); + }); + } + + return new Promise((resolve, reject) => { + this.queuedPromise!.then(resolve, reject); + }); + } + + this.activePromise = promiseFactory(); + + return new Promise((resolve, reject) => { + this.activePromise!.then((result: T) => { + this.activePromise = null; + resolve(result); + }, (err: unknown) => { + this.activePromise = null; + reject(err); + }); + }); + } + + dispose(): void { + this.isDisposed = true; + } +} diff --git a/extensions/typescript-language-features/src/utils/hash.ts b/extensions/typescript-language-features/src/utils/hash.ts new file mode 100644 index 00000000000..b009808968d --- /dev/null +++ b/extensions/typescript-language-features/src/utils/hash.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Return a hash value for an object. + */ +export function hash(obj: any, hashVal = 0): number { + switch (typeof obj) { + case 'object': + if (obj === null) { + return numberHash(349, hashVal); + } else if (Array.isArray(obj)) { + return arrayHash(obj, hashVal); + } + return objectHash(obj, hashVal); + case 'string': + return stringHash(obj, hashVal); + case 'boolean': + return booleanHash(obj, hashVal); + case 'number': + return numberHash(obj, hashVal); + case 'undefined': + return 937 * 31; + default: + return numberHash(obj, 617); + } +} + +function numberHash(val: number, initialHashVal: number): number { + return (((initialHashVal << 5) - initialHashVal) + val) | 0; // hashVal * 31 + ch, keep as int32 +} + +function booleanHash(b: boolean, initialHashVal: number): number { + return numberHash(b ? 433 : 863, initialHashVal); +} + +function stringHash(s: string, hashVal: number) { + hashVal = numberHash(149417, hashVal); + for (let i = 0, length = s.length; i < length; i++) { + hashVal = numberHash(s.charCodeAt(i), hashVal); + } + return hashVal; +} + +function arrayHash(arr: any[], initialHashVal: number): number { + initialHashVal = numberHash(104579, initialHashVal); + return arr.reduce((hashVal, item) => hash(item, hashVal), initialHashVal); +} + +function objectHash(obj: any, initialHashVal: number): number { + initialHashVal = numberHash(181387, initialHashVal); + return Object.keys(obj).sort().reduce((hashVal, key) => { + hashVal = stringHash(key, hashVal); + return hash(obj[key], hashVal); + }, initialHashVal); +} diff --git a/extensions/typescript-language-features/web/src/serverHost.ts b/extensions/typescript-language-features/web/src/serverHost.ts index f2f9ca95996..dedec85991f 100644 --- a/extensions/typescript-language-features/web/src/serverHost.ts +++ b/extensions/typescript-language-features/web/src/serverHost.ts @@ -11,6 +11,7 @@ import { FileWatcherManager } from './fileWatcherManager'; import { Logger } from './logging'; import { PathMapper, looksLikeNodeModules, mapUri } from './pathMapper'; import { findArgument, hasArgument } from './util/args'; +import { URI } from 'vscode-uri'; type ServerHostWithImport = ts.server.ServerHost & { importPlugin(root: string, moduleName: string): Promise }; @@ -338,13 +339,24 @@ function createServerHost( // For module resolution only. `node_modules` is also automatically mapped // as if all node_modules-like paths are symlinked. function realpath(path: string): string { + if (path.startsWith('/^/')) { + // In memory file. No mapping needed + return path; + } + const isNm = looksLikeNodeModules(path) && !path.startsWith('/vscode-global-typings/'); // skip paths without .. or ./ or /. And things that look like node_modules if (!isNm && !path.match(/\.\.|\/\.|\.\//)) { return path; } - let uri = pathMapper.toResource(path); + let uri: URI; + try { + uri = pathMapper.toResource(path); + } catch { + return path; + } + if (isNm) { uri = mapUri(uri, 'vscode-node-modules'); } diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 61828a784fb..8a1032fef2c 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -47,7 +47,6 @@ "telemetry", "terminalDataWriteEvent", "terminalDimensions", - "terminalShellIntegration", "testObserver", "textSearchProvider", "timeline", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts index a5a83c8be45..ac6287c2f35 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts @@ -10,7 +10,8 @@ import { assertNoRpc } from '../utils'; // Terminal integration tests are disabled on web https://github.com/microsoft/vscode/issues/92826 // Windows images will often not have functional shell integration -(env.uiKind === UIKind.Web || platform() === 'win32' ? suite.skip : suite)('vscode API - Terminal.shellIntegration', () => { +// TODO: Linux https://github.com/microsoft/vscode/issues/221399 +(env.uiKind === UIKind.Web || platform() === 'win32' || platform() === 'linux' ? suite.skip : suite)('vscode API - Terminal.shellIntegration', () => { const disposables: Disposable[] = []; suiteSetup(async () => { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_less.json b/extensions/vscode-colorize-tests/test/colorize-results/test_less.json index 08d8b9f4fb5..a66224dd9e6 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_less.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_less.json @@ -897,16 +897,16 @@ }, { "c": " ", - "t": "source.css.less meta.property-list.less meta.property-value.less", + "t": "source.css.less meta.property-list.less meta.property-value.less variable.other.readwrite.less", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_plus": "source.css variable: #9CDCFE", + "light_plus": "source.css variable: #E50000", + "dark_vs": "source.css variable: #9CDCFE", + "light_vs": "source.css variable: #E50000", + "hc_black": "source.css variable: #D4D4D4", + "dark_modern": "source.css variable: #9CDCFE", + "hc_light": "source.css variable: #264F78", + "light_modern": "source.css variable: #E50000" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json index 9ab753e7556..0908e19e3ea 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json @@ -701,7 +701,7 @@ }, { "c": "spotSize:", - "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml invalid.illegal.unrecognized.yaml markup.strikethrough", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml invalid.illegal.unrecognized.yaml", "r": { "dark_plus": "invalid: #F44747", "light_plus": "invalid: #CD3131", diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index 8e12e622e05..2fab3ec306a 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -164,8 +164,8 @@ export function activate(context: vscode.ExtensionContext) { const serverCommandPath = path.join(vscodePath, 'scripts', serverCommand); outputChannel.appendLine(`Launching server: "${serverCommandPath}" ${commandArgs.join(' ')}`); - - extHostProcess = cp.spawn(serverCommandPath, commandArgs, { env, cwd: vscodePath }); + const shell = (process.platform === 'win32'); + extHostProcess = cp.spawn(serverCommandPath, commandArgs, { env, cwd: vscodePath, shell }); } else { const extensionToInstall = process.env['TESTRESOLVER_INSTALL_BUILTIN_EXTENSION']; if (extensionToInstall) { @@ -182,8 +182,8 @@ export function activate(context: vscode.ExtensionContext) { outputChannel.appendLine(`Using server build at ${serverLocation}`); outputChannel.appendLine(`Server arguments ${commandArgs.join(' ')}`); - - extHostProcess = cp.spawn(path.join(serverLocation, 'bin', serverCommand), commandArgs, { env, cwd: serverLocation }); + const shell = (process.platform === 'win32'); + extHostProcess = cp.spawn(path.join(serverLocation, 'bin', serverCommand), commandArgs, { env, cwd: serverLocation, shell }); } extHostProcess.stdout!.on('data', (data: Buffer) => processOutput(data.toString())); extHostProcess.stderr!.on('data', (data: Buffer) => processOutput(data.toString())); diff --git a/extensions/yaml/cgmanifest.json b/extensions/yaml/cgmanifest.json index 43dc4ca637e..18882ead40a 100644 --- a/extensions/yaml/cgmanifest.json +++ b/extensions/yaml/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "RedCMD/YAML-Syntax-Highlighter", "repositoryUrl": "https://github.com/RedCMD/YAML-Syntax-Highlighter", - "commitHash": "287c71aeb0773759497822b5e5ce4bdc4d5ef2aa" + "commitHash": "d4dca9f38a654ebbb13c1b72b7881e3c5864a778" } }, "licenseDetail": [ @@ -21,7 +21,7 @@ "THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." ], "license": "MIT", - "version": "1.0.0" + "version": "1.1.1" } ], "version": 1 diff --git a/extensions/yaml/package.json b/extensions/yaml/package.json index d19c507bdfe..2b0ee013964 100644 --- a/extensions/yaml/package.json +++ b/extensions/yaml/package.json @@ -57,25 +57,31 @@ "path": "./syntaxes/yaml.tmLanguage.json" }, { - "scopeName": "source.yaml.1.3", - "path": "./syntaxes/yaml-1.3.tmLanguage.json" - }, - { - "scopeName": "source.yaml.1.2", - "path": "./syntaxes/yaml-1.2.tmLanguage.json" - }, - { - "scopeName": "source.yaml.1.1", - "path": "./syntaxes/yaml-1.1.tmLanguage.json" - }, - { - "scopeName": "source.yaml.1.0", - "path": "./syntaxes/yaml-1.0.tmLanguage.json" - }, + "scopeName": "source.yaml.1.3", + "path": "./syntaxes/yaml-1.3.tmLanguage.json" + }, + { + "scopeName": "source.yaml.1.2", + "path": "./syntaxes/yaml-1.2.tmLanguage.json" + }, + { + "scopeName": "source.yaml.1.1", + "path": "./syntaxes/yaml-1.1.tmLanguage.json" + }, + { + "scopeName": "source.yaml.1.0", + "path": "./syntaxes/yaml-1.0.tmLanguage.json" + }, { "language": "yaml", "scopeName": "source.yaml", - "path": "./syntaxes/yaml.tmLanguage.json" + "path": "./syntaxes/yaml.tmLanguage.json", + "unbalancedBracketScopes": [ + "invalid.illegal", + "meta.scalar.yaml", + "storage.type.tag.shorthand.yaml", + "keyword.control.flow" + ] } ], "configurationDefaults": { diff --git a/extensions/yaml/syntaxes/yaml-1.0.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.0.tmLanguage.json index 9f2a401ca5e..7ae77112860 100644 --- a/extensions/yaml/syntaxes/yaml-1.0.tmLanguage.json +++ b/extensions/yaml/syntaxes/yaml-1.0.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/RedCMD/YAML-Syntax-Highlighter/commit/287c71aeb0773759497822b5e5ce4bdc4d5ef2aa", + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/dfd7e5f4f71f9695c5d8697ca57f81240165aa04", "name": "YAML 1.0", "scopeName": "source.yaml.1.0", "comment": "https://yaml.org/spec/1.0/", @@ -22,6 +22,9 @@ "while": "^", "name": "meta.stream.yaml", "patterns": [ + { + "include": "source.yaml.1.1#byte-order-mark" + }, { "include": "#directives" }, @@ -29,7 +32,7 @@ "include": "#document" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -38,6 +41,9 @@ "while": "\\G", "name": "meta.stream.yaml", "patterns": [ + { + "include": "source.yaml.1.1#byte-order-mark" + }, { "include": "#directives" }, @@ -45,7 +51,7 @@ "include": "#document" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] } @@ -78,13 +84,13 @@ "name": "meta.directives.yaml", "patterns": [ { - "include": "#directive-invalid" + "include": "source.yaml.1.1#directive-invalid" }, { "include": "#directives" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -96,12 +102,12 @@ "include": "#document" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -150,7 +156,7 @@ } }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -159,44 +165,7 @@ "name": "invalid.illegal.entity.other.document.end.yaml" }, { - "include": "#presentation-detail" - } - ] - } - ] - }, - "directive-invalid": { - "patterns": [ - { - "match": "\\G\\.{3}(?=[\\x{85 2028 2029}\r\n\t ])", - "name": "invalid.illegal.entity.other.document.end.yaml" - }, - { - "begin": "\\G(%)(YAML)", - "end": "$", - "beginCaptures": { - "1": { - "name": "punctuation.definition.directive.begin.yaml" - }, - "2": { - "name": "invalid.illegal.keyword.other.directive.yaml.yaml" - } - }, - "name": "meta.directive.yaml", - "patterns": [ - { - "match": "\\G([\t ]++|:)([0-9]++\\.[0-9]++)?+", - "captures": { - "1": { - "name": "punctuation.whitespace.separator.yaml" - }, - "2": { - "name": "constant.numeric.yaml-version.yaml" - } - } - }, - { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] } @@ -234,12 +203,15 @@ }, "patterns": [ { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#byte-order-mark" + }, + { + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -267,13 +239,13 @@ "include": "#block-scalar" }, { - "include": "#anchor-property" + "include": "source.yaml.1.1#anchor-property" }, { "include": "#tag-property" }, { - "include": "#alias" + "include": "source.yaml.1.1#alias" }, { "begin": "(?=\"|')", @@ -284,7 +256,7 @@ "while": "\\G", "patterns": [ { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -292,45 +264,45 @@ "include": "#double" }, { - "include": "#single" + "include": "source.yaml.1.1#single" } ] }, { - "begin": "(?=\\[|{)", - "while": "\\G", + "begin": "(?={)", + "end": "$", "patterns": [ - { - "include": "#block-mapping" - }, - { - "begin": "(?!\\G)(?![\r\n\t ])", - "while": "\\G", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - }, { "include": "#flow-mapping" }, { - "include": "#flow-sequence" + "include": "source.yaml.1.1#presentation-detail" } ] }, { - "include": "#block-plain-out" + "begin": "(?=\\[)", + "end": "$", + "patterns": [ + { + "include": "#flow-sequence" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#block-plain-out" + }, + { + "include": "source.yaml.1.1#presentation-detail" } ] }, "block-mapping": { "//": "The check for plain keys is expensive", - "begin": "(?=((?<=[-?:]) )?+)(?[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Plain)(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))(?>[^:#]++|:(?![\\x{85 2028 2029}\r\n\t ])|(?[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Flow-Map){(?>[^\\x{85 2028 2029}}]++|}[ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++}|(?#Flow-Seq)\\[(?>[^\\x{85 2028 2029}\\]]++|][ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++]|(?#Plain)(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))(?>[^:#]++|:(?![\\x{85 2028 2029}\r\n\t ])|(?(\\1\\2)((?>[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", "beginCaptures": { "2": { @@ -346,18 +318,15 @@ "include": "#tag-property" }, { - "include": "#anchor-property" + "include": "source.yaml.1.1#anchor-property" }, { - "include": "#alias" + "include": "source.yaml.1.1#alias" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] - }, - "5": { - "name": "punctuation.whitespace.separator.yaml" } }, "whileCaptures": { @@ -371,13 +340,13 @@ "include": "#tag-property" }, { - "include": "#anchor-property" + "include": "source.yaml.1.1#anchor-property" }, { - "include": "#alias" + "include": "source.yaml.1.1#alias" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -394,73 +363,20 @@ "name": "meta.mapping.yaml", "patterns": [ { - "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", - "begin": "\\G\"", - "end": "\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": ".[\t ]*+$", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "include": "#double-escape" - } - ] + "include": "#block-map-key-double" }, { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", - "begin": "\\G'", - "end": "'(?!')", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": ".[\t ]*+$", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "match": "''", - "name": "constant.character.escape.single-quote.yaml" - } - ] + "include": "source.yaml#block-map-key-single" }, { - "include": "#block-key-plain" + "include": "source.yaml.1.1#block-map-key-plain" + }, + { + "include": "#block-map-key-explicit" }, { "include": "#block-map-value" }, - { - "include": "#block-map-explicit" - }, { "include": "#flow-mapping" }, @@ -468,7 +384,7 @@ "include": "#flow-sequence" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -505,10 +421,10 @@ } ] }, - "block-map-explicit": { + "block-map-key-explicit": { "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-explicit-key", "begin": "(?=((?<=[-?:]) )?+)\\G( *+)(\\?)(?=[\\x{85 2028 2029}\r\n\t ])", - "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029 FEFF}]])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029}]])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", "beginCaptures": { "2": { "name": "punctuation.whitespace.indentation.yaml" @@ -540,10 +456,10 @@ "include": "#key-double" }, { - "include": "#key-single" + "include": "source.yaml#key-single" }, { - "include": "#flow-key-plain-out" + "include": "source.yaml.1.1#flow-key-plain-out" }, { "include": "#block-map-value" @@ -553,12 +469,42 @@ } ] }, + "block-map-key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, "block-map-value": { "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", - "begin": "(:)(?=[\\x{85 2028 2029}\r\n\t ])", - "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029 FEFF}]]|-[^\\x{85 2028 2029}\r\n\t ])", + "//": "Assumming 3rd party preprocessing variables `{{...}}` turn into valid map-keys when inside a block-mapping", + "begin": ":(?=[\\x{85 2028 2029}\r\n\t ])|(?<=}})(?=[\t ]++#|[\t ]*+$)", + "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029}]]|-[^\\x{85 2028 2029}\r\n\t ])", "beginCaptures": { - "1": { + "0": { "name": "punctuation.separator.map.value.yaml" } }, @@ -569,35 +515,6 @@ } ] }, - "block-key-plain": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (BLOCK-KEY)", - "begin": "\\G(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", - "end": "(?=[\t ]*+:[\\x{85 2028 2029}\r\n\t ]|(?>[\t ]++|\\G)#)", - "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", - "patterns": [ - { - "include": "#tag-implicit-plain-out" - }, - { - "match": "\\G([\t ]++)(.)", - "captures": { - "1": { - "name": "punctuation.whitespace.separator.yaml" - }, - "2": { - "name": "invalid.illegal.multiline-key.yaml" - } - } - }, - { - "match": "[\t ]++$", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "include": "#non-printable" - } - ] - }, "block-scalar": { "comment": "https://yaml.org/spec/1.2.2/#81-block-scalar-styles", "patterns": [ @@ -641,7 +558,7 @@ "contentName": "string.unquoted.block.yaml", "patterns": [ { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, @@ -650,7 +567,7 @@ "end": "$", "patterns": [ { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] } @@ -701,7 +618,7 @@ "contentName": "string.unquoted.block.yaml", "patterns": [ { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, @@ -734,7 +651,7 @@ "contentName": "string.unquoted.block.yaml", "patterns": [ { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] } @@ -746,7 +663,7 @@ "while": "\\G", "patterns": [ { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] } @@ -758,7 +675,7 @@ "end": "$", "patterns": [ { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] } @@ -766,47 +683,6 @@ } ] }, - "block-plain-out": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-OUT)", - "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", - "while": "\\G", - "patterns": [ - { - "begin": "\\G", - "end": "(?=(?>[\t ]++|\\G)#)", - "name": "string.unquoted.plain.out.yaml", - "patterns": [ - { - "include": "#tag-implicit-plain-out" - }, - { - "match": ":(?=[\\x{85 2028 2029}\r\n\t ])", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "match": "\\G[\t ]++", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": "[\t ]++$", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "include": "#non-printable" - } - ] - }, - { - "begin": "(?!\\G)", - "while": "\\G", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - } - ] - }, "flow-node": { "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-seq-entry (FLOW-IN)", "patterns": [ @@ -819,7 +695,7 @@ "end": "(?=[:,\\]}])", "patterns": [ { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -832,13 +708,13 @@ ] }, { - "include": "#anchor-property" + "include": "source.yaml.1.1#anchor-property" }, { "include": "#tag-property" }, { - "include": "#alias" + "include": "source.yaml.1.1#alias" }, { "begin": "(?=\"|')", @@ -849,7 +725,7 @@ "end": "(?=[:,\\]}])", "patterns": [ { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -857,15 +733,15 @@ "include": "#double" }, { - "include": "#single" + "include": "source.yaml.1.1#single" } ] }, { - "include": "#flow-plain-in" + "include": "source.yaml.1.1#flow-plain-in" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -900,12 +776,12 @@ "name": "invalid.illegal.separator.sequence.yaml" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, { - "include": "#flow-map-key-mapping" + "include": "#flow-mapping-map-key" }, { "include": "#flow-map-value-yaml" @@ -949,12 +825,12 @@ "name": "invalid.illegal.separator.sequence.yaml" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, { - "include": "#flow-map-key-sequence" + "include": "#flow-sequence-map-key" }, { "include": "#flow-map-value-yaml" @@ -967,7 +843,7 @@ } ] }, - "flow-map-key-mapping": { + "flow-mapping-map-key": { "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", "patterns": [ { @@ -981,7 +857,7 @@ "name": "meta.flow.map.explicit.yaml", "patterns": [ { - "include": "#flow-map-key-mapping" + "include": "#flow-mapping-map-key" }, { "include": "#flow-map-value-yaml" @@ -996,12 +872,12 @@ }, { "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", - "begin": "(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])))", + "begin": "(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])))", "end": "(?=[,\\[\\]{}])", "name": "meta.flow.map.implicit.yaml", "patterns": [ { - "include": "#flow-key-plain-in" + "include": "source.yaml.1.1#flow-key-plain-in" }, { "match": ":(?=\\[|{)", @@ -1011,7 +887,7 @@ "include": "#flow-map-value-yaml" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] }, @@ -1025,19 +901,19 @@ "include": "#key-double" }, { - "include": "#key-single" + "include": "source.yaml#key-single" }, { "include": "#flow-map-value-json" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.1#presentation-detail" } ] } ] }, - "flow-map-key-sequence": { + "flow-sequence-map-key": { "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", "patterns": [ { @@ -1051,7 +927,7 @@ "name": "meta.flow.map.explicit.yaml", "patterns": [ { - "include": "#flow-map-key-mapping" + "include": "#flow-mapping-map-key" }, { "include": "#flow-map-value-yaml" @@ -1066,12 +942,12 @@ }, { "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", - "begin": "(?<=[\t ,\\[{]|^)(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))(?>[^:#,\\[\\]{}]++|:(?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])|(?[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))(?>[^:#,\\[\\]{}]++|:(?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])|(?'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "end": "(?=(?>[\t ]++|\\G)#|[\t ]*+[,\\[\\]{}])", - "name": "string.unquoted.plain.in.yaml", - "patterns": [ - { - "include": "#tag-implicit-plain-in" - }, - { - "match": "\\G[\t ]++", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": "[\t ]++$", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": ":(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "include": "#non-printable" - } - ] - }, - "flow-key-plain-out": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (FLOW-OUT)", - "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", - "end": "(?=[\t ]*+:[\\x{85 2028 2029}\r\n\t ]|[\t ]++#)", - "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", - "patterns": [ - { - "include": "#tag-implicit-plain-out" - }, - { - "match": "\\G[\t ]++", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": "[\t ]++$", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "include": "#non-printable" - } - ] - }, - "flow-key-plain-in": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-implicit-yaml-key (FLOW-KEY)", - "begin": "\\G(?![\\x{85 2028 2029}\r\n\t #])", - "end": "(?=[\t ]*+(?>:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]|[,\\[\\]{}])|[\t ]++#)", - "name": "meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", - "patterns": [ - { - "include": "#tag-implicit-plain-in" - }, - { - "include": "#non-printable" - } - ] - }, "key-double": { - "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", - "begin": "\\G\"", + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (FLOW-OUT)", + "begin": "\"", "end": "\"", "beginCaptures": { "0": { @@ -1226,32 +1040,6 @@ } ] }, - "key-single": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", - "begin": "\\G'", - "end": "'(?!')", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "match": "''", - "name": "constant.character.escape.single-quote.yaml" - } - ] - }, "double": { "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", "begin": "\"", @@ -1285,40 +1073,6 @@ } ] }, - "single": { - "comment": "https://yaml.org/spec/1.2.2/#single-quoted-style", - "begin": "'", - "end": "'(?!')", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "string.quoted.single.yaml", - "patterns": [ - { - "match": "(?null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.language.null.yaml" - }, - { - "match": "\\G(?>true|True|TRUE|false|False|FALSE|y|Y|yes|Yes|YES|n|N|no|No|NO|on|On|ON|off|Off|OFF)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.language.boolean.yaml" - }, - { - "match": "\\G[-+]?+(0|[1-9][0-9_]*+)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.integer.decimal.yaml" - }, - { - "match": "\\G[-+]?+0b[0-1_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.integer.binary.yaml" - }, - { - "match": "\\G[-+]?0[0-7_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.integer.octal.yaml" - }, - { - "match": "\\G[-+]?+0x[0-9a-fA-F_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.integer.hexadecimal.yaml" - }, - { - "match": "\\G[-+]?+[1-9][0-9_]*+(?>:[0-5]?[0-9])++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.integer.Sexagesimal.yaml" - }, - { - "match": "\\G[-+]?+(?>[0-9][0-9_]*+)?+\\.[0-9.]*+(?>[eE][-+][0-9]+)?+(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.float.decimal.yaml" - }, - { - "match": "\\G[-+]?+[0-9][0-9_]*+(?>:[0-5]?[0-9])++\\.[0-9_]*+(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.float.Sexagesimal.yaml" - }, - { - "match": "\\G[-+]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.float.inf.yaml" - }, - { - "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.float.nan.yaml" - }, - { - "comment": "https://www.w3.org/TR/NOTE-datetime does not allow spaces, however https://yaml.org/type/timestamp.html does, but the provided regex doesn't match the TZD space in many of the YAML examples", - "match": "\\G(?>[0-9]{4}-[0-9]{2,1}-[0-9]{2,1}(?>T|t|[\t ]++)[0-9]{2,1}:[0-9]{2}:[0-9]{2}(?>\\.[0-9]*+)?+[\t ]*+(?>Z|[-+][0-9]{2,1}(?>:[0-9]{2})?+)?+|[0-9]{4}-[0-9]{2}-[0-9]{2})(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.timestamp.yaml" - }, - { - "match": "\\G<<(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.language.merge.yaml" - }, - { - "match": "\\G=(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.language.value.yaml" - }, - { - "match": "\\G(?>!|&|\\*)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", - "name": "constant.language.yaml.yaml" - } - ] - }, - "tag-implicit-plain-out": { - "comment": "https://yaml.org/type/index.html", - "patterns": [ - { - "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.language.null.yaml" - }, - { - "match": "\\G(?>true|True|TRUE|false|False|FALSE|yes|Yes|YES|y|Y|no|No|NO|n|N|on|On|ON|off|Off|OFF)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.language.boolean.yaml" - }, - { - "match": "\\G[-+]?+(0|[1-9][0-9_]*+)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.numeric.integer.decimal.yaml" - }, - { - "match": "\\G[-+]?+0b[0-1_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.numeric.integer.binary.yaml" - }, - { - "match": "\\G[-+]?0[0-7_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.numeric.integer.octal.yaml" - }, - { - "match": "\\G[-+]?+0x[0-9a-fA-F_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.numeric.integer.hexadecimal.yaml" - }, - { - "match": "\\G[-+]?+[1-9][0-9_]*+(?>:[0-5]?[0-9])++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.numeric.integer.Sexagesimal.yaml" - }, - { - "match": "\\G[-+]?+(?>[0-9][0-9_]*+)?+\\.[0-9.]*+(?>[eE][-+][0-9]+)?+(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.numeric.float.decimal.yaml" - }, - { - "match": "\\G[-+]?+[0-9][0-9_]*+(?>:[0-5]?[0-9])++\\.[0-9_]*+(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.numeric.float.Sexagesimal.yaml" - }, - { - "match": "\\G[-+]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.numeric.float.inf.yaml" - }, - { - "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.numeric.float.nan.yaml" - }, - { - "comment": "https://www.w3.org/TR/NOTE-datetime does not allow spaces, however https://yaml.org/type/timestamp.html does, but the provided regex doesn't match the TZD space in many of the YAML examples", - "match": "\\G(?>[0-9]{4}-[0-9]{2,1}-[0-9]{2,1}(?>T|t|[\t ]++)[0-9]{2,1}:[0-9]{2}:[0-9]{2}(?>\\.[0-9]*+)?+[\t ]*+(?>Z|[-+][0-9]{2,1}(?>:[0-9]{2})?+)?+|[0-9]{4}-[0-9]{2}-[0-9]{2})(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.numeric.timestamp.yaml" - }, - { - "match": "\\G<<(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.language.merge.yaml" - }, - { - "match": "\\G=(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.language.value.yaml" - }, - { - "match": "\\G(?>!|&|\\*)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", - "name": "constant.language.yaml.yaml" - } - ] - }, "tag-property": { "comment": "https://yaml.org/spec/1.0/#c-ns-tag-property", "//": [ @@ -1520,102 +1142,9 @@ "include": "#double-escape" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] - }, - "anchor-property": { - "match": "(&)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)|(&)", - "captures": { - "0": { - "name": "keyword.control.flow.anchor.yaml" - }, - "1": { - "name": "punctuation.definition.anchor.yaml" - }, - "2": { - "name": "variable.other.anchor.yaml" - }, - "3": { - "name": "invalid.illegal.flow.anchor.yaml" - } - } - }, - "alias": { - "begin": "(\\*)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)|(\\*)", - "end": "(?=:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]|[,\\[\\]{}])", - "captures": { - "0": { - "name": "keyword.control.flow.alias.yaml" - }, - "1": { - "name": "punctuation.definition.alias.yaml" - }, - "2": { - "name": "variable.other.alias.yaml" - }, - "3": { - "name": "invalid.illegal.flow.alias.yaml" - } - }, - "patterns": [ - { - "include": "#presentation-detail" - } - ] - }, - "presentation-detail": { - "patterns": [ - { - "match": "[\t ]++", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "include": "#non-printable" - }, - { - "include": "#comment" - }, - { - "include": "#unknown" - } - ] - }, - "non-printable": { - "//": { - "85": "…", - "2028": "
", - "2029": "
", - "10000": "𐀀", - "A0": " ", - "D7FF": "퟿", - "E000": "", - "FFFD": "�", - "FFFF": "￿", - "10FFFF": "􏿿" - }, - "match": "[^\t\n\r -~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]++", - "name": "invalid.illegal.non-printable.yaml" - }, - "comment": { - "comment": "Comments must be separated from other tokens by white space characters. `space`, `tab`, `newline` or `carriage-return`. `#(.*)` causes performance issues", - "begin": "(?<=^|[\\x{85 2028 2029} ])#", - "end": "[\\x{85 2028 2029}\r\n]", - "captures": { - "0": { - "name": "punctuation.definition.comment.yaml" - } - }, - "name": "comment.line.number-sign.yaml", - "patterns": [ - { - "include": "#non-printable" - } - ] - }, - "unknown": { - "match": ".[[^\\x{85}#\"':,\\[\\]{}]&&!-~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", - "name": "invalid.illegal.unrecognized.yaml markup.strikethrough" } } } \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml-1.1.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.1.tmLanguage.json index bda3a191ce8..57a96ac1e4f 100644 --- a/extensions/yaml/syntaxes/yaml-1.1.tmLanguage.json +++ b/extensions/yaml/syntaxes/yaml-1.1.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/RedCMD/YAML-Syntax-Highlighter/commit/287c71aeb0773759497822b5e5ce4bdc4d5ef2aa", + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/dfd7e5f4f71f9695c5d8697ca57f81240165aa04", "name": "YAML 1.1", "scopeName": "source.yaml.1.1", "comment": "https://yaml.org/spec/1.1/", @@ -182,7 +182,7 @@ "name": "invalid.illegal.character.uri.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" }, { "match": "[^\\x{85 2028 2029}\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]]++", @@ -348,7 +348,7 @@ "include": "#block-mapping" }, { - "include": "#block-scalar" + "include": "source.yaml.1.2#block-scalar" }, { "include": "#anchor-property" @@ -381,26 +381,26 @@ ] }, { - "begin": "(?=\\[|{)", - "while": "\\G", + "begin": "(?={)", + "end": "$", "patterns": [ - { - "include": "#block-mapping" - }, - { - "begin": "(?!\\G)(?![\r\n\t ])", - "while": "\\G", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - }, { "include": "#flow-mapping" }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "(?=\\[)", + "end": "$", + "patterns": [ { "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" } ] }, @@ -414,7 +414,7 @@ }, "block-mapping": { "//": "The check for plain keys is expensive", - "begin": "(?=((?<=[-?:]) )?+)(?[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Plain)(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))(?>[^:#]++|:(?![\\x{85 2028 2029}\r\n\t ])|(?[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Flow-Map){(?>[^\\x{85 2028 2029}}]++|}[ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++}|(?#Flow-Seq)\\[(?>[^\\x{85 2028 2029}\\]]++|][ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++]|(?#Plain)(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))(?>[^:#]++|:(?![\\x{85 2028 2029}\r\n\t ])|(?(\\1\\2)((?>[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", "beginCaptures": { "2": { @@ -439,9 +439,6 @@ "include": "#presentation-detail" } ] - }, - "5": { - "name": "punctuation.whitespace.separator.yaml" } }, "whileCaptures": { @@ -478,73 +475,20 @@ "name": "meta.mapping.yaml", "patterns": [ { - "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", - "begin": "\\G\"", - "end": "\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": ".[\t ]*+$", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "include": "#double-escape" - } - ] + "include": "#block-map-key-double" }, { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", - "begin": "\\G'", - "end": "'(?!')", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": ".[\t ]*+$", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "match": "''", - "name": "constant.character.escape.single-quote.yaml" - } - ] + "include": "source.yaml#block-map-key-single" }, { - "include": "#block-key-plain" + "include": "#block-map-key-plain" + }, + { + "include": "#block-map-key-explicit" }, { "include": "#block-map-value" }, - { - "include": "#block-map-explicit" - }, { "include": "#flow-mapping" }, @@ -589,10 +533,10 @@ } ] }, - "block-map-explicit": { + "block-map-key-explicit": { "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-explicit-key", "begin": "(?=((?<=[-?:]) )?+)\\G( *+)(\\?)(?=[\\x{85 2028 2029}\r\n\t ])", - "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029 FEFF}]])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029}]])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", "beginCaptures": { "2": { "name": "punctuation.whitespace.indentation.yaml" @@ -624,7 +568,7 @@ "include": "#key-double" }, { - "include": "#key-single" + "include": "source.yaml#key-single" }, { "include": "#flow-key-plain-out" @@ -637,23 +581,36 @@ } ] }, - "block-map-value": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", - "begin": "(:)(?=[\\x{85 2028 2029}\r\n\t ])", - "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029 FEFF}]]|-[^\\x{85 2028 2029}\r\n\t ])", + "block-map-key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", + "begin": "\\G\"", + "end": "\"", "beginCaptures": { - "1": { - "name": "punctuation.separator.map.value.yaml" + "0": { + "name": "punctuation.definition.string.begin.yaml" } }, - "name": "meta.map.value.yaml", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", "patterns": [ { - "include": "#block-node" + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" } ] }, - "block-key-plain": { + "block-map-key-plain": { "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (BLOCK-KEY)", "begin": "\\G(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", "end": "(?=[\t ]*+:[\\x{85 2028 2029}\r\n\t ]|(?>[\t ]++|\\G)#)", @@ -678,175 +635,24 @@ "name": "punctuation.whitespace.separator.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, - "block-scalar": { - "comment": "https://yaml.org/spec/1.2.2/#81-block-scalar-styles", + "block-map-value": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", + "//": "Assumming 3rd party preprocessing variables `{{...}}` turn into valid map-keys when inside a block-mapping", + "begin": ":(?=[\\x{85 2028 2029}\r\n\t ])|(?<=}})(?=[\t ]++#|[\t ]*+$)", + "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029}]]|-[^\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.map.value.yaml", "patterns": [ { - "comment": "https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator", - "begin": "([\t ]*+)(?>(\\|)|(>))(?[+-])?+((1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)|(9))(?()|([+-]))?+", - "while": "\\G(?>(?>(?!\\6) |(?!\\7) {2}|(?!\\8) {3}|(?!\\9) {4}|(?!\\10) {5}|(?!\\11) {6}|(?!\\12) {7}|(?!\\13) {8}|(?!\\14) {9})| *+($|[^#]))", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.separator.yaml" - }, - "2": { - "name": "keyword.control.flow.block-scalar.literal.yaml" - }, - "3": { - "name": "keyword.control.flow.block-scalar.folded.yaml" - }, - "4": { - "name": "storage.modifier.chomping-indicator.yaml" - }, - "5": { - "name": "constant.numeric.indentation-indicator.yaml" - }, - "15": { - "name": "storage.modifier.chomping-indicator.yaml" - } - }, - "whileCaptures": { - "0": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "1": { - "name": "invalid.illegal.expected-indentation.yaml" - } - }, - "name": "meta.scalar.yaml", - "patterns": [ - { - "begin": "$", - "while": "\\G", - "contentName": "string.unquoted.block.yaml", - "patterns": [ - { - "include": "#non-printable" - } - ] - }, - { - "begin": "\\G", - "end": "$", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-b-block-header", - "//": "Soooooooo many edge cases", - "begin": "([\t ]*+)(?>(\\|)|(>))([+-]?+)", - "while": "\\G", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.separator.yaml" - }, - "2": { - "name": "keyword.control.flow.block-scalar.literal.yaml" - }, - "3": { - "name": "keyword.control.flow.block-scalar.folded.yaml" - }, - "4": { - "name": "storage.modifier.chomping-indicator.yaml" - } - }, - "name": "meta.scalar.yaml", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#rule-l-literal-content", - "begin": "$", - "while": "\\G", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", - "//": "Find the highest indented line", - "begin": "\\G( ++)$", - "while": "\\G(?>(\\1)$|(?!\\1)( *+)($|.))", - "captures": { - "1": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "2": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "3": { - "name": "invalid.illegal.expected-indentation.yaml" - } - }, - "contentName": "string.unquoted.block.yaml", - "patterns": [ - { - "include": "#non-printable" - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-b-nb-literal-next", - "//": [ - "Funky wrapper function", - "The `end` pattern clears the parent `\\G` anchor", - "Affectively forcing this rule to only match at most once", - "https://github.com/microsoft/vscode-textmate/issues/114" - ], - "begin": "\\G(?!$)(?=( *+))", - "end": "\\G(?!\\1)(?=[\t ]*+#)", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", - "begin": "\\G( *+)", - "while": "\\G(?>(\\1)|( *+)($|[^\t#]|[\t ]++[^#]))", - "captures": { - "1": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "2": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "3": { - "name": "invalid.illegal.expected-indentation.yaml" - } - }, - "contentName": "string.unquoted.block.yaml", - "patterns": [ - { - "include": "#non-printable" - } - ] - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-l-chomped-empty", - "begin": "(?!\\G)(?=[\t ]*+#)", - "while": "\\G", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - } - ] - }, - { - "comment": "Header Comment", - "begin": "\\G", - "end": "$", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - } - ] + "include": "#block-node" } ] }, @@ -876,7 +682,7 @@ "name": "punctuation.whitespace.separator.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, @@ -989,7 +795,7 @@ ] }, { - "include": "#flow-map-key-mapping" + "include": "#flow-mapping-map-key" }, { "include": "#flow-map-value-yaml" @@ -1038,7 +844,7 @@ ] }, { - "include": "#flow-map-key-sequence" + "include": "#flow-sequence-map-key" }, { "include": "#flow-map-value-yaml" @@ -1051,7 +857,7 @@ } ] }, - "flow-map-key-mapping": { + "flow-mapping-map-key": { "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", "patterns": [ { @@ -1065,7 +871,7 @@ "name": "meta.flow.map.explicit.yaml", "patterns": [ { - "include": "#flow-map-key-mapping" + "include": "#flow-mapping-map-key" }, { "include": "#flow-map-value-yaml" @@ -1109,7 +915,7 @@ "include": "#key-double" }, { - "include": "#key-single" + "include": "source.yaml#key-single" }, { "include": "#flow-map-value-json" @@ -1121,7 +927,7 @@ } ] }, - "flow-map-key-sequence": { + "flow-sequence-map-key": { "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", "patterns": [ { @@ -1135,7 +941,7 @@ "name": "meta.flow.map.explicit.yaml", "patterns": [ { - "include": "#flow-map-key-mapping" + "include": "#flow-mapping-map-key" }, { "include": "#flow-map-value-yaml" @@ -1179,7 +985,7 @@ "include": "#key-double" }, { - "include": "#key-single" + "include": "source.yaml#key-single" }, { "include": "#flow-map-value-json" @@ -1245,7 +1051,7 @@ "name": "invalid.illegal.multiline-key.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, @@ -1267,7 +1073,7 @@ "name": "punctuation.whitespace.separator.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, @@ -1281,13 +1087,13 @@ "include": "#tag-implicit-plain-in" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, "key-double": { - "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", - "begin": "\\G\"", + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (FLOW-OUT)", + "begin": "\"", "end": "\"", "beginCaptures": { "0": { @@ -1310,32 +1116,6 @@ } ] }, - "key-single": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", - "begin": "\\G'", - "end": "'(?!')", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "match": "''", - "name": "constant.character.escape.single-quote.yaml" - } - ] - }, "double": { "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", "begin": "\"", @@ -1601,7 +1381,7 @@ "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" }, { "match": "[^\\x{85 2028 2029}\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]%>]++", @@ -1646,7 +1426,7 @@ "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" }, { "match": "[^\\x{85 2028 2029}\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.~*'()\\[\\]%]++", @@ -1741,7 +1521,7 @@ "name": "punctuation.separator.line-break.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" }, { "include": "#comment" @@ -1751,23 +1531,6 @@ } ] }, - "non-printable": { - "//": { - "85": "…", - "2028": "
", - "2029": "
", - "10000": "𐀀", - "A0": " ", - "D7FF": "퟿", - "E000": "", - "FFFD": "�", - "FEFF": "", - "FFFF": "￿", - "10FFFF": "􏿿" - }, - "match": "[^\t\n\r -~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]++", - "name": "invalid.illegal.non-printable.yaml" - }, "comment": { "comment": "Comments must be separated from other tokens by white space characters. `space`, `newline` or `carriage-return`. `#(.*)` causes performance issues", "begin": "(?<=^|[\\x{FEFF 85 2028 2029} ])#", @@ -1780,13 +1543,13 @@ "name": "comment.line.number-sign.yaml", "patterns": [ { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, "unknown": { - "match": ".[[^\\x{85}#\"':,\\[\\]{}]&&!-~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", - "name": "invalid.illegal.unrecognized.yaml markup.strikethrough" + "match": ".[[^\\x{85 2028 2029}#\"':,\\[\\]{}]&&!-~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", + "name": "invalid.illegal.unrecognized.yaml" } } } \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml-1.2.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.2.tmLanguage.json index b2a921a5dd1..965b6040816 100644 --- a/extensions/yaml/syntaxes/yaml-1.2.tmLanguage.json +++ b/extensions/yaml/syntaxes/yaml-1.2.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/RedCMD/YAML-Syntax-Highlighter/commit/287c71aeb0773759497822b5e5ce4bdc4d5ef2aa", + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/d4dca9f38a654ebbb13c1b72b7881e3c5864a778", "name": "YAML 1.2", "scopeName": "source.yaml.1.2", "comment": "https://yaml.org/spec/1.2.2", @@ -37,6 +37,7 @@ ] }, { + "comment": "For when YAML is embedded inside a Markdown code-block", "begin": "\\G", "while": "\\G", "name": "meta.stream.yaml", @@ -170,7 +171,7 @@ "name": "invalid.illegal.character.uri.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" }, { "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]]++", @@ -369,26 +370,26 @@ ] }, { - "begin": "(?=\\[|{)", - "while": "\\G", + "begin": "(?={)", + "end": "$", "patterns": [ - { - "include": "#block-mapping" - }, - { - "begin": "(?!\\G)(?![\r\n\t ])", - "while": "\\G", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - }, { "include": "#flow-mapping" }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "(?=\\[)", + "end": "$", + "patterns": [ { "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" } ] }, @@ -402,7 +403,7 @@ }, "block-mapping": { "//": "The check for plain keys is expensive", - "begin": "(?=((?<=[-?:]) )?+)(?((?>[!&*][^\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Plain)(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))(?>[^:#]++|:(?![\r\n\t ])|(?((?>[!&*][^\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Flow-Map){(?>[^}]++|}[ \t]*+(?!:[\r\n\t ]))++}|(?#Flow-Seq)\\[(?>[^]]++|][ \t]*+(?!:[\r\n\t ]))++]|(?#Plain)(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))(?>[^:#]++|:(?![\r\n\t ])|(?(\\1\\2)((?>[!&*][^\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\r\n#])?+)", "beginCaptures": { "2": { @@ -427,9 +428,6 @@ "include": "#presentation-detail" } ] - }, - "5": { - "name": "punctuation.whitespace.separator.yaml" } }, "whileCaptures": { @@ -469,7 +467,7 @@ "include": "#block-map-key-double" }, { - "include": "#block-map-key-single" + "include": "source.yaml#block-map-key-single" }, { "include": "#block-map-key-plain" @@ -559,7 +557,7 @@ "include": "#key-double" }, { - "include": "#key-single" + "include": "source.yaml#key-single" }, { "include": "#flow-key-plain-out" @@ -601,36 +599,6 @@ } ] }, - "block-map-key-single": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", - "begin": "\\G'", - "end": "'(?!')", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": ".[\t ]*+$", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "match": "''", - "name": "constant.character.escape.single-quote.yaml" - } - ] - }, "block-map-key-plain": { "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (BLOCK-KEY)", "begin": "\\G(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", @@ -660,13 +628,14 @@ "name": "invalid.illegal.bom.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, "block-map-value": { "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", - "begin": ":(?=[\r\n\t ])", + "//": "Assumming 3rd party preprocessing variables `{{...}}` turn into valid map-keys when inside a block-mapping", + "begin": ":(?=[\r\n\t ])|(?<=}})(?=[\t ]++#|[\t ]*+$)", "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{FEFF}]]|-[^\r\n\t ])", "beginCaptures": { "0": { @@ -723,7 +692,7 @@ "contentName": "string.unquoted.block.yaml", "patterns": [ { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, @@ -783,7 +752,7 @@ "contentName": "string.unquoted.block.yaml", "patterns": [ { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, @@ -816,7 +785,7 @@ "contentName": "string.unquoted.block.yaml", "patterns": [ { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] } @@ -878,7 +847,7 @@ "name": "invalid.illegal.bom.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, @@ -1111,7 +1080,7 @@ "include": "#key-double" }, { - "include": "#key-single" + "include": "source.yaml#key-single" }, { "include": "#flow-map-value-json" @@ -1181,7 +1150,7 @@ "include": "#key-double" }, { - "include": "#key-single" + "include": "source.yaml#key-single" }, { "include": "#flow-map-value-json" @@ -1251,7 +1220,7 @@ "name": "invalid.illegal.bom.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, @@ -1277,7 +1246,7 @@ "name": "invalid.illegal.bom.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, @@ -1295,13 +1264,13 @@ "name": "invalid.illegal.bom.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, "key-double": { - "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", - "begin": "\\G\"", + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (FLOW-OUT)", + "begin": "\"", "end": "\"", "beginCaptures": { "0": { @@ -1324,32 +1293,6 @@ } ] }, - "key-single": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", - "begin": "\\G'", - "end": "'(?!')", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "match": "''", - "name": "constant.character.escape.single-quote.yaml" - } - ] - }, "double": { "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", "begin": "\"", @@ -1557,7 +1500,7 @@ "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" }, { "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]%>]++", @@ -1602,7 +1545,7 @@ "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" }, { "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$_.~*'()%]++", @@ -1664,7 +1607,7 @@ "name": "punctuation.whitespace.separator.yaml" }, { - "include": "#non-printable" + "include": "source.yaml#non-printable" }, { "include": "#comment" @@ -1674,22 +1617,6 @@ } ] }, - "non-printable": { - "//": { - "85": "…", - "10000": "𐀀", - "A0": " ", - "D7FF": "퟿", - "E000": "", - "FFFD": "�", - "FEFF": "", - "FFFF": "￿", - "10FFFF": "􏿿" - }, - "//match": "[\\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}&&[^\t\n\r\\x{85}]]++", - "match": "[^\t\n\r -~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]++", - "name": "invalid.illegal.non-printable.yaml" - }, "comment": { "comment": "Comments must be separated from other tokens by white space characters. `space`, `tab`, `newline` or `carriage-return`. `#(.*)` causes performance issues", "begin": "(?<=[\\x{FEFF}\t ]|^)#", @@ -1702,13 +1629,13 @@ "name": "comment.line.number-sign.yaml", "patterns": [ { - "include": "#non-printable" + "include": "source.yaml#non-printable" } ] }, "unknown": { "match": ".[[^\"':,\\[\\]{}]&&!-~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", - "name": "invalid.illegal.unrecognized.yaml markup.strikethrough" + "name": "invalid.illegal.unrecognized.yaml" } } } \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json index 56444fd9fa2..8df69f61c8c 100644 --- a/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json +++ b/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json @@ -4,59 +4,16 @@ "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/RedCMD/YAML-Syntax-Highlighter/commit/287c71aeb0773759497822b5e5ce4bdc4d5ef2aa", + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/274009903e20ac6dc37ba5763fb853744e28c9b2", "name": "YAML 1.3", "scopeName": "source.yaml.1.3", "comment": "https://spec.yaml.io/main/spec/1.3.0/", "patterns": [ { - "include": "#stream" + "include": "source.yaml" } ], "repository": { - "stream": { - "patterns": [ - { - "comment": "allows me to just use `\\G` instead of the performance heavy `(^|\\G)`", - "begin": "^(?!\\G)", - "while": "^", - "name": "meta.stream.yaml", - "patterns": [ - { - "include": "#byte-order-mark" - }, - { - "include": "#directives" - }, - { - "include": "#document" - }, - { - "include": "#presentation-detail" - } - ] - }, - { - "begin": "\\G", - "while": "\\G", - "name": "meta.stream.yaml", - "patterns": [ - { - "include": "#byte-order-mark" - }, - { - "include": "#directives" - }, - { - "include": "#document" - }, - { - "include": "#presentation-detail" - } - ] - } - ] - }, "directive-YAML": { "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", "begin": "(?=%YAML[\t ]+1\\.3(?=[\r\n\t ]))", @@ -84,1631 +41,20 @@ "name": "meta.directives.yaml", "patterns": [ { - "include": "#directive-invalid" + "include": "source.yaml.1.2#directive-invalid" }, { - "include": "#directives" + "include": "source.yaml.1.2#directives" }, { - "include": "#presentation-detail" + "include": "source.yaml.1.2#presentation-detail" } ] }, { - "include": "#document" + "include": "source.yaml.1.2#document" } ] - }, - "directives": { - "comment": "https://yaml.org/spec/1.2.2/#68-directives", - "patterns": [ - { - "include": "source.yaml.1.3#directive-YAML" - }, - { - "include": "source.yaml.1.2#directive-YAML" - }, - { - "include": "source.yaml.1.1#directive-YAML" - }, - { - "include": "source.yaml.1.0#directive-YAML" - }, - { - "begin": "(?=%)", - "while": "\\G(?!%|---[\r\n\t ])", - "name": "meta.directives.yaml", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#682-tag-directives", - "begin": "\\G(%)(TAG)(?>([\t ]++)((!)(?>[0-9A-Za-z-]*+(!))?+))?+", - "end": "$", - "applyEndPatternLast": true, - "beginCaptures": { - "1": { - "name": "punctuation.definition.directive.begin.yaml" - }, - "2": { - "name": "keyword.other.directive.tag.yaml" - }, - "3": { - "name": "punctuation.whitespace.separator.yaml" - }, - "4": { - "name": "storage.type.tag-handle.yaml" - }, - "5": { - "name": "punctuation.definition.tag.begin.yaml" - }, - "6": { - "name": "punctuation.definition.tag.end.yaml" - }, - "comment": "https://yaml.org/spec/1.2.2/#rule-c-tag-handle" - }, - "patterns": [ - { - "comment": "technically the beginning should only validate against a valid uri scheme [A-Za-z][A-Za-z0-9.+-]*", - "begin": "\\G[\t ]++(?!#)", - "end": "(?=[\r\n\t ])", - "beginCaptures": { - "0": { - "name": "punctuation.whitespace.separator.yaml" - } - }, - "contentName": "support.type.tag-prefix.yaml", - "patterns": [ - { - "match": "%[0-9a-fA-F]{2}", - "name": "constant.character.escape.unicode.8-bit.yaml" - }, - { - "match": "%[^\r\n\t ]{2,0}", - "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" - }, - { - "match": "\\G[,\\[\\]{}]", - "name": "invalid.illegal.character.uri.yaml" - }, - { - "include": "#non-printable" - }, - { - "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]]++", - "name": "invalid.illegal.unrecognized.yaml" - } - ] - }, - { - "include": "#presentation-detail" - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-reserved-directive", - "begin": "(%)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", - "end": "$", - "beginCaptures": { - "1": { - "name": "punctuation.definition.directive.begin.yaml" - }, - "2": { - "name": "keyword.other.directive.other.yaml" - } - }, - "patterns": [ - { - "match": "\\G([\t ]++)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", - "captures": { - "1": { - "name": "punctuation.whitespace.separator.yaml" - }, - "2": { - "name": "string.unquoted.directive-name.yaml" - } - } - }, - { - "match": "([\t ]++)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", - "captures": { - "1": { - "name": "punctuation.whitespace.separator.yaml" - }, - "2": { - "name": "string.unquoted.directive-parameter.yaml" - } - } - }, - { - "include": "#presentation-detail" - } - ] - }, - { - "match": "\\G\\.{3}(?=[\r\n\t ])", - "name": "invalid.illegal.entity.other.document.end.yaml" - }, - { - "include": "#presentation-detail" - } - ] - } - ] - }, - "directive-invalid": { - "patterns": [ - { - "match": "\\G\\.{3}(?=[\r\n\t ])", - "name": "invalid.illegal.entity.other.document.end.yaml" - }, - { - "begin": "\\G(%)(YAML)", - "end": "$", - "beginCaptures": { - "1": { - "name": "punctuation.definition.directive.begin.yaml" - }, - "2": { - "name": "invalid.illegal.keyword.other.directive.yaml.yaml" - } - }, - "name": "meta.directive.yaml", - "patterns": [ - { - "match": "\\G([\t ]++|:)([0-9]++\\.[0-9]++)?+", - "captures": { - "1": { - "name": "punctuation.whitespace.separator.yaml" - }, - "2": { - "name": "constant.numeric.yaml-version.yaml" - } - } - }, - { - "include": "#presentation-detail" - } - ] - } - ] - }, - "document": { - "comment": "https://yaml.org/spec/1.2.2/#91-documents", - "patterns": [ - { - "begin": "---(?=[\r\n\t ])", - "while": "\\G(?!(?>\\.{3}|---)[\r\n\t ])", - "beginCaptures": { - "0": { - "name": "entity.other.document.begin.yaml" - } - }, - "name": "meta.document.yaml", - "patterns": [ - { - "include": "#block-node" - } - ] - }, - { - "begin": "(?=\\.{3}[\r\n\t ])", - "while": "\\G(?=[\t \\x{FEFF}]*+(?>#|$))", - "patterns": [ - { - "begin": "\\G\\.{3}", - "end": "$", - "beginCaptures": { - "0": { - "name": "entity.other.document.end.yaml" - } - }, - "patterns": [ - { - "include": "#presentation-detail" - } - ] - }, - { - "include": "#byte-order-mark" - }, - { - "include": "#presentation-detail" - } - ] - }, - { - "begin": "\\G(?!%|[\t \\x{FEFF}]*+(?>#|$))", - "while": "\\G(?!(?>\\.{3}|---)[\r\n\t ])", - "name": "meta.document.yaml", - "patterns": [ - { - "include": "#block-node" - } - ] - } - ] - }, - "block-node": { - "patterns": [ - { - "include": "#block-sequence" - }, - { - "include": "#block-mapping" - }, - { - "include": "#block-scalar" - }, - { - "include": "#anchor-property" - }, - { - "include": "#tag-property" - }, - { - "include": "#alias" - }, - { - "begin": "(?=\"|')", - "while": "\\G", - "patterns": [ - { - "begin": "(?!\\G)", - "while": "\\G", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - }, - { - "include": "#double" - }, - { - "include": "#single" - } - ] - }, - { - "begin": "(?=\\[|{)", - "while": "\\G", - "patterns": [ - { - "include": "#block-mapping" - }, - { - "begin": "(?!\\G)(?![\r\n\t ])", - "while": "\\G", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - }, - { - "include": "#flow-mapping" - }, - { - "include": "#flow-sequence" - } - ] - }, - { - "include": "#block-plain-out" - }, - { - "include": "#presentation-detail" - } - ] - }, - "block-mapping": { - "//": "The check for plain keys is expensive", - "begin": "(?=((?<=[-?:]) )?+)(?((?>[!&*][^\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Plain)(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))(?>[^:#]++|:(?![\r\n\t ])|(?(\\1\\2)((?>[!&*][^\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\r\n#])?+)", - "beginCaptures": { - "2": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "3": { - "name": "punctuation.whitespace.separator.yaml" - }, - "4": { - "comment": "May cause lag on long lines starting with a tag, anchor or alias", - "patterns": [ - { - "include": "#tag-property" - }, - { - "include": "#anchor-property" - }, - { - "include": "#alias" - }, - { - "include": "#presentation-detail" - } - ] - }, - "5": { - "name": "punctuation.whitespace.separator.yaml" - } - }, - "whileCaptures": { - "1": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "2": { - "comment": "May cause lag on long lines starting with a tag, anchor or alias", - "patterns": [ - { - "include": "#tag-property" - }, - { - "include": "#anchor-property" - }, - { - "include": "#alias" - }, - { - "include": "#presentation-detail" - } - ] - }, - "3": { - "name": "invalid.illegal.expected-indentation.yaml" - }, - "4": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "5": { - "name": "invalid.illegal.expected-indentation.yaml" - } - }, - "name": "meta.mapping.yaml", - "patterns": [ - { - "include": "#block-map-key-double" - }, - { - "include": "#block-map-key-single" - }, - { - "include": "#block-map-key-plain" - }, - { - "include": "#block-map-key-explicit" - }, - { - "include": "#block-map-value" - }, - { - "include": "#flow-mapping" - }, - { - "include": "#flow-sequence" - }, - { - "include": "#presentation-detail" - } - ] - }, - "block-sequence": { - "comment": "https://yaml.org/spec/1.2.2/#rule-l+block-sequence", - "begin": "(?=((?<=[-?:]) )?+)(?(\\1\\2)(?!-[\r\n\t ])((?>\t[\t ]*+)?+[^\r\n\t #\\]}])?+|(?!\\1\\2)( *+)([\t ]*+[^\r\n#])?+)", - "beginCaptures": { - "2": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "3": { - "name": "punctuation.definition.block.sequence.item.yaml" - } - }, - "whileCaptures": { - "1": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "2": { - "name": "invalid.illegal.expected-indentation.yaml" - }, - "3": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "4": { - "name": "invalid.illegal.expected-indentation.yaml" - } - }, - "name": "meta.block.sequence.yaml", - "patterns": [ - { - "include": "#block-node" - } - ] - }, - "block-map-key-explicit": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-explicit-key", - "begin": "(?=((?<=[-?:]) )?+)\\G( *+)(\\?)(?=[\r\n\t ])", - "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{FEFF}]])((?>\t[\t ]*+)?+[^\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\r\n#])?+)", - "beginCaptures": { - "2": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "3": { - "name": "punctuation.definition.map.key.yaml" - }, - "4": { - "name": "punctuation.whitespace.separator.yaml" - } - }, - "whileCaptures": { - "1": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "2": { - "name": "invalid.illegal.expected-indentation.yaml" - }, - "3": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "4": { - "name": "invalid.illegal.expected-indentation.yaml" - } - }, - "name": "meta.map.explicit.yaml", - "patterns": [ - { - "include": "#key-double" - }, - { - "include": "#key-single" - }, - { - "include": "#flow-key-plain-out" - }, - { - "include": "#block-map-value" - }, - { - "include": "#block-node" - } - ] - }, - "block-map-key-double": { - "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", - "begin": "\\G\"", - "end": "\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": ".[\t ]*+$", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "include": "#double-escape" - } - ] - }, - "block-map-key-single": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", - "begin": "\\G'", - "end": "'(?!')", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": ".[\t ]*+$", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "match": "''", - "name": "constant.character.escape.single-quote.yaml" - } - ] - }, - "block-map-key-plain": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (BLOCK-KEY)", - "begin": "\\G(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", - "end": "(?=[\t ]*+:[\r\n\t ]|(?>[\t ]++|\\G)#)", - "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", - "patterns": [ - { - "include": "#tag-implicit-plain-out" - }, - { - "match": "\\G([\t ]++)(.)", - "captures": { - "1": { - "name": "punctuation.whitespace.separator.yaml" - }, - "2": { - "name": "invalid.illegal.multiline-key.yaml" - } - } - }, - { - "match": "[\t ]++$", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": "\\x{FEFF}", - "name": "invalid.illegal.bom.yaml" - }, - { - "include": "#non-printable" - } - ] - }, - "block-map-value": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", - "begin": ":(?=[\r\n\t ])", - "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{FEFF}]]|-[^\r\n\t ])", - "beginCaptures": { - "0": { - "name": "punctuation.separator.map.value.yaml" - } - }, - "name": "meta.map.value.yaml", - "patterns": [ - { - "include": "#block-node" - } - ] - }, - "block-scalar": { - "comment": "https://yaml.org/spec/1.2.2/#81-block-scalar-styles", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator", - "begin": "([\t ]*+)(?>(\\|)|(>))(?[+-])?+((1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)|(9))(?()|([+-]))?+", - "while": "\\G(?>(?>(?!\\6) |(?!\\7) {2}|(?!\\8) {3}|(?!\\9) {4}|(?!\\10) {5}|(?!\\11) {6}|(?!\\12) {7}|(?!\\13) {8}|(?!\\14) {9})| *+($|[^#]))", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.separator.yaml" - }, - "2": { - "name": "keyword.control.flow.block-scalar.literal.yaml" - }, - "3": { - "name": "keyword.control.flow.block-scalar.folded.yaml" - }, - "4": { - "name": "storage.modifier.chomping-indicator.yaml" - }, - "5": { - "name": "constant.numeric.indentation-indicator.yaml" - }, - "15": { - "name": "storage.modifier.chomping-indicator.yaml" - } - }, - "whileCaptures": { - "0": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "1": { - "name": "invalid.illegal.expected-indentation.yaml" - } - }, - "name": "meta.scalar.yaml", - "patterns": [ - { - "begin": "$", - "while": "\\G", - "contentName": "string.unquoted.block.yaml", - "patterns": [ - { - "include": "#non-printable" - } - ] - }, - { - "begin": "\\G", - "end": "$", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-b-block-header", - "//": "Soooooooo many edge cases", - "begin": "([\t ]*+)(?>(\\|)|(>))([+-]?+)", - "while": "\\G", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.separator.yaml" - }, - "2": { - "name": "keyword.control.flow.block-scalar.literal.yaml" - }, - "3": { - "name": "keyword.control.flow.block-scalar.folded.yaml" - }, - "4": { - "name": "storage.modifier.chomping-indicator.yaml" - } - }, - "name": "meta.scalar.yaml", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#rule-l-literal-content", - "begin": "$", - "while": "\\G", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", - "//": "Find the highest indented line", - "begin": "\\G( ++)$", - "while": "\\G(?>(\\1)$|(?!\\1)( *+)($|.))", - "captures": { - "1": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "2": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "3": { - "name": "invalid.illegal.expected-indentation.yaml" - } - }, - "contentName": "string.unquoted.block.yaml", - "patterns": [ - { - "include": "#non-printable" - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-b-nb-literal-next", - "//": [ - "Funky wrapper function", - "The `end` pattern clears the parent `\\G` anchor", - "Affectively forcing this rule to only match at most once", - "https://github.com/microsoft/vscode-textmate/issues/114" - ], - "begin": "\\G(?!$)(?=( *+))", - "end": "\\G(?!\\1)(?=[\t ]*+#)", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", - "begin": "\\G( *+)", - "while": "\\G(?>(\\1)|( *+)($|[^\t#]|[\t ]++[^#]))", - "captures": { - "1": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "2": { - "name": "punctuation.whitespace.indentation.yaml" - }, - "3": { - "name": "invalid.illegal.expected-indentation.yaml" - } - }, - "contentName": "string.unquoted.block.yaml", - "patterns": [ - { - "include": "#non-printable" - } - ] - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-l-chomped-empty", - "begin": "(?!\\G)(?=[\t ]*+#)", - "while": "\\G", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - } - ] - }, - { - "comment": "Header Comment", - "begin": "\\G", - "end": "$", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - } - ] - } - ] - }, - "block-plain-out": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-OUT)", - "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", - "while": "\\G", - "patterns": [ - { - "begin": "\\G", - "end": "(?=(?>[\t ]++|\\G)#)", - "name": "string.unquoted.plain.out.yaml", - "patterns": [ - { - "include": "#tag-implicit-plain-out" - }, - { - "match": ":(?=[\r\n\t ])", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "match": "\\G[\t ]++", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": "[\t ]++$", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": "\\x{FEFF}", - "name": "invalid.illegal.bom.yaml" - }, - { - "include": "#non-printable" - } - ] - }, - { - "begin": "(?!\\G)", - "while": "\\G", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - } - ] - }, - "flow-node": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-seq-entry (FLOW-IN)", - "patterns": [ - { - "begin": "(?=\\[|{)", - "end": "(?=[:,\\]}])", - "patterns": [ - { - "begin": "(?!\\G)", - "end": "(?=[:,\\]}])", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - }, - { - "include": "#flow-mapping" - }, - { - "include": "#flow-sequence" - } - ] - }, - { - "include": "#anchor-property" - }, - { - "include": "#tag-property" - }, - { - "include": "#alias" - }, - { - "begin": "(?=\"|')", - "end": "(?=[:,\\]}])", - "patterns": [ - { - "begin": "(?!\\G)", - "end": "(?=[:,\\]}])", - "patterns": [ - { - "include": "#presentation-detail" - } - ] - }, - { - "include": "#double" - }, - { - "include": "#single" - } - ] - }, - { - "include": "#flow-plain-in" - }, - { - "include": "#presentation-detail" - } - ] - }, - "flow-mapping": { - "comment": "https://yaml.org/spec/1.2.2/#742-flow-mappings", - "begin": "{", - "end": "}", - "beginCaptures": { - "0": { - "name": "punctuation.definition.mapping.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.mapping.end.yaml" - } - }, - "name": "meta.flow.mapping.yaml", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-map-entries", - "begin": "(?<={)\\G(?=[\r\n\t ,#])|,", - "end": "(?=[^\r\n\t ,#])", - "beginCaptures": { - "0": { - "name": "punctuation.separator.mapping.yaml" - } - }, - "patterns": [ - { - "match": ",++", - "name": "invalid.illegal.separator.sequence.yaml" - }, - { - "include": "#presentation-detail" - } - ] - }, - { - "include": "#flow-mapping-map-key" - }, - { - "include": "#flow-map-value-yaml" - }, - { - "include": "#flow-map-value-json" - }, - { - "include": "#flow-node" - } - ] - }, - "flow-sequence": { - "comment": "https://yaml.org/spec/1.2.2/#741-flow-sequences", - "begin": "\\[", - "end": "]", - "beginCaptures": { - "0": { - "name": "punctuation.definition.sequence.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.sequence.end.yaml" - } - }, - "name": "meta.flow.sequence.yaml", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-seq-entries", - "begin": "(?<=\\[)\\G(?=[\r\n\t ,#])|,", - "end": "(?=[^\r\n\t ,#])", - "beginCaptures": { - "0": { - "name": "punctuation.separator.sequence.yaml" - } - }, - "patterns": [ - { - "match": ",++", - "name": "invalid.illegal.separator.sequence.yaml" - }, - { - "include": "#presentation-detail" - } - ] - }, - { - "include": "#flow-sequence-map-key" - }, - { - "include": "#flow-map-value-yaml" - }, - { - "include": "#flow-map-value-json" - }, - { - "include": "#flow-node" - } - ] - }, - "flow-mapping-map-key": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", - "patterns": [ - { - "begin": "\\?(?=[\r\n\t ,\\[\\]{}])", - "end": "(?=[,\\[\\]{}])", - "beginCaptures": { - "0": { - "name": "punctuation.definition.map.key.yaml" - } - }, - "name": "meta.flow.map.explicit.yaml", - "patterns": [ - { - "include": "#flow-mapping-map-key" - }, - { - "include": "#flow-map-value-yaml" - }, - { - "include": "#flow-map-value-json" - }, - { - "include": "#flow-node" - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", - "begin": "(?=(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}])))", - "end": "(?=[,\\[\\]{}])", - "name": "meta.flow.map.implicit.yaml", - "patterns": [ - { - "include": "#flow-key-plain-in" - }, - { - "match": ":(?=\\[|{)", - "name": "invalid.illegal.separator.map.yaml" - }, - { - "include": "#flow-map-value-yaml" - }, - { - "include": "#presentation-detail" - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", - "begin": "(?=\"|')", - "end": "(?=[,\\[\\]{}])", - "name": "meta.flow.map.implicit.yaml", - "patterns": [ - { - "include": "#key-double" - }, - { - "include": "#key-single" - }, - { - "include": "#flow-map-value-json" - }, - { - "include": "#presentation-detail" - } - ] - } - ] - }, - "flow-sequence-map-key": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", - "patterns": [ - { - "begin": "\\?(?=[\r\n\t ,\\[\\]{}])", - "end": "(?=[,\\[\\]{}])", - "beginCaptures": { - "0": { - "name": "punctuation.definition.map.key.yaml" - } - }, - "name": "meta.flow.map.explicit.yaml", - "patterns": [ - { - "include": "#flow-mapping-map-key" - }, - { - "include": "#flow-map-value-yaml" - }, - { - "include": "#flow-map-value-json" - }, - { - "include": "#flow-node" - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", - "begin": "(?<=[\t ,\\[{]|^)(?=(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}]))(?>[^:#,\\[\\]{}]++|:(?![\r\n\t ,\\[\\]{}])|(?\"(?>[^\\\\\"]++|\\\\.)*+\"|'(?>[^']++|'')*+')[\t ]*+:)", - "end": "(?=[,\\[\\]{}])", - "name": "meta.flow.map.implicit.yaml", - "patterns": [ - { - "include": "#key-double" - }, - { - "include": "#key-single" - }, - { - "include": "#flow-map-value-json" - }, - { - "include": "#presentation-detail" - } - ] - } - ] - }, - "flow-map-value-yaml": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", - "begin": ":(?=[\r\n\t ,\\[\\]{}])", - "end": "(?=[,\\]}])", - "beginCaptures": { - "0": { - "name": "punctuation.separator.map.value.yaml" - } - }, - "name": "meta.flow.pair.value.yaml", - "patterns": [ - { - "include": "#flow-node" - } - ] - }, - "flow-map-value-json": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", - "begin": "(?<=(?>[\"'\\]}]|^)[\t ]*+):", - "end": "(?=[,\\]}])", - "beginCaptures": { - "0": { - "name": "punctuation.separator.map.value.yaml" - } - }, - "name": "meta.flow.pair.value.yaml", - "patterns": [ - { - "include": "#flow-node" - } - ] - }, - "flow-plain-in": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-IN)", - "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}]))", - "end": "(?=(?>[\t ]++|\\G)#|[\t ]*+[,\\[\\]{}])", - "name": "string.unquoted.plain.in.yaml", - "patterns": [ - { - "include": "#tag-implicit-plain-in" - }, - { - "match": "\\G[\t ]++", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": "[\t ]++$", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": ":(?=[\r\n\t ,\\[\\]{}])", - "name": "invalid.illegal.multiline-key.yaml" - }, - { - "match": "\\x{FEFF}", - "name": "invalid.illegal.bom.yaml" - }, - { - "include": "#non-printable" - } - ] - }, - "flow-key-plain-out": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (FLOW-OUT)", - "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", - "end": "(?=[\t ]*+:[\r\n\t ]|[\t ]++#)", - "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", - "patterns": [ - { - "include": "#tag-implicit-plain-out" - }, - { - "match": "\\G[\t ]++", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": "[\t ]++$", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "match": "\\x{FEFF}", - "name": "invalid.illegal.bom.yaml" - }, - { - "include": "#non-printable" - } - ] - }, - "flow-key-plain-in": { - "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-implicit-yaml-key (FLOW-KEY)", - "begin": "\\G(?![\r\n\t #])", - "end": "(?=[\t ]*+(?>:[\r\n\t ,\\[\\]{}]|[,\\[\\]{}])|[\t ]++#)", - "name": "meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", - "patterns": [ - { - "include": "#tag-implicit-plain-in" - }, - { - "match": "\\x{FEFF}", - "name": "invalid.illegal.bom.yaml" - }, - { - "include": "#non-printable" - } - ] - }, - "key-double": { - "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", - "begin": "\\G\"", - "end": "\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "include": "#double-escape" - } - ] - }, - "key-single": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", - "begin": "\\G'", - "end": "'(?!')", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", - "patterns": [ - { - "match": "[^\t -\\x{10FFFF}]++", - "name": "invalid.illegal.character.yaml" - }, - { - "match": "''", - "name": "constant.character.escape.single-quote.yaml" - } - ] - }, - "double": { - "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", - "begin": "\"", - "end": "\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "string.quoted.double.yaml", - "patterns": [ - { - "match": "(?x[^\"]{2,0}|u[^\"]{4,0}|U[^\"]{8,0}|.)", - "name": "invalid.illegal.constant.character.escape.yaml" - } - ] - }, - "tag-implicit-plain-in": { - "comment": "https://yaml.org/spec/1.2.2/#103-core-schema", - "patterns": [ - { - "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", - "name": "constant.language.null.yaml" - }, - { - "match": "\\G(?>true|True|TRUE|false|False|FALSE)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", - "name": "constant.language.boolean.yaml" - }, - { - "match": "\\G[+-]?+[0-9]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.integer.decimal.yaml" - }, - { - "match": "\\G0o[0-7]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.integer.octal.yaml" - }, - { - "match": "\\G0x[0-9a-fA-F]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.integer.hexadecimal.yaml" - }, - { - "match": "\\G[+-]?+(?>\\.[0-9]++|[0-9]++(?>\\.[0-9]*+)?+)(?>[eE][+-]?+[0-9]++)?+(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.float.yaml" - }, - { - "match": "\\G[+-]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.float.inf.yaml" - }, - { - "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", - "name": "constant.numeric.float.nan.yaml" - } - ] - }, - "tag-implicit-plain-out": { - "comment": "https://yaml.org/spec/1.2.2/#103-core-schema", - "patterns": [ - { - "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", - "name": "constant.language.null.yaml" - }, - { - "match": "\\G(?>true|True|TRUE|false|False|FALSE)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", - "name": "constant.language.boolean.yaml" - }, - { - "match": "\\G[+-]?+[0-9]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", - "name": "constant.numeric.integer.decimal.yaml" - }, - { - "match": "\\G0o[0-7]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", - "name": "constant.numeric.integer.octal.yaml" - }, - { - "match": "\\G0x[0-9a-fA-F]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", - "name": "constant.numeric.integer.hexadecimal.yaml" - }, - { - "match": "\\G[+-]?+(?>\\.[0-9]++|[0-9]++(?>\\.[0-9]*+)?+)(?>[eE][+-]?+[0-9]++)?+(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", - "name": "constant.numeric.float.yaml" - }, - { - "match": "\\G[+-]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", - "name": "constant.numeric.float.inf.yaml" - }, - { - "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", - "name": "constant.numeric.float.nan.yaml" - } - ] - }, - "tag-property": { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-tag-property", - "//": [ - "!", - "!!", - "!<>", - "!...", - "!!...", - "!<...>", - "!...!..." - ], - "patterns": [ - { - "match": "!(?=[\r\n\t ])", - "name": "storage.type.tag.non-specific.yaml punctuation.definition.tag.non-specific.yaml" - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-verbatim-tag", - "begin": "!<", - "end": ">", - "beginCaptures": { - "0": { - "name": "punctuation.definition.tag.begin.yaml" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.tag.end.yaml" - } - }, - "name": "storage.type.tag.verbatim.yaml", - "patterns": [ - { - "match": "%[0-9a-fA-F]{2}", - "name": "constant.character.escape.unicode.8-bit.yaml" - }, - { - "match": "%[^\r\n\t ]{2,0}", - "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" - }, - { - "include": "#non-printable" - }, - { - "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]%>]++", - "name": "invalid.illegal.unrecognized.yaml" - } - ] - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-shorthand-tag", - "begin": "(?=!)", - "end": "(?=[\r\n\t ,\\[\\]{}])", - "name": "storage.type.tag.shorthand.yaml", - "patterns": [ - { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", - "match": "\\G!!", - "name": "punctuation.definition.tag.secondary.yaml" - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", - "match": "\\G(!)[0-9A-Za-z-]++(!)", - "captures": { - "1": { - "name": "punctuation.definition.tag.named.yaml" - }, - "2": { - "name": "punctuation.definition.tag.named.yaml" - } - } - }, - { - "comment": "https://yaml.org/spec/1.2.2/#rule-c-primary-tag-handle", - "match": "\\G!", - "name": "punctuation.definition.tag.primary.yaml" - }, - { - "match": "%[0-9a-fA-F]{2}", - "name": "constant.character.escape.unicode.8-bit.yaml" - }, - { - "match": "%[^\r\n\t ]{2,0}", - "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" - }, - { - "include": "#non-printable" - }, - { - "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$_.~*'()%]++", - "name": "invalid.illegal.unrecognized.yaml" - } - ] - } - ] - }, - "anchor-property": { - "match": "(&)([\\x{85}[^ ,\\[\\]{}\\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)|(&)", - "captures": { - "0": { - "name": "keyword.control.flow.anchor.yaml" - }, - "1": { - "name": "punctuation.definition.anchor.yaml" - }, - "2": { - "name": "variable.other.anchor.yaml" - }, - "3": { - "name": "invalid.illegal.flow.anchor.yaml" - } - } - }, - "alias": { - "begin": "(\\*)([\\x{85}[^ ,\\[\\]{}\\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)|(\\*)", - "end": "(?=:[\r\n\t ,\\[\\]{}]|[,\\[\\]{}])", - "captures": { - "0": { - "name": "keyword.control.flow.alias.yaml" - }, - "1": { - "name": "punctuation.definition.alias.yaml" - }, - "2": { - "name": "variable.other.alias.yaml" - }, - "3": { - "name": "invalid.illegal.flow.alias.yaml" - } - }, - "patterns": [ - { - "include": "#presentation-detail" - } - ] - }, - "byte-order-mark": { - "comment": "", - "match": "\\G\\x{FEFF}++", - "name": "byte-order-mark.yaml" - }, - "presentation-detail": { - "patterns": [ - { - "match": "[\t ]++", - "name": "punctuation.whitespace.separator.yaml" - }, - { - "include": "#non-printable" - }, - { - "include": "#comment" - }, - { - "include": "#unknown" - } - ] - }, - "non-printable": { - "//": { - "85": "…", - "10000": "𐀀", - "A0": " ", - "D7FF": "퟿", - "E000": "", - "FFFD": "�", - "FEFF": "", - "FFFF": "￿", - "10FFFF": "􏿿" - }, - "//match": "[\\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}&&[^\t\n\r\\x{85}]]++", - "match": "[^\t\n\r -~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]++", - "name": "invalid.illegal.non-printable.yaml" - }, - "comment": { - "comment": "Comments must be separated from other tokens by white space characters. `space`, `tab`, `newline` or `carriage-return`. `#(.*)` causes performance issues", - "begin": "(?<=[\\x{FEFF}\t ]|^)#", - "end": "\r|\n", - "captures": { - "0": { - "name": "punctuation.definition.comment.yaml" - } - }, - "name": "comment.line.number-sign.yaml", - "patterns": [ - { - "include": "#non-printable" - } - ] - }, - "unknown": { - "match": ".[[^\"':,\\[\\]{}]&&!-~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", - "name": "invalid.illegal.unrecognized.yaml markup.strikethrough" } } } \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml.tmLanguage.json b/extensions/yaml/syntaxes/yaml.tmLanguage.json index 39d8e586442..3a64fbd850e 100644 --- a/extensions/yaml/syntaxes/yaml.tmLanguage.json +++ b/extensions/yaml/syntaxes/yaml.tmLanguage.json @@ -4,10 +4,21 @@ "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/RedCMD/YAML-Syntax-Highlighter/commit/5d2a15e2ee4bb9c2cc9a86a0b72aea8fa2aba1e1", + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/d4dca9f38a654ebbb13c1b72b7881e3c5864a778", "name": "YAML Ain't Markup Language", "scopeName": "source.yaml", "patterns": [ + { + "comment": "Support legacy FrontMatter integration", + "//": "https://github.com/microsoft/vscode-markdown-tm-grammar/pull/162", + "begin": "(?<=^-{3,}\\s*+)\\G$", + "while": "^(?! {3,0}-{3,}[ \t]*+$|[ \t]*+\\.{3}$)", + "patterns": [ + { + "include": "source.yaml.1.2" + } + ] + }, { "comment": "Default to YAML version 1.2", "include": "source.yaml.1.2" @@ -15,7 +26,81 @@ ], "repository": { "parity": { - "comment": "Yes... That is right. Due to the changes with \\x2028, \\x2029, \\x85 and 'tags'. This is all the code I was able to reuse between all versions 1.3, 1.2, 1.1 and 1.0" + "comment": "Yes... That is right. Due to the changes with \\x2028, \\x2029, \\x85 and 'tags'. This is all the code I was able to reuse between all YAML versions 1.3, 1.2, 1.1 and 1.0" + }, + "block-map-key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", + "begin": "\\G'", + "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + "key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (FLOW-OUT)", + "begin": "'", + "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + "non-printable": { + "//": { + "85": "…", + "2028": "", + "2029": "", + "10000": "𐀀", + "A0": " ", + "D7FF": "퟿", + "E000": "", + "FFFD": "�", + "FEFF": "", + "FFFF": "￿", + "10FFFF": "􏿿" + }, + "//match": "[\\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}&&[^\t\n\r\\x{85}]]++", + "match": "[^\t\n\r -~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]++", + "name": "invalid.illegal.non-printable.yaml" } } } \ No newline at end of file diff --git a/extensions/yarn.lock b/extensions/yarn.lock index b981143bdd0..962d9f4b001 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,120 +2,125 @@ # yarn lockfile v1 -"@esbuild/aix-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" - integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== +"@esbuild/aix-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259" + integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ== -"@esbuild/android-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" - integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== +"@esbuild/android-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832" + integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ== -"@esbuild/android-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" - integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== +"@esbuild/android-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99" + integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g== -"@esbuild/android-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" - integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== +"@esbuild/android-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6" + integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ== -"@esbuild/darwin-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" - integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== +"@esbuild/darwin-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e" + integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow== -"@esbuild/darwin-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" - integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== +"@esbuild/darwin-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c" + integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ== -"@esbuild/freebsd-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" - integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== +"@esbuild/freebsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4" + integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw== -"@esbuild/freebsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" - integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== +"@esbuild/freebsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d" + integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ== -"@esbuild/linux-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" - integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== +"@esbuild/linux-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a" + integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw== -"@esbuild/linux-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" - integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== +"@esbuild/linux-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad" + integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw== -"@esbuild/linux-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" - integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== +"@esbuild/linux-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238" + integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA== -"@esbuild/linux-loong64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" - integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== +"@esbuild/linux-loong64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280" + integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A== -"@esbuild/linux-mips64el@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" - integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== +"@esbuild/linux-mips64el@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5" + integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w== -"@esbuild/linux-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" - integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== +"@esbuild/linux-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6" + integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw== -"@esbuild/linux-riscv64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" - integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== +"@esbuild/linux-riscv64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780" + integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw== -"@esbuild/linux-s390x@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" - integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== +"@esbuild/linux-s390x@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8" + integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg== -"@esbuild/linux-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" - integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== +"@esbuild/linux-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910" + integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ== -"@esbuild/netbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" - integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== +"@esbuild/netbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c" + integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw== -"@esbuild/openbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" - integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== +"@esbuild/openbsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db" + integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ== -"@esbuild/sunos-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" - integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== +"@esbuild/openbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8" + integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg== -"@esbuild/win32-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" - integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== +"@esbuild/sunos-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8" + integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA== -"@esbuild/win32-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" - integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== +"@esbuild/win32-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d" + integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ== -"@esbuild/win32-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" - integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== +"@esbuild/win32-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7" + integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA== + +"@esbuild/win32-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" + integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== "@parcel/watcher@2.1.0": version "2.1.0" @@ -146,34 +151,35 @@ cson-parser@^4.0.9: dependencies: coffeescript "1.12.7" -esbuild@0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4" - integrity sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA== +esbuild@0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.0.tgz#de06002d48424d9fdb7eb52dbe8e95927f852599" + integrity sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA== optionalDependencies: - "@esbuild/aix-ppc64" "0.20.0" - "@esbuild/android-arm" "0.20.0" - "@esbuild/android-arm64" "0.20.0" - "@esbuild/android-x64" "0.20.0" - "@esbuild/darwin-arm64" "0.20.0" - "@esbuild/darwin-x64" "0.20.0" - "@esbuild/freebsd-arm64" "0.20.0" - "@esbuild/freebsd-x64" "0.20.0" - "@esbuild/linux-arm" "0.20.0" - "@esbuild/linux-arm64" "0.20.0" - "@esbuild/linux-ia32" "0.20.0" - "@esbuild/linux-loong64" "0.20.0" - "@esbuild/linux-mips64el" "0.20.0" - "@esbuild/linux-ppc64" "0.20.0" - "@esbuild/linux-riscv64" "0.20.0" - "@esbuild/linux-s390x" "0.20.0" - "@esbuild/linux-x64" "0.20.0" - "@esbuild/netbsd-x64" "0.20.0" - "@esbuild/openbsd-x64" "0.20.0" - "@esbuild/sunos-x64" "0.20.0" - "@esbuild/win32-arm64" "0.20.0" - "@esbuild/win32-ia32" "0.20.0" - "@esbuild/win32-x64" "0.20.0" + "@esbuild/aix-ppc64" "0.23.0" + "@esbuild/android-arm" "0.23.0" + "@esbuild/android-arm64" "0.23.0" + "@esbuild/android-x64" "0.23.0" + "@esbuild/darwin-arm64" "0.23.0" + "@esbuild/darwin-x64" "0.23.0" + "@esbuild/freebsd-arm64" "0.23.0" + "@esbuild/freebsd-x64" "0.23.0" + "@esbuild/linux-arm" "0.23.0" + "@esbuild/linux-arm64" "0.23.0" + "@esbuild/linux-ia32" "0.23.0" + "@esbuild/linux-loong64" "0.23.0" + "@esbuild/linux-mips64el" "0.23.0" + "@esbuild/linux-ppc64" "0.23.0" + "@esbuild/linux-riscv64" "0.23.0" + "@esbuild/linux-s390x" "0.23.0" + "@esbuild/linux-x64" "0.23.0" + "@esbuild/netbsd-x64" "0.23.0" + "@esbuild/openbsd-arm64" "0.23.0" + "@esbuild/openbsd-x64" "0.23.0" + "@esbuild/sunos-x64" "0.23.0" + "@esbuild/win32-arm64" "0.23.0" + "@esbuild/win32-ia32" "0.23.0" + "@esbuild/win32-x64" "0.23.0" fast-plist@0.1.2: version "0.1.2" @@ -234,10 +240,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -typescript@^5.5.2: - version "5.5.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507" - integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew== +typescript@^5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== vscode-grammar-updater@^1.1.0: version "1.1.0" diff --git a/package.json b/package.json index 7a7ca63dd80..49aa85688d9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.92.0", - "distro": "58e7d90e05684b6937db21dd372f7a088bdc9dc1", + "version": "1.93.0", + "distro": "0f583d589f6051015e7af0b7586a31e3529be6f0", "author": { "name": "Microsoft Corporation" }, @@ -73,32 +73,33 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", - "@vscode/proxy-agent": "^0.21.0", + "@vscode/proxy-agent": "^0.22.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.6-vscode", "@vscode/sudo-prompt": "9.3.1", + "@vscode/tree-sitter-wasm": "^0.0.1", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "0.2.0-beta.19", - "@xterm/addon-image": "0.9.0-beta.36", - "@xterm/addon-search": "0.16.0-beta.36", - "@xterm/addon-serialize": "0.14.0-beta.36", - "@xterm/addon-unicode11": "0.9.0-beta.36", - "@xterm/addon-webgl": "0.19.0-beta.36", - "@xterm/headless": "5.6.0-beta.36", - "@xterm/xterm": "5.6.0-beta.36", + "@xterm/addon-clipboard": "0.2.0-beta.35", + "@xterm/addon-image": "0.9.0-beta.52", + "@xterm/addon-search": "0.16.0-beta.52", + "@xterm/addon-serialize": "0.14.0-beta.52", + "@xterm/addon-unicode11": "0.9.0-beta.52", + "@xterm/addon-webgl": "0.19.0-beta.52", + "@xterm/headless": "5.6.0-beta.52", + "@xterm/xterm": "5.6.0-beta.52", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.3", - "kerberos": "^2.0.1", + "kerberos": "2.1.1", "minimist": "^1.2.6", "native-is-elevated": "0.7.0", "native-keymap": "^3.3.5", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta11", + "node-pty": "1.1.0-beta20", "open": "^8.4.2", "tas-client-umd": "0.2.0", "v8-inspect-profiler": "^0.1.1", @@ -136,7 +137,7 @@ "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", "@vscode/test-cli": "^0.0.6", - "@vscode/test-electron": "^2.3.8", + "@vscode/test-electron": "^2.4.0", "@vscode/test-web": "^0.0.56", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.14", @@ -150,7 +151,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "29.4.0", + "electron": "30.3.1", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", @@ -209,7 +210,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.6.0-dev.20240703", + "typescript": "^5.6.0-dev.20240805", "util": "^0.12.4", "vscode-nls-dev": "^3.3.1", "webpack": "^5.91.0", diff --git a/product.json b/product.json index 27ae53fe16b..1803992a004 100644 --- a/product.json +++ b/product.json @@ -34,8 +34,8 @@ "builtInExtensions": [ { "name": "ms-vscode.js-debug-companion", - "version": "1.1.2", - "sha256": "e034b8b41beb4e97e02c70f7175bd88abe66048374c2bd629f54bb33354bc2aa", + "version": "1.1.3", + "sha256": "7380a890787452f14b2db7835dfa94de538caf358ebc263f9d46dd68ac52de93", "repo": "https://github.com/microsoft/vscode-js-debug-companion", "metadata": { "id": "99cb0b7f-7354-4278-b8da-6cc79972169d", @@ -50,8 +50,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.91.0", - "sha256": "53b99146c7fa280f00c74414e09721530c622bf3e5eac2c967ddfb9906b51c80", + "version": "1.92.0", + "sha256": "e5d0a74728292423631f79d076ecb2bc129f9637bcbc2529e48a0fd53baa69cc", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", diff --git a/remote/.yarnrc b/remote/.yarnrc index 4c99388e889..748c8d4a77a 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,5 +1,5 @@ disturl "https://nodejs.org/dist" -target "20.11.1" -ms_build_id "275039" +target "20.15.1" +ms_build_id "287145" runtime "node" build_from_source "true" diff --git a/remote/package.json b/remote/package.json index bb615b88ea4..66c0414ef31 100644 --- a/remote/package.json +++ b/remote/package.json @@ -8,28 +8,29 @@ "@parcel/watcher": "2.1.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.21.0", + "@vscode/proxy-agent": "^0.22.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", + "@vscode/tree-sitter-wasm": "^0.0.1", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "0.2.0-beta.19", - "@xterm/addon-image": "0.9.0-beta.36", - "@xterm/addon-search": "0.16.0-beta.36", - "@xterm/addon-serialize": "0.14.0-beta.36", - "@xterm/addon-unicode11": "0.9.0-beta.36", - "@xterm/addon-webgl": "0.19.0-beta.36", - "@xterm/headless": "5.6.0-beta.36", - "@xterm/xterm": "5.6.0-beta.36", + "@xterm/addon-clipboard": "0.2.0-beta.35", + "@xterm/addon-image": "0.9.0-beta.52", + "@xterm/addon-search": "0.16.0-beta.52", + "@xterm/addon-serialize": "0.14.0-beta.52", + "@xterm/addon-unicode11": "0.9.0-beta.52", + "@xterm/addon-webgl": "0.19.0-beta.52", + "@xterm/headless": "5.6.0-beta.52", + "@xterm/xterm": "5.6.0-beta.52", "cookie": "^0.4.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.3", - "kerberos": "^2.0.1", + "kerberos": "2.1.1", "minimist": "^1.2.6", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta11", + "node-pty": "1.1.0-beta20", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", diff --git a/remote/web/package.json b/remote/web/package.json index f6b57db5ea0..c7fdaf96b33 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -6,14 +6,15 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", + "@vscode/tree-sitter-wasm": "^0.0.1", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "0.2.0-beta.19", - "@xterm/addon-image": "0.9.0-beta.36", - "@xterm/addon-search": "0.16.0-beta.36", - "@xterm/addon-serialize": "0.14.0-beta.36", - "@xterm/addon-unicode11": "0.9.0-beta.36", - "@xterm/addon-webgl": "0.19.0-beta.36", - "@xterm/xterm": "5.6.0-beta.36", + "@xterm/addon-clipboard": "0.2.0-beta.35", + "@xterm/addon-image": "0.9.0-beta.52", + "@xterm/addon-search": "0.16.0-beta.52", + "@xterm/addon-serialize": "0.14.0-beta.52", + "@xterm/addon-unicode11": "0.9.0-beta.52", + "@xterm/addon-webgl": "0.19.0-beta.52", + "@xterm/xterm": "5.6.0-beta.52", "jschardet": "3.1.3", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index a28d00372f7..cdc4d7918b8 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -43,47 +43,52 @@ resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== +"@vscode/tree-sitter-wasm@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.1.tgz#ffb2e295a416698f4c77cbffeca3b28567d6754b" + integrity sha512-m0GKnQ3BxWnVd+20KLGwr1+Qvt/RiiaJmKAqHNU35pNydDtduUzyBm7ETz/T0vOVKoeIAaiYsJOA1aKWs7Y1tA== + "@vscode/vscode-languagedetection@1.0.21": version "1.0.21" resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== -"@xterm/addon-clipboard@0.2.0-beta.19": - version "0.2.0-beta.19" - resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.19.tgz#da2ea7a0d6e51383d4a21cbb04fb7fbd9db7d853" - integrity sha512-A/NxJQoOq21kE1ykZ07Cw3IxD5cQFxba1iMxnSFvWGVC71ZdHGwUveLeY8nHWEL8PfLsZxAgIzlMTfWgfkQ+CA== +"@xterm/addon-clipboard@0.2.0-beta.35": + version "0.2.0-beta.35" + resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.35.tgz#4c9b553ea63ce02a3c8075fea7df1637d52092ef" + integrity sha512-B8AulZEjsfvSEaLKp8oyRu7yJ7FJb5R3W0wpPbI/rOMVAuBwxDJsz0CxLvJUXnJX7OJwd5cjnyTnEcXJfMJycA== dependencies: js-base64 "^3.7.5" -"@xterm/addon-image@0.9.0-beta.36": - version "0.9.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.36.tgz#79024103c48f4e401ca15afe49fad4f3834c023c" - integrity sha512-m8c5OfJBzPYfv90mSgc0bX/P+qUsgczVajHW+kE59UoC311ng13IlCg6a4bJHb2EHqGsq19fIrYCn6+JsMdRsQ== +"@xterm/addon-image@0.9.0-beta.52": + version "0.9.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.52.tgz#a3115a50f884e5ba2f8ce09118a3d7e833fceb7b" + integrity sha512-1fWhnCIvLeO0aQ3CKqkTB9ye1bUsocpgFdDOgmwfW4XhLXpvu+QcyMGQMtWJHt8JWBN2w0cgR9eyfKw7orN+9Q== -"@xterm/addon-search@0.16.0-beta.36": - version "0.16.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.36.tgz#22deda3250552f24de05f8112299d15f3fe90f01" - integrity sha512-lN66vYpKvNBxbvtJXLbuidirirmIzySXnl8JvarcrDaw4HlqluOvvjEdVYKofWV5ZGSaPfIAijwJW1f0KjUhJw== +"@xterm/addon-search@0.16.0-beta.52": + version "0.16.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.52.tgz#f8c77629b95ceff7d6b93d95c4b085857f405470" + integrity sha512-ZLVh0O91dcjxCjrU3vadl+40Z/mBnYXhKNA58oU/dGWFtFxtUB9SaZoOUtBvnfDpQIloYAK6raC2AfVsKHzD8A== -"@xterm/addon-serialize@0.14.0-beta.36": - version "0.14.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.36.tgz#1407c13fe1bd869ad4f26e7b7da4e7fa87442021" - integrity sha512-6KpzHlQIuHakPv70dKhQp8f6e9hk4q1fNuuTD1rEzDg8DeKRfUDjorw1vPkKTB/DD+3zaMUBtg7DFVVEi+/+Cw== +"@xterm/addon-serialize@0.14.0-beta.52": + version "0.14.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.52.tgz#19708cdd2895ddbd983b771ae9d14d7bc54cf7c9" + integrity sha512-1+ckKya1OURFmELH1Tjjoxz3Gnj78Dxj+NNRrEunfINkvyzaY+n8wT28FQxIlU5gJq+a0VGvlhNgTkMwgOn6aw== -"@xterm/addon-unicode11@0.9.0-beta.36": - version "0.9.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.36.tgz#158dcdd707a466958a256a960e5d9a967a97a9dc" - integrity sha512-BKP2ml0fYOHnfaTp0LorSluNXjHRSEwf3yrD3K6jEZfYTBePhee1TAxOdNH/TdqwNYZYaYHaK87A5mSuYpKPBQ== +"@xterm/addon-unicode11@0.9.0-beta.52": + version "0.9.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.52.tgz#15afdf03c20d2e46b4eca68bd461eea71c8dd37f" + integrity sha512-5tZR/8c+vf0YNSYS6B9pEv8gyWWZpPYOf/BRQDkTGtYAnFf04MzggVE/U7tKUXGDzBhzwTPODq5qPNTX1xpGgw== -"@xterm/addon-webgl@0.19.0-beta.36": - version "0.19.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.36.tgz#8926a0434e5ce74eee12a965c06cd5f601391f18" - integrity sha512-bJA1enVNlIMRkBU9i7i8qX26Zs2/CrGedREW5WI0NZUAn0IHlatWlj3aOfTuI2MYWUPGE8ul30PyipYP6P+fmA== +"@xterm/addon-webgl@0.19.0-beta.52": + version "0.19.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.52.tgz#695b20a5fa88ff92e115624149592080fad59594" + integrity sha512-kbPO9iR166xW8qgRkYmKX2Vu0kQHXpxYLQ9jY/01e5kvNrI/rqRDV63FIq14ncOi7N3+dmTuUkjvbg8anCpuIw== -"@xterm/xterm@5.6.0-beta.36": - version "5.6.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.36.tgz#fd0fd598b67e3bcba61a59bb1a33b131ad86eea3" - integrity sha512-YtFKQIggbvV2brWifksZAtLi447j0DFdoSRoq4vQi/N7KFC0pguGdG3YzYkDOyqoeLMPu569e2b5oevMe6d2aQ== +"@xterm/xterm@5.6.0-beta.52": + version "5.6.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.52.tgz#ed7456a8b78ea1d00431737d078b5ab0bafbcdfe" + integrity sha512-aZh8IBdZPb2N4NjTt/fQ/C90z/PM3Zxkfoqhmlsp5+v3Otmyw5kd3DepeHK1SFW/pz0/xdj4KFgX8t8Y2lDRbA== js-base64@^3.7.5: version "3.7.7" diff --git a/remote/yarn.lock b/remote/yarn.lock index 0f65b688c17..3c6d1ff985e 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -66,10 +66,10 @@ resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== -"@vscode/proxy-agent@^0.21.0": - version "0.21.0" - resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.21.0.tgz#93c818b863ad20b42679032ecc1e3ecdc6306f12" - integrity sha512-9YcpBq+ZhMr3EQY/5ScyHc9kIIU/AcYOQn3DXq0N9tl81ViVsUvii3Fh+FAtD0YQ/qWtDfGxt8VCWZtuyh2D0g== +"@vscode/proxy-agent@^0.22.0": + version "0.22.0" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.22.0.tgz#bf571509d77c02c684be8c8526b7d0ac238c25cb" + integrity sha512-TQrv456pbrjmD6G+iOoXE1Mflm+8Ic/Kny4QU7ioiYe2+0HisvqzJM/CUa3Am5SWrNjMbntTHISjgmSaSlorrA== dependencies: "@tootallnate/once" "^3.0.0" agent-base "^7.0.1" @@ -98,6 +98,11 @@ mkdirp "^1.0.4" node-addon-api "7.1.0" +"@vscode/tree-sitter-wasm@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.1.tgz#ffb2e295a416698f4c77cbffeca3b28567d6754b" + integrity sha512-m0GKnQ3BxWnVd+20KLGwr1+Qvt/RiiaJmKAqHNU35pNydDtduUzyBm7ETz/T0vOVKoeIAaiYsJOA1aKWs7Y1tA== + "@vscode/vscode-languagedetection@1.0.21": version "1.0.21" resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" @@ -122,47 +127,47 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-clipboard@0.2.0-beta.19": - version "0.2.0-beta.19" - resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.19.tgz#da2ea7a0d6e51383d4a21cbb04fb7fbd9db7d853" - integrity sha512-A/NxJQoOq21kE1ykZ07Cw3IxD5cQFxba1iMxnSFvWGVC71ZdHGwUveLeY8nHWEL8PfLsZxAgIzlMTfWgfkQ+CA== +"@xterm/addon-clipboard@0.2.0-beta.35": + version "0.2.0-beta.35" + resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.35.tgz#4c9b553ea63ce02a3c8075fea7df1637d52092ef" + integrity sha512-B8AulZEjsfvSEaLKp8oyRu7yJ7FJb5R3W0wpPbI/rOMVAuBwxDJsz0CxLvJUXnJX7OJwd5cjnyTnEcXJfMJycA== dependencies: js-base64 "^3.7.5" -"@xterm/addon-image@0.9.0-beta.36": - version "0.9.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.36.tgz#79024103c48f4e401ca15afe49fad4f3834c023c" - integrity sha512-m8c5OfJBzPYfv90mSgc0bX/P+qUsgczVajHW+kE59UoC311ng13IlCg6a4bJHb2EHqGsq19fIrYCn6+JsMdRsQ== +"@xterm/addon-image@0.9.0-beta.52": + version "0.9.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.52.tgz#a3115a50f884e5ba2f8ce09118a3d7e833fceb7b" + integrity sha512-1fWhnCIvLeO0aQ3CKqkTB9ye1bUsocpgFdDOgmwfW4XhLXpvu+QcyMGQMtWJHt8JWBN2w0cgR9eyfKw7orN+9Q== -"@xterm/addon-search@0.16.0-beta.36": - version "0.16.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.36.tgz#22deda3250552f24de05f8112299d15f3fe90f01" - integrity sha512-lN66vYpKvNBxbvtJXLbuidirirmIzySXnl8JvarcrDaw4HlqluOvvjEdVYKofWV5ZGSaPfIAijwJW1f0KjUhJw== +"@xterm/addon-search@0.16.0-beta.52": + version "0.16.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.52.tgz#f8c77629b95ceff7d6b93d95c4b085857f405470" + integrity sha512-ZLVh0O91dcjxCjrU3vadl+40Z/mBnYXhKNA58oU/dGWFtFxtUB9SaZoOUtBvnfDpQIloYAK6raC2AfVsKHzD8A== -"@xterm/addon-serialize@0.14.0-beta.36": - version "0.14.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.36.tgz#1407c13fe1bd869ad4f26e7b7da4e7fa87442021" - integrity sha512-6KpzHlQIuHakPv70dKhQp8f6e9hk4q1fNuuTD1rEzDg8DeKRfUDjorw1vPkKTB/DD+3zaMUBtg7DFVVEi+/+Cw== +"@xterm/addon-serialize@0.14.0-beta.52": + version "0.14.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.52.tgz#19708cdd2895ddbd983b771ae9d14d7bc54cf7c9" + integrity sha512-1+ckKya1OURFmELH1Tjjoxz3Gnj78Dxj+NNRrEunfINkvyzaY+n8wT28FQxIlU5gJq+a0VGvlhNgTkMwgOn6aw== -"@xterm/addon-unicode11@0.9.0-beta.36": - version "0.9.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.36.tgz#158dcdd707a466958a256a960e5d9a967a97a9dc" - integrity sha512-BKP2ml0fYOHnfaTp0LorSluNXjHRSEwf3yrD3K6jEZfYTBePhee1TAxOdNH/TdqwNYZYaYHaK87A5mSuYpKPBQ== +"@xterm/addon-unicode11@0.9.0-beta.52": + version "0.9.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.52.tgz#15afdf03c20d2e46b4eca68bd461eea71c8dd37f" + integrity sha512-5tZR/8c+vf0YNSYS6B9pEv8gyWWZpPYOf/BRQDkTGtYAnFf04MzggVE/U7tKUXGDzBhzwTPODq5qPNTX1xpGgw== -"@xterm/addon-webgl@0.19.0-beta.36": - version "0.19.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.36.tgz#8926a0434e5ce74eee12a965c06cd5f601391f18" - integrity sha512-bJA1enVNlIMRkBU9i7i8qX26Zs2/CrGedREW5WI0NZUAn0IHlatWlj3aOfTuI2MYWUPGE8ul30PyipYP6P+fmA== +"@xterm/addon-webgl@0.19.0-beta.52": + version "0.19.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.52.tgz#695b20a5fa88ff92e115624149592080fad59594" + integrity sha512-kbPO9iR166xW8qgRkYmKX2Vu0kQHXpxYLQ9jY/01e5kvNrI/rqRDV63FIq14ncOi7N3+dmTuUkjvbg8anCpuIw== -"@xterm/headless@5.6.0-beta.36": - version "5.6.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.6.0-beta.36.tgz#cf3e690024019eac2e22d87e0e9f04da6e99cfa9" - integrity sha512-X0Te4ssxcVZ3/YlYEjzN+4w5e4f3Ni/kdjBUKoyZSRpA1+Er54HC/I3t1jc4amqI9xysnVwhq+Ey+LjygIfALw== +"@xterm/headless@5.6.0-beta.52": + version "5.6.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.6.0-beta.52.tgz#7f934a7d7c5bbf88e2d78c22317cd2464bc9638a" + integrity sha512-f4QITQwotblRLW6YOHnK801wHJWfFDnjD7jUEwaaAXtSp32xH3jguWnMP9/uuQX45q9ndjDjnm1t5aXX7gwqBQ== -"@xterm/xterm@5.6.0-beta.36": - version "5.6.0-beta.36" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.36.tgz#fd0fd598b67e3bcba61a59bb1a33b131ad86eea3" - integrity sha512-YtFKQIggbvV2brWifksZAtLi447j0DFdoSRoq4vQi/N7KFC0pguGdG3YzYkDOyqoeLMPu569e2b5oevMe6d2aQ== +"@xterm/xterm@5.6.0-beta.52": + version "5.6.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.52.tgz#ed7456a8b78ea1d00431737d078b5ab0bafbcdfe" + integrity sha512-aZh8IBdZPb2N4NjTt/fQ/C90z/PM3Zxkfoqhmlsp5+v3Otmyw5kd3DepeHK1SFW/pz0/xdj4KFgX8t8Y2lDRbA== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" @@ -388,14 +393,14 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -kerberos@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/kerberos/-/kerberos-2.0.1.tgz#663b0b46883b4da84495f60f2e9e399a43a33ef5" - integrity sha512-O/jIgbdGK566eUhFwIcgalbqirYU/r76MW7/UFw06Fd9x5bSwgyZWL/Vm26aAmezQww/G9KYkmmJBkEkPk5HLw== +kerberos@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/kerberos/-/kerberos-2.1.1.tgz#170fc64d1a23e8c6d6ae2736d01189e2a1dbe0f7" + integrity sha512-414s1G/qgK2T60cXnZsHbtRj8Ynjg0DBlQWeY99tkyqQ2e8vGgFHvxRdvjTlLHg/SxBA0zLQcGE6Pk6Dfq/BCA== dependencies: bindings "^1.5.0" - node-addon-api "^4.3.0" - prebuild-install "7.1.1" + node-addon-api "^6.1.0" + prebuild-install "^7.1.2" lru-cache@^6.0.0: version "6.0.0" @@ -469,20 +474,20 @@ node-addon-api@^3.2.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== -node-addon-api@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" - integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== +node-addon-api@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" + integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== node-gyp-build@4.8.1, node-gyp-build@^4.3.0: version "4.8.1" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== -node-pty@1.1.0-beta11: - version "1.1.0-beta11" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta11.tgz#909d5dd8f9aa2a7857e7b632fd4d2d4768bdf69f" - integrity sha512-vTjF+VrvSCfPDILUkIT+YrG1Fdn06/eBRS2fc9a3JzYAvknMB1Ip8aoJhxl8hNpjWAbprmCEiV91mlfNpCD+GQ== +node-pty@1.1.0-beta20: + version "1.1.0-beta20" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta20.tgz#bd723d3a36c5770d244d847e123da4a004fd0fa2" + integrity sha512-tCOLI5dEhoV75CVaT9pLuZjdx5asJ2DPEA3QtHE9sNYUf6ELXfy0SrlUzFilYcDzkhidBDrPWY26SdmlTaEjyw== dependencies: node-addon-api "^7.1.0" @@ -503,10 +508,10 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -prebuild-install@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" - integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== +prebuild-install@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" + integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== dependencies: detect-libc "^2.0.0" expand-template "^2.0.3" diff --git a/resources/linux/debian/postinst.template b/resources/linux/debian/postinst.template index 16acb1481bf..b292cff8b29 100755 --- a/resources/linux/debian/postinst.template +++ b/resources/linux/debian/postinst.template @@ -31,24 +31,54 @@ if [ "@@NAME@@" != "code-oss" ]; then # Register apt repository eval $(apt-config shell APT_SOURCE_PARTS Dir::Etc::sourceparts/d) CODE_SOURCE_PART=${APT_SOURCE_PARTS}vscode.list + CODE_SOURCE_PART_DEB822=${APT_SOURCE_PARTS}vscode.sources eval $(apt-config shell APT_TRUSTED_PARTS Dir::Etc::trustedparts/d) CODE_TRUSTED_PART=${APT_TRUSTED_PARTS}microsoft.gpg - # Install repository source list + RET=true + if [ -e '/usr/share/debconf/confmodule' ]; then + . /usr/share/debconf/confmodule + db_get @@NAME@@/add-microsoft-repo || true + fi + + # Determine whether to install the repository source list WRITE_SOURCE=0 - if [ ! -f $CODE_SOURCE_PART ] && [ ! -f /etc/rpi-issue ]; then - # Write source list if it does not exist and we're not running on Raspberry Pi OS - WRITE_SOURCE=1 - elif grep -Eq "http:\/\/packages\.microsoft\.com\/repos\/vscode" $CODE_SOURCE_PART; then + if [ "$RET" = false ]; then + # The user does not want to add the Microsoft repository + WRITE_SOURCE=0 + elif [ -f "$CODE_SOURCE_PART_DEB822" ]; then + # The user has migrated themselves to the DEB822 format + WRITE_SOURCE=0 + elif [ -f "$CODE_SOURCE_PART" ] && (grep -q "http://packages.microsoft.com/repos/vscode" $CODE_SOURCE_PART); then # Migrate from old repository + WRITE_SOURCE=2 + elif [ -f "$CODE_SOURCE_PART" ] && (grep -q "http://packages.microsoft.com/repos/code" $CODE_SOURCE_PART); then + # Migrate from old repository + WRITE_SOURCE=2 + elif apt-cache policy | grep -q "https://packages.microsoft.com/repos/code"; then + # The user is already on the new repository + WRITE_SOURCE=0 + elif [ ! -f $CODE_SOURCE_PART ] && [ ! -f /etc/rpi-issue ]; then + # Write source list if it does not exist and we're not running on Raspberry Pi OS WRITE_SOURCE=1 elif grep -q "# disabled on upgrade to" $CODE_SOURCE_PART; then # Write source list if it was disabled by OS upgrade WRITE_SOURCE=1 fi - if [ "$WRITE_SOURCE" -eq "1" ]; then + if [ "$WRITE_SOURCE" -eq "1" ] && [ -e '/usr/share/debconf/confmodule' ]; then + # Ask the user whether to actually write the source list + db_input high @@NAME@@/add-microsoft-repo || true + db_go || true + + db_get @@NAME@@/add-microsoft-repo + if [ "$RET" = false ]; then + WRITE_SOURCE=0 + fi + fi + + if [ "$WRITE_SOURCE" -ne "0" ]; then echo "### THIS FILE IS AUTOMATICALLY CONFIGURED ### # You may comment out this entry, but any other modifications may be lost. deb [arch=amd64,arm64,armhf] https://packages.microsoft.com/repos/code stable main" > $CODE_SOURCE_PART diff --git a/resources/linux/debian/postrm.template b/resources/linux/debian/postrm.template index fb36d522f38..dcbfda95ea0 100755 --- a/resources/linux/debian/postrm.template +++ b/resources/linux/debian/postrm.template @@ -14,3 +14,22 @@ fi if hash update-mime-database 2>/dev/null; then update-mime-database /usr/share/mime fi + +RET=true +if [ -e '/usr/share/debconf/confmodule' ]; then + . /usr/share/debconf/confmodule + db_get @@NAME@@/add-microsoft-repo || true +fi +if [ "$RET" = "true" ]; then + eval $(apt-config shell APT_SOURCE_PARTS Dir::Etc::sourceparts/d) + CODE_SOURCE_PART=${APT_SOURCE_PARTS}vscode.list + rm -f $CODE_SOURCE_PART + + eval $(apt-config shell APT_TRUSTED_PARTS Dir::Etc::trustedparts/d) + CODE_TRUSTED_PART=${APT_TRUSTED_PARTS}microsoft.gpg + rm -f $CODE_TRUSTED_PART +fi + +if [ "$1" = "purge" ] && [ -e '/usr/share/debconf/confmodule' ]; then + db_purge +fi diff --git a/resources/linux/debian/templates.template b/resources/linux/debian/templates.template new file mode 100644 index 00000000000..7be5e039b26 --- /dev/null +++ b/resources/linux/debian/templates.template @@ -0,0 +1,6 @@ +Template: @@NAME@@/add-microsoft-repo +Type: boolean +Default: true +Description: Add Microsoft apt repository for Visual Studio Code? + The installer would like to add the Microsoft repository and signing + key to update VS Code through apt. diff --git a/resources/server/bin/helpers/check-requirements-linux.sh b/resources/server/bin/helpers/check-requirements-linux.sh index 079557869e3..31a618fbd85 100644 --- a/resources/server/bin/helpers/check-requirements-linux.sh +++ b/resources/server/bin/helpers/check-requirements-linux.sh @@ -27,6 +27,7 @@ fi ARCH=$(uname -m) found_required_glibc=0 found_required_glibcxx=0 +MIN_GLIBCXX_VERSION="3.4.25" # Extract the ID value from /etc/os-release if [ -f /etc/os-release ]; then @@ -40,7 +41,10 @@ fi # Based on https://github.com/bminor/glibc/blob/520b1df08de68a3de328b65a25b86300a7ddf512/elf/cache.c#L162-L245 case $ARCH in x86_64) LDCONFIG_ARCH="x86-64";; - armv7l | armv8l) LDCONFIG_ARCH="hard-float";; + armv7l | armv8l) + MIN_GLIBCXX_VERSION="3.4.26" + LDCONFIG_ARCH="hard-float" + ;; arm64 | aarch64) BITNESS=$(getconf LONG_BIT) if [ "$BITNESS" = "32" ]; then @@ -81,7 +85,7 @@ if [ "$OS_ID" != "alpine" ]; then libstdcpp_real_path=$(readlink -f "$libstdcpp_path_line") libstdcpp_version=$(grep -ao 'GLIBCXX_[0-9]*\.[0-9]*\.[0-9]*' "$libstdcpp_real_path" | sort -V | tail -1) libstdcpp_version_number=$(echo "$libstdcpp_version" | sed 's/GLIBCXX_//') - if [ "$(printf '%s\n' "3.4.24" "$libstdcpp_version_number" | sort -V | head -n1)" = "3.4.24" ]; then + if [ "$(printf '%s\n' "$MIN_GLIBCXX_VERSION" "$libstdcpp_version_number" | sort -V | head -n1)" = "$MIN_GLIBCXX_VERSION" ]; then found_required_glibcxx=1 break fi @@ -92,7 +96,7 @@ else found_required_glibcxx=1 fi if [ "$found_required_glibcxx" = "0" ]; then - echo "Warning: Missing GLIBCXX >= 3.4.25! from $libstdcpp_real_path" + echo "Warning: Missing GLIBCXX >= $MIN_GLIBCXX_VERSION! from $libstdcpp_real_path" fi if [ "$OS_ID" = "alpine" ]; then diff --git a/scripts/code-server.js b/scripts/code-server.js index c043bf2671b..56945e76ca7 100644 --- a/scripts/code-server.js +++ b/scripts/code-server.js @@ -69,4 +69,3 @@ function startServer(programArgs) { } main(); - diff --git a/scripts/xterm-update.ps1 b/scripts/xterm-update.ps1 index 11c282de888..2eae7fffe77 100644 --- a/scripts/xterm-update.ps1 +++ b/scripts/xterm-update.ps1 @@ -1 +1,2 @@ -node $PSScriptRoot\xterm-update.js (Get-Location) +$scriptPath = Join-Path $PSScriptRoot "xterm-update.js" +node $scriptPath (Get-Location) diff --git a/src/bootstrap-amd.js b/src/bootstrap-amd.js index 27d15eb76d5..c228faccfed 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -7,10 +7,47 @@ 'use strict'; /** - * @typedef {import('./vs/nls').INLSConfiguration} INLSConfiguration + * @import { INLSConfiguration } from './vs/nls' * @import { IProductConfiguration } from './vs/base/common/product' */ +// ESM-comment-begin +const isESM = false; +// ESM-comment-end +// ESM-uncomment-begin +// import * as path from 'path'; +// import * as fs from 'fs'; +// import { fileURLToPath } from 'url'; +// import { createRequire, register } from 'node:module'; +// import { product, pkg } from './bootstrap-meta.js'; +// import * as bootstrapNode from './bootstrap-node.js'; +// import * as performance from './vs/base/common/performance.js'; +// +// const require = createRequire(import.meta.url); +// const isESM = true; +// const module = { exports: {} }; +// const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// +// // Install a hook to module resolution to map 'fs' to 'original-fs' +// if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) { +// const jsCode = ` +// export async function resolve(specifier, context, nextResolve) { +// if (specifier === 'fs') { +// return { +// format: 'builtin', +// shortCircuit: true, +// url: 'node:original-fs' +// }; +// } + +// // Defer to the next hook in the chain, which would be the +// // Node.js default resolve if this is the last user-specified loader. +// return nextResolve(specifier, context); +// }`; +// register(`data:text/javascript;base64,${Buffer.from(jsCode).toString('base64')}`, import.meta.url); +// } +// ESM-uncomment-end + // Store the node.js require function in a variable // before loading our AMD loader to avoid issues // when this file is bundled with other files. @@ -21,7 +58,12 @@ globalThis._VSCODE_NODE_MODULES = new Proxy(Object.create(null), { get: (_target // VSCODE_GLOBALS: package/product.json /** @type Partial */ +// ESM-comment-begin globalThis._VSCODE_PRODUCT_JSON = require('./bootstrap-meta').product; +// ESM-comment-end +// ESM-uncomment-begin +// globalThis._VSCODE_PRODUCT_JSON = { ...product }; +// ESM-uncomment-end if (process.env['VSCODE_DEV']) { // Patch product overrides when running out of sources try { @@ -30,29 +72,21 @@ if (process.env['VSCODE_DEV']) { globalThis._VSCODE_PRODUCT_JSON = Object.assign(globalThis._VSCODE_PRODUCT_JSON, overrides); } catch (error) { /* ignore */ } } +// ESM-comment-begin globalThis._VSCODE_PACKAGE_JSON = require('./bootstrap-meta').pkg; +// ESM-comment-end +// ESM-uncomment-begin +// globalThis._VSCODE_PACKAGE_JSON = { ...pkg }; +// ESM-uncomment-end -// @ts-ignore -const loader = require('./vs/loader'); -const bootstrap = require('./bootstrap'); -const performance = require('./vs/base/common/performance'); +// VSCODE_GLOBALS: file root of all resources +globalThis._VSCODE_FILE_ROOT = __dirname; + +// ESM-comment-begin +const bootstrapNode = require('./bootstrap-node'); +const performance = require(`./vs/base/common/performance`); const fs = require('fs'); - -// Bootstrap: Loader -loader.config({ - baseUrl: bootstrap.fileUriFromPath(__dirname, { isWindows: process.platform === 'win32' }), - catchError: true, - nodeRequire, - amdModulesPattern: /^vs\//, - recordStats: true -}); - -// Running in Electron -if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) { - loader.define('fs', ['original-fs'], function (/** @type {import('fs')} */originalFS) { - return originalFS; // replace the patched electron fs with the original node fs for all AMD code - }); -} +// ESM-comment-end //#region NLS helpers @@ -91,7 +125,6 @@ async function doSetupNLS() { messagesFile = nlsConfig.defaultMessagesFile; } - // VSCODE_GLOBALS: NLS globalThis._VSCODE_NLS_LANGUAGE = nlsConfig?.resolvedLanguage; } catch (e) { console.error(`Error reading VSCODE_NLS_CONFIG from environment: ${e}`); @@ -106,7 +139,6 @@ async function doSetupNLS() { } try { - // VSCODE_GLOBALS: NLS globalThis._VSCODE_NLS_MESSAGES = JSON.parse((await fs.promises.readFile(messagesFile)).toString()); } catch (error) { console.error(`Error reading NLS messages file ${messagesFile}: ${error}`); @@ -123,7 +155,6 @@ async function doSetupNLS() { // Fallback to the default message file to ensure english translation at least if (nlsConfig?.defaultMessagesFile && nlsConfig.defaultMessagesFile !== messagesFile) { try { - // VSCODE_GLOBALS: NLS globalThis._VSCODE_NLS_MESSAGES = JSON.parse((await fs.promises.readFile(nlsConfig.defaultMessagesFile)).toString()); } catch (error) { console.error(`Error reading default NLS messages file ${nlsConfig.defaultMessagesFile}: ${error}`); @@ -138,31 +169,82 @@ async function doSetupNLS() { //#endregion -/** - * @param {string=} entrypoint - * @param {(value: any) => void=} onLoad - * @param {(err: Error) => void=} onError - */ -exports.load = function (entrypoint, onLoad, onError) { - if (!entrypoint) { - return; - } +//#region Loader Config - // code cache config - if (process.env['VSCODE_CODE_CACHE_PATH']) { - loader.config({ - nodeCachedData: { - path: process.env['VSCODE_CODE_CACHE_PATH'], - seed: entrypoint - } +if (isESM) { + + /** + * @param {string=} entrypoint + * @param {(value: any) => void} [onLoad] + * @param {(err: Error) => void} [onError] + */ + module.exports.load = function (entrypoint, onLoad, onError) { + if (!entrypoint) { + return; + } + + entrypoint = `./${entrypoint}.js`; + + onLoad = onLoad || function () { }; + onError = onError || function (err) { console.error(err); }; + + setupNLS().then(() => { + performance.mark(`code/fork/willLoadCode`); + import(entrypoint).then(onLoad, onError); + }); + }; +} else { + + // @ts-ignore + const loader = require('./vs/loader'); + + loader.config({ + baseUrl: bootstrapNode.fileUriFromPath(__dirname, { isWindows: process.platform === 'win32' }), + catchError: true, + nodeRequire, + amdModulesPattern: /^vs\//, + recordStats: true + }); + + // Running in Electron + if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) { + loader.define('fs', ['original-fs'], function (/** @type {import('fs')} */originalFS) { + return originalFS; // replace the patched electron fs with the original node fs for all AMD code }); } - onLoad = onLoad || function () { }; - onError = onError || function (err) { console.error(err); }; + /** + * @param {string=} entrypoint + * @param {(value: any) => void} [onLoad] + * @param {(err: Error) => void} [onError] + */ + module.exports.load = function (entrypoint, onLoad, onError) { + if (!entrypoint) { + return; + } - setupNLS().then(() => { - performance.mark('code/fork/willLoadCode'); - loader([entrypoint], onLoad, onError); - }); -}; + // code cache config + if (process.env['VSCODE_CODE_CACHE_PATH']) { + loader.config({ + nodeCachedData: { + path: process.env['VSCODE_CODE_CACHE_PATH'], + seed: entrypoint + } + }); + } + + onLoad = onLoad || function () { }; + onError = onError || function (err) { console.error(err); }; + + setupNLS().then(() => { + performance.mark('code/fork/willLoadCode'); + loader([entrypoint], onLoad, onError); + }); + }; +} + +//#endregion + +// ESM-uncomment-begin +// export const load = module.exports.load; +// ESM-uncomment-end diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index 9de1e6f0d15..009fe3e7e2d 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -6,11 +6,18 @@ //@ts-check 'use strict'; +// ESM-comment-begin const performance = require('./vs/base/common/performance'); -performance.mark('code/fork/start'); - -const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); +const bootstrapAmd = require('./bootstrap-amd'); +// ESM-comment-end +// ESM-uncomment-begin +// import * as performance from './vs/base/common/performance.js'; +// import * as bootstrapNode from './bootstrap-node.js'; +// import * as bootstrapAmd from './bootstrap-amd.js'; +// ESM-uncomment-end + +performance.mark('code/fork/start'); // Crash reporter configureCrashReporter(); @@ -19,7 +26,7 @@ configureCrashReporter(); bootstrapNode.removeGlobalNodeModuleLookupPaths(); // Enable ASAR in our forked processes -bootstrap.enableASARSupport(); +bootstrapNode.enableASARSupport(); if (process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']) { bootstrapNode.injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); @@ -41,7 +48,7 @@ if (process.env['VSCODE_PARENT_PID']) { } // Load AMD entry point -require('./bootstrap-amd').load(process.env['VSCODE_AMD_ENTRYPOINT']); +bootstrapAmd.load(process.env['VSCODE_AMD_ENTRYPOINT']); //#region Helpers diff --git a/src/bootstrap-meta.js b/src/bootstrap-meta.js index 7924b77eec8..5415bc5df6a 100644 --- a/src/bootstrap-meta.js +++ b/src/bootstrap-meta.js @@ -10,19 +10,31 @@ * @import { IProductConfiguration } from './vs/base/common/product' */ +// ESM-uncomment-begin +// import { createRequire } from 'node:module'; +// +// const require = createRequire(import.meta.url); +// const module = { exports: {} }; +// ESM-uncomment-end + /** @type Partial & { BUILD_INSERT_PRODUCT_CONFIGURATION?: string } */ -let product = { BUILD_INSERT_PRODUCT_CONFIGURATION: 'BUILD_INSERT_PRODUCT_CONFIGURATION' }; // DO NOT MODIFY, PATCHED DURING BUILD -if (product['BUILD_INSERT_PRODUCT_CONFIGURATION']) { +let productObj = { BUILD_INSERT_PRODUCT_CONFIGURATION: 'BUILD_INSERT_PRODUCT_CONFIGURATION' }; // DO NOT MODIFY, PATCHED DURING BUILD +if (productObj['BUILD_INSERT_PRODUCT_CONFIGURATION']) { // @ts-ignore - product = require('../product.json'); // Running out of sources + productObj = require('../product.json'); // Running out of sources } /** @type object & { BUILD_INSERT_PACKAGE_CONFIGURATION?: string } */ -let pkg = { BUILD_INSERT_PACKAGE_CONFIGURATION: 'BUILD_INSERT_PACKAGE_CONFIGURATION' }; // DO NOT MODIFY, PATCHED DURING BUILD -if (pkg['BUILD_INSERT_PACKAGE_CONFIGURATION']) { +let pkgObj = { BUILD_INSERT_PACKAGE_CONFIGURATION: 'BUILD_INSERT_PACKAGE_CONFIGURATION' }; // DO NOT MODIFY, PATCHED DURING BUILD +if (pkgObj['BUILD_INSERT_PACKAGE_CONFIGURATION']) { // @ts-ignore - pkg = require('../package.json'); // Running out of sources + pkgObj = require('../package.json'); // Running out of sources } -exports.product = product; -exports.pkg = pkg; +module.exports.product = productObj; +module.exports.pkg = pkgObj; + +// ESM-uncomment-begin +// export const product = module.exports.product; +// export const pkg = module.exports.pkg; +// ESM-uncomment-end diff --git a/src/bootstrap-node.js b/src/bootstrap-node.js index 914b8290380..5183526e1d5 100644 --- a/src/bootstrap-node.js +++ b/src/bootstrap-node.js @@ -6,12 +6,48 @@ //@ts-check 'use strict'; +// ESM-comment-begin +const path = require('path'); +const fs = require('fs'); +const Module = require('module'); + +const isESM = false; +// ESM-comment-end +// ESM-uncomment-begin +// import * as path from 'path'; +// import * as fs from 'fs'; +// import { fileURLToPath } from 'url'; +// import { createRequire } from 'node:module'; +// +// const require = createRequire(import.meta.url); +// const Module = require('module'); +// const isESM = true; +// const module = { exports: {} }; +// const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// ESM-uncomment-end + +// increase number of stack frames(from 10, https://github.com/v8/v8/wiki/Stack-Trace-API) +Error.stackTraceLimit = 100; + +if (!process.env['VSCODE_HANDLES_SIGPIPE']) { + // Workaround for Electron not installing a handler to ignore SIGPIPE + // (https://github.com/electron/electron/issues/13254) + let didLogAboutSIGPIPE = false; + process.on('SIGPIPE', () => { + // See https://github.com/microsoft/vscode-remote-release/issues/6543 + // In certain situations, the console itself can be in a broken pipe state + // so logging SIGPIPE to the console will cause an infinite async loop + if (!didLogAboutSIGPIPE) { + didLogAboutSIGPIPE = true; + console.error(new Error(`Unexpected SIGPIPE`)); + } + }); +} + // Setup current working directory in all our node & electron processes // - Windows: call `process.chdir()` to always set application folder as cwd // - all OS: store the `process.cwd()` inside `VSCODE_CWD` for consistent lookups function setupCurrentWorkingDirectory() { - const path = require('path'); - try { // Store the `process.cwd()` inside `VSCODE_CWD` @@ -38,36 +74,41 @@ setupCurrentWorkingDirectory(); * * @param {string} injectPath */ -exports.injectNodeModuleLookupPath = function (injectPath) { +module.exports.injectNodeModuleLookupPath = function (injectPath) { if (!injectPath) { throw new Error('Missing injectPath'); } - const Module = require('module'); - const path = require('path'); + const Module = require('node:module'); + if (isESM) { + // register a loader hook + // ESM-uncomment-begin + // Module.register('./loader-lookup-path.mjs', { parentURL: import.meta.url, data: injectPath }); + // ESM-uncomment-end + } else { + const nodeModulesPath = path.join(__dirname, '../node_modules'); - const nodeModulesPath = path.join(__dirname, '../node_modules'); + // @ts-ignore + const originalResolveLookupPaths = Module._resolveLookupPaths; - // @ts-ignore - const originalResolveLookupPaths = Module._resolveLookupPaths; - - // @ts-ignore - Module._resolveLookupPaths = function (moduleName, parent) { - const paths = originalResolveLookupPaths(moduleName, parent); - if (Array.isArray(paths)) { - for (let i = 0, len = paths.length; i < len; i++) { - if (paths[i] === nodeModulesPath) { - paths.splice(i, 0, injectPath); - break; + // @ts-ignore + Module._resolveLookupPaths = function (moduleName, parent) { + const paths = originalResolveLookupPaths(moduleName, parent); + if (Array.isArray(paths)) { + for (let i = 0, len = paths.length; i < len; i++) { + if (paths[i] === nodeModulesPath) { + paths.splice(i, 0, injectPath); + break; + } } } - } - return paths; - }; + return paths; + }; + } }; -exports.removeGlobalNodeModuleLookupPaths = function () { +module.exports.removeGlobalNodeModuleLookupPaths = function () { const Module = require('module'); // @ts-ignore const globalPaths = Module.globalPaths; @@ -95,10 +136,7 @@ exports.removeGlobalNodeModuleLookupPaths = function () { * @param {Partial} product * @returns {{ portableDataPath: string; isPortable: boolean; }} */ -exports.configurePortable = function (product) { - const fs = require('fs'); - const path = require('path'); - +module.exports.configurePortable = function (product) { const appRoot = path.dirname(__dirname); /** @@ -158,3 +196,75 @@ exports.configurePortable = function (product) { isPortable }; }; + +/** + * Helper to enable ASAR support. + */ +module.exports.enableASARSupport = function () { + const NODE_MODULES_PATH = path.join(__dirname, '../node_modules'); + const NODE_MODULES_ASAR_PATH = `${NODE_MODULES_PATH}.asar`; + + // @ts-ignore + const originalResolveLookupPaths = Module._resolveLookupPaths; + + // @ts-ignore + Module._resolveLookupPaths = function (request, parent) { + const paths = originalResolveLookupPaths(request, parent); + if (Array.isArray(paths)) { + for (let i = 0, len = paths.length; i < len; i++) { + if (paths[i] === NODE_MODULES_PATH) { + paths.splice(i, 0, NODE_MODULES_ASAR_PATH); + break; + } + } + } + + return paths; + }; +}; + +/** + * Helper to convert a file path to a URI. + * + * TODO@bpasero check for removal once ESM has landed. + * + * @param {string} path + * @param {{ isWindows?: boolean, scheme?: string, fallbackAuthority?: string }} config + * @returns {string} + */ +module.exports.fileUriFromPath = function (path, config) { + + // Since we are building a URI, we normalize any backslash + // to slashes and we ensure that the path begins with a '/'. + let pathName = path.replace(/\\/g, '/'); + if (pathName.length > 0 && pathName.charAt(0) !== '/') { + pathName = `/${pathName}`; + } + + /** @type {string} */ + let uri; + + // Windows: in order to support UNC paths (which start with '//') + // that have their own authority, we do not use the provided authority + // but rather preserve it. + if (config.isWindows && pathName.startsWith('//')) { + uri = encodeURI(`${config.scheme || 'file'}:${pathName}`); + } + + // Otherwise we optionally add the provided authority if specified + else { + uri = encodeURI(`${config.scheme || 'file'}://${config.fallbackAuthority || ''}${pathName}`); + } + + return uri.replace(/#/g, '%23'); +}; + +//#endregion + +// ESM-uncomment-begin +// export const injectNodeModuleLookupPath = module.exports.injectNodeModuleLookupPath; +// export const removeGlobalNodeModuleLookupPaths = module.exports.removeGlobalNodeModuleLookupPaths; +// export const configurePortable = module.exports.configurePortable; +// export const enableASARSupport = module.exports.enableASARSupport; +// export const fileUriFromPath = module.exports.fileUriFromPath; +// ESM-uncomment-end diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index cd859a847f9..b3216ac0f2e 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -10,28 +10,28 @@ /** * @import { ISandboxConfiguration } from './vs/base/parts/sandbox/common/sandboxTypes' + * @typedef {any} LoaderConfig */ /* eslint-disable no-restricted-globals */ -// Simple module style to support node.js and browser environments -(function (globalThis, factory) { +// ESM-comment-begin +const isESM = false; +// ESM-comment-end +// ESM-uncomment-begin +// const isESM = true; +// ESM-uncomment-end - // Node.js - if (typeof exports === 'object') { - module.exports = factory(); - } - - // Browser - else { - // @ts-ignore - globalThis.MonacoBootstrapWindow = factory(); - } -}(this, function () { - const bootstrapLib = bootstrap(); +(function (factory) { + // @ts-ignore + globalThis.MonacoBootstrapWindow = factory(); +}(function () { const preloadGlobals = sandboxGlobals(); const safeProcess = preloadGlobals.process; + // increase number of stack frames(from 10, https://github.com/v8/v8/wiki/Stack-Trace-API) + Error.stackTraceLimit = 100; + /** * @param {string[]} modulePaths * @param {(result: unknown, configuration: ISandboxConfiguration) => Promise | undefined} resultCallback @@ -82,7 +82,6 @@ developerDeveloperKeybindingsDisposable = registerDeveloperKeybindings(disallowReloadKeybinding); } - // VSCODE_GLOBALS: NLS globalThis._VSCODE_NLS_MESSAGES = configuration.nls.messages; globalThis._VSCODE_NLS_LANGUAGE = configuration.nls.language; let language = configuration.nls.language || 'en'; @@ -96,59 +95,137 @@ window['MonacoEnvironment'] = {}; - /** @type {any} */ - const loaderConfig = { - baseUrl: `${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32', scheme: 'vscode-file', fallbackAuthority: 'vscode-app' })}/out`, - preferScriptTags: true - }; + if (isESM) { - // use a trusted types policy when loading via script tags - loaderConfig.trustedTypesPolicy = window.trustedTypes?.createPolicy('amdLoader', { - createScriptURL(value) { - if (value.startsWith(window.location.origin)) { - return value; - } - throw new Error(`Invalid script url: ${value}`); + // Signal before require() + if (typeof options?.beforeRequire === 'function') { + options.beforeRequire(configuration); } - }); - // Teach the loader the location of the node modules we use in renderers - // This will enable to load these modules via - + diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js index a81bd8c04ed..98e67e7c25c 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ //@ts-check +'use strict'; + (function () { - 'use strict'; /** * @import { ISandboxConfiguration } from '../../../base/parts/sandbox/common/sandboxTypes' diff --git a/src/vs/code/electron-sandbox/workbench/workbench-dev.html b/src/vs/code/electron-sandbox/workbench/workbench-dev.html index a92bea242a4..b1be2b7527c 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench-dev.html +++ b/src/vs/code/electron-sandbox/workbench/workbench-dev.html @@ -68,8 +68,7 @@ - - + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index 9f41e508bb3..5ad97839d23 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -6,8 +6,9 @@ /// //@ts-check +'use strict'; + (function () { - 'use strict'; /** * @import {INativeWindowConfiguration} from '../../../platform/window/common/window' diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 211d1dbf3e5..9acf37f930f 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -39,10 +39,6 @@ function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { || !!argv['telemetry']; } -interface IMainCli { - main: (argv: NativeParsedArgs) => Promise; -} - export async function main(argv: string[]): Promise { let args: NativeParsedArgs; @@ -59,19 +55,28 @@ export async function main(argv: string[]): Promise { console.error(`'${subcommand}' command not supported in ${product.applicationName}`); return; } + const env: IProcessEnvironment = { + ...process.env + }; + // bootstrap-amd.js determines the electron environment based + // on the following variable. For the server we need to unset + // it to prevent importing any electron specific modules. + // Refs https://github.com/microsoft/vscode/issues/221883 + delete env['ELECTRON_RUN_AS_NODE']; + const tunnelArgs = argv.slice(argv.indexOf(subcommand) + 1); // all arguments behind `tunnel` return new Promise((resolve, reject) => { let tunnelProcess: ChildProcess; const stdio: StdioOptions = ['ignore', 'pipe', 'pipe']; if (process.env['VSCODE_DEV']) { - tunnelProcess = spawn('cargo', ['run', '--', subcommand, ...tunnelArgs], { cwd: join(getAppRoot(), 'cli'), stdio }); + tunnelProcess = spawn('cargo', ['run', '--', subcommand, ...tunnelArgs], { cwd: join(getAppRoot(), 'cli'), stdio, env }); } else { const appPath = process.platform === 'darwin' // ./Contents/MacOS/Electron => ./Contents/Resources/app/bin/code-tunnel-insiders ? join(dirname(dirname(process.execPath)), 'Resources', 'app') : dirname(process.execPath); const tunnelCommand = join(appPath, 'bin', `${product.tunnelApplicationName}${isWindows ? '.exe' : ''}`); - tunnelProcess = spawn(tunnelCommand, [subcommand, ...tunnelArgs], { cwd: cwd(), stdio }); + tunnelProcess = spawn(tunnelCommand, [subcommand, ...tunnelArgs], { cwd: cwd(), stdio, env }); } tunnelProcess.stdout!.pipe(process.stdout); @@ -112,7 +117,8 @@ export async function main(argv: string[]): Promise { // Extensions Management else if (shouldSpawnCliProcess(args)) { - const cli = await new Promise((resolve, reject) => require(['vs/code/node/cliProcessMain'], resolve, reject)); + + const cli = await import('vs/code/node/cliProcessMain'); await cli.main(args); return; diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 195aee2cf4f..4bca7d678e9 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -190,6 +190,8 @@ export class ObservableCodeEditor extends Disposable { public readonly scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft()); public readonly layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo()); + public readonly layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft); + public readonly layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft); public readonly contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); diff --git a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts index cf9b355831f..46f7423aa3a 100644 --- a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts @@ -71,14 +71,17 @@ export class ManagedHoverWidget implements IDisposable { const hoverOptions: IHoverDelegateOptions = { content, target: this.target, + actions: options?.actions, + linkHandler: options?.linkHandler, + trapFocus: options?.trapFocus, appearance: { showPointer: this.hoverDelegate.placement === 'element', skipFadeInAnimation: !this.fadeInAnimation || !!oldHoverWidget, // do not fade in if the hover is already showing + showHoverHint: options?.appearance?.showHoverHint, }, position: { hoverPosition: HoverPosition.BELOW, }, - ...options }; this._hoverWidget = this.hoverDelegate.showHover(hoverOptions, focus); diff --git a/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts new file mode 100644 index 00000000000..649aaa45bea --- /dev/null +++ b/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts @@ -0,0 +1,374 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TreeSitterTokenizationRegistry } from 'vs/editor/common/languages'; +import type { Parser } from '@vscode/tree-sitter-wasm'; +import { AppResourcePath, FileAccess, nodeModulesAsarUnpackedPath, nodeModulesPath } from 'vs/base/common/network'; +import { ITreeSitterParserService } from 'vs/editor/common/services/treeSitterParserService'; +import { IModelService } from 'vs/editor/common/services/model'; +import { Disposable, DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { ITextModel } from 'vs/editor/common/model'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { setTimeout0 } from 'vs/base/common/platform'; +import { importAMDNodeModule } from 'vs/amdX'; +import { Event } from 'vs/base/common/event'; +import { cancelOnDispose } from 'vs/base/common/cancellation'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { canASAR } from 'vs/base/common/amd'; + +const EDITOR_EXPERIMENTAL_PREFER_TREESITTER = 'editor.experimental.preferTreeSitter'; +const EDITOR_TREESITTER_TELEMETRY = 'editor.experimental.treeSitterTelemetry'; +const MODULE_LOCATION_SUBPATH = `@vscode/tree-sitter-wasm/wasm`; +const FILENAME_TREESITTER_WASM = `tree-sitter.wasm`; + +function getModuleLocation(environmentService: IEnvironmentService): AppResourcePath { + return `${(canASAR && environmentService.isBuilt) ? nodeModulesAsarUnpackedPath : nodeModulesPath}/${MODULE_LOCATION_SUBPATH}`; +} + + +export class TextModelTreeSitter extends Disposable { + private _treeSitterTree: TreeSitterTree | undefined; + + // Not currently used since we just get telemetry, but later this will be needed. + get tree() { return this._treeSitterTree; } + + constructor(readonly model: ITextModel, + private readonly _treeSitterParser: TreeSitterLanguages, + private readonly _treeSitterImporter: TreeSitterImporter, + private readonly _logService: ILogService, + private readonly _telemetryService: ITelemetryService + ) { + super(); + this._register(Event.runAndSubscribe(this.model.onDidChangeLanguage, (e => this._onDidChangeLanguage(e ? e.newLanguage : this.model.getLanguageId())))); + } + + private readonly _languageSessionDisposables = this._register(new DisposableStore()); + /** + * Be very careful when making changes to this method as it is easy to introduce race conditions. + */ + private async _onDidChangeLanguage(languageId: string) { + this._languageSessionDisposables.clear(); + this._treeSitterTree = undefined; + + const token = cancelOnDispose(this._languageSessionDisposables); + const language = await this._treeSitterParser.getLanguage(languageId); + if (!language || token.isCancellationRequested) { + return; + } + + const Parser = await this._treeSitterImporter.getParserClass(); + if (token.isCancellationRequested) { + return; + } + + const treeSitterTree = this._languageSessionDisposables.add(new TreeSitterTree(new Parser(), language, this._logService, this._telemetryService)); + this._languageSessionDisposables.add(this.model.onDidChangeContent(e => this._onDidChangeContent(treeSitterTree, e))); + await this._onDidChangeContent(treeSitterTree); + if (token.isCancellationRequested) { + return; + } + + this._treeSitterTree = treeSitterTree; + } + + private async _onDidChangeContent(treeSitterTree: TreeSitterTree, e?: IModelContentChangedEvent) { + return treeSitterTree.onDidChangeContent(this.model, e); + } +} + +const enum TelemetryParseType { + Full = 'fullParse', + Incremental = 'incrementalParse' +} + +export class TreeSitterTree implements IDisposable { + private _tree: Parser.Tree | undefined; + private _isDisposed: boolean = false; + constructor(public readonly parser: Parser, + public /** exposed for tests **/ readonly language: Parser.Language, + private readonly _logService: ILogService, + private readonly _telemetryService: ITelemetryService) { + this.parser.setTimeoutMicros(50 * 1000); // 50 ms + this.parser.setLanguage(language); + } + dispose(): void { + this._isDisposed = true; + this._tree?.delete(); + this.parser?.delete(); + } + get tree() { return this._tree; } + set tree(newTree: Parser.Tree | undefined) { + this._tree?.delete(); + this._tree = newTree; + } + get isDisposed() { return this._isDisposed; } + + private _onDidChangeContentQueue: Promise = Promise.resolve(); + public async onDidChangeContent(model: ITextModel, e?: IModelContentChangedEvent) { + this._onDidChangeContentQueue = this._onDidChangeContentQueue.then(() => { + if (this.isDisposed) { + // No need to continue the queue if we are disposed + return; + } + return this._onDidChangeContent(model, e); + }).catch((e) => { + this._logService.error('Error parsing tree-sitter tree', e); + }); + return this._onDidChangeContentQueue; + } + + private async _onDidChangeContent(model: ITextModel, e?: IModelContentChangedEvent) { + if (e) { + for (const change of e.changes) { + const newEndOffset = change.rangeOffset + change.text.length; + const newEndPosition = model.getPositionAt(newEndOffset); + + this.tree?.edit({ + startIndex: change.rangeOffset, + oldEndIndex: change.rangeOffset + change.rangeLength, + newEndIndex: change.rangeOffset + change.text.length, + startPosition: { row: change.range.startLineNumber - 1, column: change.range.startColumn - 1 }, + oldEndPosition: { row: change.range.endLineNumber - 1, column: change.range.endColumn - 1 }, + newEndPosition: { row: newEndPosition.lineNumber - 1, column: newEndPosition.column - 1 } + }); + } + } + + this.tree = await this.parse(model); + } + + private parse(model: ITextModel): Promise { + let parseType: TelemetryParseType = TelemetryParseType.Full; + if (this.tree) { + parseType = TelemetryParseType.Incremental; + } + return this._parseAndYield(model, parseType); + } + + private async _parseAndYield(model: ITextModel, parseType: TelemetryParseType): Promise { + const language = model.getLanguageId(); + let tree: Parser.Tree | undefined; + let time: number = 0; + let passes: number = 0; + do { + const timer = performance.now(); + try { + tree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this.tree); + } catch (e) { + // parsing can fail when the timeout is reached, will resume upon next loop + } finally { + time += performance.now() - timer; + passes++; + } + + // Even if the model changes and edits are applied, the tree parsing will continue correctly after the await. + await new Promise(resolve => setTimeout0(resolve)); + + if (model.isDisposed() || this.isDisposed) { + return; + } + } while (!tree); + this.sendParseTimeTelemetry(parseType, language, time, passes); + return tree; + } + + private _parseCallback(textModel: ITextModel, index: number): string | null { + return textModel.getTextBuffer().getNearestChunk(index); + } + + private sendParseTimeTelemetry(parseType: TelemetryParseType, languageId: string, time: number, passes: number): void { + this._logService.debug(`Tree parsing (${parseType}) took ${time} ms and ${passes} passes.`); + type ParseTimeClassification = { + owner: 'alros'; + comment: 'Used to understand how long it takes to parse a tree-sitter tree'; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The programming language ID.' }; + time: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ms it took to parse' }; + passes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of passes it took to parse' }; + }; + if (parseType === TelemetryParseType.Full) { + this._telemetryService.publicLog2<{ languageId: string; time: number; passes: number }, ParseTimeClassification>(`treeSitter.fullParse`, { languageId, time, passes }); + } else { + this._telemetryService.publicLog2<{ languageId: string; time: number; passes: number }, ParseTimeClassification>(`treeSitter.incrementalParse`, { languageId, time, passes }); + } + } +} + +export class TreeSitterLanguages extends Disposable { + private _languages: Map = new Map(); + + constructor(private readonly _treeSitterImporter: TreeSitterImporter, + private readonly _fileService: IFileService, + private readonly _environmentService: IEnvironmentService + ) { + super(); + } + + public async getLanguage(languageId: string): Promise { + let language = this._languages.get(languageId); + if (!language) { + language = await this._fetchLanguage(languageId); + if (!language) { + return undefined; + } + this._languages.set(languageId, language); + } + return language; + } + + private async _fetchLanguage(languageId: string): Promise { + const grammarName = TreeSitterTokenizationRegistry.get(languageId); + const languageLocation = this._getLanguageLocation(languageId); + if (!grammarName || !languageLocation) { + return undefined; + } + const wasmPath: AppResourcePath = `${languageLocation}/${grammarName.name}.wasm`; + const languageFile = await (this._fileService.readFile(FileAccess.asFileUri(wasmPath))); + const Parser = await this._treeSitterImporter.getParserClass(); + return Parser.Language.load(languageFile.value.buffer); + } + + private _getLanguageLocation(languageId: string): AppResourcePath | undefined { + const grammarName = TreeSitterTokenizationRegistry.get(languageId); + if (!grammarName) { + return undefined; + } + return getModuleLocation(this._environmentService); + } +} + +export class TreeSitterImporter { + private _treeSitterImport: typeof import('@vscode/tree-sitter-wasm') | undefined; + private async _getTreeSitterImport() { + if (!this._treeSitterImport) { + this._treeSitterImport = await importAMDNodeModule('@vscode/tree-sitter-wasm', 'wasm/tree-sitter.js'); + } + return this._treeSitterImport; + } + + private _parserClass: typeof Parser | undefined; + public async getParserClass() { + if (!this._parserClass) { + this._parserClass = (await this._getTreeSitterImport()).Parser; + } + return this._parserClass; + } +} + +export class TreeSitterTextModelService extends Disposable implements ITreeSitterParserService { + readonly _serviceBrand: undefined; + private _init!: Promise; + private _textModelTreeSitters: DisposableMap = this._register(new DisposableMap()); + private _registeredLanguages: DisposableMap = this._register(new DisposableMap()); + private readonly _treeSitterImporter: TreeSitterImporter = new TreeSitterImporter(); + private readonly _treeSitterParser: TreeSitterLanguages; + + constructor(@IModelService private readonly _modelService: IModelService, + @IFileService fileService: IFileService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService + ) { + super(); + this._treeSitterParser = this._register(new TreeSitterLanguages(this._treeSitterImporter, fileService, this._environmentService)); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(EDITOR_EXPERIMENTAL_PREFER_TREESITTER)) { + this._supportedLanguagesChanged(); + } + })); + this._supportedLanguagesChanged(); + } + + private async _doInitParser() { + const Parser = await this._treeSitterImporter.getParserClass(); + const environmentService = this._environmentService; + await Parser.init({ + locateFile(_file: string, _folder: string) { + return FileAccess.asBrowserUri(`${getModuleLocation(environmentService)}/${FILENAME_TREESITTER_WASM}`).toString(true); + } + }); + return true; + } + + private _hasInit: boolean = false; + private async _initParser(hasLanguages: boolean): Promise { + if (this._hasInit) { + return this._init; + } + + if (hasLanguages) { + this._hasInit = true; + this._init = this._doInitParser(); + + // New init, we need to deal with all the existing text models and set up listeners + this._init.then(() => this._registerModelServiceListeners()); + } else { + this._init = Promise.resolve(false); + } + return this._init; + } + + private async _supportedLanguagesChanged() { + const setting = this._getSetting(); + + let hasLanguages = true; + if (setting.length === 0) { + hasLanguages = false; + } + + if (await this._initParser(hasLanguages)) { + // Eventually, this should actually use an extension point to add tree sitter grammars, but for now they are hard coded in core + if (setting.includes('typescript')) { + this._addGrammar('typescript', 'tree-sitter-typescript'); + } else { + this._removeGrammar('typescript'); + } + } + } + + private _getSetting(): string[] { + const setting = this._configurationService.getValue(EDITOR_EXPERIMENTAL_PREFER_TREESITTER); + if (setting && setting.length > 0) { + return setting; + } else { + const expSetting = this._configurationService.getValue(EDITOR_TREESITTER_TELEMETRY); + if (expSetting) { + return ['typescript']; + } + } + return []; + } + + private async _registerModelServiceListeners() { + this._register(this._modelService.onModelAdded(model => { + this._createTextModelTreeSitter(model); + })); + this._register(this._modelService.onModelRemoved(model => { + this._textModelTreeSitters.deleteAndDispose(model); + })); + this._modelService.getModels().forEach(model => this._createTextModelTreeSitter(model)); + } + + private _createTextModelTreeSitter(model: ITextModel) { + const textModelTreeSitter = new TextModelTreeSitter(model, this._treeSitterParser, this._treeSitterImporter, this._logService, this._telemetryService); + this._textModelTreeSitters.set(model, textModelTreeSitter); + } + + private _addGrammar(languageId: string, grammarName: string) { + if (!TreeSitterTokenizationRegistry.get(languageId)) { + this._registeredLanguages.set(languageId, TreeSitterTokenizationRegistry.register(languageId, { name: grammarName })); + } + } + + private _removeGrammar(languageId: string) { + if (this._registeredLanguages.has(languageId)) { + this._registeredLanguages.deleteAndDispose('typescript'); + } + } +} diff --git a/src/vs/editor/browser/stableEditorScroll.ts b/src/vs/editor/browser/stableEditorScroll.ts index 4d7539435cd..986e18ac6c0 100644 --- a/src/vs/editor/browser/stableEditorScroll.ts +++ b/src/vs/editor/browser/stableEditorScroll.ts @@ -20,16 +20,6 @@ export class StableEditorScrollState { const visibleRanges = editor.getVisibleRanges(); if (visibleRanges.length > 0) { visiblePosition = visibleRanges[0].getStartPosition(); - - const cursorPos = editor.getPosition(); - if (cursorPos) { - const isVisible = visibleRanges.some(range => range.containsPosition(cursorPos)); - if (isVisible) { - // Keep cursor pos fixed if it is visible - visiblePosition = cursorPos; - } - } - const visiblePositionScrollTop = editor.getTopForPosition(visiblePosition.lineNumber, visiblePosition.column); visiblePositionScrollDelta = editor.getScrollTop() - visiblePositionScrollTop; } diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index 0ca31065a45..8e3eb5666fa 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -512,9 +512,7 @@ export class DecorationsOverviewRuler extends ViewPart { canvasCtx.strokeStyle = this._settings.borderColor; canvasCtx.moveTo(0, 0); canvasCtx.lineTo(0, canvasHeight); - canvasCtx.stroke(); - - canvasCtx.moveTo(0, 0); + canvasCtx.moveTo(1, 0); canvasCtx.lineTo(canvasWidth, 0); canvasCtx.stroke(); } diff --git a/src/vs/editor/browser/widget/codeEditor/editor.css b/src/vs/editor/browser/widget/codeEditor/editor.css index 5ef7246d0fb..a6d82d5845f 100644 --- a/src/vs/editor/browser/widget/codeEditor/editor.css +++ b/src/vs/editor/browser/widget/codeEditor/editor.css @@ -23,6 +23,7 @@ -webkit-text-size-adjust: 100%; color: var(--vscode-editor-foreground); background-color: var(--vscode-editor-background); + overflow-wrap: initial; } .monaco-editor-background { background-color: var(--vscode-editor-background); diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorDecorations.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorDecorations.ts index a5992771a9a..efbbe00f6c1 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorDecorations.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorDecorations.ts @@ -6,6 +6,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IObservable, derived } from 'vs/base/common/observable'; import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors'; +import { allowsTrueInlineDiffRendering } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones'; import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorOptions'; import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; @@ -28,7 +29,8 @@ export class DiffEditorDecorations extends Disposable { } private readonly _decorations = derived(this, (reader) => { - const diff = this._diffModel.read(reader)?.diff.read(reader); + const diffModel = this._diffModel.read(reader); + const diff = diffModel?.diff.read(reader); if (!diff) { return null; } @@ -56,13 +58,29 @@ export class DiffEditorDecorations extends Disposable { modifiedDecorations.push({ range: m.lineRangeMapping.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); } } else { + const useInlineDiff = this._options.useTrueInlineDiffRendering.read(reader) && allowsTrueInlineDiffRendering(m.lineRangeMapping); for (const i of m.lineRangeMapping.innerChanges || []) { // Don't show empty markers outside the line range if (m.lineRangeMapping.original.contains(i.originalRange.startLineNumber)) { originalDecorations.push({ range: i.originalRange, options: (i.originalRange.isEmpty() && showEmptyDecorations) ? diffDeleteDecorationEmpty : diffDeleteDecoration }); } if (m.lineRangeMapping.modified.contains(i.modifiedRange.startLineNumber)) { - modifiedDecorations.push({ range: i.modifiedRange, options: (i.modifiedRange.isEmpty() && showEmptyDecorations) ? diffAddDecorationEmpty : diffAddDecoration }); + modifiedDecorations.push({ range: i.modifiedRange, options: (i.modifiedRange.isEmpty() && showEmptyDecorations && !useInlineDiff) ? diffAddDecorationEmpty : diffAddDecoration }); + } + if (useInlineDiff) { + const deletedText = diffModel!.model.original.getValueInRange(i.originalRange); + modifiedDecorations.push({ + range: i.modifiedRange, + options: { + description: 'deleted-text', + before: { + content: deletedText, + inlineClassName: 'inline-deleted-text', + }, + zIndex: 100000, + showIfCollapsed: true, + } + }); } } } diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts index 55b5d4a1e54..ee7935fd520 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts @@ -30,7 +30,10 @@ export class DiffEditorEditors extends Disposable { public readonly modifiedScrollTop = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); public readonly modifiedScrollHeight = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); - public readonly modifiedModel = observableCodeEditor(this.modified).model; + public readonly modifiedObs = observableCodeEditor(this.modified); + public readonly originalObs = observableCodeEditor(this.original); + + public readonly modifiedModel = this.modifiedObs.model; public readonly modifiedSelections = observableFromEvent(this, this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); public readonly modifiedCursor = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1)); diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts index 6a5283f2491..3b1b222de0d 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts @@ -30,6 +30,7 @@ import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewMod import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { DiffEditorOptions } from '../../diffEditorOptions'; +import { Range } from 'vs/editor/common/core/range'; /** * Ensures both editors have the same height by aligning unchanged lines. @@ -187,7 +188,7 @@ export class DiffEditorViewZones extends Disposable { const renderOptions = RenderOptions.fromEditor(this._editors.modified); for (const a of alignmentsVal) { - if (a.diff && !renderSideBySide) { + if (a.diff && !renderSideBySide && (!this._options.useTrueInlineDiffRendering.read(reader) || !allowsTrueInlineDiffRendering(a.diff))) { if (!a.originalRange.isEmpty) { originalModelTokenizationCompleted.read(reader); // Update view-zones once tokenization completes @@ -627,3 +628,17 @@ function getAdditionalLineHeights(editor: CodeEditorWidget, viewZonesToIgnore: R return result; } + +export function allowsTrueInlineDiffRendering(mapping: DetailedLineRangeMapping): boolean { + if (!mapping.innerChanges) { + return false; + } + return mapping.innerChanges.every(c => + (rangeIsSingleLine(c.modifiedRange) && rangeIsSingleLine(c.originalRange)) + || c.originalRange.equalsRange(new Range(1, 1, 1, 1)) + ); +} + +function rangeIsSingleLine(range: Range): boolean { + return range.startLineNumber === range.endLineNumber; +} diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts index b53bb2660e3..ea3506b74ae 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts @@ -4,9 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { IObservable, ISettableObservable, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; +import { derivedConstOnceDefined } from 'vs/base/common/observableInternal/utils'; import { Constants } from 'vs/base/common/uint'; +import { allowsTrueInlineDiffRendering } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones'; +import { DiffEditorViewModel, DiffState } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; import { diffEditorDefaultOptions } from 'vs/editor/common/config/diffEditor'; import { IDiffEditorBaseOptions, IDiffEditorOptions, IEditorOptions, ValidDiffEditorBaseOptions, clampedFloat, clampedInt, boolean as validateBooleanOption, stringSet as validateStringSetOption } from 'vs/editor/common/config/editorOptions'; +import { LineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export class DiffEditorOptions { @@ -31,9 +35,16 @@ export class DiffEditorOptions { ); public readonly renderOverviewRuler = derived(this, reader => this._options.read(reader).renderOverviewRuler); - public readonly renderSideBySide = derived(this, reader => this._options.read(reader).renderSideBySide - && !(this._options.read(reader).useInlineViewWhenSpaceIsLimited && this.couldShowInlineViewBecauseOfSize.read(reader) && !this._screenReaderMode.read(reader)) - ); + public readonly renderSideBySide = derived(this, reader => { + if (this.compactMode.read(reader)) { + if (this.shouldRenderInlineViewInSmartMode.read(reader)) { + return false; + } + } + + return this._options.read(reader).renderSideBySide + && !(this._options.read(reader).useInlineViewWhenSpaceIsLimited && this.couldShowInlineViewBecauseOfSize.read(reader) && !this._screenReaderMode.read(reader)); + }); public readonly readOnly = derived(this, reader => this._options.read(reader).readOnly); public readonly shouldRenderOldRevertArrows = derived(this, reader => { @@ -59,6 +70,14 @@ export class DiffEditorOptions { public readonly diffAlgorithm = derived(this, reader => this._options.read(reader).diffAlgorithm); public readonly showEmptyDecorations = derived(this, reader => this._options.read(reader).experimental.showEmptyDecorations!); public readonly onlyShowAccessibleDiffViewer = derived(this, reader => this._options.read(reader).onlyShowAccessibleDiffViewer); + public readonly compactMode = derived(this, reader => this._options.read(reader).compactMode); + private readonly trueInlineDiffRenderingEnabled: IObservable = derived(this, reader => + this._options.read(reader).experimental.useTrueInlineView! + ); + + public readonly useTrueInlineDiffRendering: IObservable = derived(this, reader => + !this.renderSideBySide.read(reader) && this.trueInlineDiffRenderingEnabled.read(reader) + ); public readonly hideUnchangedRegions = derived(this, reader => this._options.read(reader).hideUnchangedRegions.enabled!); public readonly hideUnchangedRegionsRevealLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.revealLineCount!); @@ -74,9 +93,37 @@ export class DiffEditorOptions { public setWidth(width: number): void { this._diffEditorWidth.set(width, undefined); } + + private readonly _model = observableValue(this, undefined); + + public setModel(model: DiffEditorViewModel | undefined) { + this._model.set(model, undefined); + } + + private readonly shouldRenderInlineViewInSmartMode = this._model + .map(this, model => derivedConstOnceDefined(this, reader => { + const diffs = model?.diff.read(reader); + return diffs ? isSimpleDiff(diffs, this.trueInlineDiffRenderingEnabled.read(reader)) : undefined; + })) + .flatten() + .map(this, v => !!v); + + public readonly inlineViewHideOriginalLineNumbers = this.compactMode; } -function validateDiffEditorOptions(options: Readonly, defaults: ValidDiffEditorBaseOptions): ValidDiffEditorBaseOptions { +function isSimpleDiff(diff: DiffState, supportsTrueDiffRendering: boolean): boolean { + return diff.mappings.every(m => isInsertion(m.lineRangeMapping) || isDeletion(m.lineRangeMapping) || (supportsTrueDiffRendering && allowsTrueInlineDiffRendering(m.lineRangeMapping))); +} + +function isInsertion(mapping: LineRangeMapping): boolean { + return mapping.original.length === 0; +} + +function isDeletion(mapping: LineRangeMapping): boolean { + return mapping.modified.length === 0; +} + +function validateDiffEditorOptions(options: Readonly, defaults: typeof diffEditorDefaultOptions | ValidDiffEditorBaseOptions): ValidDiffEditorBaseOptions { return { enableSplitViewResizing: validateBooleanOption(options.enableSplitViewResizing, defaults.enableSplitViewResizing), splitViewDefaultRatio: clampedFloat(options.splitViewDefaultRatio, 0.5, 0.1, 0.9), @@ -95,6 +142,7 @@ function validateDiffEditorOptions(options: Readonly, defaul experimental: { showMoves: validateBooleanOption(options.experimental?.showMoves, defaults.experimental.showMoves!), showEmptyDecorations: validateBooleanOption(options.experimental?.showEmptyDecorations, defaults.experimental.showEmptyDecorations!), + useTrueInlineView: validateBooleanOption(options.experimental?.useTrueInlineView, defaults.experimental.useTrueInlineView!), }, hideUnchangedRegions: { enabled: validateBooleanOption(options.hideUnchangedRegions?.enabled ?? (options.experimental as any)?.collapseUnchangedRegions, defaults.hideUnchangedRegions.enabled!), @@ -107,5 +155,6 @@ function validateDiffEditorOptions(options: Readonly, defaul renderSideBySideInlineBreakpoint: clampedInt(options.renderSideBySideInlineBreakpoint, defaults.renderSideBySideInlineBreakpoint, 0, Constants.MAX_SAFE_SMALL_INTEGER), useInlineViewWhenSpaceIsLimited: validateBooleanOption(options.useInlineViewWhenSpaceIsLimited, defaults.useInlineViewWhenSpaceIsLimited), renderGutterMenu: validateBooleanOption(options.renderGutterMenu, defaults.renderGutterMenu), + compactMode: validateBooleanOption(options.compactMode, defaults.compactMode), }; } diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts index 67f8505cc28..557b757586c 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts @@ -394,6 +394,7 @@ function normalizeRangeMapping(rangeMapping: RangeMapping, original: ITextModel, let originalRange = rangeMapping.originalRange; let modifiedRange = rangeMapping.modifiedRange; if ( + originalRange.startColumn === 1 && modifiedRange.startColumn === 1 && (originalRange.endColumn !== 1 || modifiedRange.endColumn !== 1) && originalRange.endColumn === original.getLineMaxColumn(originalRange.endLineNumber) && modifiedRange.endColumn === modified.getLineMaxColumn(modifiedRange.endLineNumber) diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 9c239778773..e22b0291dd0 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -8,7 +8,7 @@ import { findLast } from 'vs/base/common/arraysFind'; import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, ITransaction, autorun, autorunWithStore, derived, observableFromEvent, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from 'vs/base/common/observable'; +import { IObservable, ITransaction, autorun, autorunWithStore, derived, disposableObservableValue, observableFromEvent, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from 'vs/base/common/observable'; import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; import 'vs/css!./style'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; @@ -26,7 +26,7 @@ import { HideUnchangedRegionsFeature } from 'vs/editor/browser/widget/diffEditor import { MovedBlocksLinesFeature } from 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; import { RevertButtonsFeature } from 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature'; -import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { CSSStyle, ObservableElementSizeObserver, RefCounted, applyStyle, applyViewZones, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; import { bindContextKey } from 'vs/platform/observable/common/platformObservableUtils'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -62,8 +62,8 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%', } }), h('div.accessibleDiffViewer@accessibleDiffViewer', { style: { position: 'absolute', height: '100%' } }), ]); - private readonly _diffModel = observableValue(this, undefined); - private _shouldDisposeDiffModel = false; + private readonly _diffModelSrc = this._register(disposableObservableValue | undefined>(this, undefined)); + private readonly _diffModel = derived(this, reader => this._diffModelSrc.read(reader)?.object); public readonly onDidChangeModel = Event.fromObservableLight(this._diffModel); public get onDidContentSizeChange() { return this._editors.onDidContentSizeChange; } @@ -318,12 +318,6 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } })); - this._register(toDisposable(() => { - if (this._shouldDisposeDiffModel) { - this._diffModel.get()?.dispose(); - } - })); - this._register(autorunWithStore((reader, store) => { store.add(new (readHotReloadableExport(RevertButtonsFeature, reader))(this._editors, this._diffModel, this._options, this)); })); @@ -338,6 +332,10 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { })); } })); + + this._register(autorun(reader => { + this._options.setModel(this._diffModel.read(reader)); + })); } public getViewWidth(): number { @@ -387,8 +385,13 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } else { gutterLeft = 0; + const shouldHideOriginalLineNumbers = this._options.inlineViewHideOriginalLineNumbers.read(reader); originalLeft = gutterWidth; - originalWidth = Math.max(5, this._editors.original.getLayoutInfo().decorationsLeft); + if (shouldHideOriginalLineNumbers) { + originalWidth = 0; + } else { + originalWidth = Math.max(5, this._editors.originalObs.layoutInfoDecorationsLeft.read(reader)); + } modifiedLeft = gutterWidth + originalWidth; modifiedWidth = fullWidth - modifiedLeft - overviewRulerPartWidth; @@ -474,30 +477,36 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { override getModel(): IDiffEditorModel | null { return this._diffModel.get()?.model ?? null; } - override setModel(model: IDiffEditorModel | null | IDiffEditorViewModel, tx?: ITransaction): void { - if (!model && this._diffModel.get()) { + override setModel(model: IDiffEditorModel | null | IDiffEditorViewModel): void { + const vm = !model ? null + : ('model' in model) ? RefCounted.create(model).createNewRef(this) + : RefCounted.create(this.createViewModel(model), this); + this.setDiffModel(vm); + } + + setDiffModel(viewModel: RefCounted | null, tx?: ITransaction): void { + const currentModel = this._diffModel.get(); + + if (!viewModel && currentModel) { // Transitioning from a model to no-model this._accessibleDiffViewer.get().close(); } - const vm = model ? ('model' in model) ? { model, shouldDispose: false } : { model: this.createViewModel(model), shouldDispose: true } : undefined; - - if (this._diffModel.get() !== vm?.model) { + if (this._diffModel.get() !== viewModel?.object) { subtransaction(tx, tx => { + const vm = viewModel?.object; /** @description DiffEditorWidget.setModel */ observableFromEvent.batchEventsGlobally(tx, () => { - this._editors.original.setModel(vm ? vm.model.model.original : null); - this._editors.modified.setModel(vm ? vm.model.model.modified : null); + this._editors.original.setModel(vm ? vm.model.original : null); + this._editors.modified.setModel(vm ? vm.model.modified : null); }); - const prevValue = this._diffModel.get(); - const shouldDispose = this._shouldDisposeDiffModel; - - this._shouldDisposeDiffModel = vm?.shouldDispose ?? false; - this._diffModel.set(vm?.model as (DiffEditorViewModel | undefined), tx); - - if (shouldDispose) { - prevValue?.dispose(); - } + const prevValueRef = this._diffModelSrc.get()?.createNewRef(this); + this._diffModelSrc.set(viewModel?.createNewRef(this) as RefCounted | undefined, tx); + setTimeout(() => { + // async, so that this runs after the transaction finished. + // TODO: use the transaction to schedule disposal + prevValueRef?.dispose(); + }, 0); }); } } diff --git a/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts b/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts index 467518518d1..19cee5997cc 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts @@ -152,7 +152,7 @@ export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, I // max 10 items in cache if (WorkerBasedDocumentDiffProvider.diffCache.size > 10) { - WorkerBasedDocumentDiffProvider.diffCache.delete(WorkerBasedDocumentDiffProvider.diffCache.keys().next().value); + WorkerBasedDocumentDiffProvider.diffCache.delete(WorkerBasedDocumentDiffProvider.diffCache.keys().next().value!); } WorkerBasedDocumentDiffProvider.diffCache.set(uriKey, { result, context }); diff --git a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts index 248f2b46d81..336bdb4304d 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts @@ -8,11 +8,12 @@ import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/i import { Codicon } from 'vs/base/common/codicons'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, autorun, derived, derivedWithStore, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, IReader, autorun, derived, derivedWithStore, observableValue, transaction } from 'vs/base/common/observable'; import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; import { ThemeIcon } from 'vs/base/common/themables'; import { isDefined } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors'; import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorOptions'; import { DiffEditorViewModel, RevealPreference, UnchangedRegion } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; @@ -31,7 +32,14 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti * Make sure to add the view zones to the editor! */ export class HideUnchangedRegionsFeature extends Disposable { - private static readonly _breadcrumbsSourceFactory = observableValue<((textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource) | undefined>('breadcrumbsSourceFactory', undefined); + private static readonly _breadcrumbsSourceFactory = observableValue<((textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource)>( + HideUnchangedRegionsFeature, () => ({ + dispose() { + }, + getBreadcrumbItems(startRange, reader) { + return []; + }, + })); public static setBreadcrumbsSourceFactory(factory: (textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource) { this._breadcrumbsSourceFactory.set(factory, undefined); } @@ -97,41 +105,72 @@ export class HideUnchangedRegionsFeature extends Disposable { const modViewZones: IObservableViewZone[] = []; const sideBySide = this._options.renderSideBySide.read(reader); + const compactMode = this._options.compactMode.read(reader); + const curUnchangedRegions = unchangedRegions.read(reader); - for (const r of curUnchangedRegions) { + for (let i = 0; i < curUnchangedRegions.length; i++) { + const r = curUnchangedRegions[i]; if (r.shouldHideControls(reader)) { continue; } - { - const d = derived(this, reader => /** @description hiddenOriginalRangeStart */ r.getHiddenOriginalRange(reader).startLineNumber - 1); - const origVz = new PlaceholderViewZone(d, 24); - origViewZones.push(origVz); - store.add(new CollapsedCodeOverlayWidget( - this._editors.original, - origVz, - r, - r.originalUnchangedRange, - !sideBySide, - modifiedOutlineSource, - l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, RevealPreference.FromBottom, undefined), - this._options, - )); + if (compactMode && (i === 0 || i === curUnchangedRegions.length - 1)) { + continue; } - { - const d = derived(this, reader => /** @description hiddenModifiedRangeStart */ r.getHiddenModifiedRange(reader).startLineNumber - 1); - const modViewZone = new PlaceholderViewZone(d, 24); - modViewZones.push(modViewZone); - store.add(new CollapsedCodeOverlayWidget( - this._editors.modified, - modViewZone, - r, - r.modifiedUnchangedRange, - false, - modifiedOutlineSource, - l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, RevealPreference.FromBottom, undefined), - this._options, - )); + + if (compactMode) { + { + const d = derived(this, reader => /** @description hiddenOriginalRangeStart */ r.getHiddenOriginalRange(reader).startLineNumber - 1); + const origVz = new PlaceholderViewZone(d, 12); + origViewZones.push(origVz); + store.add(new CompactCollapsedCodeOverlayWidget( + this._editors.original, + origVz, + r, + !sideBySide, + )); + } + { + const d = derived(this, reader => /** @description hiddenModifiedRangeStart */ r.getHiddenModifiedRange(reader).startLineNumber - 1); + const modViewZone = new PlaceholderViewZone(d, 12); + modViewZones.push(modViewZone); + store.add(new CompactCollapsedCodeOverlayWidget( + this._editors.modified, + modViewZone, + r, + )); + } + } else { + { + const d = derived(this, reader => /** @description hiddenOriginalRangeStart */ r.getHiddenOriginalRange(reader).startLineNumber - 1); + const origVz = new PlaceholderViewZone(d, 24); + origViewZones.push(origVz); + store.add(new CollapsedCodeOverlayWidget( + this._editors.original, + origVz, + r, + r.originalUnchangedRange, + !sideBySide, + modifiedOutlineSource, + l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, RevealPreference.FromBottom, undefined), + this._options, + )); + } + { + const d = derived(this, reader => /** @description hiddenModifiedRangeStart */ r.getHiddenModifiedRange(reader).startLineNumber - 1); + const modViewZone = new PlaceholderViewZone(d, 24); + modViewZones.push(modViewZone); + store.add(new CollapsedCodeOverlayWidget( + this._editors.modified, + modViewZone, + r, + r.modifiedUnchangedRange, + false, + modifiedOutlineSource, + l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, RevealPreference.FromBottom, undefined), + this._options, + )); + } } } @@ -228,6 +267,39 @@ export class HideUnchangedRegionsFeature extends Disposable { } } +class CompactCollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { + private readonly _nodes = h('div.diff-hidden-lines-compact', [ + h('div.line-left', []), + h('div.text@text', []), + h('div.line-right', []) + ]); + + constructor( + editor: ICodeEditor, + _viewZone: PlaceholderViewZone, + private readonly _unchangedRegion: UnchangedRegion, + private readonly _hide: boolean = false, + ) { + const root = h('div.diff-hidden-lines-widget'); + super(editor, _viewZone, root.root); + root.root.appendChild(this._nodes.root); + + if (this._hide) { + this._nodes.root.replaceChildren(); + } + + this._register(autorun(reader => { + /** @description update labels */ + + if (!this._hide) { + const lineCount = this._unchangedRegion.getHiddenModifiedRange(reader).length; + const linesHiddenText = localize('hiddenLines', '{0} hidden lines', lineCount); + this._nodes.text.innerText = linesHiddenText; + } + })); + } +} + class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { private readonly _nodes = h('div.diff-hidden-lines', [ h('div.top@top', { title: localize('diff.hiddenLines.top', 'Click or drag to show more above') }), @@ -255,12 +327,8 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { super(_editor, _viewZone, root.root); root.root.appendChild(this._nodes.root); - const layoutInfo = observableFromEvent(this._editor.onDidLayoutChange, () => - this._editor.getLayoutInfo() - ); - if (!this._hide) { - this._register(applyStyle(this._nodes.first, { width: layoutInfo.map((l) => l.contentLeft) })); + this._register(applyStyle(this._nodes.first, { width: observableCodeEditor(this._editor).layoutInfoContentLeft })); } else { reset(this._nodes.first); } diff --git a/src/vs/editor/browser/widget/diffEditor/style.css b/src/vs/editor/browser/widget/diffEditor/style.css index ebb52234658..4489d84be38 100644 --- a/src/vs/editor/browser/widget/diffEditor/style.css +++ b/src/vs/editor/browser/widget/diffEditor/style.css @@ -276,10 +276,14 @@ background-color: var(--vscode-diffEditorGutter-insertedLineBackground, var(--vscode-diffEditor-insertedLineBackground), var(--vscode-diffEditor-insertedTextBackground)); } -.monaco-editor .char-delete, .monaco-diff-editor .char-delete { +.monaco-editor .char-delete, .monaco-diff-editor .char-delete, .monaco-editor .inline-deleted-text { background-color: var(--vscode-diffEditor-removedTextBackground); } +.monaco-editor .inline-deleted-text { + text-decoration: line-through; +} + .monaco-editor .line-delete, .monaco-diff-editor .line-delete { background-color: var(--vscode-diffEditor-removedLineBackground, var(--vscode-diffEditor-removedTextBackground)); } @@ -395,3 +399,29 @@ } } } + + +.monaco-diff-editor .diff-hidden-lines-compact { + display: flex; + height: 11px; + .line-left, .line-right { + height: 1px; + border-top: 1px solid; + border-color: var(--vscode-editorCodeLens-foreground); + opacity: 0.5; + margin: auto; + width: 100%; + } + + .line-left { + width: 20px; + } + + .text { + color: var(--vscode-editorCodeLens-foreground); + text-wrap: nowrap; + font-size: 11px; + line-height: 11px; + margin: 0 4px; + } +} diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 1e6d5eee428..ed24ac50430 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -6,7 +6,7 @@ import { IDimension } from 'vs/base/browser/dom'; import { findLast } from 'vs/base/common/arraysFind'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; import { IObservable, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableValue, transaction } from 'vs/base/common/observable'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { ICodeEditor, IOverlayWidget, IViewZone } from 'vs/editor/browser/editorBrowser'; @@ -415,3 +415,104 @@ export function filterWithPrevious(arr: T[], filter: (cur: T, prev: T | undef return result; }); } + +export interface IRefCounted extends IDisposable { + createNewRef(): this; +} + +export abstract class RefCounted implements IDisposable, IReference { + public static create(value: T, debugOwner: object | undefined = undefined): RefCounted { + return new BaseRefCounted(value, value, debugOwner); + } + + public static createWithDisposable(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted { + const store = new DisposableStore(); + store.add(disposable); + store.add(value); + return new BaseRefCounted(value, store, debugOwner); + } + + public static createOfNonDisposable(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted { + return new BaseRefCounted(value, disposable, debugOwner); + } + + public abstract createNewRef(debugOwner?: object | undefined): RefCounted; + + public abstract dispose(): void; + + public abstract get object(): T; +} + +class BaseRefCounted extends RefCounted { + private _refCount = 1; + private _isDisposed = false; + private readonly _owners: object[] = []; + + constructor( + public override readonly object: T, + private readonly _disposable: IDisposable, + private readonly _debugOwner: object | undefined, + ) { + super(); + + if (_debugOwner) { + this._addOwner(_debugOwner); + } + } + + private _addOwner(debugOwner: object | undefined) { + if (debugOwner) { + this._owners.push(debugOwner); + } + } + + public createNewRef(debugOwner?: object | undefined): RefCounted { + this._refCount++; + if (debugOwner) { + this._addOwner(debugOwner); + } + return new ClonedRefCounted(this, debugOwner); + } + + public dispose(): void { + if (this._isDisposed) { return; } + this._isDisposed = true; + this._decreaseRefCount(this._debugOwner); + } + + public _decreaseRefCount(debugOwner?: object | undefined): void { + this._refCount--; + if (this._refCount === 0) { + this._disposable.dispose(); + } + + if (debugOwner) { + const idx = this._owners.indexOf(debugOwner); + if (idx !== -1) { + this._owners.splice(idx, 1); + } + } + } +} + +class ClonedRefCounted extends RefCounted { + private _isDisposed = false; + constructor( + private readonly _base: BaseRefCounted, + private readonly _debugOwner: object | undefined, + ) { + super(); + } + + public get object(): T { return this._base.object; } + + public createNewRef(debugOwner?: object | undefined): RefCounted { + return this._base.createNewRef(debugOwner); + } + + public dispose(): void { + if (this._isDisposed) { return; } + this._isDisposed = true; + this._base._decreaseRefCount(this._debugOwner); + } +} diff --git a/src/vs/editor/browser/widget/multiDiffEditor/colors.ts b/src/vs/editor/browser/widget/multiDiffEditor/colors.ts index 297e5e86465..c9e2cca5fb6 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/colors.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/colors.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorBackground } from 'vs/platform/theme/common/colorRegistry'; export const multiDiffEditorHeaderBackground = registerColor( 'multiDiffEditor.headerBackground', @@ -14,7 +14,7 @@ export const multiDiffEditorHeaderBackground = registerColor( export const multiDiffEditorBackground = registerColor( 'multiDiffEditor.background', - 'editorBackground', + editorBackground, localize('multiDiffEditor.background', 'The background color of the multi file diff editor') ); diff --git a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts index 7ea85f66a72..8f7d4a12ab0 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts @@ -6,20 +6,20 @@ import { h } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { autorun, derived, observableFromEvent } from 'vs/base/common/observable'; -import { IObservable, globalTransaction, observableValue } from 'vs/base/common/observableInternal/base'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { autorun, derived } from 'vs/base/common/observable'; +import { globalTransaction, observableValue } from 'vs/base/common/observableInternal/base'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { DocumentDiffItemViewModel } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel'; import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditor/workbenchUIElementFactory'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IObjectData, IPooledObject } from './objectPool'; import { ActionRunnerWithContext } from './utils'; -import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export class TemplateData implements IObjectData { constructor( @@ -81,8 +81,8 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< overflowWidgetsDomNode: this._overflowWidgetsDomNode, }, {})); - private readonly isModifedFocused = isFocused(this.editor.getModifiedEditor()); - private readonly isOriginalFocused = isFocused(this.editor.getOriginalEditor()); + private readonly isModifedFocused = observableCodeEditor(this.editor.getModifiedEditor()).isFocused; + private readonly isOriginalFocused = observableCodeEditor(this.editor.getOriginalEditor()).isFocused; public readonly isFocused = derived(this, reader => this.isModifedFocused.read(reader) || this.isOriginalFocused.read(reader)); private readonly _resourceLabel = this._workbenchUIElementFactory.createResourceLabel @@ -177,7 +177,7 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< private _data: TemplateData | undefined; - public setData(data: TemplateData): void { + public setData(data: TemplateData | undefined): void { this._data = data; function updateOptions(options: IDiffEditorOptions): IDiffEditorOptions { return { @@ -198,13 +198,17 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< }; } - const value = data.viewModel.entry.value!; // TODO - - if (value.onOptionsDidChange) { - this._dataStore.add(value.onOptionsDidChange(() => { - this.editor.updateOptions(updateOptions(value.options ?? {})); - })); + if (!data) { + globalTransaction(tx => { + this._viewModel.set(undefined, tx); + this.editor.setDiffModel(null, tx); + this._dataStore.clear(); + }); + return; } + + const value = data.viewModel.documentDiffItem; + globalTransaction(tx => { this._resourceLabel?.setUri(data.viewModel.modifiedUri ?? data.viewModel.originalUri!, { strikethrough: data.viewModel.modifiedUri === undefined }); @@ -231,9 +235,19 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< this._dataStore.clear(); this._viewModel.set(data.viewModel, tx); - this.editor.setModel(data.viewModel.diffEditorViewModel, tx); + this.editor.setDiffModel(data.viewModel.diffEditorViewModelRef, tx); this.editor.updateOptions(updateOptions(value.options ?? {})); }); + if (value.onOptionsDidChange) { + this._dataStore.add(value.onOptionsDidChange(() => { + this.editor.updateOptions(updateOptions(value.options ?? {})); + })); + } + data.viewModel.isAlive.recomputeInitiallyAndOnChange(this._dataStore, value => { + if (!value) { + this.setData(undefined); + } + }); } private readonly _headerHeight = /*this._elements.header.clientHeight*/ 40; @@ -276,15 +290,3 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< this._elements.root.style.visibility = 'hidden'; // Some editor parts are still visible } } - -function isFocused(editor: ICodeEditor): IObservable { - return observableFromEvent( - h => { - const store = new DisposableStore(); - store.add(editor.onDidFocusEditorWidget(() => h(true))); - store.add(editor.onDidBlurEditorWidget(() => h(false))); - return store; - }, - () => editor.hasTextFocus() - ); -} diff --git a/src/vs/editor/browser/widget/multiDiffEditor/model.ts b/src/vs/editor/browser/widget/multiDiffEditor/model.ts index de8f43e4edc..f2b11dc998c 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/model.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/model.ts @@ -4,37 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { Event, IValueWithChangeEvent } from 'vs/base/common/event'; +import { RefCounted } from 'vs/editor/browser/widget/diffEditor/utils'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ITextModel } from 'vs/editor/common/model'; import { ContextKeyValue } from 'vs/platform/contextkey/common/contextkey'; export interface IMultiDiffEditorModel { - readonly documents: IValueWithChangeEvent[]>; + readonly documents: IValueWithChangeEvent[]>; readonly contextKeys?: Record; } -export interface LazyPromise { - request(): Promise; - readonly value: T | undefined; - readonly onHasValueDidChange: Event; -} - -export class ConstLazyPromise implements LazyPromise { - public readonly onHasValueDidChange = Event.None; - - constructor( - private readonly _value: T - ) { } - - public request(): Promise { - return Promise.resolve(this._value); - } - - public get value(): T { - return this._value; - } -} - export interface IDocumentDiffItem { /** * undefined if the file was created. diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index b58d7303e66..30924647869 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { IObservable, ITransaction, derived, observableValue, transaction } from 'vs/base/common/observable'; import { constObservable, derivedObservableWithWritableCache, mapObservableArrayCached, observableFromValueWithChangeEvent } from 'vs/base/common/observableInternal/utils'; import { URI } from 'vs/base/common/uri'; import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorOptions'; import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; -import { IDocumentDiffItem, IMultiDiffEditorModel, LazyPromise } from 'vs/editor/browser/widget/multiDiffEditor/model'; +import { RefCounted } from 'vs/editor/browser/widget/diffEditor/utils'; +import { IDocumentDiffItem, IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditor/model'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Selection } from 'vs/editor/common/core/selection'; import { IDiffEditorViewModel } from 'vs/editor/common/editorCommon'; @@ -18,9 +19,13 @@ import { ContextKeyValue } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class MultiDiffEditorViewModel extends Disposable { - private readonly _documents = observableFromValueWithChangeEvent(this.model, this.model.documents); + private readonly _documents: IObservable[]> = observableFromValueWithChangeEvent(this.model, this.model.documents); - public readonly items = mapObservableArrayCached(this, this._documents, (d, store) => store.add(this._instantiationService.createInstance(DocumentDiffItemViewModel, d, this))) + public readonly items: IObservable = mapObservableArrayCached( + this, + this._documents, + (d, store) => store.add(this._instantiationService.createInstance(DocumentDiffItemViewModel, d, this)) + ) .recomputeInitiallyAndOnChange(this._store); public readonly focusedDiffItem = derived(this, reader => this.items.read(reader).find(i => i.isFocused.read(reader))); @@ -61,7 +66,13 @@ export class MultiDiffEditorViewModel extends Disposable { } export class DocumentDiffItemViewModel extends Disposable { - public readonly diffEditorViewModel: IDiffEditorViewModel; + /** + * The diff editor view model keeps its inner objects alive. + */ + public readonly diffEditorViewModelRef: RefCounted; + public get diffEditorViewModel(): IDiffEditorViewModel { + return this.diffEditorViewModelRef.object; + } public readonly collapsed = observableValue(this, false); public readonly lastTemplateData = observableValue<{ contentHeight: number; selections: Selection[] | undefined }>( @@ -69,7 +80,6 @@ export class DocumentDiffItemViewModel extends Disposable { { contentHeight: 500, selections: undefined, } ); - public get documentDiffItem(): IDocumentDiffItem { return this.entry.value!; } public get originalUri(): URI | undefined { return this.documentDiffItem.original?.uri; } public get modifiedUri(): URI | undefined { return this.documentDiffItem.modified?.uri; } @@ -82,14 +92,27 @@ export class DocumentDiffItemViewModel extends Disposable { this._isFocusedSource.set(source, tx); } + private readonly documentDiffItemRef: RefCounted; + public get documentDiffItem(): IDocumentDiffItem { + return this.documentDiffItemRef.object; + } + + public readonly isAlive = observableValue(this, true); + constructor( - public readonly entry: LazyPromise, + documentDiffItem: RefCounted, private readonly _editorViewModel: MultiDiffEditorViewModel, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IModelService private readonly _modelService: IModelService, ) { super(); + this._register(toDisposable(() => { + this.isAlive.set(false, undefined); + })); + + this.documentDiffItemRef = this._register(documentDiffItem.createNewRef(this)); + function updateOptions(options: IDiffEditorOptions): IDiffEditorOptions { return { ...options, @@ -99,20 +122,26 @@ export class DocumentDiffItemViewModel extends Disposable { }; } - const options = this._instantiationService.createInstance(DiffEditorOptions, updateOptions(this.entry.value!.options || {})); - if (this.entry.value!.onOptionsDidChange) { - this._register(this.entry.value!.onOptionsDidChange(() => { - options.updateOptions(updateOptions(this.entry.value!.options || {})); + const options = this._instantiationService.createInstance(DiffEditorOptions, updateOptions(this.documentDiffItem.options || {})); + if (this.documentDiffItem.onOptionsDidChange) { + this._register(this.documentDiffItem.onOptionsDidChange(() => { + options.updateOptions(updateOptions(this.documentDiffItem.options || {})); })); } - const originalTextModel = this.entry.value!.original ?? this._register(this._modelService.createModel('', null)); - const modifiedTextModel = this.entry.value!.modified ?? this._register(this._modelService.createModel('', null)); + const diffEditorViewModelStore = new DisposableStore(); + const originalTextModel = this.documentDiffItem.original ?? diffEditorViewModelStore.add(this._modelService.createModel('', null)); + const modifiedTextModel = this.documentDiffItem.modified ?? diffEditorViewModelStore.add(this._modelService.createModel('', null)); + diffEditorViewModelStore.add(this.documentDiffItemRef.createNewRef(this)); - this.diffEditorViewModel = this._register(this._instantiationService.createInstance(DiffEditorViewModel, { - original: originalTextModel, - modified: modifiedTextModel, - }, options)); + this.diffEditorViewModelRef = this._register(RefCounted.createWithDisposable( + this._instantiationService.createInstance(DiffEditorViewModel, { + original: originalTextModel, + modified: modifiedTextModel, + }, options), + diffEditorViewModelStore, + this + )); } public getKey(): string { diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index 96cccc0afb8..a8a2f96f7d2 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -406,7 +406,7 @@ class VirtualizedViewItem extends Disposable { } public override toString(): string { - return `VirtualViewItem(${this.viewModel.entry.value!.modified?.uri.toString()})`; + return `VirtualViewItem(${this.viewModel.documentDiffItem.modified?.uri.toString()})`; } public getKey(): string { diff --git a/src/vs/editor/common/config/diffEditor.ts b/src/vs/editor/common/config/diffEditor.ts index 2d0a312357e..ff97a530fb6 100644 --- a/src/vs/editor/common/config/diffEditor.ts +++ b/src/vs/editor/common/config/diffEditor.ts @@ -24,6 +24,7 @@ export const diffEditorDefaultOptions = { experimental: { showMoves: false, showEmptyDecorations: true, + useTrueInlineView: false, }, hideUnchangedRegions: { enabled: false, @@ -35,4 +36,5 @@ export const diffEditorDefaultOptions = { onlyShowAccessibleDiffViewer: false, renderSideBySideInlineBreakpoint: 900, useInlineViewWhenSpaceIsLimited: true, + compactMode: false, } satisfies ValidDiffEditorBaseOptions; diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index ab2b8cc70c6..c2d124936b4 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -109,6 +109,12 @@ const editorConfiguration: IConfigurationNode = { description: nls.localize('editor.experimental.asyncTokenizationVerification', "Controls whether async tokenization should be verified against legacy background tokenization. Might slow down tokenization. For debugging only."), tags: ['experimental'], }, + 'editor.experimental.treeSitterTelemetry': { + type: 'boolean', + default: false, + markdownDescription: nls.localize('editor.experimental.treeSitterTelemetry', "Controls whether tree sitter parsing should be turned on and telemetry collected. Setting `editor.experimental.preferTreeSitter` for specific languages will take precedence."), + tags: ['experimental'] + }, 'editor.language.brackets': { type: ['array', 'null'], default: null, // We want to distinguish the empty array from not configured. @@ -247,7 +253,12 @@ const editorConfiguration: IConfigurationNode = { type: 'boolean', default: diffEditorDefaultOptions.experimental.showEmptyDecorations, description: nls.localize('showEmptyDecorations', "Controls whether the diff editor shows empty decorations to see where characters got inserted or deleted."), - } + }, + 'diffEditor.experimental.useTrueInlineView': { + type: 'boolean', + default: diffEditorDefaultOptions.experimental.useTrueInlineView, + description: nls.localize('useTrueInlineView', "If enabled and the editor uses the inline view, word changes are rendered inline."), + }, } }; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 64394443765..0c099aa07e3 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -771,75 +771,96 @@ export interface IDiffEditorBaseOptions { * Defaults to true. */ enableSplitViewResizing?: boolean; + /** * The default ratio when rendering side-by-side editors. * Must be a number between 0 and 1, min sizes apply. * Defaults to 0.5 */ splitViewDefaultRatio?: number; + /** * Render the differences in two side-by-side editors. * Defaults to true. */ renderSideBySide?: boolean; + /** * When `renderSideBySide` is enabled, `useInlineViewWhenSpaceIsLimited` is set, * and the diff editor has a width less than `renderSideBySideInlineBreakpoint`, the inline view is used. */ renderSideBySideInlineBreakpoint?: number | undefined; + /** * When `renderSideBySide` is enabled, `useInlineViewWhenSpaceIsLimited` is set, * and the diff editor has a width less than `renderSideBySideInlineBreakpoint`, the inline view is used. */ useInlineViewWhenSpaceIsLimited?: boolean; + + /** + * If set, the diff editor is optimized for small views. + * Defaults to `false`. + */ + compactMode?: boolean; + /** * Timeout in milliseconds after which diff computation is cancelled. * Defaults to 5000. */ maxComputationTime?: number; + /** * Maximum supported file size in MB. * Defaults to 50. */ maxFileSize?: number; + /** * Compute the diff by ignoring leading/trailing whitespace * Defaults to true. */ ignoreTrimWhitespace?: boolean; + /** * Render +/- indicators for added/deleted changes. * Defaults to true. */ renderIndicators?: boolean; + /** * Shows icons in the glyph margin to revert changes. * Default to true. */ renderMarginRevertIcon?: boolean; + /** * Indicates if the gutter menu should be rendered. */ renderGutterMenu?: boolean; + /** * Original model should be editable? * Defaults to false. */ originalEditable?: boolean; + /** * Should the diff editor enable code lens? * Defaults to false. */ diffCodeLens?: boolean; + /** * Is the diff editor should render overview ruler * Defaults to true */ renderOverviewRuler?: boolean; + /** * Control the wrapping of the diff editor. */ diffWordWrap?: 'off' | 'on' | 'inherit'; + /** * Diff Algorithm */ @@ -857,6 +878,11 @@ export interface IDiffEditorBaseOptions { showMoves?: boolean; showEmptyDecorations?: boolean; + + /** + * Only applies when `renderSideBySide` is set to false. + */ + useTrueInlineView?: boolean; }; /** @@ -1916,12 +1942,14 @@ export interface IGotoLocationOptions { multipleDeclarations?: GoToLocationValues; multipleImplementations?: GoToLocationValues; multipleReferences?: GoToLocationValues; + multipleTests?: GoToLocationValues; alternativeDefinitionCommand?: string; alternativeTypeDefinitionCommand?: string; alternativeDeclarationCommand?: string; alternativeImplementationCommand?: string; alternativeReferenceCommand?: string; + alternativeTestsCommand?: string; } /** @@ -1939,11 +1967,13 @@ class EditorGoToLocation extends BaseEditorOption(input.multipleDeclarations, 'peek', ['peek', 'gotoAndPeek', 'goto']), multipleImplementations: input.multipleImplementations ?? stringSet(input.multipleImplementations, 'peek', ['peek', 'gotoAndPeek', 'goto']), multipleReferences: input.multipleReferences ?? stringSet(input.multipleReferences, 'peek', ['peek', 'gotoAndPeek', 'goto']), + multipleTests: input.multipleTests ?? stringSet(input.multipleTests, 'peek', ['peek', 'gotoAndPeek', 'goto']), alternativeDefinitionCommand: EditorStringOption.string(input.alternativeDefinitionCommand, this.defaultValue.alternativeDefinitionCommand), alternativeTypeDefinitionCommand: EditorStringOption.string(input.alternativeTypeDefinitionCommand, this.defaultValue.alternativeTypeDefinitionCommand), alternativeDeclarationCommand: EditorStringOption.string(input.alternativeDeclarationCommand, this.defaultValue.alternativeDeclarationCommand), alternativeImplementationCommand: EditorStringOption.string(input.alternativeImplementationCommand, this.defaultValue.alternativeImplementationCommand), alternativeReferenceCommand: EditorStringOption.string(input.alternativeReferenceCommand, this.defaultValue.alternativeReferenceCommand), + alternativeTestsCommand: EditorStringOption.string(input.alternativeTestsCommand, this.defaultValue.alternativeTestsCommand), }; } } @@ -2767,7 +2799,7 @@ export type EditorLightbulbOptions = Readonly> class EditorLightbulb extends BaseEditorOption { constructor() { - const defaults: EditorLightbulbOptions = { enabled: ShowLightbulbIconMode.On }; + const defaults: EditorLightbulbOptions = { enabled: ShowLightbulbIconMode.OnCode }; super( EditorOption.lightbulb, 'lightbulb', defaults, { diff --git a/src/vs/editor/common/core/editorColorRegistry.ts b/src/vs/editor/common/core/editorColorRegistry.ts index b7c71f99721..b8c8157b26b 100644 --- a/src/vs/editor/common/core/editorColorRegistry.ts +++ b/src/vs/editor/common/core/editorColorRegistry.ts @@ -80,7 +80,7 @@ export const editorBracketHighlightingForeground4 = registerColor('editorBracket export const editorBracketHighlightingForeground5 = registerColor('editorBracketHighlight.foreground5', '#00000000', nls.localize('editorBracketHighlightForeground5', 'Foreground color of brackets (5). Requires enabling bracket pair colorization.')); export const editorBracketHighlightingForeground6 = registerColor('editorBracketHighlight.foreground6', '#00000000', nls.localize('editorBracketHighlightForeground6', 'Foreground color of brackets (6). Requires enabling bracket pair colorization.')); -export const editorBracketHighlightingUnexpectedBracketForeground = registerColor('editorBracketHighlight.unexpectedBracket.foreground', { dark: new Color(new RGBA(255, 18, 18, 0.8)), light: new Color(new RGBA(255, 18, 18, 0.8)), hcDark: new Color(new RGBA(255, 50, 50, 1)), hcLight: '' }, nls.localize('editorBracketHighlightUnexpectedBracketForeground', 'Foreground color of unexpected brackets.')); +export const editorBracketHighlightingUnexpectedBracketForeground = registerColor('editorBracketHighlight.unexpectedBracket.foreground', { dark: new Color(new RGBA(255, 18, 18, 0.8)), light: new Color(new RGBA(255, 18, 18, 0.8)), hcDark: 'new Color(new RGBA(255, 50, 50, 1))', hcLight: '#B5200D' }, nls.localize('editorBracketHighlightUnexpectedBracketForeground', 'Foreground color of unexpected brackets.')); export const editorBracketPairGuideBackground1 = registerColor('editorBracketPairGuide.background1', '#00000000', nls.localize('editorBracketPairGuide.background1', 'Background color of inactive bracket pair guides (1). Requires enabling bracket pair guides.')); export const editorBracketPairGuideBackground2 = registerColor('editorBracketPairGuide.background2', '#00000000', nls.localize('editorBracketPairGuide.background2', 'Background color of inactive bracket pair guides (2). Requires enabling bracket pair guides.')); diff --git a/src/vs/editor/common/cursor/cursor.ts b/src/vs/editor/common/cursor/cursor.ts index f84b5135d13..242283e7657 100644 --- a/src/vs/editor/common/cursor/cursor.ts +++ b/src/vs/editor/common/cursor/cursor.ts @@ -740,7 +740,7 @@ interface ICommandsData { hadTrackedEditOperation: boolean; } -class CommandExecutor { +export class CommandExecutor { public static executeCommands(model: ITextModel, selectionsBefore: Selection[], commands: (editorCommon.ICommand | null)[]): Selection[] | null { diff --git a/src/vs/editor/common/languageFeatureRegistry.ts b/src/vs/editor/common/languageFeatureRegistry.ts index 47679f308f4..32489739254 100644 --- a/src/vs/editor/common/languageFeatureRegistry.ts +++ b/src/vs/editor/common/languageFeatureRegistry.ts @@ -10,10 +10,10 @@ import { LanguageFilter, LanguageSelector, score } from 'vs/editor/common/langua import { URI } from 'vs/base/common/uri'; interface Entry { - selector: LanguageSelector; - provider: T; + readonly selector: LanguageSelector; + readonly provider: T; _score: number; - _time: number; + readonly _time: number; } function isExclusive(selector: LanguageSelector): boolean { @@ -40,14 +40,16 @@ class MatchCandidate { readonly uri: URI, readonly languageId: string, readonly notebookUri: URI | undefined, - readonly notebookType: string | undefined + readonly notebookType: string | undefined, + readonly recursive: boolean, ) { } equals(other: MatchCandidate): boolean { return this.notebookType === other.notebookType && this.languageId === other.languageId && this.uri.toString() === other.uri.toString() - && this.notebookUri?.toString() === other.notebookUri?.toString(); + && this.notebookUri?.toString() === other.notebookUri?.toString() + && this.recursive === other.recursive; } } @@ -96,7 +98,7 @@ export class LanguageFeatureRegistry { return []; } - this._updateScores(model); + this._updateScores(model, false); const result: T[] = []; // from registry @@ -113,9 +115,9 @@ export class LanguageFeatureRegistry { return this._entries.map(entry => entry.provider); } - ordered(model: ITextModel): T[] { + ordered(model: ITextModel, recursive = false): T[] { const result: T[] = []; - this._orderedForEach(model, entry => result.push(entry.provider)); + this._orderedForEach(model, recursive, entry => result.push(entry.provider)); return result; } @@ -124,7 +126,7 @@ export class LanguageFeatureRegistry { let lastBucket: T[]; let lastBucketScore: number; - this._orderedForEach(model, entry => { + this._orderedForEach(model, false, entry => { if (lastBucket && lastBucketScore === entry._score) { lastBucket.push(entry.provider); } else { @@ -137,9 +139,9 @@ export class LanguageFeatureRegistry { return result; } - private _orderedForEach(model: ITextModel, callback: (provider: Entry) => any): void { + private _orderedForEach(model: ITextModel, recursive: boolean, callback: (provider: Entry) => any): void { - this._updateScores(model); + this._updateScores(model, recursive); for (const entry of this._entries) { if (entry._score > 0) { @@ -150,15 +152,15 @@ export class LanguageFeatureRegistry { private _lastCandidate: MatchCandidate | undefined; - private _updateScores(model: ITextModel): void { + private _updateScores(model: ITextModel, recursive: boolean): void { const notebookInfo = this._notebookInfoResolver?.(model.uri); // use the uri (scheme, pattern) of the notebook info iff we have one // otherwise it's the model's/document's uri const candidate = notebookInfo - ? new MatchCandidate(model.uri, model.getLanguageId(), notebookInfo.uri, notebookInfo.type) - : new MatchCandidate(model.uri, model.getLanguageId(), undefined, undefined); + ? new MatchCandidate(model.uri, model.getLanguageId(), notebookInfo.uri, notebookInfo.type, recursive) + : new MatchCandidate(model.uri, model.getLanguageId(), undefined, undefined, recursive); if (this._lastCandidate?.equals(candidate)) { // nothing has changed @@ -171,13 +173,17 @@ export class LanguageFeatureRegistry { entry._score = score(entry.selector, candidate.uri, candidate.languageId, shouldSynchronizeModel(model), candidate.notebookUri, candidate.notebookType); if (isExclusive(entry.selector) && entry._score > 0) { - // support for one exclusive selector that overwrites - // any other selector - for (const entry of this._entries) { + if (recursive) { entry._score = 0; + } else { + // support for one exclusive selector that overwrites + // any other selector + for (const entry of this._entries) { + entry._score = 0; + } + entry._score = 1000; + break; } - entry._score = 1000; - break; } } diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 3159cdc6ae5..f1f65bfe99f 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -82,6 +82,14 @@ export class EncodedTokenizationResult { } } +/** + * An intermediate interface for scaffolding the new tree sitter tokenization support. Not final. + * @internal + */ +export interface ITreeSitterTokenizationSupport { + name: string; +} + /** * @internal */ @@ -1876,7 +1884,7 @@ export interface CommentThread { range: T | undefined; label: string | undefined; contextValue: string | undefined; - comments: Comment[] | undefined; + comments: ReadonlyArray | undefined; onDidChangeComments: Event; collapsibleState?: CommentThreadCollapsibleState; initialCollapsibleState?: CommentThreadCollapsibleState; @@ -2106,14 +2114,14 @@ export interface ITokenizationSupportChangedEvent { /** * @internal */ -export interface ILazyTokenizationSupport { - get tokenizationSupport(): Promise; +export interface ILazyTokenizationSupport { + get tokenizationSupport(): Promise; } /** * @internal */ -export class LazyTokenizationSupport implements IDisposable, ILazyTokenizationSupport { +export class LazyTokenizationSupport implements IDisposable, ILazyTokenizationSupport { private _tokenizationSupport: Promise | null = null; constructor(private readonly createSupport: () => Promise) { @@ -2140,7 +2148,7 @@ export class LazyTokenizationSupport implements IDisposable, ILazyTokenizationSu /** * @internal */ -export interface ITokenizationRegistry { +export interface ITokenizationRegistry { /** * An event triggered when: @@ -2158,24 +2166,24 @@ export interface ITokenizationRegistry { /** * Register a tokenization support. */ - register(languageId: string, support: ITokenizationSupport): IDisposable; + register(languageId: string, support: TSupport): IDisposable; /** * Register a tokenization support factory. */ - registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable; + registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable; /** * Get or create the tokenization support for a language. * Returns `null` if not found. */ - getOrCreate(languageId: string): Promise; + getOrCreate(languageId: string): Promise; /** * Get the tokenization support for a language. * Returns `null` if not found. */ - get(languageId: string): ITokenizationSupport | null; + get(languageId: string): TSupport | null; /** * Returns false if a factory is still pending. @@ -2195,8 +2203,12 @@ export interface ITokenizationRegistry { /** * @internal */ -export const TokenizationRegistry: ITokenizationRegistry = new TokenizationRegistryImpl(); +export const TokenizationRegistry: ITokenizationRegistry = new TokenizationRegistryImpl(); +/** + * @internal + */ +export const TreeSitterTokenizationRegistry: ITokenizationRegistry = new TokenizationRegistryImpl(); /** * @internal @@ -2256,6 +2268,10 @@ export interface MappedEditsContext { } export interface MappedEditsProvider { + /** + * @internal + */ + readonly displayName: string; // internal /** * Provider maps code blocks from the chat into a workspace edit. diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 97b5a483fc3..f960ac580e5 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -18,7 +18,6 @@ import { URI } from 'vs/base/common/uri'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { countEOL } from 'vs/editor/common/core/eolCounter'; import { normalizeIndentation } from 'vs/editor/common/core/indentation'; -import { LineRange } from 'vs/editor/common/core/lineRange'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -39,10 +38,12 @@ import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/ import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { SearchParams, TextModelSearch } from 'vs/editor/common/model/textModelSearch'; import { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart'; +import { AttachedViews } from 'vs/editor/common/model/tokens'; import { IBracketPairsTextModelPart } from 'vs/editor/common/textModelBracketPairs'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/textModelEvents'; import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; @@ -299,6 +300,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @ILanguageService private readonly _languageService: ILanguageService, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); @@ -327,13 +329,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._bracketPairs = this._register(new BracketPairsTextModelPart(this, this._languageConfigurationService)); this._guidesTextModelPart = this._register(new GuidesTextModelPart(this, this._languageConfigurationService)); this._decorationProvider = this._register(new ColorizedBracketPairsDecorationProvider(this)); - this._tokenizationTextModelPart = new TokenizationTextModelPart( - this._languageService, - this._languageConfigurationService, + this._tokenizationTextModelPart = this.instantiationService.createInstance(TokenizationTextModelPart, this, this._bracketPairs, languageId, - this._attachedViews, + this._attachedViews ); const bufferLineCount = this._buffer.getLineCount(); @@ -2528,43 +2528,3 @@ class DidChangeContentEmitter extends Disposable { this._slowEmitter.fire(e); } } - -/** - * @internal - */ -export class AttachedViews { - private readonly _onDidChangeVisibleRanges = new Emitter<{ view: model.IAttachedView; state: IAttachedViewState | undefined }>(); - public readonly onDidChangeVisibleRanges = this._onDidChangeVisibleRanges.event; - - private readonly _views = new Set(); - - public attachView(): model.IAttachedView { - const view = new AttachedViewImpl((state) => { - this._onDidChangeVisibleRanges.fire({ view, state }); - }); - this._views.add(view); - return view; - } - - public detachView(view: model.IAttachedView): void { - this._views.delete(view as AttachedViewImpl); - this._onDidChangeVisibleRanges.fire({ view, state: undefined }); - } -} - -/** - * @internal - */ -export interface IAttachedViewState { - readonly visibleLineRanges: readonly LineRange[]; - readonly stabilized: boolean; -} - -class AttachedViewImpl implements model.IAttachedView { - constructor(private readonly handleStateChange: (state: IAttachedViewState) => void) { } - - setVisibleLines(visibleLines: { startLineNumber: number; endLineNumber: number }[], stabilized: boolean): void { - const visibleLineRanges = visibleLines.map((line) => new LineRange(line.startLineNumber, line.endLineNumber + 1)); - this.handleStateChange({ visibleLineRanges, stabilized }); - } -} diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index 40c6c921afc..a20d85f76ea 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -3,12 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals } from 'vs/base/common/arrays'; -import { RunOnceScheduler } from 'vs/base/common/async'; import { CharCode } from 'vs/base/common/charCode'; import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableMap, MutableDisposable } from 'vs/base/common/lifecycle'; +import { DisposableMap, MutableDisposable } from 'vs/base/common/lifecycle'; import { countEOL } from 'vs/editor/common/core/eolCounter'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { IPosition, Position } from 'vs/editor/common/core/position'; @@ -20,9 +18,10 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILanguageConfigurationService, LanguageConfigurationServiceChangeEvent, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IAttachedView } from 'vs/editor/common/model'; import { BracketPairsTextModelPart } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl'; -import { AttachedViews, IAttachedViewState, TextModel } from 'vs/editor/common/model/textModel'; +import { TextModel } from 'vs/editor/common/model/textModel'; import { TextModelPart } from 'vs/editor/common/model/textModelPart'; import { DefaultBackgroundTokenizer, TokenizerWithStateStoreAndTextModel, TrackingTokenizationStateStore } from 'vs/editor/common/model/textModelTokens'; +import { AbstractTokens, AttachedViewHandler, AttachedViews } from 'vs/editor/common/model/tokens'; import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; import { BackgroundTokenizationState, ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens'; @@ -44,23 +43,23 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz private readonly _onDidChangeTokens: Emitter = this._register(new Emitter()); public readonly onDidChangeTokens: Event = this._onDidChangeTokens.event; - private readonly grammarTokens = this._register(new GrammarTokens(this._languageService.languageIdCodec, this._textModel, () => this._languageId, this._attachedViews)); + private readonly tokens = this._register(new GrammarTokens(this._languageService.languageIdCodec, this._textModel, () => this._languageId, this._attachedViews)); constructor( - private readonly _languageService: ILanguageService, - private readonly _languageConfigurationService: ILanguageConfigurationService, private readonly _textModel: TextModel, private readonly _bracketPairsTextModelPart: BracketPairsTextModelPart, private _languageId: string, private readonly _attachedViews: AttachedViews, + @ILanguageService private readonly _languageService: ILanguageService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(); - this._register(this.grammarTokens.onDidChangeTokens(e => { + this._register(this.tokens.onDidChangeTokens(e => { this._emitModelTokensChangedEvent(e); })); - this._register(this.grammarTokens.onDidChangeBackgroundTokenizationState(e => { + this._register(this.tokens.onDidChangeBackgroundTokenizationState(e => { this._bracketPairsTextModelPart.handleDidChangeBackgroundTokenizationState(); })); } @@ -94,11 +93,11 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz } } - this.grammarTokens.handleDidChangeContent(e); + this.tokens.handleDidChangeContent(e); } public handleDidChangeAttached(): void { - this.grammarTokens.handleDidChangeAttached(); + this.tokens.handleDidChangeAttached(); } /** @@ -106,7 +105,7 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz */ public getLineTokens(lineNumber: number): LineTokens { this.validateLineNumber(lineNumber); - const syntacticTokens = this.grammarTokens.getLineTokens(lineNumber); + const syntacticTokens = this.tokens.getLineTokens(lineNumber); return this._semanticTokens.addSparseTokens(lineNumber, syntacticTokens); } @@ -126,43 +125,43 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz } public get hasTokens(): boolean { - return this.grammarTokens.hasTokens; + return this.tokens.hasTokens; } public resetTokenization() { - this.grammarTokens.resetTokenization(); + this.tokens.resetTokenization(); } public get backgroundTokenizationState() { - return this.grammarTokens.backgroundTokenizationState; + return this.tokens.backgroundTokenizationState; } public forceTokenization(lineNumber: number): void { this.validateLineNumber(lineNumber); - this.grammarTokens.forceTokenization(lineNumber); + this.tokens.forceTokenization(lineNumber); } public hasAccurateTokensForLine(lineNumber: number): boolean { this.validateLineNumber(lineNumber); - return this.grammarTokens.hasAccurateTokensForLine(lineNumber); + return this.tokens.hasAccurateTokensForLine(lineNumber); } public isCheapToTokenize(lineNumber: number): boolean { this.validateLineNumber(lineNumber); - return this.grammarTokens.isCheapToTokenize(lineNumber); + return this.tokens.isCheapToTokenize(lineNumber); } public tokenizeIfCheap(lineNumber: number): void { this.validateLineNumber(lineNumber); - this.grammarTokens.tokenizeIfCheap(lineNumber); + this.tokens.tokenizeIfCheap(lineNumber); } public getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType { - return this.grammarTokens.getTokenTypeIfInsertingCharacter(lineNumber, column, character); + return this.tokens.getTokenTypeIfInsertingCharacter(lineNumber, column, character); } public tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null { - return this.grammarTokens.tokenizeLineWithEdit(position, length, newText); + return this.tokens.tokenizeLineWithEdit(position, length, newText); } // #endregion @@ -327,7 +326,7 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz this._languageId = languageId; this._bracketPairsTextModelPart.handleDidChangeLanguage(e); - this.grammarTokens.resetTokenization(); + this.tokens.resetTokenization(); this._onDidChangeLanguage.fire(e); this._onDidChangeLanguageConfiguration.fire({}); } @@ -335,7 +334,7 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz // #endregion } -class GrammarTokens extends Disposable { +class GrammarTokens extends AbstractTokens { private _tokenizer: TokenizerWithStateStoreAndTextModel | null = null; private _defaultBackgroundTokenizer: DefaultBackgroundTokenizer | null = null; private readonly _backgroundTokenizer = this._register(new MutableDisposable()); @@ -346,28 +345,15 @@ class GrammarTokens extends Disposable { private readonly _debugBackgroundTokenizer = this._register(new MutableDisposable()); - private _backgroundTokenizationState = BackgroundTokenizationState.InProgress; - public get backgroundTokenizationState(): BackgroundTokenizationState { - return this._backgroundTokenizationState; - } - - private readonly _onDidChangeBackgroundTokenizationState = this._register(new Emitter()); - /** @internal, should not be exposed by the text model! */ - public readonly onDidChangeBackgroundTokenizationState: Event = this._onDidChangeBackgroundTokenizationState.event; - - private readonly _onDidChangeTokens = this._register(new Emitter()); - /** @internal, should not be exposed by the text model! */ - public readonly onDidChangeTokens: Event = this._onDidChangeTokens.event; - private readonly _attachedViewStates = this._register(new DisposableMap()); constructor( - private readonly _languageIdCodec: ILanguageIdCodec, - private readonly _textModel: TextModel, - private getLanguageId: () => string, + languageIdCodec: ILanguageIdCodec, + textModel: TextModel, + getLanguageId: () => string, attachedViews: AttachedViews, ) { - super(); + super(languageIdCodec, textModel, getLanguageId); this._register(TokenizationRegistry.onDidChange((e) => { const languageId = this.getLanguageId(); @@ -587,12 +573,6 @@ class GrammarTokens extends Disposable { return this._tokenizer.isCheapToTokenize(lineNumber); } - public tokenizeIfCheap(lineNumber: number): void { - if (this.isCheapToTokenize(lineNumber)) { - this.forceTokenization(lineNumber); - } - } - public getLineTokens(lineNumber: number): LineTokens { const lineText = this._textModel.getLineContent(lineNumber); const result = this._tokens.getTokens( @@ -639,33 +619,3 @@ class GrammarTokens extends Disposable { return this._tokens.hasTokens; } } - -class AttachedViewHandler extends Disposable { - private readonly runner = this._register(new RunOnceScheduler(() => this.update(), 50)); - - private _computedLineRanges: readonly LineRange[] = []; - private _lineRanges: readonly LineRange[] = []; - public get lineRanges(): readonly LineRange[] { return this._lineRanges; } - - constructor(private readonly _refreshTokens: () => void) { - super(); - } - - private update(): void { - if (equals(this._computedLineRanges, this._lineRanges, (a, b) => a.equals(b))) { - return; - } - this._computedLineRanges = this._lineRanges; - this._refreshTokens(); - } - - public handleStateChange(state: IAttachedViewState): void { - this._lineRanges = state.visibleLineRanges; - if (state.stabilized) { - this.runner.cancel(); - this.update(); - } else { - this.runner.schedule(); - } - } -} diff --git a/src/vs/editor/common/model/tokens.ts b/src/vs/editor/common/model/tokens.ts new file mode 100644 index 00000000000..da46f267c6b --- /dev/null +++ b/src/vs/editor/common/model/tokens.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equals } from 'vs/base/common/arrays'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { IPosition } from 'vs/editor/common/core/position'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { ILanguageIdCodec } from 'vs/editor/common/languages'; +import { IAttachedView } from 'vs/editor/common/model'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { IModelContentChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; +import { BackgroundTokenizationState } from 'vs/editor/common/tokenizationTextModelPart'; +import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; + +/** + * @internal + */ +export class AttachedViews { + private readonly _onDidChangeVisibleRanges = new Emitter<{ view: IAttachedView; state: IAttachedViewState | undefined }>(); + public readonly onDidChangeVisibleRanges = this._onDidChangeVisibleRanges.event; + + private readonly _views = new Set(); + + public attachView(): IAttachedView { + const view = new AttachedViewImpl((state) => { + this._onDidChangeVisibleRanges.fire({ view, state }); + }); + this._views.add(view); + return view; + } + + public detachView(view: IAttachedView): void { + this._views.delete(view as AttachedViewImpl); + this._onDidChangeVisibleRanges.fire({ view, state: undefined }); + } +} + +/** + * @internal + */ +export interface IAttachedViewState { + readonly visibleLineRanges: readonly LineRange[]; + readonly stabilized: boolean; +} + +class AttachedViewImpl implements IAttachedView { + constructor(private readonly handleStateChange: (state: IAttachedViewState) => void) { } + + setVisibleLines(visibleLines: { startLineNumber: number; endLineNumber: number }[], stabilized: boolean): void { + const visibleLineRanges = visibleLines.map((line) => new LineRange(line.startLineNumber, line.endLineNumber + 1)); + this.handleStateChange({ visibleLineRanges, stabilized }); + } +} + + +export class AttachedViewHandler extends Disposable { + private readonly runner = this._register(new RunOnceScheduler(() => this.update(), 50)); + + private _computedLineRanges: readonly LineRange[] = []; + private _lineRanges: readonly LineRange[] = []; + public get lineRanges(): readonly LineRange[] { return this._lineRanges; } + + constructor(private readonly _refreshTokens: () => void) { + super(); + } + + private update(): void { + if (equals(this._computedLineRanges, this._lineRanges, (a, b) => a.equals(b))) { + return; + } + this._computedLineRanges = this._lineRanges; + this._refreshTokens(); + } + + public handleStateChange(state: IAttachedViewState): void { + this._lineRanges = state.visibleLineRanges; + if (state.stabilized) { + this.runner.cancel(); + this.update(); + } else { + this.runner.schedule(); + } + } +} + +export abstract class AbstractTokens extends Disposable { + protected _backgroundTokenizationState = BackgroundTokenizationState.InProgress; + public get backgroundTokenizationState(): BackgroundTokenizationState { + return this._backgroundTokenizationState; + } + + protected readonly _onDidChangeBackgroundTokenizationState = this._register(new Emitter()); + /** @internal, should not be exposed by the text model! */ + public readonly onDidChangeBackgroundTokenizationState: Event = this._onDidChangeBackgroundTokenizationState.event; + + protected readonly _onDidChangeTokens = this._register(new Emitter()); + /** @internal, should not be exposed by the text model! */ + public readonly onDidChangeTokens: Event = this._onDidChangeTokens.event; + + constructor( + protected readonly _languageIdCodec: ILanguageIdCodec, + protected readonly _textModel: TextModel, + protected getLanguageId: () => string, + ) { + super(); + } + + public abstract resetTokenization(fireTokenChangeEvent?: boolean): void; + + public abstract handleDidChangeAttached(): void; + + public abstract handleDidChangeContent(e: IModelContentChangedEvent): void; + + public abstract forceTokenization(lineNumber: number): void; + + public abstract hasAccurateTokensForLine(lineNumber: number): boolean; + + public abstract isCheapToTokenize(lineNumber: number): boolean; + + public tokenizeIfCheap(lineNumber: number): void { + if (this.isCheapToTokenize(lineNumber)) { + this.forceTokenization(lineNumber); + } + } + + public abstract getLineTokens(lineNumber: number): LineTokens; + + public abstract getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType; + + public abstract tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null; + + public abstract get hasTokens(): boolean; +} diff --git a/src/vs/editor/common/services/languageService.ts b/src/vs/editor/common/services/languageService.ts index 6d96f2a2502..f0a7f835069 100644 --- a/src/vs/editor/common/services/languageService.ts +++ b/src/vs/editor/common/services/languageService.ts @@ -11,6 +11,7 @@ import { ILanguageNameIdPair, ILanguageSelection, ILanguageService, ILanguageIco import { firstOrDefault } from 'vs/base/common/arrays'; import { ILanguageIdCodec, TokenizationRegistry } from 'vs/editor/common/languages'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IObservable, observableFromEvent } from 'vs/base/common/observable'; export class LanguageService extends Disposable implements ILanguageService { public _serviceBrand: undefined; @@ -150,51 +151,15 @@ export class LanguageService extends Disposable implements ILanguageService { } class LanguageSelection implements ILanguageSelection { + private readonly _value: IObservable; + public readonly onDidChange: Event; - public languageId: string; - - private _listener: IDisposable | null = null; - private _emitter: Emitter | null = null; - - constructor( - private readonly _onDidChangeLanguages: Event, - private readonly _selector: () => string - ) { - this.languageId = this._selector(); + constructor(onDidChangeLanguages: Event, selector: () => string) { + this._value = observableFromEvent(this, onDidChangeLanguages, () => selector()); + this.onDidChange = Event.fromObservable(this._value); } - private _dispose(): void { - if (this._listener) { - this._listener.dispose(); - this._listener = null; - } - if (this._emitter) { - this._emitter.dispose(); - this._emitter = null; - } - } - - public get onDidChange(): Event { - if (!this._listener) { - this._listener = this._onDidChangeLanguages(() => this._evaluate()); - } - if (!this._emitter) { - this._emitter = new Emitter({ - onDidRemoveLastListener: () => { - this._dispose(); - } - }); - } - return this._emitter.event; - } - - private _evaluate(): void { - const languageId = this._selector(); - if (languageId === this.languageId) { - // no change - return; - } - this.languageId = languageId; - this._emitter?.fire(this.languageId); + public get languageId(): string { + return this._value.get(); } } diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index 2bbda14a026..00019d6f961 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -14,7 +14,7 @@ import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults'; import { IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; -import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageSelection } from 'vs/editor/common/languages/language'; import { IModelService } from 'vs/editor/common/services/model'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -23,7 +23,7 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { isEditStackElement } from 'vs/editor/common/model/editStack'; import { Schemas } from 'vs/base/common/network'; import { equals } from 'vs/base/common/objects'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -107,8 +107,7 @@ export class ModelService extends Disposable implements IModelService { @IConfigurationService private readonly _configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly _resourcePropertiesService: ITextResourcePropertiesService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, - @ILanguageService private readonly _languageService: ILanguageService, - @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); this._modelCreationOptionsByLanguageAndResource = Object.create(null); @@ -314,14 +313,11 @@ export class ModelService extends Disposable implements IModelService { private _createModelData(value: string | ITextBufferFactory, languageIdOrSelection: string | ILanguageSelection, resource: URI | undefined, isForSimpleWidget: boolean): ModelData { // create & save the model const options = this.getCreationOptions(languageIdOrSelection, resource, isForSimpleWidget); - const model: TextModel = new TextModel( + const model: TextModel = this._instantiationService.createInstance(TextModel, value, languageIdOrSelection, options, - resource, - this._undoRedoService, - this._languageService, - this._languageConfigurationService, + resource ); if (resource && this._disposedModels.has(MODEL_ID(resource))) { const disposedModelData = this._removeDisposedModel(resource)!; diff --git a/src/vs/editor/common/services/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitterParserService.ts new file mode 100644 index 00000000000..e3e911efc49 --- /dev/null +++ b/src/vs/editor/common/services/treeSitterParserService.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ITreeSitterParserService = createDecorator('treeSitterParserService'); + +/** + * Currently this service just logs telemetry about how long it takes to parse files. + * Actual API will come later as we add features like syntax highlighting. + */ +export interface ITreeSitterParserService { + readonly _serviceBrand: undefined; +} diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index 81ffaa07895..9afbc5f6075 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -18,14 +18,18 @@ export namespace AccessibilityHelpNLS { export const auto_off = nls.localize("auto_off", "The application is configured to never be optimized for usage with a Screen Reader."); export const screenReaderModeEnabled = nls.localize("screenReaderModeEnabled", "Screen Reader Optimized Mode enabled."); export const screenReaderModeDisabled = nls.localize("screenReaderModeDisabled", "Screen Reader Optimized Mode disabled."); - export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior"); - export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior"); - export const stickScroll = nls.localize("stickScrollKb", "Focus Sticky Scroll to focus the currently nested scopes."); + export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior{0}.", ''); + export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior{0}", ''); + export const stickScroll = nls.localize("stickScrollKb", "Focus Sticky Scroll{0} to focus the currently nested scopes.", ''); export const showAccessibilityHelpAction = nls.localize("showAccessibilityHelpAction", "Show Accessibility Help"); export const listSignalSounds = nls.localize("listSignalSoundsCommand", "Run the command: List Signal Sounds for an overview of all sounds and their current status."); export const listAlerts = nls.localize("listAnnouncementsCommand", "Run the command: List Signal Announcements for an overview of announcements and their current status."); - export const quickChat = nls.localize("quickChatCommand", "Toggle quick chat to open or close a chat session.",); - export const startInlineChat = nls.localize("startInlineChatCommand", "Start inline chat to create an in editor chat session."); + export const quickChat = nls.localize("quickChatCommand", "Toggle quick chat{0} to open or close a chat session.", ''); + export const startInlineChat = nls.localize("startInlineChatCommand", "Start inline chat{0} to create an in editor chat session.", ''); + export const startDebugging = nls.localize('debug.startDebugging', "The Debug: Start Debugging command{0} will start a debug session.", ''); + export const setBreakpoint = nls.localize('debugConsole.setBreakpoint', "The Debug: Inline Breakpoint command{0} will set or unset a breakpoint at the current cursor position in the active editor.", ''); + export const addToWatch = nls.localize('debugConsole.addToWatch', "The Debug: Add to Watch command{0} will add the selected text to the watch view.", ''); + export const debugExecuteSelection = nls.localize('debugConsole.executeSelection', "The Debug: Execute Selection command{0} will execute the selected text in the debug console.", ''); } export namespace InspectTokensNLS { @@ -52,7 +56,6 @@ export namespace QuickOutlineNLS { export namespace StandaloneCodeEditorNLS { export const editorViewAccessibleLabel = nls.localize('editorViewAccessibleLabel', "Editor content"); - export const accessibilityHelpMessage = nls.localize('accessibilityHelpMessage', "Press Alt+F1 for Accessibility Options."); } export namespace ToggleHighContrastNLS { diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 58c720ac87c..7d63afec8e8 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -55,6 +55,9 @@ export interface IModelContentChange { * An event describing a change in the text of a model. */ export interface IModelContentChangedEvent { + /** + * The changes are ordered from the end of the document to the beginning, so they should be safe to apply in sequence. + */ readonly changes: IModelContentChange[]; /** * The (new) end-of-line character. diff --git a/src/vs/editor/common/tokenizationRegistry.ts b/src/vs/editor/common/tokenizationRegistry.ts index d9fb1bba82f..15ad1b85159 100644 --- a/src/vs/editor/common/tokenizationRegistry.ts +++ b/src/vs/editor/common/tokenizationRegistry.ts @@ -6,13 +6,13 @@ import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ITokenizationRegistry, ITokenizationSupport, ITokenizationSupportChangedEvent, ILazyTokenizationSupport } from 'vs/editor/common/languages'; +import { ITokenizationRegistry, ITokenizationSupportChangedEvent, ILazyTokenizationSupport } from 'vs/editor/common/languages'; import { ColorId } from 'vs/editor/common/encodedTokenAttributes'; -export class TokenizationRegistry implements ITokenizationRegistry { +export class TokenizationRegistry implements ITokenizationRegistry { - private readonly _tokenizationSupports = new Map(); - private readonly _factories = new Map(); + private readonly _tokenizationSupports = new Map(); + private readonly _factories = new Map>(); private readonly _onDidChange = new Emitter(); public readonly onDidChange: Event = this._onDidChange.event; @@ -30,7 +30,7 @@ export class TokenizationRegistry implements ITokenizationRegistry { }); } - public register(languageId: string, support: ITokenizationSupport): IDisposable { + public register(languageId: string, support: TSupport): IDisposable { this._tokenizationSupports.set(languageId, support); this.handleChange([languageId]); return toDisposable(() => { @@ -42,11 +42,11 @@ export class TokenizationRegistry implements ITokenizationRegistry { }); } - public get(languageId: string): ITokenizationSupport | null { + public get(languageId: string): TSupport | null { return this._tokenizationSupports.get(languageId) || null; } - public registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable { + public registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable { this._factories.get(languageId)?.dispose(); const myData = new TokenizationSupportFactoryData(this, languageId, factory); this._factories.set(languageId, myData); @@ -60,7 +60,7 @@ export class TokenizationRegistry implements ITokenizationRegistry { }); } - public async getOrCreate(languageId: string): Promise { + public async getOrCreate(languageId: string): Promise { // check first if the support is already set const tokenizationSupport = this.get(languageId); if (tokenizationSupport) { @@ -112,7 +112,7 @@ export class TokenizationRegistry implements ITokenizationRegistry { } } -class TokenizationSupportFactoryData extends Disposable { +class TokenizationSupportFactoryData extends Disposable { private _isDisposed: boolean = false; private _resolvePromise: Promise | null = null; @@ -123,9 +123,9 @@ class TokenizationSupportFactoryData extends Disposable { } constructor( - private readonly _registry: TokenizationRegistry, + private readonly _registry: TokenizationRegistry, private readonly _languageId: string, - private readonly _factory: ILazyTokenizationSupport, + private readonly _factory: ILazyTokenizationSupport, ) { super(); } diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts b/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts index 4a11f63060c..1dd22e4c494 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts @@ -45,3 +45,15 @@ Registry.as(Extensions.Configuration).registerConfigurat }, } }); + +Registry.as(Extensions.Configuration).registerConfiguration({ + ...editorConfigurationBaseNode, + properties: { + 'editor.codeActions.triggerOnFocusChange': { + type: 'boolean', + scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, + markdownDescription: nls.localize('triggerOnFocusChange', 'Enable triggering {0} when {1} is set to {2}. Code Actions must be set to {3} to be triggered for window and focus changes.', '`#editor.codeActionsOnSave#`', '`#files.autoSave#`', '`afterDelay`', '`always`'), + default: false, + }, + } +}); diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 612fd4d2923..5ac2deed516 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -157,11 +157,12 @@ export class CodeActionController extends Disposable implements IEditorContribut public hideLightBulbWidget(): void { this._lightBulbWidget.rawValue?.hide(); + this._lightBulbWidget.rawValue?.gutterHide(); } private async update(newState: CodeActionsState.State): Promise { if (newState.type !== CodeActionsState.Type.Triggered) { - this._lightBulbWidget.rawValue?.hide(); + this.hideLightBulbWidget(); return; } @@ -186,7 +187,7 @@ export class CodeActionController extends Disposable implements IEditorContribut const validActionToApply = this.tryGetValidActionToApply(newState.trigger, actions); if (validActionToApply) { try { - this._lightBulbWidget.value?.hide(); + this.hideLightBulbWidget(); await this._applyCodeAction(validActionToApply, false, false, ApplyCodeActionReason.FromCodeActions); } finally { actions.dispose(); diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css index cbadb2348ef..4961d4c92cb 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css @@ -43,3 +43,23 @@ opacity: 0.3; z-index: 1; } + +/* gutter decoration */ +.monaco-editor .glyph-margin-widgets .cgmr[class*="codicon-gutter-lightbulb"] { + display: block; + cursor: pointer; +} + +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb, +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-sparkle { + color: var(--vscode-editorLightBulb-foreground); +} + +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-auto-fix, +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-aifix-auto-fix { + color: var(--vscode-editorLightBulbAutoFix-foreground, var(--vscode-editorLightBulb-foreground)); +} + +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-sparkle-filled { + color: var(--vscode-editorLightBulbAi-foreground, var(--vscode-icon-foreground)); +} diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts index 45fc9aa9cb7..6bc8eb0c1b3 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts @@ -10,14 +10,24 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./lightBulbWidget'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; +import { GlyphMarginLane, IModelDecorationsChangeAccessor, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { computeIndentLevel } from 'vs/editor/common/model/utils'; import { autoFixCommandId, quickFixCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionSet, CodeActionTrigger } from 'vs/editor/contrib/codeAction/common/types'; import * as nls from 'vs/nls'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Range } from 'vs/editor/common/core/range'; + +const GUTTER_LIGHTBULB_ICON = registerIcon('gutter-lightbulb', Codicon.lightBulb, nls.localize('gutterLightbulbWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor.')); +const GUTTER_LIGHTBULB_AUTO_FIX_ICON = registerIcon('gutter-lightbulb-auto-fix', Codicon.lightbulbAutofix, nls.localize('gutterLightbulbAutoFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and a quick fix is available.')); +const GUTTER_LIGHTBULB_AIFIX_ICON = registerIcon('gutter-lightbulb-sparkle', Codicon.lightbulbSparkle, nls.localize('gutterLightbulbAIFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix is available.')); +const GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON = registerIcon('gutter-lightbulb-aifix-auto-fix', Codicon.lightbulbSparkleAutofix, nls.localize('gutterLightbulbAIFixAutoFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix and a quick fix is available.')); +const GUTTER_SPARKLE_FILLED_ICON = registerIcon('gutter-lightbulb-sparkle-filled', Codicon.sparkleFilled, nls.localize('gutterLightbulbSparkleFilledWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix and a quick fix is available.')); namespace LightBulbState { @@ -43,6 +53,14 @@ namespace LightBulbState { } export class LightBulbWidget extends Disposable implements IContentWidget { + private _gutterDecorationID: string | undefined; + + private static readonly GUTTER_DECORATION = ModelDecorationOptions.register({ + description: 'codicon-gutter-lightbulb-decoration', + glyphMarginClassName: ThemeIcon.asClassName(Codicon.lightBulb), + glyphMargin: { position: GlyphMarginLane.Left }, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + }); public static readonly ID = 'editor.contrib.lightbulbWidget'; @@ -54,11 +72,14 @@ export class LightBulbWidget extends Disposable implements IContentWidget { public readonly onClick = this._onClick.event; private _state: LightBulbState.State = LightBulbState.Hidden; + private _gutterState: LightBulbState.State = LightBulbState.Hidden; private _iconClasses: string[] = []; private _preferredKbLabel?: string; private _quickFixKbLabel?: string; + private gutterDecoration: ModelDecorationOptions = LightBulbWidget.GUTTER_DECORATION; + constructor( private readonly _editor: ICodeEditor, @IKeybindingService private readonly _keybindingService: IKeybindingService @@ -77,6 +98,10 @@ export class LightBulbWidget extends Disposable implements IContentWidget { if (this.state.type !== LightBulbState.Type.Showing || !editorModel || this.state.editorPosition.lineNumber >= editorModel.getLineCount()) { this.hide(); } + + if (this.gutterState.type !== LightBulbState.Type.Showing || !editorModel || this.gutterState.editorPosition.lineNumber >= editorModel.getLineCount()) { + this.gutterHide(); + } })); this._register(dom.addStandardDisposableGenericMouseDownListener(this._domNode, e => { @@ -119,14 +144,54 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._register(Event.runAndSubscribe(this._keybindingService.onDidUpdateKeybindings, () => { this._preferredKbLabel = this._keybindingService.lookupKeybinding(autoFixCommandId)?.getLabel() ?? undefined; this._quickFixKbLabel = this._keybindingService.lookupKeybinding(quickFixCommandId)?.getLabel() ?? undefined; - this._updateLightBulbTitleAndIcon(); })); + + this._register(this._editor.onMouseDown(async (e: IEditorMouseEvent) => { + const lightbulbClasses = [ + 'codicon-' + GUTTER_LIGHTBULB_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AUTO_FIX_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AIFIX_ICON.id, + 'codicon-' + GUTTER_SPARKLE_FILLED_ICON.id + ]; + + if (!e.target.element || !lightbulbClasses.some(cls => e.target.element && e.target.element.classList.contains(cls))) { + return; + } + + if (this.gutterState.type !== LightBulbState.Type.Showing) { + return; + } + + // Make sure that focus / cursor location is not lost when clicking widget icon + this._editor.focus(); + + // a bit of extra work to make sure the menu + // doesn't cover the line-text + const { top, height } = dom.getDomNodePagePosition(e.target.element); + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + + let pad = Math.floor(lineHeight / 3); + if (this.gutterState.widgetPosition.position !== null && this.gutterState.widgetPosition.position.lineNumber < this.gutterState.editorPosition.lineNumber) { + pad += lineHeight; + } + + this._onClick.fire({ + x: e.event.posx, + y: top + height + pad, + actions: this.gutterState.actions, + trigger: this.gutterState.trigger, + }); + })); } override dispose(): void { super.dispose(); this._editor.removeContentWidget(this); + if (this._gutterDecorationID) { + this._removeGutterDecoration(this._gutterDecorationID); + } } getId(): string { @@ -143,17 +208,20 @@ export class LightBulbWidget extends Disposable implements IContentWidget { public update(actions: CodeActionSet, trigger: CodeActionTrigger, atPosition: IPosition) { if (actions.validActions.length <= 0) { + this.gutterHide(); return this.hide(); } const options = this._editor.getOptions(); if (!options.get(EditorOption.lightbulb).enabled) { + this.gutterHide(); return this.hide(); } const model = this._editor.getModel(); if (!model) { + this.gutterHide(); return this.hide(); } @@ -171,7 +239,6 @@ export class LightBulbWidget extends Disposable implements IContentWidget { let effectiveLineNumber = lineNumber; let effectiveColumnNumber = 1; if (!lineHasSpace) { - // Checks if line is empty or starts with any amount of whitespace const isLineEmptyOrIndented = (lineNumber: number): boolean => { const lineContent = model.getLineContent(lineNumber); @@ -186,12 +253,37 @@ export class LightBulbWidget extends Disposable implements IContentWidget { const currLineEmptyOrIndented = isLineEmptyOrIndented(lineNumber); const notEmpty = !nextLineEmptyOrIndented && !prevLineEmptyOrIndented; - // check above and below. if both are blocked, display lightbulb below. - if (prevLineEmptyOrIndented || endLine || (notEmpty && !currLineEmptyOrIndented)) { + let hasDecoration = false; + const currLineDecorations = this._editor.getLineDecorations(lineNumber); + if (currLineDecorations) { + for (const decoration of currLineDecorations) { + if (decoration.options.glyphMarginClassName?.includes(Codicon.debugBreakpoint.id)) { + hasDecoration = true; + } + } + } + + // check above and below. if both are blocked, display lightbulb in the gutter. + if (!nextLineEmptyOrIndented && !prevLineEmptyOrIndented && !hasDecoration) { + this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, { + position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, + preference: LightBulbWidget._posPref + }); + this.renderGutterLightbub(); + return this.hide(); + } else if (prevLineEmptyOrIndented || endLine || (notEmpty && !currLineEmptyOrIndented)) { effectiveLineNumber -= 1; } else if (nextLineEmptyOrIndented || (notEmpty && currLineEmptyOrIndented)) { effectiveLineNumber += 1; } + } else if (lineNumber === 1 && (lineNumber === model.getLineCount() || !isLineEmptyOrIndented(lineNumber + 1) && !isLineEmptyOrIndented(lineNumber))) { + // special checks for first line blocked vs. not blocked. + this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, { + position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, + preference: LightBulbWidget._posPref + }); + this.renderGutterLightbub(); + return this.hide(); } else if ((lineNumber < model.getLineCount()) && !isFolded(lineNumber + 1)) { effectiveLineNumber += 1; } else if (column * fontInfo.spaceWidth < 22) { @@ -207,6 +299,11 @@ export class LightBulbWidget extends Disposable implements IContentWidget { preference: LightBulbWidget._posPref }); + if (this._gutterDecorationID) { + this._removeGutterDecoration(this._gutterDecorationID); + this.gutterHide(); + } + const validActions = actions.validActions; const actionKind = actions.validActions[0].action.kind; if (validActions.length !== 1 || !actionKind) { @@ -214,7 +311,6 @@ export class LightBulbWidget extends Disposable implements IContentWidget { return; } - this._editor.layoutContentWidget(this); } @@ -227,6 +323,18 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._editor.layoutContentWidget(this); } + public gutterHide(): void { + if (this.gutterState === LightBulbState.Hidden) { + return; + } + + if (this._gutterDecorationID) { + this._removeGutterDecoration(this._gutterDecorationID); + } + + this.gutterState = LightBulbState.Hidden; + } + private get state(): LightBulbState.State { return this._state; } private set state(value) { @@ -234,6 +342,13 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._updateLightBulbTitleAndIcon(); } + private get gutterState(): LightBulbState.State { return this._gutterState; } + + private set gutterState(value) { + this._gutterState = value; + this._updateGutterLightBulbTitleAndIcon(); + } + private _updateLightBulbTitleAndIcon(): void { this._domNode.classList.remove(...this._iconClasses); this._iconClasses = []; @@ -263,6 +378,74 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._domNode.classList.add(...this._iconClasses); } + private _updateGutterLightBulbTitleAndIcon(): void { + if (this.gutterState.type !== LightBulbState.Type.Showing) { + return; + } + let icon: ThemeIcon; + let autoRun = false; + if (this.gutterState.actions.allAIFixes) { + icon = GUTTER_SPARKLE_FILLED_ICON; + if (this.gutterState.actions.validActions.length === 1) { + autoRun = true; + } + } else if (this.gutterState.actions.hasAutoFix) { + if (this.gutterState.actions.hasAIFix) { + icon = GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON; + } else { + icon = GUTTER_LIGHTBULB_AUTO_FIX_ICON; + } + } else if (this.gutterState.actions.hasAIFix) { + icon = GUTTER_LIGHTBULB_AIFIX_ICON; + } else { + icon = GUTTER_LIGHTBULB_ICON; + } + this._updateLightbulbTitle(this.gutterState.actions.hasAutoFix, autoRun); + + const GUTTER_DECORATION = ModelDecorationOptions.register({ + description: 'codicon-gutter-lightbulb-decoration', + glyphMarginClassName: ThemeIcon.asClassName(icon), + glyphMargin: { position: GlyphMarginLane.Left }, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + }); + + this.gutterDecoration = GUTTER_DECORATION; + } + + /* Gutter Helper Functions */ + private renderGutterLightbub(): void { + const selection = this._editor.getSelection(); + if (!selection) { + return; + } + + if (this._gutterDecorationID === undefined) { + this._addGutterDecoration(selection.startLineNumber); + } else { + this._updateGutterDecoration(this._gutterDecorationID, selection.startLineNumber); + } + } + + private _addGutterDecoration(lineNumber: number) { + this._editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => { + this._gutterDecorationID = accessor.addDecoration(new Range(lineNumber, 0, lineNumber, 0), this.gutterDecoration); + }); + } + + private _removeGutterDecoration(decorationId: string) { + this._editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => { + accessor.removeDecoration(decorationId); + this._gutterDecorationID = undefined; + }); + } + + private _updateGutterDecoration(decorationId: string, lineNumber: number) { + this._editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => { + accessor.changeDecoration(decorationId, new Range(lineNumber, 0, lineNumber, 0)); + accessor.changeDecorationOptions(decorationId, this.gutterDecoration); + }); + } + private _updateLightbulbTitle(autoFix: boolean, autoRun: boolean): void { if (this.state.type !== LightBulbState.Type.Showing) { return; diff --git a/src/vs/editor/contrib/colorPicker/browser/colorContributions.ts b/src/vs/editor/contrib/colorPicker/browser/colorContributions.ts index e7642a17e7d..af1162debe5 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorContributions.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorContributions.ts @@ -11,7 +11,7 @@ import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ColorDecorationInjectedTextMarker } from 'vs/editor/contrib/colorPicker/browser/colorDetector'; import { ColorHoverParticipant } from 'vs/editor/contrib/colorPicker/browser/colorHoverParticipant'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; @@ -56,7 +56,7 @@ export class ColorContribution extends Disposable implements IEditorContribution return; } - const hoverController = this._editor.getContribution(HoverController.ID); + const hoverController = this._editor.getContribution(ContentHoverController.ID); if (!hoverController) { return; } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index ad79eb6a01f..3406894faa7 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -148,12 +148,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi return; } - if (platform.isWeb) { - // Explicitly clear the web resources clipboard. - // This is needed because on web, the browser clipboard is faked out using an in-memory store. - // This means the resources clipboard is not properly updated when copying from the editor. - this._clipboardService.writeResources([]); - } + // Explicitly clear the clipboard internal state. + // This is needed because on web, the browser clipboard is faked out using an in-memory store. + // This means the resources clipboard is not properly updated when copying from the editor. + this._clipboardService.clearInternalState?.(); if (!e.clipboardData || !this.isPasteAsEnabled()) { return; diff --git a/src/vs/editor/contrib/find/browser/findModel.ts b/src/vs/editor/contrib/find/browser/findModel.ts index a4611d37daf..a6958ce4970 100644 --- a/src/vs/editor/contrib/find/browser/findModel.ts +++ b/src/vs/editor/contrib/find/browser/findModel.ts @@ -97,7 +97,12 @@ export class FindModelBoundToEditorModel { this._decorations = new FindDecorations(editor); this._toDispose.add(this._decorations); - this._updateDecorationsScheduler = new RunOnceScheduler(() => this.research(false), 100); + this._updateDecorationsScheduler = new RunOnceScheduler(() => { + if (!this._editor.hasModel()) { + return; + } + return this.research(false); + }, 100); this._toDispose.add(this._updateDecorationsScheduler); this._toDispose.add(this._editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { diff --git a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts index 76b08cc63c2..63541f54790 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts @@ -253,7 +253,7 @@ export abstract class SymbolNavigationAction extends EditorAction2 { export class DefinitionAction extends SymbolNavigationAction { protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, position, token), nls.localize('def.title', 'Definitions')); + return new ReferencesModel(await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, position, false, token), nls.localize('def.title', 'Definitions')); } protected _getNoResultFoundMessage(info: IWordAtPosition | null): string { @@ -380,7 +380,7 @@ registerAction2(class PeekDefinitionAction extends DefinitionAction { class DeclarationAction extends SymbolNavigationAction { protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, position, token), nls.localize('decl.title', 'Declarations')); + return new ReferencesModel(await getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, position, false, token), nls.localize('decl.title', 'Declarations')); } protected _getNoResultFoundMessage(info: IWordAtPosition | null): string { @@ -467,7 +467,7 @@ registerAction2(class PeekDeclarationAction extends DeclarationAction { class TypeDefinitionAction extends SymbolNavigationAction { protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, position, token), nls.localize('typedef.title', 'Type Definitions')); + return new ReferencesModel(await getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, position, false, token), nls.localize('typedef.title', 'Type Definitions')); } protected _getNoResultFoundMessage(info: IWordAtPosition | null): string { @@ -553,7 +553,7 @@ registerAction2(class PeekTypeDefinitionAction extends TypeDefinitionAction { class ImplementationAction extends SymbolNavigationAction { protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, position, token), nls.localize('impl.title', 'Implementations')); + return new ReferencesModel(await getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, position, false, token), nls.localize('impl.title', 'Implementations')); } protected _getNoResultFoundMessage(info: IWordAtPosition | null): string { @@ -695,7 +695,7 @@ registerAction2(class GoToReferencesAction extends ReferencesAction { } protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, true, token), nls.localize('ref.title', 'References')); + return new ReferencesModel(await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, true, false, token), nls.localize('ref.title', 'References')); } }); @@ -723,7 +723,7 @@ registerAction2(class PeekReferencesAction extends ReferencesAction { } protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, false, token), nls.localize('ref.title', 'References')); + return new ReferencesModel(await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, false, false, token), nls.localize('ref.title', 'References')); } }); @@ -846,7 +846,7 @@ CommandsRegistry.registerCommand({ return undefined; } - const references = createCancelablePromise(token => getReferencesAtPosition(languageFeaturesService.referenceProvider, control.getModel(), corePosition.Position.lift(position), false, token).then(references => new ReferencesModel(references, nls.localize('ref.title', 'References')))); + const references = createCancelablePromise(token => getReferencesAtPosition(languageFeaturesService.referenceProvider, control.getModel(), corePosition.Position.lift(position), false, false, token).then(references => new ReferencesModel(references, nls.localize('ref.title', 'References')))); const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); return Promise.resolve(controller.toggleWidget(range, references, false)); }); diff --git a/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts b/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts index 738af806684..1477a394036 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts @@ -22,7 +22,7 @@ function shouldIncludeLocationLink(sourceModel: ITextModel, loc: LocationLink): } // Otherwise filter out locations from internal schemes - if (matchesSomeScheme(loc.uri, Schemas.walkThroughSnippet, Schemas.vscodeChatCodeBlock, Schemas.vscodeChatCodeCompareBlock, Schemas.vscodeCopilotBackingChatCodeBlock)) { + if (matchesSomeScheme(loc.uri, Schemas.walkThroughSnippet, Schemas.vscodeChatCodeBlock, Schemas.vscodeChatCodeCompareBlock)) { return false; } @@ -33,9 +33,10 @@ async function getLocationLinks( model: ITextModel, position: Position, registry: LanguageFeatureRegistry, + recursive: boolean, provide: (provider: T, model: ITextModel, position: Position) => ProviderResult ): Promise { - const provider = registry.ordered(model); + const provider = registry.ordered(model, recursive); // get results const promises = provider.map((provider): Promise => { @@ -49,32 +50,32 @@ async function getLocationLinks( return coalesce(values.flat()).filter(loc => shouldIncludeLocationLink(model, loc)); } -export function getDefinitionsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getLocationLinks(model, position, registry, (provider, model, position) => { +export function getDefinitionsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, recursive: boolean, token: CancellationToken): Promise { + return getLocationLinks(model, position, registry, recursive, (provider, model, position) => { return provider.provideDefinition(model, position, token); }); } -export function getDeclarationsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getLocationLinks(model, position, registry, (provider, model, position) => { +export function getDeclarationsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, recursive: boolean, token: CancellationToken): Promise { + return getLocationLinks(model, position, registry, recursive, (provider, model, position) => { return provider.provideDeclaration(model, position, token); }); } -export function getImplementationsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getLocationLinks(model, position, registry, (provider, model, position) => { +export function getImplementationsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, recursive: boolean, token: CancellationToken): Promise { + return getLocationLinks(model, position, registry, recursive, (provider, model, position) => { return provider.provideImplementation(model, position, token); }); } -export function getTypeDefinitionsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getLocationLinks(model, position, registry, (provider, model, position) => { +export function getTypeDefinitionsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, recursive: boolean, token: CancellationToken): Promise { + return getLocationLinks(model, position, registry, recursive, (provider, model, position) => { return provider.provideTypeDefinition(model, position, token); }); } -export function getReferencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, compact: boolean, token: CancellationToken): Promise { - return getLocationLinks(model, position, registry, async (provider, model, position) => { +export function getReferencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, compact: boolean, recursive: boolean, token: CancellationToken): Promise { + return getLocationLinks(model, position, registry, recursive, async (provider, model, position) => { const result = (await provider.provideReferences(model, position, { includeDeclaration: true }, token))?.filter(ref => shouldIncludeLocationLink(model, ref)); if (!compact || !result || result.length !== 2) { return result; @@ -99,30 +100,59 @@ async function _sortedAndDeduped(callback: () => Promise): Promi registerModelAndPositionCommand('_executeDefinitionProvider', (accessor, model, position) => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const promise = getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, position, CancellationToken.None); + const promise = getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, position, false, CancellationToken.None); + return _sortedAndDeduped(() => promise); +}); + +registerModelAndPositionCommand('_executeDefinitionProvider_recursive', (accessor, model, position) => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const promise = getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, position, true, CancellationToken.None); return _sortedAndDeduped(() => promise); }); registerModelAndPositionCommand('_executeTypeDefinitionProvider', (accessor, model, position) => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const promise = getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, position, CancellationToken.None); + const promise = getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, position, false, CancellationToken.None); + return _sortedAndDeduped(() => promise); +}); + +registerModelAndPositionCommand('_executeTypeDefinitionProvider_recursive', (accessor, model, position) => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const promise = getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, position, true, CancellationToken.None); return _sortedAndDeduped(() => promise); }); registerModelAndPositionCommand('_executeDeclarationProvider', (accessor, model, position) => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const promise = getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, position, CancellationToken.None); + const promise = getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, position, false, CancellationToken.None); + return _sortedAndDeduped(() => promise); +}); +registerModelAndPositionCommand('_executeDeclarationProvider_recursive', (accessor, model, position) => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const promise = getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, position, true, CancellationToken.None); return _sortedAndDeduped(() => promise); }); registerModelAndPositionCommand('_executeReferenceProvider', (accessor, model, position) => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const promise = getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, false, CancellationToken.None); + const promise = getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, false, false, CancellationToken.None); + return _sortedAndDeduped(() => promise); +}); + +registerModelAndPositionCommand('_executeReferenceProvider_recursive', (accessor, model, position) => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const promise = getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, false, true, CancellationToken.None); return _sortedAndDeduped(() => promise); }); registerModelAndPositionCommand('_executeImplementationProvider', (accessor, model, position) => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const promise = getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, position, CancellationToken.None); + const promise = getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, position, false, CancellationToken.None); + return _sortedAndDeduped(() => promise); +}); + +registerModelAndPositionCommand('_executeImplementationProvider_recursive', (accessor, model, position) => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const promise = getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, position, true, CancellationToken.None); return _sortedAndDeduped(() => promise); }); diff --git a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts index f5a08f94c1e..097de76f71e 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts @@ -297,7 +297,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri return Promise.resolve(null); } - return getDefinitionsAtPosition(this.languageFeaturesService.definitionProvider, model, position, token); + return getDefinitionsAtPosition(this.languageFeaturesService.definitionProvider, model, position, false, token); } private gotoDefinition(position: Position, openToSide: boolean): Promise { diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts index 0941671756d..6503b7df220 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts @@ -23,9 +23,7 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import { Location } from 'vs/editor/common/languages'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { AccessibilityProvider, DataSource, Delegate, FileReferencesRenderer, IdentityProvider, OneReferenceRenderer, StringRepresentationProvider, TreeElement } from 'vs/editor/contrib/gotoSymbol/browser/peek/referencesTree'; import * as peekView from 'vs/editor/contrib/peekView/browser/peekView'; @@ -35,7 +33,6 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkbenchAsyncDataTreeOptions, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { FileReferences, OneReference, ReferencesModel } from '../referencesModel'; class DecorationsManager implements IDisposable { @@ -224,10 +221,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { @IInstantiationService private readonly _instantiationService: IInstantiationService, @peekView.IPeekViewService private readonly _peekViewService: peekView.IPeekViewService, @ILabelService private readonly _uriLabel: ILabelService, - @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ILanguageService private readonly _languageService: ILanguageService, - @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, supportOnTitleClick: true }, _instantiationService); @@ -315,7 +309,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { }; this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, {}, this.editor); dom.hide(this._previewContainer); - this._previewNotAvailableMessage = new TextModel(nls.localize('missingPreviewMessage', "no preview available"), PLAINTEXT_LANGUAGE_ID, TextModel.DEFAULT_CREATION_OPTIONS, null, this._undoRedoService, this._languageService, this._languageConfigurationService); + this._previewNotAvailableMessage = this._instantiationService.createInstance(TextModel, nls.localize('missingPreviewMessage', "no preview available"), PLAINTEXT_LANGUAGE_ID, TextModel.DEFAULT_CREATION_OPTIONS, null); // tree this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); diff --git a/src/vs/editor/contrib/hover/browser/hoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController2.ts similarity index 80% rename from src/vs/editor/contrib/hover/browser/hoverController.ts rename to src/vs/editor/contrib/hover/browser/contentHoverController2.ts index b6ef34860ca..f2bc88729ae 100644 --- a/src/vs/editor/contrib/hover/browser/hoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController2.ts @@ -7,7 +7,7 @@ import { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution, IScrollEvent } from 'vs/editor/common/editorCommon'; @@ -19,10 +19,9 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import { HoverVerbosityAction } from 'vs/editor/common/languages'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { ContentHoverWidget } from 'vs/editor/contrib/hover/browser/contentHoverWidget'; -import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController'; +import { isMousePositionWithinElement } from 'vs/editor/contrib/hover/browser/hoverUtils'; +import { ContentHoverWidgetWrapper } from 'vs/editor/contrib/hover/browser/contentHoverWidgetWrapper'; import 'vs/css!./hover'; -import { MarginHoverWidget } from 'vs/editor/contrib/hover/browser/marginHoverWidget'; import { Emitter } from 'vs/base/common/event'; // sticky hover widget which doesn't disappear on focus out and such @@ -41,24 +40,18 @@ interface IHoverState { activatedByDecoratorClick: boolean; } -const enum HoverWidgetType { - Content, - Glyph, -} - -export class HoverController extends Disposable implements IEditorContribution { +export class ContentHoverController extends Disposable implements IEditorContribution { private readonly _onHoverContentsChanged = this._register(new Emitter()); public readonly onHoverContentsChanged = this._onHoverContentsChanged.event; - public static readonly ID = 'editor.contrib.hover'; + public static readonly ID = 'editor.contrib.contentHover'; public shouldKeepOpenOnEditorMouseMoveOrLeave: boolean = false; private readonly _listenersStore = new DisposableStore(); - private _glyphWidget: MarginHoverWidget | undefined; - private _contentWidget: ContentHoverController | undefined; + private _contentWidget: ContentHoverWidgetWrapper | undefined; private _mouseMoveEvent: IEditorMouseEvent | undefined; private _reactToEditorMouseMoveRunner: RunOnceScheduler; @@ -89,8 +82,8 @@ export class HoverController extends Disposable implements IEditorContribution { })); } - static get(editor: ICodeEditor): HoverController | null { - return editor.getContribution(HoverController.ID); + static get(editor: ICodeEditor): ContentHoverController | null { + return editor.getContribution(ContentHoverController.ID); } private _hookListeners(): void { @@ -149,30 +142,15 @@ export class HoverController extends Disposable implements IEditorContribution { } private _shouldNotHideCurrentHoverWidget(mouseEvent: IPartialEditorMouseEvent): boolean { - if ( - this._isMouseOnContentHoverWidget(mouseEvent) - || this._isMouseOnMarginHoverWidget(mouseEvent) - || this._isContentWidgetResizing() - ) { - return true; - } - return false; - } - - private _isMouseOnMarginHoverWidget(mouseEvent: IPartialEditorMouseEvent): boolean { - const target = mouseEvent.target; - if (!target) { - return false; - } - return target.type === MouseTargetType.OVERLAY_WIDGET && target.detail === MarginHoverWidget.ID; + return this._isMouseOnContentHoverWidget(mouseEvent) || this._isContentWidgetResizing(); } private _isMouseOnContentHoverWidget(mouseEvent: IPartialEditorMouseEvent): boolean { - const target = mouseEvent.target; - if (!target) { - return false; + const contentWidgetNode = this._contentWidget?.getDomNode(); + if (contentWidgetNode) { + return isMousePositionWithinElement(contentWidgetNode, mouseEvent.event.posx, mouseEvent.event.posy); } - return target.type === MouseTargetType.CONTENT_WIDGET && target.detail === ContentHoverWidget.ID; + return false; } private _onEditorMouseUp(): void { @@ -200,35 +178,25 @@ export class HoverController extends Disposable implements IEditorContribution { const isHoverSticky = this._hoverSettings.sticky; - const isMouseOnStickyMarginHoverWidget = (mouseEvent: IEditorMouseEvent, isHoverSticky: boolean) => { - const isMouseOnMarginHoverWidget = this._isMouseOnMarginHoverWidget(mouseEvent); - return isHoverSticky && isMouseOnMarginHoverWidget; - }; - const isMouseOnStickyContentHoverWidget = (mouseEvent: IEditorMouseEvent, isHoverSticky: boolean) => { + const isMouseOnStickyContentHoverWidget = (mouseEvent: IEditorMouseEvent, isHoverSticky: boolean): boolean => { const isMouseOnContentHoverWidget = this._isMouseOnContentHoverWidget(mouseEvent); return isHoverSticky && isMouseOnContentHoverWidget; }; - const isMouseOnColorPicker = (mouseEvent: IEditorMouseEvent) => { + const isMouseOnColorPicker = (mouseEvent: IEditorMouseEvent): boolean => { const isMouseOnContentHoverWidget = this._isMouseOnContentHoverWidget(mouseEvent); - const isColorPickerVisible = this._contentWidget?.isColorPickerVisible; + const isColorPickerVisible = this._contentWidget?.isColorPickerVisible ?? false; return isMouseOnContentHoverWidget && isColorPickerVisible; }; // TODO@aiday-mar verify if the following is necessary code - const isTextSelectedWithinContentHoverWidget = (mouseEvent: IEditorMouseEvent, sticky: boolean) => { - return sticky + const isTextSelectedWithinContentHoverWidget = (mouseEvent: IEditorMouseEvent, sticky: boolean): boolean => { + return (sticky && this._contentWidget?.containsNode(mouseEvent.event.browserEvent.view?.document.activeElement) - && !mouseEvent.event.browserEvent.view?.getSelection()?.isCollapsed; + && !mouseEvent.event.browserEvent.view?.getSelection()?.isCollapsed) ?? false; }; - if ( - isMouseOnStickyMarginHoverWidget(mouseEvent, isHoverSticky) - || isMouseOnStickyContentHoverWidget(mouseEvent, isHoverSticky) + return isMouseOnStickyContentHoverWidget(mouseEvent, isHoverSticky) || isMouseOnColorPicker(mouseEvent) - || isTextSelectedWithinContentHoverWidget(mouseEvent, isHoverSticky) - ) { - return true; - } - return false; + || isTextSelectedWithinContentHoverWidget(mouseEvent, isHoverSticky); } private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { @@ -294,44 +262,20 @@ export class HoverController extends Disposable implements IEditorContribution { return; } - const contentHoverShowsOrWillShow = this._tryShowHoverWidget(mouseEvent, HoverWidgetType.Content); + const contentHoverShowsOrWillShow = this._tryShowHoverWidget(mouseEvent); if (contentHoverShowsOrWillShow) { return; } - const glyphWidgetShowsOrWillShow = this._tryShowHoverWidget(mouseEvent, HoverWidgetType.Glyph); - if (glyphWidgetShowsOrWillShow) { - return; - } if (_sticky) { return; } this._hideWidgets(); } - private _tryShowHoverWidget(mouseEvent: IEditorMouseEvent, hoverWidgetType: HoverWidgetType): boolean { + private _tryShowHoverWidget(mouseEvent: IEditorMouseEvent): boolean { const contentWidget: IHoverWidget = this._getOrCreateContentWidget(); - const glyphWidget: IHoverWidget = this._getOrCreateGlyphWidget(); - let currentWidget: IHoverWidget; - let otherWidget: IHoverWidget; - switch (hoverWidgetType) { - case HoverWidgetType.Content: - currentWidget = contentWidget; - otherWidget = glyphWidget; - break; - case HoverWidgetType.Glyph: - currentWidget = glyphWidget; - otherWidget = contentWidget; - break; - default: - throw new Error(`HoverWidgetType ${hoverWidgetType} is unrecognized`); - } - - const showsOrWillShow = currentWidget.showsOrWillShow(mouseEvent); - if (showsOrWillShow) { - otherWidget.hide(); - } - return showsOrWillShow; + return contentWidget.showsOrWillShow(mouseEvent); } private _onKeyDown(e: IKeyboardEvent): void { @@ -379,25 +323,17 @@ export class HoverController extends Disposable implements IEditorContribution { return; } this._hoverState.activatedByDecoratorClick = false; - this._glyphWidget?.hide(); this._contentWidget?.hide(); } - private _getOrCreateContentWidget(): ContentHoverController { + private _getOrCreateContentWidget(): ContentHoverWidgetWrapper { if (!this._contentWidget) { - this._contentWidget = this._instantiationService.createInstance(ContentHoverController, this._editor); + this._contentWidget = this._instantiationService.createInstance(ContentHoverWidgetWrapper, this._editor); this._listenersStore.add(this._contentWidget.onContentsChanged(() => this._onHoverContentsChanged.fire())); } return this._contentWidget; } - private _getOrCreateGlyphWidget(): MarginHoverWidget { - if (!this._glyphWidget) { - this._glyphWidget = this._instantiationService.createInstance(MarginHoverWidget, this._editor); - } - return this._glyphWidget; - } - public hideContentHover(): void { this._hideWidgets(); } @@ -493,7 +429,6 @@ export class HoverController extends Disposable implements IEditorContribution { super.dispose(); this._unhookListeners(); this._listenersStore.dispose(); - this._glyphWidget?.dispose(); this._contentWidget?.dispose(); } } diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts similarity index 94% rename from src/vs/editor/contrib/hover/browser/contentHoverController.ts rename to src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts index d79984f24c1..492bce8b7e3 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts @@ -20,8 +20,9 @@ import { ContentHoverComputer } from 'vs/editor/contrib/hover/browser/contentHov import { HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; import { Emitter } from 'vs/base/common/event'; import { RenderedContentHover } from 'vs/editor/contrib/hover/browser/contentHoverRendered'; +import { isMousePositionWithinElement } from 'vs/editor/contrib/hover/browser/hoverUtils'; -export class ContentHoverController extends Disposable implements IHoverWidget { +export class ContentHoverWidgetWrapper extends Disposable implements IHoverWidget { private _currentResult: HoverResult | null = null; private _renderedContentHover: RenderedContentHover | undefined; @@ -69,11 +70,15 @@ export class ContentHoverController extends Disposable implements IHoverWidget { const messages = (result.hasLoadingMessage ? this._addLoadingMessage(result.value) : result.value); this._withResult(new HoverResult(this._computer.anchor, messages, result.isComplete)); })); - this._register(dom.addStandardDisposableListener(this._contentHoverWidget.getDomNode(), 'keydown', (e) => { + const contentHoverWidgetNode = this._contentHoverWidget.getDomNode(); + this._register(dom.addStandardDisposableListener(contentHoverWidgetNode, 'keydown', (e) => { if (e.equals(KeyCode.Escape)) { this.hide(); } })); + this._register(dom.addStandardDisposableListener(contentHoverWidgetNode, 'mouseleave', (e) => { + this._onMouseLeave(e); + })); this._register(TokenizationRegistry.onDidChange(() => { if (this._contentHoverWidget.position && this._currentResult) { this._setCurrentResult(this._currentResult); // render again @@ -281,6 +286,14 @@ export class ContentHoverController extends Disposable implements IHoverWidget { return anchorCandidates; } + private _onMouseLeave(e: MouseEvent): void { + const editorDomNode = this._editor.getDomNode(); + const isMousePositionOutsideOfEditor = !editorDomNode || !isMousePositionWithinElement(editorDomNode, e.x, e.y); + if (isMousePositionOutsideOfEditor) { + this.hide(); + } + } + public startShowingAtRange(range: Range, mode: HoverStartMode, source: HoverStartSource, focus: boolean): void { this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, source, focus, null); } @@ -363,6 +376,10 @@ export class ContentHoverController extends Disposable implements IHoverWidget { this._setCurrentResult(null); } + public getDomNode(): HTMLElement { + return this._contentHoverWidget.getDomNode(); + } + public get isColorPickerVisible(): boolean { return this._renderedContentHover?.isColorPickerVisible() ?? false; } diff --git a/src/vs/editor/contrib/hover/browser/getHover.ts b/src/vs/editor/contrib/hover/browser/getHover.ts index 608f8a1dc96..9fc1d78fb01 100644 --- a/src/vs/editor/contrib/hover/browser/getHover.ts +++ b/src/vs/editor/contrib/hover/browser/getHover.ts @@ -34,14 +34,14 @@ async function executeProvider(provider: HoverProvider, ordinal: number, model: return new HoverProviderResult(provider, result, ordinal); } -export function getHoverProviderResultsAsAsyncIterable(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): AsyncIterableObject { - const providers = registry.ordered(model); +export function getHoverProviderResultsAsAsyncIterable(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken, recursive = false): AsyncIterableObject { + const providers = registry.ordered(model, recursive); const promises = providers.map((provider, index) => executeProvider(provider, index, model, position, token)); return AsyncIterableObject.fromPromises(promises).coalesce(); } -export function getHoversPromise(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getHoverProviderResultsAsAsyncIterable(registry, model, position, token).map(item => item.hover).toPromise(); +export function getHoversPromise(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken, recursive = false): Promise { + return getHoverProviderResultsAsAsyncIterable(registry, model, position, token, recursive).map(item => item.hover).toPromise(); } registerModelAndPositionCommand('_executeHoverProvider', (accessor, model, position): Promise => { @@ -49,6 +49,11 @@ registerModelAndPositionCommand('_executeHoverProvider', (accessor, model, posit return getHoversPromise(languageFeaturesService.hoverProvider, model, position, CancellationToken.None); }); +registerModelAndPositionCommand('_executeHoverProvider_recursive', (accessor, model, position): Promise => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + return getHoversPromise(languageFeaturesService.hoverProvider, model, position, CancellationToken.None, true); +}); + function isValid(result: Hover) { const hasRange = (typeof result.range !== 'undefined'); const hasHtmlContent = typeof result.contents !== 'undefined' && result.contents && result.contents.length > 0; diff --git a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts index 41a7fbdfce9..05eff6acdb5 100644 --- a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts +++ b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; -import { AccessibleViewType, AccessibleViewProviderId, AdvancedContentProvider, IAccessibleViewContentProvider, IAccessibleViewOptions } from 'vs/platform/accessibility/browser/accessibleView'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; +import { AccessibleViewType, AccessibleViewProviderId, AccessibleContentProvider, IAccessibleViewContentProvider, IAccessibleViewOptions } from 'vs/platform/accessibility/browser/accessibleView'; import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IHoverService } from 'vs/platform/hover/browser/hover'; @@ -23,10 +23,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { labelForHoverVerbosityAction } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; namespace HoverAccessibilityHelpNLS { - export const introHoverPart = localize('introHoverPart', 'The focused hover part content is the following:'); - export const introHoverFull = localize('introHoverFull', 'The full focused hover content is the following:'); - export const increaseVerbosity = localize('increaseVerbosity', '- The focused hover part verbosity level can be increased with the Increase Hover Verbosity command.', INCREASE_HOVER_VERBOSITY_ACTION_ID); - export const decreaseVerbosity = localize('decreaseVerbosity', '- The focused hover part verbosity level can be decreased with the Decrease Hover Verbosity command.', DECREASE_HOVER_VERBOSITY_ACTION_ID); + export const increaseVerbosity = localize('increaseVerbosity', '- The focused hover part verbosity level can be increased with the Increase Hover Verbosity command.', ``); + export const decreaseVerbosity = localize('decreaseVerbosity', '- The focused hover part verbosity level can be decreased with the Decrease Hover Verbosity command.', ``); } export class HoverAccessibleView implements IAccessibleViewImplentation { @@ -36,25 +34,18 @@ export class HoverAccessibleView implements IAccessibleViewImplentation { public readonly name = 'hover'; public readonly when = EditorContextKeys.hoverFocused; - private _provider: HoverAccessibleViewProvider | undefined; - - getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined { + getProvider(accessor: ServicesAccessor): AccessibleContentProvider | undefined { const codeEditorService = accessor.get(ICodeEditorService); const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); if (!codeEditor) { throw new Error('No active or focused code editor'); } - const hoverController = HoverController.get(codeEditor); + const hoverController = ContentHoverController.get(codeEditor); if (!hoverController) { return; } const keybindingService = accessor.get(IKeybindingService); - this._provider = accessor.get(IInstantiationService).createInstance(HoverAccessibleViewProvider, keybindingService, codeEditor, hoverController); - return this._provider; - } - - dispose(): void { - this._provider?.dispose(); + return accessor.get(IInstantiationService).createInstance(HoverAccessibleViewProvider, keybindingService, codeEditor, hoverController); } } @@ -65,24 +56,18 @@ export class HoverAccessibilityHelp implements IAccessibleViewImplentation { public readonly type = AccessibleViewType.Help; public readonly when = EditorContextKeys.hoverVisible; - private _provider: HoverAccessibleViewProvider | undefined; - - getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined { + getProvider(accessor: ServicesAccessor): AccessibleContentProvider | undefined { const codeEditorService = accessor.get(ICodeEditorService); const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); if (!codeEditor) { throw new Error('No active or focused code editor'); } - const hoverController = HoverController.get(codeEditor); + const hoverController = ContentHoverController.get(codeEditor); if (!hoverController) { return; } return accessor.get(IInstantiationService).createInstance(HoverAccessibilityHelpProvider, hoverController); } - - dispose(): void { - this._provider?.dispose(); - } } abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAccessibleViewContentProvider { @@ -98,7 +83,7 @@ abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAc protected _focusedHoverPartIndex: number = -1; - constructor(protected readonly _hoverController: HoverController) { + constructor(protected readonly _hoverController: ContentHoverController) { super(); } @@ -124,7 +109,6 @@ abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAc } this._focusedHoverPartIndex = -1; this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = false; - this.dispose(); } provideContentAtIndex(focusedHoverIndex: number, includeVerbosityActions: boolean): string { @@ -137,18 +121,16 @@ abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAc if (includeVerbosityActions) { contents.push(...this._descriptionsOfVerbosityActionsForIndex(focusedHoverIndex)); } - contents.push(HoverAccessibilityHelpNLS.introHoverPart); contents.push(accessibleContent); - return contents.join('\n\n'); + return contents.join('\n'); } else { const accessibleContent = this._hoverController.getAccessibleWidgetContent(); if (accessibleContent === undefined) { return ''; } const contents: string[] = []; - contents.push(HoverAccessibilityHelpNLS.introHoverFull); contents.push(accessibleContent); - return contents.join('\n\n'); + return contents.join('\n'); } } @@ -183,7 +165,7 @@ export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvi public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; - constructor(hoverController: HoverController) { + constructor(hoverController: ContentHoverController) { super(hoverController); } @@ -199,7 +181,7 @@ export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider constructor( private readonly _keybindingService: IKeybindingService, private readonly _editor: ICodeEditor, - hoverController: HoverController, + hoverController: ContentHoverController, ) { super(hoverController); this._initializeOptions(this._editor, hoverController); @@ -239,7 +221,7 @@ export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider }); } - private _initializeOptions(editor: ICodeEditor, hoverController: HoverController): void { + private _initializeOptions(editor: ICodeEditor, hoverController: ContentHoverController): void { const helpProvider = this._register(new HoverAccessibilityHelpProvider(hoverController)); this.options.language = editor.getModel()?.getLanguageId(); this.options.customHelp = () => { return helpProvider.provideContentAtIndex(this._focusedHoverPartIndex, true); }; @@ -247,12 +229,11 @@ export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider } export class ExtHoverAccessibleView implements IAccessibleViewImplentation { - public readonly type = AccessibleViewType.View; public readonly priority = 90; public readonly name = 'extension-hover'; - getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined { + getProvider(accessor: ServicesAccessor): AccessibleContentProvider | undefined { const contextViewService = accessor.get(IContextViewService); const contextViewElement = contextViewService.getContextViewElement(); const extensionHoverContent = contextViewElement?.textContent ?? undefined; @@ -262,16 +243,14 @@ export class ExtHoverAccessibleView implements IAccessibleViewImplentation { // The accessible view, itself, uses the context view service to display the text. We don't want to read that. return; } - return { - id: AccessibleViewProviderId.Hover, - verbositySettingKey: 'accessibility.verbosity.hover', - provideContent() { return extensionHoverContent; }, - onClose() { + return new AccessibleContentProvider( + AccessibleViewProviderId.Hover, + { language: 'typescript', type: AccessibleViewType.View }, + () => { return extensionHoverContent; }, + () => { hoverService.showAndFocusLastHover(); }, - options: { language: 'typescript', type: AccessibleViewType.View } - }; + 'accessibility.verbosity.hover', + ); } - - dispose() { } } diff --git a/src/vs/editor/contrib/hover/browser/hoverActions.ts b/src/vs/editor/contrib/hover/browser/hoverActions.ts index 37eea40441a..5e48e53d6c9 100644 --- a/src/vs/editor/contrib/hover/browser/hoverActions.ts +++ b/src/vs/editor/contrib/hover/browser/hoverActions.ts @@ -14,7 +14,7 @@ import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/go import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; import { HoverVerbosityAction } from 'vs/editor/common/languages'; import * as nls from 'vs/nls'; import 'vs/css!./hover'; @@ -74,7 +74,7 @@ export class ShowOrFocusHoverAction extends EditorAction { return; } - const controller = HoverController.get(editor); + const controller = ContentHoverController.get(editor); if (!controller) { return; } @@ -128,7 +128,7 @@ export class ShowDefinitionPreviewHoverAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); + const controller = ContentHoverController.get(editor); if (!controller) { return; } @@ -176,7 +176,7 @@ export class ScrollUpHoverAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); + const controller = ContentHoverController.get(editor); if (!controller) { return; } @@ -209,7 +209,7 @@ export class ScrollDownHoverAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); + const controller = ContentHoverController.get(editor); if (!controller) { return; } @@ -242,7 +242,7 @@ export class ScrollLeftHoverAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); + const controller = ContentHoverController.get(editor); if (!controller) { return; } @@ -275,7 +275,7 @@ export class ScrollRightHoverAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); + const controller = ContentHoverController.get(editor); if (!controller) { return; } @@ -309,7 +309,7 @@ export class PageUpHoverAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); + const controller = ContentHoverController.get(editor); if (!controller) { return; } @@ -343,7 +343,7 @@ export class PageDownHoverAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); + const controller = ContentHoverController.get(editor); if (!controller) { return; } @@ -377,7 +377,7 @@ export class GoToTopHoverAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); + const controller = ContentHoverController.get(editor); if (!controller) { return; } @@ -412,7 +412,7 @@ export class GoToBottomHoverAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); + const controller = ContentHoverController.get(editor); if (!controller) { return; } @@ -432,7 +432,7 @@ export class IncreaseHoverVerbosityLevel extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void { - const hoverController = HoverController.get(editor); + const hoverController = ContentHoverController.get(editor); if (!hoverController) { return; } @@ -453,11 +453,11 @@ export class DecreaseHoverVerbosityLevel extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void { - const hoverController = HoverController.get(editor); + const hoverController = ContentHoverController.get(editor); if (!hoverController) { return; } const index = args?.index !== undefined ? args.index : hoverController.focusedHoverPartIndex(); - HoverController.get(editor)?.updateHoverVerbosityLevel(HoverVerbosityAction.Decrease, index, args?.focus); + ContentHoverController.get(editor)?.updateHoverVerbosityLevel(HoverVerbosityAction.Decrease, index, args?.focus); } } diff --git a/src/vs/editor/contrib/hover/browser/hoverContribution.ts b/src/vs/editor/contrib/hover/browser/hoverContribution.ts index bf24cdc1c69..2724c71ddec 100644 --- a/src/vs/editor/contrib/hover/browser/hoverContribution.ts +++ b/src/vs/editor/contrib/hover/browser/hoverContribution.ts @@ -10,12 +10,14 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; import { MarkerHoverParticipant } from 'vs/editor/contrib/hover/browser/markerHoverParticipant'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; +import { MarginHoverController } from 'vs/editor/contrib/hover/browser/marginHoverController'; import 'vs/css!./hover'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ExtHoverAccessibleView, HoverAccessibilityHelp, HoverAccessibleView } from 'vs/editor/contrib/hover/browser/hoverAccessibleViews'; -registerEditorContribution(HoverController.ID, HoverController, EditorContributionInstantiation.BeforeFirstInteraction); +registerEditorContribution(ContentHoverController.ID, ContentHoverController, EditorContributionInstantiation.BeforeFirstInteraction); +registerEditorContribution(MarginHoverController.ID, MarginHoverController, EditorContributionInstantiation.BeforeFirstInteraction); registerEditorAction(ShowOrFocusHoverAction); registerEditorAction(ShowDefinitionPreviewHoverAction); registerEditorAction(ScrollUpHoverAction); diff --git a/src/vs/editor/contrib/hover/browser/hoverUtils.ts b/src/vs/editor/contrib/hover/browser/hoverUtils.ts new file mode 100644 index 00000000000..3f9ab067c1d --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/hoverUtils.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; + +export function isMousePositionWithinElement(element: HTMLElement, posx: number, posy: number): boolean { + const elementRect = dom.getDomNodePagePosition(element); + if (posx < elementRect.left + || posx > elementRect.left + elementRect.width + || posy < elementRect.top + || posy > elementRect.top + elementRect.height) { + return false; + } + return true; +} diff --git a/src/vs/editor/contrib/hover/browser/marginHoverController.ts b/src/vs/editor/contrib/hover/browser/marginHoverController.ts new file mode 100644 index 00000000000..f478cd93b9e --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/marginHoverController.ts @@ -0,0 +1,232 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IEditorContribution, IScrollEvent } from 'vs/editor/common/editorCommon'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { isMousePositionWithinElement } from 'vs/editor/contrib/hover/browser/hoverUtils'; +import 'vs/css!./hover'; +import { MarginHoverWidget } from 'vs/editor/contrib/hover/browser/marginHoverWidget'; + +// sticky hover widget which doesn't disappear on focus out and such +const _sticky = false + // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this + ; + +interface IHoverSettings { + readonly enabled: boolean; + readonly sticky: boolean; + readonly hidingDelay: number; +} + +interface IHoverState { + mouseDown: boolean; +} + +export class MarginHoverController extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.marginHover'; + + public shouldKeepOpenOnEditorMouseMoveOrLeave: boolean = false; + + private readonly _listenersStore = new DisposableStore(); + + private _glyphWidget: MarginHoverWidget | undefined; + private _mouseMoveEvent: IEditorMouseEvent | undefined; + private _reactToEditorMouseMoveRunner: RunOnceScheduler; + + private _hoverSettings!: IHoverSettings; + private _hoverState: IHoverState = { + mouseDown: false + }; + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + this._reactToEditorMouseMoveRunner = this._register( + new RunOnceScheduler( + () => this._reactToEditorMouseMove(this._mouseMoveEvent), 0 + ) + ); + this._hookListeners(); + this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + if (e.hasChanged(EditorOption.hover)) { + this._unhookListeners(); + this._hookListeners(); + } + })); + } + + static get(editor: ICodeEditor): MarginHoverController | null { + return editor.getContribution(MarginHoverController.ID); + } + + private _hookListeners(): void { + + const hoverOpts = this._editor.getOption(EditorOption.hover); + this._hoverSettings = { + enabled: hoverOpts.enabled, + sticky: hoverOpts.sticky, + hidingDelay: hoverOpts.delay + }; + + if (hoverOpts.enabled) { + this._listenersStore.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e))); + this._listenersStore.add(this._editor.onMouseUp(() => this._onEditorMouseUp())); + this._listenersStore.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e))); + this._listenersStore.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e))); + } else { + this._listenersStore.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e))); + this._listenersStore.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e))); + } + + this._listenersStore.add(this._editor.onMouseLeave((e) => this._onEditorMouseLeave(e))); + this._listenersStore.add(this._editor.onDidChangeModel(() => { + this._cancelScheduler(); + this._hideWidgets(); + })); + this._listenersStore.add(this._editor.onDidChangeModelContent(() => this._cancelScheduler())); + this._listenersStore.add(this._editor.onDidScrollChange((e: IScrollEvent) => this._onEditorScrollChanged(e))); + } + + private _unhookListeners(): void { + this._listenersStore.clear(); + } + + private _cancelScheduler() { + this._mouseMoveEvent = undefined; + this._reactToEditorMouseMoveRunner.cancel(); + } + + private _onEditorScrollChanged(e: IScrollEvent): void { + if (e.scrollTopChanged || e.scrollLeftChanged) { + this._hideWidgets(); + } + } + + private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void { + this._hoverState.mouseDown = true; + const shouldNotHideCurrentHoverWidget = this._isMouseOnMarginHoverWidget(mouseEvent); + if (shouldNotHideCurrentHoverWidget) { + return; + } + this._hideWidgets(); + } + + private _isMouseOnMarginHoverWidget(mouseEvent: IPartialEditorMouseEvent): boolean { + const marginHoverWidgetNode = this._glyphWidget?.getDomNode(); + if (marginHoverWidgetNode) { + return isMousePositionWithinElement(marginHoverWidgetNode, mouseEvent.event.posx, mouseEvent.event.posy); + } + return false; + } + + private _onEditorMouseUp(): void { + this._hoverState.mouseDown = false; + } + + private _onEditorMouseLeave(mouseEvent: IPartialEditorMouseEvent): void { + if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) { + return; + } + + this._cancelScheduler(); + const shouldNotHideCurrentHoverWidget = this._isMouseOnMarginHoverWidget(mouseEvent); + if (shouldNotHideCurrentHoverWidget) { + return; + } + if (_sticky) { + return; + } + this._hideWidgets(); + } + + private _shouldNotRecomputeCurrentHoverWidget(mouseEvent: IEditorMouseEvent): boolean { + const isHoverSticky = this._hoverSettings.sticky; + const isMouseOnMarginHoverWidget = this._isMouseOnMarginHoverWidget(mouseEvent); + return isHoverSticky && isMouseOnMarginHoverWidget; + } + + private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { + if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) { + return; + } + + this._mouseMoveEvent = mouseEvent; + const shouldNotRecomputeCurrentHoverWidget = this._shouldNotRecomputeCurrentHoverWidget(mouseEvent); + if (shouldNotRecomputeCurrentHoverWidget) { + this._reactToEditorMouseMoveRunner.cancel(); + return; + } + this._reactToEditorMouseMove(mouseEvent); + } + + private _reactToEditorMouseMove(mouseEvent: IEditorMouseEvent | undefined): void { + + if (!mouseEvent) { + return; + } + const glyphWidgetShowsOrWillShow = this._tryShowHoverWidget(mouseEvent); + if (glyphWidgetShowsOrWillShow) { + return; + } + if (_sticky) { + return; + } + this._hideWidgets(); + } + + private _tryShowHoverWidget(mouseEvent: IEditorMouseEvent): boolean { + const glyphWidget: IHoverWidget = this._getOrCreateGlyphWidget(); + return glyphWidget.showsOrWillShow(mouseEvent); + } + + private _onKeyDown(e: IKeyboardEvent): void { + if (!this._editor.hasModel()) { + return; + } + if (e.keyCode === KeyCode.Ctrl + || e.keyCode === KeyCode.Alt + || e.keyCode === KeyCode.Meta + || e.keyCode === KeyCode.Shift) { + // Do not hide hover when a modifier key is pressed + return; + } + this._hideWidgets(); + } + + private _hideWidgets(): void { + if (_sticky) { + return; + } + this._glyphWidget?.hide(); + } + + private _getOrCreateGlyphWidget(): MarginHoverWidget { + if (!this._glyphWidget) { + this._glyphWidget = this._instantiationService.createInstance(MarginHoverWidget, this._editor); + } + return this._glyphWidget; + } + + public hideContentHover(): void { + this._hideWidgets(); + } + + public override dispose(): void { + super.dispose(); + this._unhookListeners(); + this._listenersStore.dispose(); + this._glyphWidget?.dispose(); + } +} diff --git a/src/vs/editor/contrib/hover/browser/marginHoverWidget.ts b/src/vs/editor/contrib/hover/browser/marginHoverWidget.ts index e909575d66e..e6fb9f4ce60 100644 --- a/src/vs/editor/contrib/hover/browser/marginHoverWidget.ts +++ b/src/vs/editor/contrib/hover/browser/marginHoverWidget.ts @@ -14,6 +14,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; import { IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { IHoverMessage, LaneOrLineNumber, MarginHoverComputer } from 'vs/editor/contrib/hover/browser/marginHoverComputer'; +import { isMousePositionWithinElement } from 'vs/editor/contrib/hover/browser/hoverUtils'; const $ = dom.$; @@ -34,8 +35,8 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget, IHo constructor( editor: ICodeEditor, - languageService: ILanguageService, - openerService: IOpenerService, + @ILanguageService languageService: ILanguageService, + @IOpenerService openerService: IOpenerService, ) { super(); this._editor = editor; @@ -59,7 +60,9 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget, IHo this._updateFont(); } })); - + this._register(dom.addStandardDisposableListener(this._hover.containerDomNode, 'mouseleave', (e) => { + this._onMouseLeave(e); + })); this._editor.addOverlayWidget(this); } @@ -181,4 +184,12 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget, IHo this._hover.containerDomNode.style.left = `${left}px`; this._hover.containerDomNode.style.top = `${Math.max(Math.round(top), 0)}px`; } + + private _onMouseLeave(e: MouseEvent): void { + const editorDomNode = this._editor.getDomNode(); + const isMousePositionOutsideOfEditor = !editorDomNode || !isMousePositionWithinElement(editorDomNode, e.x, e.y); + if (isMousePositionOutsideOfEditor) { + this.hide(); + } + } } diff --git a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts index 86dba6a80e3..033b85ce217 100644 --- a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts @@ -163,9 +163,10 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant{ selection: { startLineNumber, startColumn } } + editorOptions }).catch(onUnexpectedError); } })); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts index 6182681a3b5..a028db8d081 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts @@ -3,60 +3,78 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; -import { AccessibleViewType, AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewType, AccessibleViewProviderId, IAccessibleViewContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; -export class InlineCompletionsAccessibleView extends Disposable implements IAccessibleViewImplentation { +export class InlineCompletionsAccessibleView implements IAccessibleViewImplentation { readonly type = AccessibleViewType.View; readonly priority = 95; readonly name = 'inline-completions'; readonly when = ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible); getProvider(accessor: ServicesAccessor) { const codeEditorService = accessor.get(ICodeEditorService); - function resolveProvider() { - const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - if (!editor) { - return; - } - const model = InlineCompletionsController.get(editor)?.model.get(); - const state = model?.state.get(); - if (!model || !state) { - return; - } - const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); - const ghostText = state.primaryGhostText.renderForScreenReader(lineText); - if (!ghostText) { - return; - } - const language = editor.getModel()?.getLanguageId() ?? undefined; - return { - id: AccessibleViewProviderId.InlineCompletions, - verbositySettingKey: 'accessibility.verbosity.inlineCompletions', - provideContent() { return lineText + ghostText; }, - onClose() { - model.stop(); - editor.focus(); - }, - next() { - model.next(); - setTimeout(() => resolveProvider(), 50); - }, - previous() { - model.previous(); - setTimeout(() => resolveProvider(), 50); - }, - options: { language, type: AccessibleViewType.View } - }; + const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!editor) { + return; } - return resolveProvider(); - } - constructor() { - super(); + + const model = InlineCompletionsController.get(editor)?.model.get(); + if (!model?.state.get()) { + return; + } + + return new InlineCompletionsAccessibleViewContentProvider(editor, model); } } + +class InlineCompletionsAccessibleViewContentProvider extends Disposable implements IAccessibleViewContentProvider { + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + constructor( + private readonly _editor: ICodeEditor, + private readonly _model: InlineCompletionsModel, + ) { + super(); + } + + public readonly id = AccessibleViewProviderId.InlineCompletions; + public readonly verbositySettingKey = 'accessibility.verbosity.inlineCompletions'; + public readonly options = { language: this._editor.getModel()?.getLanguageId() ?? undefined, type: AccessibleViewType.View }; + + public provideContent(): string { + const state = this._model.state.get(); + if (!state) { + throw new Error('Inline completion is visible but state is not available'); + } + const lineText = this._model.textModel.getLineContent(state.primaryGhostText.lineNumber); + const ghostText = state.primaryGhostText.renderForScreenReader(lineText); + if (!ghostText) { + throw new Error('Inline completion is visible but ghost text is not available'); + } + return lineText + ghostText; + } + public provideNextContent(): string | undefined { + // asynchronously update the model and fire the event + this._model.next().then((() => this._onDidChangeContent.fire())); + return; + } + public providePreviousContent(): string | undefined { + // asynchronously update the model and fire the event + this._model.previous().then((() => this._onDidChangeContent.fire())); + return; + } + public onClose(): void { + this._model.stop(); + this._editor.focus(); + } +} + diff --git a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts index 9a4c3a927b4..0ded87779e8 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts @@ -16,7 +16,7 @@ import { InlineDecorationType } from 'vs/editor/common/viewModel'; import { AdditionalLinesWidget, LineData } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget'; import { GhostText } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { ColumnRange, applyObservableDecorations } from 'vs/editor/contrib/inlineCompletions/browser/utils'; -import { diffDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; +import { diffDeleteDecoration, diffLineDeleteDecorationBackgroundWithIndicator } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; export const INLINE_EDIT_DESCRIPTION = 'inline-edit'; export interface IGhostTextWidgetModel { @@ -163,17 +163,7 @@ export class GhostTextWidget extends Disposable { if (uiState.isSingleLine) { ranges.push(uiState.range); } - else if (uiState.isPureRemove) { - const lines = uiState.range.endLineNumber - uiState.range.startLineNumber; - for (let i = 0; i < lines; i++) { - const line = uiState.range.startLineNumber + i; - const firstNonWhitespace = uiState.targetTextModel.getLineFirstNonWhitespaceColumn(line); - const lastNonWhitespace = uiState.targetTextModel.getLineLastNonWhitespaceColumn(line); - const range = new Range(line, firstNonWhitespace, line, lastNonWhitespace); - ranges.push(range); - } - } - else { + else if (!uiState.isPureRemove) { const lines = uiState.range.endLineNumber - uiState.range.startLineNumber; for (let i = 0; i < lines; i++) { const line = uiState.range.startLineNumber + i; @@ -190,6 +180,15 @@ export class GhostTextWidget extends Disposable { }); } } + if (uiState.range && !uiState.isSingleLine && uiState.isPureRemove) { + const r = new Range(uiState.range.startLineNumber, 1, uiState.range.endLineNumber - 1, 1); + + decorations.push({ + range: r, + options: diffLineDeleteDecorationBackgroundWithIndicator + }); + + } for (const p of uiState.inlineTexts) { diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts index 4e231fd5162..5ef1693b265 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts @@ -53,9 +53,11 @@ export class InlineEditController extends Disposable { const textToDisplay = edit.text.endsWith('\n') && !(edit.range.startLineNumber === edit.range.endLineNumber && edit.range.startColumn === edit.range.endColumn) ? edit.text.slice(0, -1) : edit.text; const ghostText = new GhostText(line, [new GhostTextPart(column, textToDisplay, false)]); //only show ghost text for single line edits + //unless it is a pure removal //multi line edits are shown in the side by side widget const isSingleLine = edit.range.startLineNumber === edit.range.endLineNumber && ghostText.parts.length === 1 && ghostText.parts[0].lines.length === 1; - if (!isSingleLine) { + const isPureRemoval = edit.text === ''; + if (!isSingleLine && !isPureRemoval) { return undefined; } const instance = this.instantiationService.createInstance(GhostTextWidget, this.editor, { diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts index bd10e0669b4..777a8b15a20 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts @@ -14,7 +14,7 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/b import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; -import { diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; +import { diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineAddDecorationBackgroundWithIndicator, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -31,9 +31,14 @@ function* range(start: number, end: number, step = 1) { for (let n = start; n < end; n += step) { yield n; } } -function removeIndentation(lines: string[]): string[] { +function removeIndentation(lines: string[]) { const indentation = lines[0].match(/^\s*/)?.[0] ?? ''; - return lines.map(l => l.replace(new RegExp('^' + indentation), '')); + const length = indentation.length; + + return { + text: lines.map(l => l.replace(new RegExp('^' + indentation), '')), + shift: length + }; } type Pos = { @@ -53,7 +58,7 @@ export class InlineEditSideBySideWidget extends Disposable { if (!ghostText || ghostText.text.length === 0) { return null; } - if (ghostText.range.startLineNumber === ghostText.range.endLineNumber) { + if (ghostText.range.startLineNumber === ghostText.range.endLineNumber && !(ghostText.range.startColumn === ghostText.range.endColumn && ghostText.range.startColumn === 1)) { //for inner-line suggestions we still want to use minimal ghost text return null; } @@ -78,9 +83,13 @@ export class InlineEditSideBySideWidget extends Disposable { private readonly _text = derived(this, reader => { const ghostText = this._model.read(reader); if (!ghostText) { - return ''; + return { text: '', shift: 0 }; } - return removeIndentation(ghostText.text.split('\n')).join('\n'); + const t = removeIndentation(ghostText.text.split('\n')); + return { + text: t.text.join('\n'), + shift: t.shift + }; }); @@ -100,8 +109,8 @@ export class InlineEditSideBySideWidget extends Disposable { if (!editorModel) { return; } - const originalText = removeIndentation(editorModel.getValueInRange(ghostText.range).split('\n')).join('\n'); - const modifiedText = removeIndentation(ghostText.text.split('\n')).join('\n'); + const originalText = removeIndentation(editorModel.getValueInRange(ghostText.range).split('\n')).text.join('\n'); + const modifiedText = removeIndentation(ghostText.text.split('\n')).text.join('\n'); this._originalModel.get().setValue(originalText); this._modifiedModel.get().setValue(modifiedText); const d = this._diffProviderFactoryService.createDiffProvider({ diffAlgorithm: 'advanced' }); @@ -135,12 +144,15 @@ export class InlineEditSideBySideWidget extends Disposable { if (!model) { return; } - + if (this._position.get() === null) { + return; + } const contentWidget = store.add(this._instantiationService.createInstance( InlineEditSideBySideContentWidget, this._editor, this._position, - this._text, + this._text.map(t => t.text), + this._text.map(t => t.shift), this._diff )); _editor.addOverlayWidget(contentWidget); @@ -156,10 +168,9 @@ class InlineEditSideBySideContentWidget extends Disposable implements IOverlayWi private static id = 0; private readonly id = `InlineEditSideBySideContentWidget${InlineEditSideBySideContentWidget.id++}`; - public readonly allowEditorOverflow = true; - public readonly suppressMouseDown = false; + public readonly allowEditorOverflow = false; - private readonly _nodes = $('div.inlineEditSideBySide', undefined,); + private readonly _nodes = $('div.inlineEditSideBySide', undefined); private readonly _scrollChanged = observableSignalFromEvent('editor.onDidScrollChange', this._editor.onDidScrollChange); @@ -184,9 +195,15 @@ class InlineEditSideBySideContentWidget extends Disposable implements IOverlayWi overviewRulerLanes: 0, lineDecorationsWidth: 0, lineNumbersMinChars: 0, - scrollbar: { vertical: 'hidden', horizontal: 'hidden' }, + scrollbar: { vertical: 'hidden', horizontal: 'hidden', alwaysConsumeMouseWheel: false, handleMouseWheel: false }, + readOnly: true, + wordWrap: 'off', + wordWrapOverride1: 'off', + wordWrapOverride2: 'off', + wrappingIndent: 'none', + wrappingStrategy: undefined, }, - { contributions: [], }, + { contributions: [], isSimpleWidget: true }, this._editor )); @@ -221,9 +238,10 @@ class InlineEditSideBySideContentWidget extends Disposable implements IOverlayWi if (diff.length === 1 && diff[0].innerChanges![0].modifiedRange.equalsRange(this._previewTextModel.getFullModelRange())) { return { org: [], mod: [] }; } + const shift = this._shift.get(); const moveRange = (range: IRange) => { - return new Range(range.startLineNumber + position.top - 1, range.startColumn, range.endLineNumber + position.top - 1, range.endColumn); + return new Range(range.startLineNumber + position.top - 1, range.startColumn + shift, range.endLineNumber + position.top - 1, range.endColumn + shift); }; for (const m of diff) { @@ -231,7 +249,7 @@ class InlineEditSideBySideContentWidget extends Disposable implements IOverlayWi originalDecorations.push({ range: moveRange(m.original.toInclusiveRange()!), options: diffLineDeleteDecorationBackgroundWithIndicator }); } if (!m.modified.isEmpty) { - // modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffLineAddDecorationBackgroundWithIndicator }); + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffLineAddDecorationBackgroundWithIndicator }); } if (m.modified.isEmpty || m.original.isEmpty) { @@ -269,6 +287,7 @@ class InlineEditSideBySideContentWidget extends Disposable implements IOverlayWi private readonly _editor: ICodeEditor, private readonly _position: IObservable, private readonly _text: IObservable, + private readonly _shift: IObservable, private readonly _diff: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -282,12 +301,11 @@ class InlineEditSideBySideContentWidget extends Disposable implements IOverlayWi this._register(autorun(reader => { const width = this._previewEditorObs.contentWidth.read(reader); - const lines = this._text.get().split('\n').length - 1; + const lines = this._text.read(reader).split('\n').length - 1; const height = this._editor.getOption(EditorOption.lineHeight) * lines; if (width <= 0) { return; } - console.log('width', width); this._previewEditor.layout({ height: height, width: width }); })); @@ -304,16 +322,6 @@ class InlineEditSideBySideContentWidget extends Disposable implements IOverlayWi if (!position) { return; } - const visibleRanges = this._editor.getVisibleRanges(); - const isVisble = visibleRanges.some(range => { - return position.top >= range.startLineNumber && position.top <= range.endLineNumber; - }); - if (!isVisble) { - this._nodes.style.display = 'none'; - } - else { - this._nodes.style.display = 'block'; - } this._editor.layoutOverlayWidget(this); })); } @@ -334,8 +342,9 @@ class InlineEditSideBySideContentWidget extends Disposable implements IOverlayWi if (!visibPos) { return null; } - const top = visibPos.top; - const left = layoutInfo.contentLeft + this._editor.getOffsetForColumn(position.left.lineNumber, position.left.column) + 10; + const top = visibPos.top - 1; //-1 to offset the border width + const offset = this._editor.getOffsetForColumn(position.left.lineNumber, position.left.column); + const left = layoutInfo.contentLeft + offset + 10; return { preference: { left, diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css index 93758171dac..3efac6c122c 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css @@ -69,6 +69,10 @@ border-bottom: 1px solid var(--vscode-editorHoverWidget-border); } +.monaco-editor .parameter-hints-widget .code { + font-family: var(--vscode-parameterHintsWidget-editorFontFamily), var(--vscode-parameterHintsWidget-editorFontFamilyDefault); +} + .monaco-editor .parameter-hints-widget .docs { padding: 0 10px 0 5px; white-space: pre-wrap; diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts index 3036277c448..32546cff237 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts @@ -14,7 +14,7 @@ import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { assertIsDefined } from 'vs/base/common/types'; import 'vs/css!./parameterHints'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/editorOptions'; import * as languages from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -126,9 +126,13 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { if (!this.domNodes) { return; } + const fontInfo = this.editor.getOption(EditorOption.fontInfo); - this.domNodes.element.style.fontSize = `${fontInfo.fontSize}px`; - this.domNodes.element.style.lineHeight = `${fontInfo.lineHeight / fontInfo.fontSize}`; + const element = this.domNodes.element; + element.style.fontSize = `${fontInfo.fontSize}px`; + element.style.lineHeight = `${fontInfo.lineHeight / fontInfo.fontSize}`; + element.style.setProperty('--vscode-parameterHintsWidget-editorFontFamily', fontInfo.fontFamily); + element.style.setProperty('--vscode-parameterHintsWidget-editorFontFamilyDefault', EDITOR_FONT_DEFAULTS.fontFamily); }; updateFont(); @@ -203,10 +207,6 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } const code = dom.append(this.domNodes.signature, $('.code')); - const fontInfo = this.editor.getOption(EditorOption.fontInfo); - code.style.fontSize = `${fontInfo.fontSize}px`; - code.style.fontFamily = fontInfo.fontFamily; - const hasParameters = signature.parameters.length > 0; const activeParameterIndex = signature.activeParameter ?? hints.activeParameter; diff --git a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts index 05c099c63c4..c809937fa72 100644 --- a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts @@ -52,7 +52,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu //#region Provider methods - provide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { + provide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const disposables = new DisposableStore(); // Apply options if any @@ -78,7 +78,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu return disposables; } - private doProvide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { + private doProvide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const disposables = new DisposableStore(); // With text control @@ -134,12 +134,12 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu /** * Subclasses to implement to provide picks for the picker when an editor is active. */ - protected abstract provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable; + protected abstract provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable; /** * Subclasses to implement to provide picks for the picker when no editor is active. */ - protected abstract provideWithoutTextEditor(picker: IQuickPick, token: CancellationToken): IDisposable; + protected abstract provideWithoutTextEditor(picker: IQuickPick, token: CancellationToken): IDisposable; protected gotoLocation({ editor }: IQuickAccessTextEditorContext, options: { range: IRange; keyMods: IKeyMods; forceSideBySide?: boolean; preserveFocus?: boolean }): void { editor.setSelection(options.range, TextEditorSelectionSource.JUMP); diff --git a/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts index 6db58ae6cc0..b1ccfa95d51 100644 --- a/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts @@ -24,7 +24,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor super({ canAcceptInBackground: true }); } - protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { + protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { const label = localize('cannotRunGotoLine', "Open a text editor first to go to a line."); picker.items = [{ label }]; @@ -33,7 +33,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor return Disposable.None; } - protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable { + protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable { const editor = context.editor; const disposables = new DisposableStore(); diff --git a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts index d34aeb3faf5..9ff33f41802 100644 --- a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts @@ -63,13 +63,13 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit this.options.canAcceptInBackground = true; } - protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { + protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { this.provideLabelPick(picker, localize('cannotRunGotoSymbolWithoutEditor', "To go to a symbol, first open a text editor with symbol information.")); return Disposable.None; } - protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { + protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const editor = context.editor; const model = this.getModel(editor); if (!model) { @@ -87,7 +87,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return this.doProvideWithoutEditorSymbols(context, model, picker, token); } - private doProvideWithoutEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick, token: CancellationToken): IDisposable { + private doProvideWithoutEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick, token: CancellationToken): IDisposable { const disposables = new DisposableStore(); // Generic pick for not having any symbol information @@ -110,7 +110,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return disposables; } - private provideLabelPick(picker: IQuickPick, label: string): void { + private provideLabelPick(picker: IQuickPick, label: string): void { picker.items = [{ label, index: 0, kind: SymbolKind.String }]; picker.ariaLabel = label; } @@ -137,7 +137,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return symbolProviderRegistryPromise.p; } - private doProvideWithEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { + private doProvideWithEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const editor = context.editor; const disposables = new DisposableStore(); diff --git a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts index 86d3bf5d62d..9eb8d2f0006 100644 --- a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts +++ b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts @@ -14,6 +14,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { Range } from 'vs/editor/common/core/range'; import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ITextModel } from 'vs/editor/common/model'; import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; @@ -29,6 +30,7 @@ import { TestTextResourcePropertiesService } from 'vs/editor/test/common/service import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { NullLogService } from 'vs/platform/log/common/log'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { ColorScheme } from 'vs/platform/theme/common/theme'; @@ -50,12 +52,14 @@ suite('ModelSemanticColoring', () => { languageFeaturesService = new LanguageFeaturesService(); languageService = disposables.add(new LanguageService(false)); const semanticTokensStylingService = disposables.add(new SemanticTokensStylingService(themeService, logService, languageService)); + const instantiationService = new TestInstantiationService(); + instantiationService.set(ILanguageService, languageService); + instantiationService.set(ILanguageConfigurationService, new TestLanguageConfigurationService()); modelService = disposables.add(new ModelService( configService, new TestTextResourcePropertiesService(configService), new UndoRedoService(new TestDialogService(), new TestNotificationService()), - languageService, - new TestLanguageConfigurationService(), + instantiationService )); const envService = new class extends mock() { override isBuilt: boolean = true; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index 22b8c67a294..65824a12798 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -324,7 +324,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib let currentHTMLChild: HTMLElement; - getDefinitionsAtPosition(this._languageFeaturesService.definitionProvider, this._editor.getModel(), new Position(range.startLineNumber, range.startColumn + 1), cancellationToken.token).then((candidateDefinitions => { + getDefinitionsAtPosition(this._languageFeaturesService.definitionProvider, this._editor.getModel(), new Position(range.startLineNumber, range.startColumn + 1), false, cancellationToken.token).then((candidateDefinitions => { if (cancellationToken.token.isCancellationRequested) { return; } diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index 31eb7c7ea5f..0e29ea762bc 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -509,15 +509,14 @@ export class SuggestController implements IEditorContribution { // clear only now - after all tasks are done Promise.all(tasks).finally(() => { - this._reportSuggestionAcceptedTelemetry(item, model, isResolved, _commandExectionDuration, _additionalEditsAppliedAsync); + this._reportSuggestionAcceptedTelemetry(item, model, isResolved, _commandExectionDuration, _additionalEditsAppliedAsync, event.index); this.model.clear(); cts.dispose(); }); } - private _reportSuggestionAcceptedTelemetry(item: CompletionItem, model: ITextModel, itemResolved: boolean, commandExectionDuration: number, additionalEditsAppliedAsync: number) { - + private _reportSuggestionAcceptedTelemetry(item: CompletionItem, model: ITextModel, itemResolved: boolean, commandExectionDuration: number, additionalEditsAppliedAsync: number, index: number): void { if (Math.floor(Math.random() * 100) === 0) { // throttle telemetry event because accepting completions happens a lot return; @@ -529,6 +528,8 @@ export class SuggestController implements IEditorContribution { resolveInfo: number; resolveDuration: number; commandDuration: number; additionalEditsAsync: number; + index: number; + label: string; }; type AcceptedSuggestionClassification = { owner: 'jrieken'; @@ -543,6 +544,8 @@ export class SuggestController implements IEditorContribution { resolveDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How long resolving took to finish' }; commandDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How long a completion item command took' }; additionalEditsAsync: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Info about asynchronously applying additional edits' }; + index: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The order of the completion item in the list.' }; + label: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the completion item.' }; }; this._telemetryService.publicLog2('suggest.acceptedSuggestion', { @@ -555,7 +558,9 @@ export class SuggestController implements IEditorContribution { resolveInfo: !item.provider.resolveCompletionItem ? -1 : itemResolved ? 1 : 0, resolveDuration: item.resolveDuration, commandDuration: commandExectionDuration, - additionalEditsAsync: additionalEditsAppliedAsync + additionalEditsAsync: additionalEditsAppliedAsync, + index, + label: item.textLabel }); } diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 2d30e5925ab..39c98256f30 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -55,7 +55,8 @@ const enum State { Empty, Open, Frozen, - Details + Details, + onDetailsKeyDown } export interface ISelectedSuggestion { diff --git a/src/vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider.ts b/src/vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider.ts index 1d7a65695d3..adc31ecd8c0 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider.ts @@ -5,7 +5,7 @@ import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/core/wordHelper'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { DocumentHighlight, DocumentHighlightKind, MultiDocumentHighlightProvider, ProviderResult } from 'vs/editor/common/languages'; +import { DocumentHighlight, DocumentHighlightKind, DocumentHighlightProvider, MultiDocumentHighlightProvider, ProviderResult } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { Position } from 'vs/editor/common/core/position'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -14,10 +14,33 @@ import { ResourceMap } from 'vs/base/common/map'; import { LanguageFilter } from 'vs/editor/common/languageSelector'; -class TextualDocumentHighlightProvider implements MultiDocumentHighlightProvider { +class TextualDocumentHighlightProvider implements DocumentHighlightProvider, MultiDocumentHighlightProvider { selector: LanguageFilter = { language: '*' }; + provideDocumentHighlights(model: ITextModel, position: Position, token: CancellationToken): ProviderResult { + const result: DocumentHighlight[] = []; + + const word = model.getWordAtPosition({ + lineNumber: position.lineNumber, + column: position.column + }); + + if (!word) { + return Promise.resolve(result); + } + + if (model.isDisposed()) { + return; + } + + const matches = model.findMatches(word.word, true, false, true, USUAL_WORD_SEPARATORS, false); + return matches.map(m => ({ + range: m.range, + kind: DocumentHighlightKind.Text + })); + } + provideMultiDocumentHighlights(primaryModel: ITextModel, position: Position, otherModels: ITextModel[], token: CancellationToken): ProviderResult> { const result = new ResourceMap(); @@ -57,7 +80,7 @@ export class TextualMultiDocumentHighlightFeature extends Disposable { @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, ) { super(); - + this._register(languageFeaturesService.documentHighlightProvider.register('*', new TextualDocumentHighlightProvider())); this._register(languageFeaturesService.multiDocumentHighlightProvider.register('*', new TextualDocumentHighlightProvider())); } } diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index cae43742902..22bd3944e14 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as arrays from 'vs/base/common/arrays'; import { alert } from 'vs/base/browser/ui/aria/aria'; -import { CancelablePromise, createCancelablePromise, first, timeout } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, Delayer, first } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -23,7 +22,7 @@ import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/commo import { IDiffEditor, IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; -import { DocumentHighlight, DocumentHighlightKind, DocumentHighlightProvider, MultiDocumentHighlightProvider } from 'vs/editor/common/languages'; +import { DocumentHighlight, DocumentHighlightProvider, MultiDocumentHighlightProvider } from 'vs/editor/common/languages'; import { IModelDeltaDecoration, ITextModel, shouldSynchronizeModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { getHighlightDecorationOptions } from 'vs/editor/contrib/wordHighlighter/browser/highlightDecorations'; @@ -33,8 +32,9 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { Schemas } from 'vs/base/common/network'; import { ResourceMap } from 'vs/base/common/map'; import { score } from 'vs/editor/common/languageSelector'; -// import { TextualMultiDocumentHighlightFeature } from 'vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider'; -// import { registerEditorFeature } from 'vs/editor/common/editorFeatures'; +import { isEqual } from 'vs/base/common/resources'; +import { TextualMultiDocumentHighlightFeature } from 'vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider'; +import { registerEditorFeature } from 'vs/editor/common/editorFeatures'; const ctxHasWordHighlights = new RawContextKey('hasWordHighlights', false); @@ -43,11 +43,12 @@ export function getOccurrencesAtPosition(registry: LanguageFeatureRegistry(orderedByScore.map(provider => () => { return Promise.resolve(provider.provideDocumentHighlights(model, position, token)) .then(undefined, onUnexpectedExternalError); - }), arrays.isNonEmptyArray).then(result => { + }), (result): result is DocumentHighlight[] => result !== undefined && result !== null).then(result => { if (result) { const map = new ResourceMap(); map.set(model.uri, result); @@ -62,17 +63,17 @@ export function getOccurrencesAcrossMultipleModels(registry: LanguageFeatureRegi // in order of score ask the occurrences provider // until someone response with a good result - // (good = none empty array) + // (good = non undefined and non null ResourceMap) + // (result of size == 0 is valid, no highlights is a valid/expected result -- not a signal to fall back to other providers) return first | null | undefined>(orderedByScore.map(provider => () => { const filteredModels = otherModels.filter(otherModel => { return shouldSynchronizeModel(otherModel); }).filter(otherModel => { return score(provider.selector, otherModel.uri, otherModel.getLanguageId(), true, undefined, undefined) > 0; }); - return Promise.resolve(provider.provideMultiDocumentHighlights(model, position, filteredModels, token)) .then(undefined, onUnexpectedExternalError); - }), (t: ResourceMap | null | undefined): t is ResourceMap => t instanceof ResourceMap && t.size > 0); + }), (result): result is ResourceMap => result !== undefined && result !== null); } interface IOccurenceAtPositionRequest { @@ -183,76 +184,13 @@ class MultiModelOccurenceRequest extends OccurenceAtPositionRequest { } } -class TextualOccurenceRequest extends OccurenceAtPositionRequest { - - private readonly _otherModels: ITextModel[]; - private readonly _selectionIsEmpty: boolean; - private readonly _word: IWordAtPosition | null; - - constructor(model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string, otherModels: ITextModel[]) { - super(model, selection, wordSeparators); - this._otherModels = otherModels; - this._selectionIsEmpty = selection.isEmpty(); - this._word = word; - } - - protected _compute(model: ITextModel, selection: Selection, wordSeparators: string, token: CancellationToken): Promise> { - return timeout(250, token).then(() => { - const result = new ResourceMap(); - - let wordResult; - if (this._word) { - wordResult = this._word; - } else { - wordResult = model.getWordAtPosition(selection.getPosition()); - } - - if (!wordResult) { - return new ResourceMap(); - } - - const allModels = [model, ...this._otherModels]; - - for (const otherModel of allModels) { - if (otherModel.isDisposed()) { - continue; - } - - const matches = otherModel.findMatches(wordResult.word, true, false, true, wordSeparators, false); - const highlights = matches.map(m => ({ - range: m.range, - kind: DocumentHighlightKind.Text - })); - - if (highlights) { - result.set(otherModel.uri, highlights); - } - } - return result; - }); - } - - public override isValid(model: ITextModel, selection: Selection, decorations: IEditorDecorationsCollection): boolean { - const currentSelectionIsEmpty = selection.isEmpty(); - if (this._selectionIsEmpty !== currentSelectionIsEmpty) { - return false; - } - return super.isValid(model, selection, decorations); - } -} function computeOccurencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string): IOccurenceAtPositionRequest { - if (registry.has(model)) { - return new SemanticOccurenceAtPositionRequest(model, selection, wordSeparators, registry); - } - return new TextualOccurenceRequest(model, selection, word, wordSeparators, []); + return new SemanticOccurenceAtPositionRequest(model, selection, wordSeparators, registry); } function computeOccurencesMultiModel(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string, otherModels: ITextModel[]): IOccurenceAtPositionRequest { - if (registry.has(model)) { - return new MultiModelOccurenceRequest(model, selection, wordSeparators, registry, otherModels); - } - return new TextualOccurenceRequest(model, selection, word, wordSeparators, otherModels); + return new MultiModelOccurenceRequest(model, selection, wordSeparators, registry, otherModels); } registerModelAndPositionCommand('_executeDocumentHighlights', async (accessor, model, position) => { @@ -266,11 +204,11 @@ class WordHighlighter { private readonly editor: IActiveCodeEditor; private readonly providers: LanguageFeatureRegistry; private readonly multiDocumentProviders: LanguageFeatureRegistry; - private occurrencesHighlight: string; private readonly model: ITextModel; private readonly decorations: IEditorDecorationsCollection; private readonly toUnhook = new DisposableStore(); private readonly codeEditorService: ICodeEditorService; + private occurrencesHighlight: string; private workerRequestTokenId: number = 0; private workerRequest: IOccurenceAtPositionRequest | null; @@ -283,7 +221,9 @@ class WordHighlighter { private readonly _hasWordHighlights: IContextKey; private _ignorePositionChangeEvent: boolean; - private static storedDecorations: ResourceMap = new ResourceMap(); + private readonly runDelayer: Delayer = this.toUnhook.add(new Delayer(50)); + + private static storedDecorationIDs: ResourceMap = new ResourceMap(); private static query: IWordHighlighterQuery | null = null; constructor(editor: IActiveCodeEditor, providers: LanguageFeatureRegistry, multiProviders: LanguageFeatureRegistry, contextKeyService: IContextKeyService, @ICodeEditorService codeEditorService: ICodeEditorService) { @@ -307,7 +247,7 @@ class WordHighlighter { return; } - this._onPositionChanged(e); + this.runDelayer.trigger(() => { this._onPositionChanged(e); }); })); this.toUnhook.add(editor.onDidFocusEditorText((e) => { if (this.occurrencesHighlight === 'off') { @@ -316,7 +256,7 @@ class WordHighlighter { } if (!this.workerRequest) { - this._run(); + this.runDelayer.trigger(() => { this._run(); }); } })); this.toUnhook.add(editor.onDidChangeModelContent((e) => { @@ -335,7 +275,22 @@ class WordHighlighter { const newValue = this.editor.getOption(EditorOption.occurrencesHighlight); if (this.occurrencesHighlight !== newValue) { this.occurrencesHighlight = newValue; - this._stopAll(); + switch (newValue) { + case 'off': + this._stopAll(); + break; + case 'singleFile': + this._stopAll(WordHighlighter.query?.modelInfo?.model); + break; + case 'multiFile': + if (WordHighlighter.query) { + this._run(true); + } + break; + default: + console.warn('Unknown occurrencesHighlight setting value:', newValue); + break; + } } })); @@ -423,13 +378,13 @@ class WordHighlighter { return; } - const currentDecorationIDs = WordHighlighter.storedDecorations.get(this.editor.getModel().uri); + const currentDecorationIDs = WordHighlighter.storedDecorationIDs.get(this.editor.getModel().uri); if (!currentDecorationIDs) { return; } this.editor.removeDecorations(currentDecorationIDs); - WordHighlighter.storedDecorations.delete(this.editor.getModel().uri); + WordHighlighter.storedDecorationIDs.delete(this.editor.getModel().uri); if (this.decorations.length > 0) { this.decorations.clear(); @@ -437,16 +392,16 @@ class WordHighlighter { } } - private _removeAllDecorations(): void { + private _removeAllDecorations(preservedModel?: ITextModel): void { const currentEditors = this.codeEditorService.listCodeEditors(); const deleteURI = []; // iterate over editors and store models in currentModels for (const editor of currentEditors) { - if (!editor.hasModel()) { + if (!editor.hasModel() || isEqual(editor.getModel().uri, preservedModel?.uri)) { continue; } - const currentDecorationIDs = WordHighlighter.storedDecorations.get(editor.getModel().uri); + const currentDecorationIDs = WordHighlighter.storedDecorationIDs.get(editor.getModel().uri); if (!currentDecorationIDs) { continue; } @@ -467,7 +422,7 @@ class WordHighlighter { } for (const uri of deleteURI) { - WordHighlighter.storedDecorations.delete(uri); + WordHighlighter.storedDecorationIDs.delete(uri); } } @@ -505,11 +460,11 @@ class WordHighlighter { } } - private _stopAll() { + private _stopAll(preservedModel?: ITextModel): void { // Remove any existing decorations // TODO: @Yoyokrazy -- this triggers as notebooks scroll, causing highlights to disappear momentarily. // maybe a nb type check? - this._removeAllDecorations(); + this._removeAllDecorations(preservedModel); // Cancel any renderDecorationsTimer if (this.renderDecorationsTimer !== -1) { @@ -623,13 +578,14 @@ class WordHighlighter { return currentModels; } - private _run(): void { + private _run(multiFileConfigChange?: boolean): void { let workerRequestIsValid; const hasTextFocus = this.editor.hasTextFocus(); if (!hasTextFocus) { // new nb cell scrolled in, didChangeModel fires if (!WordHighlighter.query) { // no previous query, nothing to highlight off of + this._stopAll(); return; } } else { // has text focus @@ -689,10 +645,22 @@ class WordHighlighter { this.renderDecorationsTimer = -1; this._beginRenderDecorations(); } - } else { + } else if (isEqual(this.editor.getModel().uri, WordHighlighter.query.modelInfo?.model.uri)) { // only trigger new worker requests from the primary model that initiated the query // case d) - // Stop all previous actions and start fresh - this._stopAll(); + + // check if the new queried word is contained in the range of a stored decoration for this model + if (!multiFileConfigChange) { + const currentModelDecorationRanges = this.decorations.getRanges(); + for (const storedRange of currentModelDecorationRanges) { + if (storedRange.containsPosition(this.editor.getPosition())) { + return; + } + } + } + + // stop all previous actions if new word is highlighted + // if we trigger the run off a setting change -> multifile highlighting, we do not want to remove decorations from this model + this._stopAll(multiFileConfigChange ? this.model : undefined); const myRequestId = ++this.workerRequestTokenId; this.workerRequestCompleted = false; @@ -703,7 +671,7 @@ class WordHighlighter { // 1) we have text focus, and a valid query was updated. // 2) we do not have text focus, and a valid query is cached. // the query will ALWAYS have the correct data for the current highlight request, so it can always be passed to the workerRequest safely - if (!WordHighlighter.query.modelInfo || WordHighlighter.query.modelInfo.model.isDisposed()) { + if (!WordHighlighter.query || !WordHighlighter.query.modelInfo || WordHighlighter.query.modelInfo.model.isDisposed()) { return; } this.workerRequest = this.computeWithModel(WordHighlighter.query.modelInfo.model, WordHighlighter.query.modelInfo.selection, WordHighlighter.query.word, otherModelsToHighlight); @@ -757,7 +725,7 @@ class WordHighlighter { const newDecorations: IModelDeltaDecoration[] = []; const uri = editor.getModel()?.uri; if (uri && this.workerRequestValue.has(uri)) { - const oldDecorationIDs: string[] | undefined = WordHighlighter.storedDecorations.get(uri); + const oldDecorationIDs: string[] | undefined = WordHighlighter.storedDecorationIDs.get(uri); const newDocumentHighlights = this.workerRequestValue.get(uri); if (newDocumentHighlights) { for (const highlight of newDocumentHighlights) { @@ -775,7 +743,7 @@ class WordHighlighter { editor.changeDecorations((changeAccessor) => { newDecorationIDs = changeAccessor.deltaDecorations(oldDecorationIDs ?? [], newDecorations); }); - WordHighlighter.storedDecorations = WordHighlighter.storedDecorations.set(uri, newDecorationIDs); + WordHighlighter.storedDecorationIDs = WordHighlighter.storedDecorationIDs.set(uri, newDecorationIDs); if (newDecorations.length > 0) { editorHighlighterContrib.wordHighlighter?.decorations.set(newDecorations); @@ -942,4 +910,4 @@ registerEditorContribution(WordHighlighterContribution.ID, WordHighlighterContri registerEditorAction(NextWordHighlightAction); registerEditorAction(PrevWordHighlightAction); registerEditorAction(TriggerWordHighlightAction); -// registerEditorFeature(TextualMultiDocumentHighlightFeature); +registerEditorFeature(TextualMultiDocumentHighlightFeature); diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index e251afa2543..9700eed5a97 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts @@ -111,7 +111,7 @@ export class StandaloneQuickInputService implements IQuickInputService { ) { } - pick>(picks: Promise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { + pick>(picks: Promise[]> | QuickPickInput[], options?: O, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { return (this.activeService as unknown as QuickInputController /* TS fail */).pick(picks, options, token); } @@ -119,8 +119,10 @@ export class StandaloneQuickInputService implements IQuickInputService { return this.activeService.input(options, token); } - createQuickPick(): IQuickPick { - return this.activeService.createQuickPick(); + createQuickPick(options: { useSeparators: true }): IQuickPick; + createQuickPick(options?: { useSeparators: boolean }): IQuickPick; + createQuickPick(options: { useSeparators: boolean } = { useSeparators: false }): IQuickPick { + return this.activeService.createQuickPick(options); } createInputBox(): IInputBox { diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 84deb53c57d..3ccfa128dc2 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -283,7 +283,6 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon ) { const options = { ..._options }; options.ariaLabel = options.ariaLabel || StandaloneCodeEditorNLS.editorViewAccessibleLabel; - options.ariaLabel = options.ariaLabel + ';' + (StandaloneCodeEditorNLS.accessibilityHelpMessage); super(domElement, options, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService); if (keybindingService instanceof StandaloneKeybindingService) { diff --git a/src/vs/editor/standalone/test/browser/monarch.test.ts b/src/vs/editor/standalone/test/browser/monarch.test.ts index fbe58b62169..7a1ba1d9d75 100644 --- a/src/vs/editor/standalone/test/browser/monarch.test.ts +++ b/src/vs/editor/standalone/test/browser/monarch.test.ts @@ -233,7 +233,7 @@ suite('Monarch', () => { uselessReplaceKey2: '@uselessReplaceKey3', uselessReplaceKey3: '@uselessReplaceKey4', uselessReplaceKey4: '@uselessReplaceKey5', - uselessReplaceKey5: '@ham' || '', + uselessReplaceKey5: '@ham', tokenizer: { root: [ { diff --git a/src/vs/editor/test/browser/services/testTreeSitterService.ts b/src/vs/editor/test/browser/services/testTreeSitterService.ts new file mode 100644 index 00000000000..f449962d6cd --- /dev/null +++ b/src/vs/editor/test/browser/services/testTreeSitterService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AppResourcePath } from 'vs/base/common/network'; +import type { Parser } from '@vscode/tree-sitter-wasm'; +import { ITextModel } from 'vs/editor/common/model'; +import { ITreeSitterParserService } from 'vs/editor/common/services/treeSitterParserService'; + +export class TestTreeSitterParserService implements ITreeSitterParserService { + getLanguage(model: ITextModel): Parser.Language | undefined { + throw new Error('Method not implemented.'); + } + getLanguageLocation(languageId: string): AppResourcePath { + throw new Error('Method not implemented.'); + } + readonly _serviceBrand: undefined; + + public initTreeSitter(): Promise { + return Promise.resolve(); + } + + public getTree(_model: ITextModel): Parser.Tree | undefined { + return undefined; + } +} diff --git a/src/vs/editor/test/browser/services/treeSitterParserService.test.ts b/src/vs/editor/test/browser/services/treeSitterParserService.test.ts new file mode 100644 index 00000000000..65598ee1873 --- /dev/null +++ b/src/vs/editor/test/browser/services/treeSitterParserService.test.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TextModelTreeSitter, TreeSitterImporter, TreeSitterLanguages } from 'vs/editor/browser/services/treeSitter/treeSitterParserService'; +import type { Parser } from '@vscode/tree-sitter-wasm'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { timeout } from 'vs/base/common/async'; +import { ConsoleMainLogger, ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { LogService } from 'vs/platform/log/common/logService'; +import { mock } from 'vs/base/test/common/mock'; + +class MockParser implements Parser { + static async init(): Promise { } + delete(): void { } + parse(input: string | Parser.Input, oldTree?: Parser.Tree, options?: Parser.Options): Parser.Tree { + return new MockTree(); + } + getIncludedRanges(): Parser.Range[] { + return []; + } + getTimeoutMicros(): number { return 0; } + setTimeoutMicros(timeout: number): void { } + reset(): void { } + getLanguage(): Parser.Language { return {} as any; } + setLanguage(): void { } + getLogger(): Parser.Logger { + throw new Error('Method not implemented.'); + } + setLogger(logFunc?: Parser.Logger | false | null): void { + throw new Error('Method not implemented.'); + } +} + +class MockTreeSitterImporter extends TreeSitterImporter { + public override async getParserClass(): Promise { + return MockParser as any; + } +} + +class MockTree implements Parser.Tree { + editorLanguage: string = ''; + editorContents: string = ''; + rootNode: Parser.SyntaxNode = {} as any; + rootNodeWithOffset(offsetBytes: number, offsetExtent: Parser.Point): Parser.SyntaxNode { + throw new Error('Method not implemented.'); + } + copy(): Parser.Tree { + throw new Error('Method not implemented.'); + } + delete(): void { } + edit(edit: Parser.Edit): Parser.Tree { + return this; + } + walk(): Parser.TreeCursor { + throw new Error('Method not implemented.'); + } + getChangedRanges(other: Parser.Tree): Parser.Range[] { + throw new Error('Method not implemented.'); + } + getIncludedRanges(): Parser.Range[] { + throw new Error('Method not implemented.'); + } + getEditedRange(other: Parser.Tree): Parser.Range { + throw new Error('Method not implemented.'); + } + getLanguage(): Parser.Language { + throw new Error('Method not implemented.'); + } +} + +class MockLanguage implements Parser.Language { + version: number = 0; + fieldCount: number = 0; + stateCount: number = 0; + nodeTypeCount: number = 0; + fieldNameForId(fieldId: number): string | null { + throw new Error('Method not implemented.'); + } + fieldIdForName(fieldName: string): number | null { + throw new Error('Method not implemented.'); + } + idForNodeType(type: string, named: boolean): number { + throw new Error('Method not implemented.'); + } + nodeTypeForId(typeId: number): string | null { + throw new Error('Method not implemented.'); + } + nodeTypeIsNamed(typeId: number): boolean { + throw new Error('Method not implemented.'); + } + nodeTypeIsVisible(typeId: number): boolean { + throw new Error('Method not implemented.'); + } + nextState(stateId: number, typeId: number): number { + throw new Error('Method not implemented.'); + } + query(source: string): Parser.Query { + throw new Error('Method not implemented.'); + } + lookaheadIterator(stateId: number): Parser.LookaheadIterable | null { + throw new Error('Method not implemented.'); + } + languageId: string = ''; +} + +suite('TreeSitterParserService', function () { + const treeSitterImporter: TreeSitterImporter = new MockTreeSitterImporter(); + let logService: ILogService; + let telemetryService: ITelemetryService; + setup(function () { + logService = new LogService(new ConsoleMainLogger()); + telemetryService = new class extends mock() { + override async publicLog2() { + // + } + }; + }); + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('TextModelTreeSitter race condition: first language is slow to load', async function () { + class MockTreeSitterParser extends TreeSitterLanguages { + public override async getLanguage(languageId: string): Promise { + if (languageId === 'javascript') { + await timeout(200); + } + const language = new MockLanguage(); + language.languageId = languageId; + return language; + } + } + + const treeSitterParser: TreeSitterLanguages = store.add(new MockTreeSitterParser(treeSitterImporter, {} as any, { isBuilt: false } as any)); + const textModel = store.add(createTextModel('console.log("Hello, world!");', 'javascript')); + const textModelTreeSitter = store.add(new TextModelTreeSitter(textModel, treeSitterParser, treeSitterImporter, logService, telemetryService)); + textModel.setLanguage('typescript'); + await timeout(300); + assert.strictEqual((textModelTreeSitter.tree?.language as MockLanguage).languageId, 'typescript'); + }); +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 584faab47ac..be2c930e9eb 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2919,6 +2919,9 @@ declare namespace monaco.editor { * An event describing a change in the text of a model. */ export interface IModelContentChangedEvent { + /** + * The changes are ordered from the end of the document to the beginning, so they should be safe to apply in sequence. + */ readonly changes: IModelContentChange[]; /** * The (new) end-of-line character. @@ -3821,6 +3824,11 @@ declare namespace monaco.editor { * and the diff editor has a width less than `renderSideBySideInlineBreakpoint`, the inline view is used. */ useInlineViewWhenSpaceIsLimited?: boolean; + /** + * If set, the diff editor is optimized for small views. + * Defaults to `false`. + */ + compactMode?: boolean; /** * Timeout in milliseconds after which diff computation is cancelled. * Defaults to 5000. @@ -3883,6 +3891,10 @@ declare namespace monaco.editor { */ showMoves?: boolean; showEmptyDecorations?: boolean; + /** + * Only applies when `renderSideBySide` is set to false. + */ + useTrueInlineView?: boolean; }; /** * Is the diff editor inside another editor @@ -4048,11 +4060,13 @@ declare namespace monaco.editor { multipleDeclarations?: GoToLocationValues; multipleImplementations?: GoToLocationValues; multipleReferences?: GoToLocationValues; + multipleTests?: GoToLocationValues; alternativeDefinitionCommand?: string; alternativeTypeDefinitionCommand?: string; alternativeDeclarationCommand?: string; alternativeImplementationCommand?: string; alternativeReferenceCommand?: string; + alternativeTestsCommand?: string; } /** diff --git a/src/vs/nls.ts b/src/vs/nls.ts index 5a546325fc7..8303d8a32e2 100644 --- a/src/vs/nls.ts +++ b/src/vs/nls.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// VSCODE_GLOBALS: NLS const isPseudo = globalThis._VSCODE_NLS_LANGUAGE === 'pseudo' || (typeof document !== 'undefined' && document.location && document.location.hash.indexOf('pseudo=true') >= 0); export interface ILocalizeInfo { @@ -87,7 +86,6 @@ export function localize(data: ILocalizeInfo | string /* | number when built */, * depending on the target context. */ function lookupMessage(index: number, fallback: string | null): string { - // VSCODE_GLOBALS: NLS const message = globalThis._VSCODE_NLS_MESSAGES?.[index]; if (typeof message !== 'string') { if (typeof fallback === 'string') { diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts index d71a5871195..071afc8fa1f 100644 --- a/src/vs/platform/accessibility/browser/accessibleView.ts +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -9,6 +9,7 @@ import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQui import { Event } from 'vs/base/common/event'; import { IAction } from 'vs/base/common/actions'; import { IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; export const IAccessibleViewService = createDecorator('accessibleViewService'); @@ -26,7 +27,10 @@ export const enum AccessibleViewProviderId { Hover = 'hover', Notification = 'notification', EmptyEditorHint = 'emptyEditorHint', - Comments = 'comments' + Comments = 'comments', + Repl = 'repl', + ReplHelp = 'replHelp', + RunAndDebug = 'runAndDebug', } export const enum AccessibleViewType { @@ -66,10 +70,15 @@ export interface IAccessibleViewOptions { * Keybinding items to configure */ configureKeybindingItems?: IQuickPickItem[]; + + /** + * Keybinding items that are already configured + */ + configuredKeybindingItems?: IQuickPickItem[]; } -export interface IAccessibleViewContentProvider extends IBasicContentProvider { +export interface IAccessibleViewContentProvider extends IBasicContentProvider, IDisposable { id: AccessibleViewProviderId; verbositySettingKey: string; /** @@ -102,6 +111,7 @@ export interface IPosition { export interface IAccessibleViewService { readonly _serviceBrand: undefined; + // The provider will be disposed when the view is closed show(provider: AccesibleViewContentProvider, position?: IPosition): void; showLastProvider(id: AccessibleViewProviderId): void; showAccessibleViewHelp(): void; @@ -111,7 +121,7 @@ export interface IAccessibleViewService { goToSymbol(): void; disableHint(): void; getPosition(id: AccessibleViewProviderId): IPosition | undefined; - setPosition(position: IPosition, reveal?: boolean): void; + setPosition(position: IPosition, reveal?: boolean, select?: boolean): void; getLastPosition(): IPosition | undefined; /** * If the setting is enabled, provides the open accessible view hint as a localized string. @@ -119,7 +129,7 @@ export interface IAccessibleViewService { */ getOpenAriaHint(verbositySettingKey: string): string | null; getCodeBlockContext(): ICodeBlockActionContext | undefined; - configureKeybindings(): void; + configureKeybindings(unassigned: boolean): void; openHelpLink(): void; } @@ -131,9 +141,9 @@ export interface ICodeBlockActionContext { element: unknown; } -export type AccesibleViewContentProvider = AdvancedContentProvider | ExtensionContentProvider; +export type AccesibleViewContentProvider = AccessibleContentProvider | ExtensionContentProvider; -export class AdvancedContentProvider implements IAccessibleViewContentProvider { +export class AccessibleContentProvider extends Disposable implements IAccessibleViewContentProvider { constructor( public id: AccessibleViewProviderId, @@ -143,16 +153,18 @@ export class AdvancedContentProvider implements IAccessibleViewContentProvider { public verbositySettingKey: string, public onOpen?: () => void, public actions?: IAction[], - public next?: () => void, - public previous?: () => void, + public provideNextContent?: () => string | undefined, + public providePreviousContent?: () => string | undefined, public onDidChangeContent?: Event, public onKeyDown?: (e: IKeyboardEvent) => void, public getSymbols?: () => IAccessibleViewSymbol[], public onDidRequestClearLastProvider?: Event, - ) { } + ) { + super(); + } } -export class ExtensionContentProvider implements IBasicContentProvider { +export class ExtensionContentProvider extends Disposable implements IBasicContentProvider { constructor( public readonly id: string, @@ -160,21 +172,23 @@ export class ExtensionContentProvider implements IBasicContentProvider { public provideContent: () => string, public onClose: () => void, public onOpen?: () => void, - public next?: () => void, - public previous?: () => void, + public provideNextContent?: () => string | undefined, + public providePreviousContent?: () => string | undefined, public actions?: IAction[], public onDidChangeContent?: Event, - ) { } + ) { + super(); + } } -export interface IBasicContentProvider { +export interface IBasicContentProvider extends IDisposable { id: string; options: IAccessibleViewOptions; onClose(): void; provideContent(): string; onOpen?(): void; actions?: IAction[]; - previous?(): void; - next?(): void; + providePreviousContent?(): void; + provideNextContent?(): void; onDidChangeContent?: Event; } diff --git a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts index 0a6369565bb..d915dd4e0f4 100644 --- a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts +++ b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts @@ -4,19 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from 'vs/base/common/lifecycle'; -import { AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { alert } from 'vs/base/browser/ui/aria/aria'; -export interface IAccessibleViewImplentation extends IDisposable { +export interface IAccessibleViewImplentation { type: AccessibleViewType; priority: number; name: string; /** * @returns the provider or undefined if the view should not be shown */ - getProvider: (accessor: ServicesAccessor) => AdvancedContentProvider | ExtensionContentProvider | undefined; + getProvider: (accessor: ServicesAccessor) => AccessibleContentProvider | ExtensionContentProvider | undefined; when?: ContextKeyExpression | undefined; } @@ -31,7 +30,6 @@ export const AccessibleViewRegistry = new class AccessibleViewRegistry { if (idx !== -1) { this._implementations.splice(idx, 1); } - implementation.dispose(); } }; } @@ -41,16 +39,3 @@ export const AccessibleViewRegistry = new class AccessibleViewRegistry { } }; -export function alertAccessibleViewFocusChange(index: number | undefined, length: number | undefined, type: 'next' | 'previous'): void { - if (index === undefined || length === undefined) { - return; - } - const number = index + 1; - - if (type === 'next' && number + 1 <= length) { - alert(`Focused ${number + 1} of ${length}`); - } else if (type === 'previous' && number - 1 > 0) { - alert(`Focused ${number - 1} of ${length}`); - } - return; -} diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 51d518e00f8..b0e99c5d0f5 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -145,7 +145,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi } private getVolumeInPercent(): number { - const volume = this.configurationService.getValue('accessibilitySignals.volume'); + const volume = this.configurationService.getValue('accessibility.signalOptions.volume'); if (typeof volume !== 'number') { return 50; } @@ -342,8 +342,7 @@ export class AccessibilitySignal { public readonly legacySoundSettingsKey: string | undefined, public readonly settingsKey: string, public readonly legacyAnnouncementSettingsKey: string | undefined, - public readonly announcementMessage: string | undefined, - public readonly delaySettingsKey: string | undefined + public readonly announcementMessage: string | undefined ) { } private static _signals = new Set(); @@ -370,7 +369,6 @@ export class AccessibilitySignal { options.settingsKey, options.legacyAnnouncementSettingsKey, options.announcementMessage, - options.delaySettingsKey ); AccessibilitySignal._signals.add(signal); return signal; diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 index 488754fdd58..3c7de8ca03b 100644 Binary files a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 and b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 differ diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 index 0532cf6b15a..d99bf74152d 100644 Binary files a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 and b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 differ diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 2354e1af5cd..251486f49d2 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -13,8 +13,8 @@ width: 100%; border: 1px solid var(--vscode-editorWidget-border) !important; border-radius: 2px; - background-color: var(--vscode-editorWidget-background); - color: var(--vscode-editorWidget-foreground); + background-color: var(--vscode-editorActionList-background); + color: var(--vscode-editorActionList-foreground); } .context-view-block { @@ -62,8 +62,8 @@ } .action-widget .monaco-list .monaco-list-row.action.focused:not(.option-disabled) { - background-color: var(--vscode-quickInputList-focusBackground) !important; - color: var(--vscode-quickInputList-focusForeground); + background-color: var(--vscode-editorActionList-focusBackground) !important; + color: var(--vscode-editorActionList-focusForeground); outline: 1px solid var(--vscode-menu-selectionBorder, transparent); outline-offset: -1px; } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 8d087d8c9d9..5bb61d3872b 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -60,6 +60,7 @@ export class MenuId { static readonly DebugWatchContext = new MenuId('DebugWatchContext'); static readonly DebugToolBar = new MenuId('DebugToolBar'); static readonly DebugToolBarStop = new MenuId('DebugToolBarStop'); + static readonly DebugCallStackToolbar = new MenuId('DebugCallStackToolbar'); static readonly EditorContext = new MenuId('EditorContext'); static readonly SimpleEditorContext = new MenuId('SimpleEditorContext'); static readonly EditorContent = new MenuId('EditorContent'); @@ -112,6 +113,7 @@ export class MenuId { static readonly ProblemsPanelContext = new MenuId('ProblemsPanelContext'); static readonly SCMInputBox = new MenuId('SCMInputBox'); static readonly SCMChangesSeparator = new MenuId('SCMChangesSeparator'); + static readonly SCMChangesContext = new MenuId('SCMChangesContext'); static readonly SCMIncomingChanges = new MenuId('SCMIncomingChanges'); static readonly SCMIncomingChangesContext = new MenuId('SCMIncomingChangesContext'); static readonly SCMIncomingChangesSetting = new MenuId('SCMIncomingChangesSetting'); @@ -143,6 +145,7 @@ export class MenuId { static readonly TestMessageContent = new MenuId('TestMessageContent'); static readonly TestPeekElement = new MenuId('TestPeekElement'); static readonly TestPeekTitle = new MenuId('TestPeekTitle'); + static readonly TestCallStack = new MenuId('TestCallStack'); static readonly TouchBarContext = new MenuId('TouchBarContext'); static readonly TitleBarContext = new MenuId('TitleBarContext'); static readonly TitleBarTitleContext = new MenuId('TitleBarTitleContext'); diff --git a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts index 0c49f0ac1e6..5d726261ea4 100644 --- a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts +++ b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts @@ -10,7 +10,7 @@ import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/e import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { IStateService } from 'vs/platform/state/node/state'; -import { hasNativeTitlebar } from 'vs/platform/window/common/window'; +import { hasNativeTitlebar, TitlebarStyle } from 'vs/platform/window/common/window'; import { IBaseWindow, WindowMode } from 'vs/platform/window/electron-main/window'; import { BaseWindow } from 'vs/platform/windows/electron-main/windowImpl'; @@ -52,7 +52,7 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { return; // already disposed } - this.doTryClaimWindow(); + this.doTryClaimWindow(options); if (options && !this.stateApplied) { this.stateApplied = true; @@ -72,7 +72,7 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { } } - private doTryClaimWindow(): void { + private doTryClaimWindow(options?: BrowserWindowConstructorOptions): void { if (this._win) { return; // already claimed } @@ -82,11 +82,11 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { this.logService.trace('[aux window] Claimed browser window instance'); // Remember - this.setWin(window); + this.setWin(window, options); // Disable Menu window.setMenu(null); - if ((isWindows || isLinux) && hasNativeTitlebar(this.configurationService)) { + if ((isWindows || isLinux) && hasNativeTitlebar(this.configurationService, options?.titleBarStyle === 'hidden' ? TitlebarStyle.CUSTOM : undefined /* unknown */)) { window.setAutoHideMenuBar(true); // Fix for https://github.com/microsoft/vscode/issues/200615 } diff --git a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts index a07b5535788..a5829812e7a 100644 --- a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts +++ b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts @@ -13,7 +13,7 @@ import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electr import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowState, WindowMode, defaultAuxWindowState } from 'vs/platform/window/electron-main/window'; -import { WindowStateValidator, defaultBrowserWindowOptions, getLastFocused } from 'vs/platform/windows/electron-main/windows'; +import { IDefaultBrowserWindowOptionsOverrides, WindowStateValidator, defaultBrowserWindowOptions, getLastFocused } from 'vs/platform/windows/electron-main/windows'; export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliaryWindowsMainService { @@ -88,13 +88,15 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar } createWindow(details: HandlerDetails): BrowserWindowConstructorOptions { - return this.instantiationService.invokeFunction(defaultBrowserWindowOptions, this.validateWindowState(details), { + const { state, overrides } = this.computeWindowStateAndOverrides(details); + return this.instantiationService.invokeFunction(defaultBrowserWindowOptions, state, overrides, { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload-aux.js').fsPath }); } - private validateWindowState(details: HandlerDetails): IWindowState { + private computeWindowStateAndOverrides(details: HandlerDetails): { readonly state: IWindowState; readonly overrides: IDefaultBrowserWindowOptionsOverrides } { const windowState: IWindowState = {}; + const overrides: IDefaultBrowserWindowOptionsOverrides = {}; const features = details.features.split(','); // for example: popup=yes,left=270,top=14.5,width=800,height=600 for (const feature of features) { @@ -118,6 +120,12 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar case 'window-fullscreen': windowState.mode = WindowMode.Fullscreen; break; + case 'window-disable-fullscreen': + overrides.disableFullscreen = true; + break; + case 'window-native-titlebar': + overrides.forceNativeTitlebar = true; + break; } } @@ -125,7 +133,7 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar this.logService.trace('[aux window] using window state', state); - return state; + return { state, overrides }; } registerWindow(webContents: WebContents): void { diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts index d22b7bb5bc0..0f4a19424a0 100644 --- a/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -15,6 +15,13 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { ILogService } from 'vs/platform/log/common/log'; +/** + * Custom mime type used for storing a list of uris in the clipboard. + * + * Requires support for custom web clipboards https://github.com/w3c/clipboard-apis/pull/175 + */ +const vscodeResourcesMime = 'application/vnd.code.resources'; + export class BrowserClipboardService extends Disposable implements IClipboardService { declare readonly _serviceBrand: undefined; @@ -34,7 +41,7 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer // and not in the clipboard, we have to invalidate // that state when the user copies other data. this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => { - disposables.add(addDisposableListener(window.document, 'copy', () => this.clearResources())); + disposables.add(addDisposableListener(window.document, 'copy', () => this.clearResourcesState())); }, { window: mainWindow, disposables: this._store })); } @@ -86,7 +93,7 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer async writeText(text: string, type?: string): Promise { // Clear resources given we are writing text - this.writeResources([]); + this.clearResourcesState(); // With type: only in-memory is supported if (type) { @@ -172,8 +179,28 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer private static readonly MAX_RESOURCE_STATE_SOURCE_LENGTH = 1000; async writeResources(resources: URI[]): Promise { + // Guard access to navigator.clipboard with try/catch + // as we have seen DOMExceptions in certain browsers + // due to security policies. + try { + await getActiveWindow().navigator.clipboard.write([ + new ClipboardItem({ + [`web ${vscodeResourcesMime}`]: new Blob([ + JSON.stringify(resources.map(x => x.toJSON())) + ], { + type: vscodeResourcesMime + }) + }) + ]); + + // Continue to write to the in-memory clipboard as well. + // This is needed because some browsers allow the paste but then can't read the custom resources. + } catch (error) { + // Noop + } + if (resources.length === 0) { - this.clearResources(); + this.clearResourcesState(); } else { this.resources = resources; this.resourcesStateHash = await this.computeResourcesStateHash(); @@ -181,9 +208,25 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer } async readResources(): Promise { + // Guard access to navigator.clipboard with try/catch + // as we have seen DOMExceptions in certain browsers + // due to security policies. + try { + const items = await getActiveWindow().navigator.clipboard.read(); + for (const item of items) { + if (item.types.includes(`web ${vscodeResourcesMime}`)) { + const blob = await item.getType(`web ${vscodeResourcesMime}`); + const resources = (JSON.parse(await blob.text()) as URI[]).map(x => URI.from(x)); + return resources; + } + } + } catch (error) { + // Noop + } + const resourcesStateHash = await this.computeResourcesStateHash(); if (this.resourcesStateHash !== resourcesStateHash) { - this.clearResources(); // state mismatch, resources no longer valid + this.clearResourcesState(); // state mismatch, resources no longer valid } return this.resources; @@ -204,10 +247,28 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer } async hasResources(): Promise { + // Guard access to navigator.clipboard with try/catch + // as we have seen DOMExceptions in certain browsers + // due to security policies. + try { + const items = await getActiveWindow().navigator.clipboard.read(); + for (const item of items) { + if (item.types.includes(`web ${vscodeResourcesMime}`)) { + return true; + } + } + } catch (error) { + // Noop + } + return this.resources.length > 0; } - private clearResources(): void { + public clearInternalState(): void { + this.clearResourcesState(); + } + + private clearResourcesState(): void { this.resources = []; this.resourcesStateHash = undefined; } diff --git a/src/vs/platform/clipboard/common/clipboardService.ts b/src/vs/platform/clipboard/common/clipboardService.ts index c4aea9f7132..8cc5cbc397e 100644 --- a/src/vs/platform/clipboard/common/clipboardService.ts +++ b/src/vs/platform/clipboard/common/clipboardService.ts @@ -46,4 +46,11 @@ export interface IClipboardService { * Find out if resources are copied to the clipboard. */ hasResources(): Promise; + + /** + * Resets the internal state of the clipboard (if any) without touching the real clipboard. + * + * Used for implementations such as web which do not always support using the real clipboard. + */ + clearInternalState?(): void; } diff --git a/src/vs/platform/encryption/electron-main/encryptionMainService.ts b/src/vs/platform/encryption/electron-main/encryptionMainService.ts index c937778c08d..423f0a2b363 100644 --- a/src/vs/platform/encryption/electron-main/encryptionMainService.ts +++ b/src/vs/platform/encryption/electron-main/encryptionMainService.ts @@ -25,12 +25,14 @@ export class EncryptionMainService implements IEncryptionMainService { ) { // if this commandLine switch is set, the user has opted in to using basic text encryption if (app.commandLine.getSwitchValue('password-store') === PasswordStoreCLIOption.basic) { + this.logService.trace('[EncryptionMainService] setting usePlainTextEncryption to true...'); safeStorage.setUsePlainTextEncryption?.(true); + this.logService.trace('[EncryptionMainService] set usePlainTextEncryption to true'); } } async encrypt(value: string): Promise { - this.logService.trace('[EncryptionMainService] Encrypting value.'); + this.logService.trace('[EncryptionMainService] Encrypting value...'); try { const result = JSON.stringify(safeStorage.encryptString(value)); this.logService.trace('[EncryptionMainService] Encrypted value.'); @@ -50,7 +52,7 @@ export class EncryptionMainService implements IEncryptionMainService { } const bufferToDecrypt = Buffer.from(parsedValue.data); - this.logService.trace('[EncryptionMainService] Decrypting value.'); + this.logService.trace('[EncryptionMainService] Decrypting value...'); const result = safeStorage.decryptString(bufferToDecrypt); this.logService.trace('[EncryptionMainService] Decrypted value.'); return result; @@ -61,7 +63,10 @@ export class EncryptionMainService implements IEncryptionMainService { } isEncryptionAvailable(): Promise { - return Promise.resolve(safeStorage.isEncryptionAvailable()); + this.logService.trace('[EncryptionMainService] Checking if encryption is available...'); + const result = safeStorage.isEncryptionAvailable(); + this.logService.trace('[EncryptionMainService] Encryption is available: ', result); + return Promise.resolve(result); } getKeyStorageProvider(): Promise { @@ -73,7 +78,9 @@ export class EncryptionMainService implements IEncryptionMainService { } if (safeStorage.getSelectedStorageBackend) { try { + this.logService.trace('[EncryptionMainService] Getting selected storage backend...'); const result = safeStorage.getSelectedStorageBackend() as KnownStorageProvider; + this.logService.trace('[EncryptionMainService] Selected storage backend: ', result); return Promise.resolve(result); } catch (e) { this.logService.error(e); @@ -95,6 +102,8 @@ export class EncryptionMainService implements IEncryptionMainService { throw new Error('Setting plain text encryption is not supported.'); } + this.logService.trace('[EncryptionMainService] Setting usePlainTextEncryption to true...'); safeStorage.setUsePlainTextEncryption(true); + this.logService.trace('[EncryptionMainService] Set usePlainTextEncryption to true'); } } diff --git a/src/vs/platform/environment/node/userDataPath.js b/src/vs/platform/environment/node/userDataPath.js index 1e89f1fee06..3661c50a3ba 100644 --- a/src/vs/platform/environment/node/userDataPath.js +++ b/src/vs/platform/environment/node/userDataPath.js @@ -6,8 +6,16 @@ /// //@ts-check +'use strict'; + +// ESM-uncomment-begin +// import * as os from 'os'; +// import * as path from 'path'; +// +// const module = { exports: {} }; +// ESM-uncomment-end + (function () { - 'use strict'; /** * @import { NativeParsedArgs } from '../../environment/common/argv' @@ -117,11 +125,17 @@ return factory(path, os, process.cwd()); // amd }); } else if (typeof module === 'object' && typeof module.exports === 'object') { + // ESM-comment-begin const path = require('path'); const os = require('os'); + // ESM-comment-end module.exports = factory(path, os, process.env['VSCODE_CWD'] || process.cwd()); // commonjs } else { throw new Error('Unknown context'); } }()); + +// ESM-uncomment-begin +// export const getUserDataPath = module.exports.getUserDataPath; +// ESM-uncomment-end diff --git a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts index 81db8b47267..fb0033b2ada 100644 --- a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts +++ b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts @@ -56,7 +56,12 @@ flakySuite('Native Modules (all platforms)', () => { }); test('@vscode/sqlite3', async () => { + // ESM-comment-begin const sqlite3 = await import('@vscode/sqlite3'); + // ESM-comment-end + // ESM-uncomment-begin + // const { default: sqlite3 } = await import('@vscode/sqlite3'); + // ESM-uncomment-end assert.ok(typeof sqlite3.Database === 'function', testErrorMessage('@vscode/sqlite3')); }); @@ -113,21 +118,18 @@ flakySuite('Native Modules (all platforms)', () => { assert.ok(typeof result === 'string' || typeof result === 'undefined', testErrorMessage('@vscode/windows-registry')); }); - test('@vscode/windows-ca-certs', async () => { - // @ts-ignore we do not directly depend on this module anymore - // but indirectly from our dependency to `@vscode/proxy-agent` - // we still want to ensure this module can work properly. - const windowsCerts = await import('@vscode/windows-ca-certs'); - const store = new windowsCerts.Crypt32(); - assert.ok(windowsCerts, testErrorMessage('@vscode/windows-ca-certs')); - let certCount = 0; - try { - while (store.next()) { - certCount++; + test('@vscode/proxy-agent', async () => { + const proxyAgent = await import('@vscode/proxy-agent'); + // This call will load `@vscode/proxy-agent` which is a native module that we want to test on Windows + const windowsCerts = await proxyAgent.loadSystemCertificates({ + log: { + trace: () => { }, + debug: () => { }, + info: () => { }, + warn: () => { }, + error: () => { } } - } finally { - store.done(); - } - assert(certCount > 0); + }); + assert.ok(windowsCerts.length > 0, testErrorMessage('@vscode/proxy-agent')); }); }); diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index f3ae52b16a2..42f230be716 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -9,6 +9,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationError, getErrorMessage, isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; import { isWeb } from 'vs/base/common/platform'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -19,7 +20,8 @@ import { InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, IProductVersion, ExtensionGalleryErrorCode, EXTENSION_INSTALL_SOURCE_CONTEXT, - DidUpdateExtensionMetadata + DidUpdateExtensionMetadata, + UninstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -48,6 +50,7 @@ export interface IInstallExtensionTask { export type UninstallExtensionTaskOptions = UninstallOptions & { readonly profileLocation: URI }; export interface IUninstallExtensionTask { + readonly options: UninstallExtensionTaskOptions; readonly extension: ILocalExtension; run(): Promise; waitUntilTaskIsFinished(): Promise; @@ -142,9 +145,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return results; } - async uninstall(extension: ILocalExtension, options: UninstallOptions = {}): Promise { + async uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { this.logService.trace('ExtensionManagementService#uninstall', extension.identifier.id); - return this.uninstallExtension(extension, options); + return this.uninstallExtensions([{ extension, options }]); } async toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise { @@ -615,43 +618,23 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return compatibleExtension; } - private async uninstallExtension(extension: ILocalExtension, options: UninstallOptions): Promise { - const uninstallOptions: UninstallExtensionTaskOptions = { - ...options, - profileLocation: extension.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation() - }; - const getUninstallExtensionTaskKey = (identifier: IExtensionIdentifier) => `${identifier.id.toLowerCase()}${uninstallOptions.versionOnly ? `-${extension.manifest.version}` : ''}${uninstallOptions.profileLocation ? `@${uninstallOptions.profileLocation.toString()}` : ''}`; - const uninstallExtensionTask = this.uninstallingExtensions.get(getUninstallExtensionTaskKey(extension.identifier)); - if (uninstallExtensionTask) { - this.logService.info('Extensions is already requested to uninstall', extension.identifier.id); - return uninstallExtensionTask.waitUntilTaskIsFinished(); - } + async uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise { - const createUninstallExtensionTask = (extension: ILocalExtension): IUninstallExtensionTask => { + const getUninstallExtensionTaskKey = (extension: ILocalExtension, uninstallOptions: UninstallExtensionTaskOptions) => `${extension.identifier.id.toLowerCase()}${uninstallOptions.versionOnly ? `-${extension.manifest.version}` : ''}@${uninstallOptions.profileLocation.toString()}`; + + const createUninstallExtensionTask = (extension: ILocalExtension, uninstallOptions: UninstallExtensionTaskOptions): IUninstallExtensionTask => { const uninstallExtensionTask = this.createUninstallExtensionTask(extension, uninstallOptions); - this.uninstallingExtensions.set(getUninstallExtensionTaskKey(uninstallExtensionTask.extension.identifier), uninstallExtensionTask); - if (uninstallOptions.profileLocation) { - this.logService.info('Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); - } else { - this.logService.info('Uninstalling extension:', `${extension.identifier.id}@${extension.manifest.version}`); - } + this.uninstallingExtensions.set(getUninstallExtensionTaskKey(uninstallExtensionTask.extension, uninstallOptions), uninstallExtensionTask); + this.logService.info('Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); this._onUninstallExtension.fire({ identifier: extension.identifier, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped }); return uninstallExtensionTask; }; - const postUninstallExtension = (extension: ILocalExtension, error?: ExtensionManagementError): void => { + const postUninstallExtension = (extension: ILocalExtension, uninstallOptions: UninstallExtensionTaskOptions, error?: ExtensionManagementError): void => { if (error) { - if (uninstallOptions.profileLocation) { - this.logService.error('Failed to uninstall extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString(), error.message); - } else { - this.logService.error('Failed to uninstall extension:', `${extension.identifier.id}@${extension.manifest.version}`, error.message); - } + this.logService.error('Failed to uninstall extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString(), error.message); } else { - if (uninstallOptions.profileLocation) { - this.logService.info('Successfully uninstalled extension from the profile', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); - } else { - this.logService.info('Successfully uninstalled extension:', `${extension.identifier.id}@${extension.manifest.version}`); - } + this.logService.info('Successfully uninstalled extension from the profile', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); } reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error }); this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: error?.code, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped }); @@ -659,64 +642,91 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const allTasks: IUninstallExtensionTask[] = []; const processedTasks: IUninstallExtensionTask[] = []; + const alreadyRequestedUninstalls: Promise[] = []; + + const installedExtensionsMap = new ResourceMap(); + + for (const { extension, options } of extensions) { + const uninstallOptions: UninstallExtensionTaskOptions = { + ...options, + profileLocation: extension.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options?.profileLocation ?? this.getCurrentExtensionsManifestLocation() + }; + const uninstallExtensionTask = this.uninstallingExtensions.get(getUninstallExtensionTaskKey(extension, uninstallOptions)); + if (uninstallExtensionTask) { + this.logService.info('Extensions is already requested to uninstall', extension.identifier.id); + alreadyRequestedUninstalls.push(uninstallExtensionTask.waitUntilTaskIsFinished()); + } else { + allTasks.push(createUninstallExtensionTask(extension, uninstallOptions)); + } + } try { - allTasks.push(createUninstallExtensionTask(extension)); - const installed = await this.getInstalled(ExtensionType.User, uninstallOptions.profileLocation); - if (uninstallOptions.donotIncludePack) { - this.logService.info('Uninstalling the extension without including packed extension', `${extension.identifier.id}@${extension.manifest.version}`); - } else { - const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed); - for (const packedExtension of packedExtensions) { - if (this.uninstallingExtensions.has(getUninstallExtensionTaskKey(packedExtension.identifier))) { - this.logService.info('Extensions is already requested to uninstall', packedExtension.identifier.id); - } else { - allTasks.push(createUninstallExtensionTask(packedExtension)); + for (const task of allTasks.slice(0)) { + let installed = installedExtensionsMap.get(task.options.profileLocation); + if (!installed) { + installedExtensionsMap.set(task.options.profileLocation, installed = await this.getInstalled(ExtensionType.User, task.options.profileLocation)); + } + + if (task.options.donotIncludePack) { + this.logService.info('Uninstalling the extension without including packed extension', `${task.extension.identifier.id}@${task.extension.manifest.version}`); + } else { + const packedExtensions = this.getAllPackExtensionsToUninstall(task.extension, installed); + for (const packedExtension of packedExtensions) { + if (this.uninstallingExtensions.has(getUninstallExtensionTaskKey(packedExtension, task.options))) { + this.logService.info('Extensions is already requested to uninstall', packedExtension.identifier.id); + } else { + allTasks.push(createUninstallExtensionTask(packedExtension, task.options)); + } } } - } - - if (uninstallOptions.donotCheckDependents) { - this.logService.info('Uninstalling the extension without checking dependents', `${extension.identifier.id}@${extension.manifest.version}`); - } else { - this.checkForDependents(allTasks.map(task => task.extension), installed, extension); + if (task.options.donotCheckDependents) { + this.logService.info('Uninstalling the extension without checking dependents', `${task.extension.identifier.id}@${task.extension.manifest.version}`); + } else { + this.checkForDependents(allTasks.map(task => task.extension), installed, task.extension); + } } // Uninstall extensions in parallel and wait until all extensions are uninstalled / failed await this.joinAllSettled(allTasks.map(async task => { try { await task.run(); - await this.joinAllSettled(this.participants.map(participant => participant.postUninstall(task.extension, uninstallOptions, CancellationToken.None))); + await this.joinAllSettled(this.participants.map(participant => participant.postUninstall(task.extension, task.options, CancellationToken.None))); // only report if extension has a mapped gallery extension. UUID identifies the gallery extension. if (task.extension.identifier.uuid) { try { await this.galleryService.reportStatistic(task.extension.manifest.publisher, task.extension.manifest.name, task.extension.manifest.version, StatisticType.Uninstall); } catch (error) { /* ignore */ } } - postUninstallExtension(task.extension); } catch (e) { const error = toExtensionManagementError(e); - postUninstallExtension(task.extension, error); + postUninstallExtension(task.extension, task.options, error); throw error; } finally { processedTasks.push(task); } })); + if (alreadyRequestedUninstalls.length) { + await this.joinAllSettled(alreadyRequestedUninstalls); + } + + for (const task of allTasks) { + postUninstallExtension(task.extension, task.options); + } } catch (e) { const error = toExtensionManagementError(e); for (const task of allTasks) { // cancel the tasks try { task.cancel(); } catch (error) { /* ignore */ } if (!processedTasks.includes(task)) { - postUninstallExtension(task.extension, error); + postUninstallExtension(task.extension, task.options, error); } } throw error; } finally { // Remove tasks from cache for (const task of allTasks) { - if (!this.uninstallingExtensions.delete(getUninstallExtensionTaskKey(task.extension.identifier))) { + if (!this.uninstallingExtensions.delete(getUninstallExtensionTaskKey(task.extension, task.options))) { this.logService.warn('Uninstallation task is not found in the cache', task.extension.identifier.id); } } @@ -797,7 +807,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract getTargetPlatform(): Promise; abstract zip(extension: ILocalExtension): Promise; - abstract unzip(zipLocation: URI): Promise; abstract getManifest(vsix: URI): Promise; abstract install(vsix: URI, options?: InstallOptions): Promise; abstract installFromLocation(location: URI, profileLocation: URI): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index e8698264bfa..bbe96046957 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -12,7 +12,7 @@ import { isWeb, platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { isBoolean } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; +import { IHeaders, IRequestContext, IRequestOptions, isOfflineError } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -214,6 +214,7 @@ const PropertyType = { WebExtension: 'Microsoft.VisualStudio.Code.WebExtension', SponsorLink: 'Microsoft.VisualStudio.Code.SponsorLink', SupportLink: 'Microsoft.VisualStudio.Services.Links.Support', + ExecutesCode: 'Microsoft.VisualStudio.Code.ExecutesCode', }; interface ICriterium { @@ -431,6 +432,11 @@ function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean { return values.length > 0 && values[0].value === 'true'; } +function executesCode(version: IRawGalleryExtensionVersion): boolean | undefined { + const values = version.properties ? version.properties.filter(p => p.key === PropertyType.ExecutesCode) : []; + return values.length > 0 ? values[0].value === 'true' : undefined; +} + function getEnabledApiProposals(version: IRawGalleryExtensionVersion): string[] { const values = version.properties ? version.properties.filter(p => p.key === PropertyType.EnabledApiProposals) : []; const value = (values.length > 0 && values[0].value) || ''; @@ -558,7 +564,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller enabledApiProposals: getEnabledApiProposals(version), localizedLanguages: getLocalizedLanguages(version), targetPlatform: getTargetPlatformForExtensionVersion(version), - isPreReleaseVersion: isPreReleaseVersion(version) + isPreReleaseVersion: isPreReleaseVersion(version), + executesCode: executesCode(version) }, hasPreReleaseVersion: isPreReleaseVersion(latestVersion), hasReleaseVersion: true, @@ -598,7 +605,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi private readonly extensionsGallerySearchUrl: string | undefined; private readonly extensionsControlUrl: string | undefined; - private readonly commonHeadersPromise: Promise>; + private readonly commonHeadersPromise: Promise; private readonly extensionsEnabledWithApiProposalVersion: string[]; constructor( @@ -1022,9 +1029,9 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return { galleryExtensions, total, - context: { + context: context.res.headers['activityid'] ? { [ACTIVITY_HEADER_NAME]: context.res.headers['activityid'] - } + } : {} }; } return { galleryExtensions: [], total }; @@ -1035,7 +1042,11 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi throw e; } else { const errorMessage = getErrorMessage(e); - errorCode = errorMessage.startsWith('XHR timeout') ? ExtensionGalleryErrorCode.Timeout : ExtensionGalleryErrorCode.Failed; + errorCode = isOfflineError(e) + ? ExtensionGalleryErrorCode.Offline + : errorMessage.startsWith('XHR timeout') + ? ExtensionGalleryErrorCode.Timeout + : ExtensionGalleryErrorCode.Failed; throw new ExtensionGalleryError(errorMessage, errorCode); } } finally { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 5786b57ea07..83ed0c519ca 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -163,6 +163,7 @@ export interface IGalleryExtensionProperties { localizedLanguages?: string[]; targetPlatform: TargetPlatform; isPreReleaseVersion: boolean; + executesCode?: boolean; } export interface IGalleryExtensionAsset { @@ -425,6 +426,7 @@ export const enum ExtensionGalleryErrorCode { Cancelled = 'Cancelled', Failed = 'Failed', DownloadFailedWriting = 'DownloadFailedWriting', + Offline = 'Offline', } export class ExtensionGalleryError extends Error { @@ -446,7 +448,6 @@ export const enum ExtensionManagementErrorCode { Download = 'Download', DownloadSignature = 'DownloadSignature', DownloadFailedWriting = ExtensionGalleryErrorCode.DownloadFailedWriting, - UpdateExistingMetadata = 'UpdateExistingMetadata', UpdateMetadata = 'UpdateMetadata', Extract = 'Extract', Scanning = 'Scanning', @@ -491,6 +492,7 @@ export type InstallOptions = { profileLocation?: URI; installOnlyNewlyAddedFromExtensionPack?: boolean; productVersion?: IProductVersion; + keepExisting?: boolean; /** * Context passed through to InstallExtensionResult */ @@ -511,6 +513,7 @@ export interface IExtensionManagementParticipant { } export type InstallExtensionInfo = { readonly extension: IGalleryExtension; readonly options: InstallOptions }; +export type UninstallExtensionInfo = { readonly extension: ILocalExtension; readonly options?: UninstallOptions }; export const IExtensionManagementService = createDecorator('extensionManagementService'); export interface IExtensionManagementService { @@ -523,7 +526,6 @@ export interface IExtensionManagementService { onDidUpdateExtensionMetadata: Event; zip(extension: ILocalExtension): Promise; - unzip(zipLocation: URI): Promise; getManifest(vsix: URI): Promise; install(vsix: URI, options?: InstallOptions): Promise; canInstall(extension: IGalleryExtension): Promise; @@ -532,6 +534,7 @@ export interface IExtensionManagementService { installFromLocation(location: URI, profileLocation: URI): Promise; installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; + uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise; toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts index 3506d94d5c9..064420130d1 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts @@ -224,7 +224,7 @@ export class ExtensionManagementCLI { } extensionsToInstall.push({ extension: gallery, - options: { ...installOptions, installGivenVersion: !!version, isApplicationScoped: installedExtension?.isApplicationScoped }, + options: { ...installOptions, installGivenVersion: !!version, isApplicationScoped: installOptions.isApplicationScoped || installedExtension?.isApplicationScoped }, }); })); diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 65e8735bd10..2785a41f718 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion, DidUpdateExtensionMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion, DidUpdateExtensionMetadata, UninstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; @@ -109,9 +109,6 @@ export class ExtensionManagementChannel implements IServerChannel { const uri = await this.service.zip(extension); return transformOutgoingURI(uri, uriTransformer); } - case 'unzip': { - return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); - } case 'install': { return this.service.install(transformIncomingURI(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer)); } @@ -140,6 +137,10 @@ export class ExtensionManagementChannel implements IServerChannel { case 'uninstall': { return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer)); } + case 'uninstallExtensions': { + const arg: UninstallExtensionInfo[] = args[0]; + return this.service.uninstallExtensions(arg.map(({ extension, options }) => ({ extension: transformIncomingExtension(extension, uriTransformer), options: transformIncomingOptions(options, uriTransformer) }))); + } case 'reinstallFromGallery': { return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); } @@ -241,10 +242,6 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(result))); } - unzip(zipLocation: URI): Promise { - return Promise.resolve(this.channel.call('unzip', [zipLocation])); - } - install(vsix: URI, options?: InstallOptions): Promise { return Promise.resolve(this.channel.call('install', [vsix, options])).then(local => transformIncomingExtension(local, null)); } @@ -278,6 +275,14 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('uninstall', [extension, options])); } + uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise { + if (extensions.some(e => e.extension.isWorkspaceScoped)) { + throw new Error('Cannot uninstall a workspace extension'); + } + return Promise.resolve(this.channel.call('uninstallExtensions', [extensions])); + + } + reinstallFromGallery(extension: ILocalExtension): Promise { return Promise.resolve(this.channel.call('reinstallFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); } diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 57831802fe4..7d4c51d21bb 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -726,7 +726,7 @@ class ExtensionsScanner extends Disposable { return null; } if (getNodeType(manifest) !== 'object') { - this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", manifestLocation.path))); + this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not a JSON object.", manifestLocation.path))); return null; } return manifest; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 60b13367651..75ab2baae27 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -111,12 +111,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return URI.file(location); } - async unzip(zipLocation: URI): Promise { - this.logService.trace('ExtensionManagementService#unzip', zipLocation.toString()); - const local = await this.install(zipLocation); - return local.identifier; - } - async getManifest(vsix: URI): Promise { const { location, cleanup } = await this.downloadVsix(vsix); const zipPath = path.resolve(location.fsPath); @@ -294,7 +288,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { - return new UninstallExtensionInProfileTask(extension, options.profileLocation, this.extensionsProfileScannerService); + return new UninstallExtensionInProfileTask(extension, options, this.extensionsProfileScannerService); } private async downloadAndExtractGalleryExtension(extensionKey: ExtensionKey, gallery: IGalleryExtension, operation: InstallOperation, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { @@ -306,7 +300,10 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } // validate manifest - await getManifest(location.fsPath); + const manifest = await getManifest(location.fsPath); + if (!new ExtensionKey(gallery.identifier, gallery.version).equals(new ExtensionKey({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, manifest.version))) { + throw new ExtensionManagementError(nls.localize('invalidManifest', "Cannot install '{0}' extension because of manifest mismatch with Marketplace", gallery.identifier.id), ExtensionManagementErrorCode.Invalid); + } const local = await this.extensionsScanner.extractUserExtension( extensionKey, @@ -354,7 +351,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi pinned: options.installGivenVersion ? true : !!options.pinned, source: 'vsix', }, - true, + options.keepExisting ?? true, token); return { local }; } @@ -381,7 +378,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi }; const files = await collectFilesFromDirectory(extension.location.fsPath); - return files.map(f => ({ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f })); + return files.map(f => ({ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f })); } private async onDidChangeExtensionsFromAnotherSource({ added, removed }: DidChangeProfileExtensionsEvent): Promise { @@ -582,71 +579,67 @@ export class ExtensionsScanner extends Disposable { const tempLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`)); const extensionLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName)); - let exists = await this.fileService.exists(extensionLocation); + if (await this.fileService.exists(extensionLocation)) { + if (!removeIfExists) { + try { + return await this.scanLocalExtension(extensionLocation, ExtensionType.User); + } catch (error) { + this.logService.warn(`Error while scanning the existing extension at ${extensionLocation.path}. Deleting the existing extension and extracting it.`, getErrorMessage(error)); + } + } - if (exists && removeIfExists) { try { await this.deleteExtensionFromLocation(extensionKey.id, extensionLocation, 'removeExisting'); } catch (error) { throw new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionLocation.fsPath, extensionKey.id), ExtensionManagementErrorCode.Delete); } - exists = false; } - if (exists) { + try { + if (token.isCancellationRequested) { + throw new CancellationError(); + } + + // Extract try { - await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); + this.logService.trace(`Started extracting the extension from ${zipPath} to ${extensionLocation.fsPath}`); + await extract(zipPath, tempLocation.fsPath, { sourcePath: 'extension', overwrite: true }, token); + this.logService.info(`Extracted extension to ${extensionLocation}:`, extensionKey.id); + } catch (e) { + throw fromExtractError(e); + } + + try { + await this.extensionsScannerService.updateMetadata(tempLocation, metadata); } catch (error) { this.telemetryService.publicLog2('extension:extract', { extensionId: extensionKey.id, code: `${toFileOperationResult(error)}` }); - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateExistingMetadata); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); } - } else { + + if (token.isCancellationRequested) { + throw new CancellationError(); + } + + // Rename try { - if (token.isCancellationRequested) { - throw new CancellationError(); - } - - // Extract - try { - this.logService.trace(`Started extracting the extension from ${zipPath} to ${extensionLocation.fsPath}`); - await extract(zipPath, tempLocation.fsPath, { sourcePath: 'extension', overwrite: true }, token); - this.logService.info(`Extracted extension to ${extensionLocation}:`, extensionKey.id); - } catch (e) { - throw fromExtractError(e); - } - - try { - await this.extensionsScannerService.updateMetadata(tempLocation, metadata); - } catch (error) { - this.telemetryService.publicLog2('extension:extract', { extensionId: extensionKey.id, code: `${toFileOperationResult(error)}` }); - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); - } - - if (token.isCancellationRequested) { - throw new CancellationError(); - } - - // Rename - try { - this.logService.trace(`Started renaming the extension from ${tempLocation.fsPath} to ${extensionLocation.fsPath}`); - await this.rename(tempLocation.fsPath, extensionLocation.fsPath); - this.logService.info('Renamed to', extensionLocation.fsPath); - } catch (error) { - if (error.code === 'ENOTEMPTY') { - this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id); - try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ } - } else { - this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempLocation); - throw error; - } - } - - this._onExtract.fire(extensionLocation); - + this.logService.trace(`Started renaming the extension from ${tempLocation.fsPath} to ${extensionLocation.fsPath}`); + await this.rename(tempLocation.fsPath, extensionLocation.fsPath); + this.logService.info('Renamed to', extensionLocation.fsPath); } catch (error) { - try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ } - throw error; + if (error.code === 'ENOTEMPTY') { + this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id); + try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ } + } else { + this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempLocation); + throw error; + } } + + this._onExtract.fire(extensionLocation); + + } catch (error) { + try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ } + throw error; } return this.scanLocalExtension(extensionLocation, ExtensionType.User); @@ -1091,14 +1084,14 @@ class UninstallExtensionInProfileTask extends AbstractExtensionTask implem constructor( readonly extension: ILocalExtension, - private readonly profileLocation: URI, + readonly options: UninstallExtensionTaskOptions, private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, ) { super(); } protected async doRun(token: CancellationToken): Promise { - await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.profileLocation); + await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); } } diff --git a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts index cedabb0c126..8d1c7fb8679 100644 --- a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts +++ b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { importAMDNodeModule } from 'vs/amdX'; import { getErrorMessage } from 'vs/base/common/errors'; import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -95,14 +96,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur private vsceSign(): Promise { if (!this.moduleLoadingPromise) { - this.moduleLoadingPromise = new Promise( - (resolve, reject) => require( - ['@vscode/vsce-sign'], - async (obj) => { - const instance = obj; - - return resolve(instance); - }, reject)); + this.moduleLoadingPromise = importAMDNodeModule('@vscode/vsce-sign', 'src/main.js'); } return this.moduleLoadingPromise; diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts index a4c9afd9c58..e4f187c323c 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -52,6 +52,7 @@ suite('Extension Gallery Service', () => { test('marketplace machine id', async () => { const headers = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService, NullTelemetryService); + assert.ok(headers['X-Market-User-Id']); assert.ok(isUUID(headers['X-Market-User-Id'])); const headers2 = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService, NullTelemetryService); assert.strictEqual(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); diff --git a/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts b/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts index f1660961c58..120b996402a 100644 --- a/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts +++ b/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts @@ -6,7 +6,6 @@ import { isWeb } from 'vs/base/common/platform'; import { format2 } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { IHeaders } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; @@ -110,8 +109,8 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi return !!this._extensionGalleryAuthority && this._extensionGalleryAuthority === this._getExtensionGalleryAuthority(uri); } - protected async getExtensionGalleryRequestHeaders(): Promise { - const headers: IHeaders = { + protected async getExtensionGalleryRequestHeaders(): Promise> { + const headers: Record = { 'X-Client-Name': `${this._productService.applicationName}${isWeb ? '-web' : ''}`, 'X-Client-Version': this._productService.version }; diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index a3f677336d1..463e0a92727 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -15,12 +15,12 @@ const _allApiProposals = { aiTextSearchProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', }, + aiTextSearchProviderNew: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProviderNew.d.ts', + }, attributableCoverage: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts', }, - authGetSessions: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', - }, authLearnMore: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', }, @@ -118,6 +118,9 @@ const _allApiProposals = { contribShareMenu: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts', }, + contribSourceControlHistoryItemChangesMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemChangesMenu.d.ts', + }, contribSourceControlHistoryItemGroupMenu: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemGroupMenu.d.ts', }, @@ -133,6 +136,9 @@ const _allApiProposals = { contribStatusBarItems: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribStatusBarItems.d.ts', }, + contribViewContainerTitle: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewContainerTitle.d.ts', + }, contribViewsRemote: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', }, @@ -190,12 +196,21 @@ const _allApiProposals = { fileSearchProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts', }, + fileSearchProviderNew: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProviderNew.d.ts', + }, findFiles2: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findFiles2.d.ts', }, + findFiles2New: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findFiles2New.d.ts', + }, findTextInFiles: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', }, + findTextInFilesNew: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFilesNew.d.ts', + }, fsChunks: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', }, @@ -225,7 +240,7 @@ const _allApiProposals = { }, lmTools: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.lmTools.d.ts', - version: 2 + version: 3 }, mappedEditsProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', @@ -275,6 +290,9 @@ const _allApiProposals = { quickDiffProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts', }, + quickInputButtonLocation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts', + }, quickPickItemTooltip: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', }, @@ -338,15 +356,24 @@ const _allApiProposals = { terminalSelection: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', }, - terminalShellIntegration: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts', + testMessageStackTrace: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testMessageStackTrace.d.ts', }, testObserver: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', }, + testRelatedCode: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testRelatedCode.d.ts', + }, + textSearchCompleteNew: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchCompleteNew.d.ts', + }, textSearchProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', }, + textSearchProviderNew: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProviderNew.d.ts', + }, timeline: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', }, diff --git a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts index 106963ac0b5..4dae8d5c88b 100644 --- a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts +++ b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts @@ -113,6 +113,7 @@ export class ExtensionHostStarter extends Disposable implements IDisposable, IEx execArgv: opts.execArgv, allowLoadingUnsignedLibraries: true, forceAllocationsToV8Sandbox: true, + respondToAuthRequestsFromMainProcess: true, correlationId: id }); const pid = await Event.toPromise(extHost.onSpawn); diff --git a/src/vs/platform/externalTerminal/node/externalTerminalService.ts b/src/vs/platform/externalTerminal/node/externalTerminalService.ts index 5086c95a802..9f85b145912 100644 --- a/src/vs/platform/externalTerminal/node/externalTerminalService.ts +++ b/src/vs/platform/externalTerminal/node/externalTerminalService.ts @@ -56,8 +56,9 @@ export class WindowsExternalTerminalService extends ExternalTerminalService impl const cmdArgs = ['/c', 'start', '/wait']; if (exec.indexOf(' ') >= 0) { // The "" argument is the window title. Without this, exec doesn't work when the path - // contains spaces - cmdArgs.push('""'); + // contains spaces. #6590 + // Title is Execution Path. #220129 + cmdArgs.push(exec); } cmdArgs.push(exec); // Add starting directory parameter for Windows Terminal (see #90734) diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts b/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts index c5f542918ba..0ee13e7c991 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts @@ -189,7 +189,7 @@ flakySuite('IndexedDBFileSystemProvider', function () { assert.strictEqual(value.mtime, undefined); assert.strictEqual(value.ctime, undefined); } else { - assert.ok(!'Unexpected value ' + basename(value.resource)); + assert.fail('Unexpected value ' + basename(value.resource)); } }); }); diff --git a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts index c3c271abbdd..681e390ec64 100644 --- a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts +++ b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts @@ -273,7 +273,7 @@ flakySuite('Disk File Service', function () { assert.strictEqual(value.mtime, undefined); assert.strictEqual(value.ctime, undefined); } else { - assert.ok(!'Unexpected value ' + basename(value.resource.fsPath)); + assert.fail('Unexpected value ' + basename(value.resource.fsPath)); } }); }); @@ -317,7 +317,7 @@ flakySuite('Disk File Service', function () { assert.ok(value.mtime > 0); assert.ok(value.ctime > 0); } else { - assert.ok(!'Unexpected value ' + basename(value.resource.fsPath)); + assert.fail('Unexpected value ' + basename(value.resource.fsPath)); } }); }); diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index 856c58c1873..26f8e8004ee 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -19,7 +19,7 @@ export interface WindowData { zoomLevel: number; } -export const enum IssueType { +export const enum OldIssueType { Bug, PerformanceIssue, FeatureRequest @@ -31,7 +31,7 @@ export enum IssueSource { Marketplace = 'marketplace' } -export interface IssueReporterStyles extends WindowStyles { +export interface OldIssueReporterStyles extends WindowStyles { textLinkColor?: string; textLinkActiveForeground?: string; inputBackground?: string; @@ -49,7 +49,7 @@ export interface IssueReporterStyles extends WindowStyles { sliderActiveColor?: string; } -export interface IssueReporterExtensionData { +export interface OldIssueReporterExtensionData { name: string; publisher: string | undefined; version: string; @@ -65,10 +65,10 @@ export interface IssueReporterExtensionData { uri?: UriComponents; } -export interface IssueReporterData extends WindowData { - styles: IssueReporterStyles; - enabledExtensions: IssueReporterExtensionData[]; - issueType?: IssueType; +export interface OldIssueReporterData extends WindowData { + styles: OldIssueReporterStyles; + enabledExtensions: OldIssueReporterExtensionData[]; + issueType?: OldIssueType; issueSource?: IssueSource; extensionId?: string; experiments?: string; @@ -109,9 +109,9 @@ export interface ProcessExplorerData extends WindowData { applicationName: string; } -export interface IssueReporterWindowConfiguration extends ISandboxConfiguration { +export interface OldIssueReporterWindowConfiguration extends ISandboxConfiguration { disableExtensions: boolean; - data: IssueReporterData; + data: OldIssueReporterData; os: { type: string; arch: string; @@ -127,13 +127,12 @@ export const IIssueMainService = createDecorator('issueServic export interface IIssueMainService { readonly _serviceBrand: undefined; - // Used by the issue reporter - openReporter(data: IssueReporterData): Promise; + openReporter(data: OldIssueReporterData): Promise; $reloadWithExtensionsDisabled(): Promise; $showConfirmCloseDialog(): Promise; $showClipboardDialog(): Promise; - $sendReporterMenu(extensionId: string, extensionName: string): Promise; + $sendReporterMenu(extensionId: string, extensionName: string): Promise; $closeReporter(): Promise; } diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 8f7ac7dfcc3..1aea4ead9b9 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -14,7 +14,7 @@ import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { localize } from 'vs/nls'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { IIssueMainService, IssueReporterData, IssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, OldIssueReporterData, OldIssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; import product from 'vs/platform/product/common/product'; @@ -53,13 +53,13 @@ export class IssueMainService implements IIssueMainService { //#region Used by renderer - async openReporter(data: IssueReporterData): Promise { + async openReporter(data: OldIssueReporterData): Promise { if (!this.issueReporterWindow) { this.issueReporterParentWindow = BrowserWindow.getFocusedWindow(); if (this.issueReporterParentWindow) { const issueReporterDisposables = new DisposableStore(); - const issueReporterWindowConfigUrl = issueReporterDisposables.add(this.protocolMainService.createIPCObjectUrl()); + const issueReporterWindowConfigUrl = issueReporterDisposables.add(this.protocolMainService.createIPCObjectUrl()); const position = this.getWindowPosition(this.issueReporterParentWindow, 700, 800); this.issueReporterWindow = this.createBrowserWindow(position, issueReporterWindowConfigUrl, { @@ -83,7 +83,6 @@ export class IssueMainService implements IIssueMainService { }, product, nls: { - // VSCODE_GLOBALS: NLS messages: globalThis._VSCODE_NLS_MESSAGES, language: globalThis._VSCODE_NLS_LANGUAGE } @@ -174,16 +173,16 @@ export class IssueMainService implements IIssueMainService { return window; } - async $sendReporterMenu(extensionId: string, extensionName: string): Promise { + async $sendReporterMenu(extensionId: string, extensionName: string): Promise { const window = this.issueReporterWindowCheck(); const replyChannel = `vscode:triggerReporterMenu`; const cts = new CancellationTokenSource(); window.sendWhenReady(replyChannel, cts.token, { replyChannel, extensionId, extensionName }); - const result = await raceTimeout(new Promise(resolve => validatedIpcMain.once(`vscode:triggerReporterMenuResponse:${extensionId}`, (_: unknown, data: IssueReporterData | undefined) => resolve(data))), 5000, () => { + const result = await raceTimeout(new Promise(resolve => validatedIpcMain.once(`vscode:triggerReporterMenuResponse:${extensionId}`, (_: unknown, data: OldIssueReporterData | undefined) => resolve(data))), 5000, () => { this.logService.error(`Error: Extension ${extensionId} timed out waiting for menu response`); cts.cancel(); }); - return result as IssueReporterData | undefined; + return result as OldIssueReporterData | undefined; } async $closeReporter(): Promise { diff --git a/src/vs/platform/issue/electron-main/processMainService.ts b/src/vs/platform/issue/electron-main/processMainService.ts index 76da45d897c..e6002ae0192 100644 --- a/src/vs/platform/issue/electron-main/processMainService.ts +++ b/src/vs/platform/issue/electron-main/processMainService.ts @@ -155,7 +155,6 @@ export class ProcessMainService implements IProcessMainService { data, product, nls: { - // VSCODE_GLOBALS: NLS messages: globalThis._VSCODE_NLS_MESSAGES, language: globalThis._VSCODE_NLS_LANGUAGE } diff --git a/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts b/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts index 08322db2bb6..039aaa6cc9b 100644 --- a/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts +++ b/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { getCompressedContent, IJSONSchema } from 'vs/base/common/jsonSchema'; import * as platform from 'vs/platform/registry/common/platform'; export const Extensions = { @@ -35,6 +35,18 @@ export interface IJSONContributionRegistry { * Get all schemas */ getSchemaContributions(): ISchemaContributions; + + /** + * Gets the (compressed) content of the schema with the given schema ID (if any) + * @param uri The id of the schema + */ + getSchemaContent(uri: string): string | undefined; + + /** + * Returns true if there's a schema that matches the given schema ID + * @param uri The id of the schema + */ + hasSchemaContent(uri: string): boolean; } @@ -74,6 +86,15 @@ class JSONContributionRegistry implements IJSONContributionRegistry { }; } + public getSchemaContent(uri: string): string | undefined { + const schema = this.schemasById[uri]; + return schema ? getCompressedContent(schema) : undefined; + } + + public hasSchemaContent(uri: string): boolean { + return !!this.schemasById[uri]; + } + } const jsonContributionRegistry = new JSONContributionRegistry(); diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 692f6ba5cb7..91120aab5b6 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -18,7 +18,6 @@ export const unsupportedSchemas = new Set([ Schemas.walkThrough, Schemas.walkThroughSnippet, Schemas.vscodeChatCodeBlock, - Schemas.vscodeCopilotBackingChatCodeBlock, ]); class DoubleResourceMap { diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 8a45868ed63..344d45395ed 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -74,6 +74,7 @@ export interface ICommonNativeHostService { getWindows(options: { includeAuxiliaryWindows: false }): Promise>; getWindowCount(): Promise; getActiveWindowId(): Promise; + getActiveWindowPosition(): Promise; openWindow(options?: IOpenEmptyWindowOptions): Promise; openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; @@ -186,6 +187,7 @@ export interface ICommonNativeHostService { // Connectivity resolveProxy(url: string): Promise; lookupAuthorization(authInfo: AuthInfo): Promise; + lookupKerberosAuthorization(url: string): Promise; loadCertificates(): Promise; findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise; diff --git a/src/vs/platform/native/electron-main/auth.ts b/src/vs/platform/native/electron-main/auth.ts index 15918242bba..52b1f51c05e 100644 --- a/src/vs/platform/native/electron-main/auth.ts +++ b/src/vs/platform/native/electron-main/auth.ts @@ -190,10 +190,11 @@ export class ProxyAuthService extends Disposable implements IProxyAuthService { // Reply with session credentials unless we used them already. // In that case we need to show a login dialog again because // they seem invalid. - if (authInfo.attempt === 1 && this.sessionCredentials.has(authInfoHash)) { + const sessionCredentials = authInfo.attempt === 1 && this.sessionCredentials.get(authInfoHash); + if (sessionCredentials) { this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found session credentials to use'); - const { username, password } = this.sessionCredentials.get(authInfoHash)!; + const { username, password } = sessionCredentials; return { username, password }; } diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index e3013c06f8c..6d1f9cc240b 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -46,7 +46,7 @@ import { IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxi import { CancellationError } from 'vs/base/common/errors'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProxyAuthService } from 'vs/platform/native/electron-main/auth'; -import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; +import { AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -67,6 +67,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @IThemeMainService private readonly themeMainService: IThemeMainService, @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IRequestService private readonly requestService: IRequestService, @IProxyAuthService private readonly proxyAuthService: IProxyAuthService ) { super(); @@ -178,6 +179,14 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return undefined; } + async getActiveWindowPosition(): Promise { + const activeWindow = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); + if (activeWindow) { + return activeWindow.getBounds(); + } + return undefined; + } + openWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise; openWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; openWindow(windowId: number | undefined, arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise { @@ -522,7 +531,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain try { const { default: open } = await import('open'); - await open(url, { + const res = await open(url, { app: { // Use `open.apps` helper to allow cross-platform browser // aliases to be looked up properly. Fallback to the @@ -530,6 +539,10 @@ export class NativeHostMainService extends Disposable implements INativeHostMain name: Object.hasOwn(open.apps, configuredBrowser) ? open.apps[(configuredBrowser as keyof typeof open['apps'])] : configuredBrowser } }); + + res.stderr?.on('data', (data: Buffer) => { + this.logService.error(`Error openening external URL '${url}' using browser '${configuredBrowser}': ${data.toString()}`); + }); } catch (error) { this.logService.error(`Unable to open external URL '${url}' using browser '${configuredBrowser}' due to ${error}.`); return shell.openExternal(url); @@ -824,9 +837,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return this.proxyAuthService.lookupAuthorization(authInfo); } + async lookupKerberosAuthorization(_windowId: number | undefined, url: string): Promise { + return this.requestService.lookupKerberosAuthorization(url); + } + async loadCertificates(_windowId: number | undefined): Promise { - const proxyAgent = await import('@vscode/proxy-agent'); - return proxyAgent.loadSystemCertificates({ log: this.logService }); + return this.requestService.loadCertificates(); } findFreePort(windowId: number | undefined, startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise { diff --git a/src/vs/platform/quickinput/browser/helpQuickAccess.ts b/src/vs/platform/quickinput/browser/helpQuickAccess.ts index 0346fc0b856..0a93158786c 100644 --- a/src/vs/platform/quickinput/browser/helpQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/helpQuickAccess.ts @@ -25,7 +25,7 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider { @IKeybindingService private readonly keybindingService: IKeybindingService ) { } - provide(picker: IQuickPick): IDisposable { + provide(picker: IQuickPick): IDisposable { const disposables = new DisposableStore(); // Open a picker with the selected value if picked diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 3afa06e5888..9108ee9ae80 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -16,7 +16,8 @@ .quick-input-titlebar { display: flex; align-items: center; - border-radius: inherit; + border-top-right-radius: 5px; + border-top-left-radius: 5px; } .quick-input-left-action-bar { @@ -25,6 +26,10 @@ flex: 1; } +.quick-input-inline-action-bar { + margin: 2px 0 0 5px; +} + .quick-input-title { padding: 3px 0px; text-align: center; @@ -172,7 +177,6 @@ box-sizing: border-box; overflow: hidden; display: flex; - height: 100%; padding: 0 6px; } @@ -295,7 +299,7 @@ .quick-input-list .quick-input-list-entry-action-bar .action-label.codicon { margin-right: 4px; - padding: 0px 2px 2px 2px; + padding: 2px; } .quick-input-list .quick-input-list-entry-action-bar { diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index 0613c557abc..eded3510549 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -133,7 +133,7 @@ export abstract class PickerQuickAccessProvider, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { + provide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const disposables = new DisposableStore(); // Apply options if any diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index 7cccc352393..b66289c83c2 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -20,7 +20,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon private readonly lastAcceptedPickerValues = new Map(); private visibleQuickAccess: { - readonly picker: IQuickPick; + readonly picker: IQuickPick; readonly descriptor: IQuickAccessProviderDescriptor | undefined; readonly value: string; } | undefined = undefined; @@ -99,7 +99,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // Create a picker for the provider to use with the initial value // and adjust the filtering to exclude the prefix from filtering const disposables = new DisposableStore(); - const picker = disposables.add(this.quickInputService.createQuickPick()); + const picker = disposables.add(this.quickInputService.createQuickPick({ useSeparators: true })); picker.value = value; this.adjustValueSelection(picker, descriptor, options); picker.placeholder = options?.placeholder ?? descriptor?.placeholder; @@ -163,7 +163,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } } - private adjustValueSelection(picker: IQuickPick, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void { + private adjustValueSelection(picker: IQuickPick, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void { let valueSelection: [number, number]; // Preserve: just always put the cursor at the end @@ -180,7 +180,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } private registerPickerListeners( - picker: IQuickPick, + picker: IQuickPick, provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string, diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 74414a631a1..afc73c9ec35 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -25,13 +25,12 @@ import Severity from 'vs/base/common/severity'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./media/quickInput'; import { localize } from 'vs/nls'; -import { IInputBox, IKeyMods, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, IQuickPickWillAcceptEvent, IQuickWidget, ItemActivation, NO_KEY_MODS, QuickInputHideReason, QuickInputType } from 'vs/platform/quickinput/common/quickInput'; +import { IInputBox, IKeyMods, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, IQuickPickWillAcceptEvent, IQuickWidget, ItemActivation, NO_KEY_MODS, QuickInputButtonLocation, QuickInputHideReason, QuickInputType, QuickPickFocus } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputBox } from './quickInputBox'; import { quickInputButtonToAction, renderQuickInputDescription } from './quickInputUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; import { QuickInputTree } from 'vs/platform/quickinput/browser/quickInputTree'; -import { QuickPickFocus } from '../common/quickInput'; import type { IHoverOptions } from 'vs/base/browser/ui/hover/hover'; import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -100,6 +99,7 @@ export interface QuickInputUI { description2: HTMLElement; widget: HTMLElement; rightActionBar: ActionBar; + inlineActionBar: ActionBar; checkAll: HTMLInputElement; inputContainer: HTMLElement; filterContainer: HTMLElement; @@ -157,7 +157,9 @@ abstract class QuickInput extends Disposable implements IQuickInput { private _contextKey: string | undefined; private _busy = false; private _ignoreFocusOut = false; - private _buttons: IQuickInputButton[] = []; + private _leftButtons: IQuickInputButton[] = []; + private _rightButtons: IQuickInputButton[] = []; + private _inlineButtons: IQuickInputButton[] = []; private buttonsUpdated = false; private _toggles: IQuickInputToggle[] = []; private togglesUpdated = false; @@ -273,12 +275,24 @@ abstract class QuickInput extends Disposable implements IQuickInput { } } + protected get titleButtons() { + return this._leftButtons.length + ? [...this._leftButtons, this._rightButtons] + : this._rightButtons; + } + get buttons() { - return this._buttons; + return [ + ...this._leftButtons, + ...this._rightButtons, + ...this._inlineButtons + ]; } set buttons(buttons: IQuickInputButton[]) { - this._buttons = buttons; + this._leftButtons = buttons.filter(b => b === backButton); + this._rightButtons = buttons.filter(b => b !== backButton && b.location !== QuickInputButtonLocation.Inline); + this._inlineButtons = buttons.filter(b => b.location === QuickInputButtonLocation.Inline); this.buttonsUpdated = true; this.update(); } @@ -407,8 +421,7 @@ abstract class QuickInput extends Disposable implements IQuickInput { if (this.buttonsUpdated) { this.buttonsUpdated = false; this.ui.leftActionBar.clear(); - const leftButtons = this.buttons - .filter(button => button === backButton) + const leftButtons = this._leftButtons .map((button, index) => quickInputButtonToAction( button, `id-${index}`, @@ -416,14 +429,21 @@ abstract class QuickInput extends Disposable implements IQuickInput { )); this.ui.leftActionBar.push(leftButtons, { icon: true, label: false }); this.ui.rightActionBar.clear(); - const rightButtons = this.buttons - .filter(button => button !== backButton) + const rightButtons = this._rightButtons .map((button, index) => quickInputButtonToAction( button, `id-${index}`, async () => this.onDidTriggerButtonEmitter.fire(button) )); this.ui.rightActionBar.push(rightButtons, { icon: true, label: false }); + this.ui.inlineActionBar.clear(); + const inlineButtons = this._inlineButtons + .map((button, index) => quickInputButtonToAction( + button, + `id-${index}`, + async () => this.onDidTriggerButtonEmitter.fire(button) + )); + this.ui.inlineActionBar.push(inlineButtons, { icon: true, label: false }); } if (this.togglesUpdated) { this.togglesUpdated = false; @@ -507,7 +527,7 @@ abstract class QuickInput extends Disposable implements IQuickInput { } } -export class QuickPick extends QuickInput implements IQuickPick { +export class QuickPick extends QuickInput implements IQuickPick { private static readonly DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results."); @@ -518,7 +538,7 @@ export class QuickPick extends QuickInput implements I private readonly onWillAcceptEmitter = this._register(new Emitter()); private readonly onDidAcceptEmitter = this._register(new Emitter()); private readonly onDidCustomEmitter = this._register(new Emitter()); - private _items: Array = []; + private _items: O extends { useSeparators: true } ? Array : Array = []; private itemsUpdated = false; private _canSelectMany = false; private _canAcceptInBackground = false; @@ -625,7 +645,7 @@ export class QuickPick extends QuickInput implements I this.ui.list.scrollTop = scrollTop; } - set items(items: Array) { + set items(items: O extends { useSeparators: true } ? Array : Array) { this._items = items; this.itemsUpdated = true; this.update(); @@ -895,7 +915,7 @@ export class QuickPick extends QuickInput implements I } })); this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => { - if (!this.canSelectMany) { + if (!this.canSelectMany || !this.visible) { return; } if (this.selectedItemsToConfirm !== this._selectedItems && equals(checkedItems, this._selectedItems, (a, b) => a === b)) { @@ -986,7 +1006,7 @@ export class QuickPick extends QuickInput implements I const scrollTopBefore = this.keepScrollPosition ? this.scrollTop : 0; const hasDescription = !!this.description; const visibilities: Visibilities = { - title: !!this.title || !!this.step || !!this.buttons.length, + title: !!this.title || !!this.step || !!this.titleButtons.length, description: hasDescription, checkAll: this.canSelectMany && !this._hideCheckAll, checkBox: this.canSelectMany, @@ -1213,7 +1233,7 @@ export class InputBox extends QuickInput implements IInputBox { this.ui.container.classList.remove('hidden-input'); const visibilities: Visibilities = { - title: !!this.title || !!this.step || !!this.buttons.length, + title: !!this.title || !!this.step || !!this.titleButtons.length, description: !!this.description || !!this.step, inputBox: true, message: true, @@ -1247,7 +1267,7 @@ export class QuickWidget extends QuickInput implements IQuickWidget { } const visibilities: Visibilities = { - title: !!this.title || !!this.step || !!this.buttons.length, + title: !!this.title || !!this.step || !!this.titleButtons.length, description: !!this.description || !!this.step }; diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 8999c5e84ab..8dc3875e13f 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -16,14 +16,13 @@ import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import { isString } from 'vs/base/common/types'; import { localize } from 'vs/nls'; -import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickInputHideReason, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickInputHideReason, QuickPickInput, QuickPickFocus } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputBox } from 'vs/platform/quickinput/browser/quickInputBox'; import { QuickInputUI, Writeable, IQuickInputStyles, IQuickInputOptions, QuickPick, backButton, InputBox, Visibilities, QuickWidget, InQuickInputContextKey, QuickInputTypeContextKey, EndOfQuickInputBoxContextKey } from 'vs/platform/quickinput/browser/quickInput'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { mainWindow } from 'vs/base/browser/window'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { QuickInputTree } from 'vs/platform/quickinput/browser/quickInputTree'; -import { QuickPickFocus } from '../common/quickInput'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import 'vs/platform/quickinput/browser/quickInputActions'; @@ -157,6 +156,9 @@ export class QuickInputController extends Disposable { countContainer.setAttribute('aria-live', 'polite'); const count = new CountBadge(countContainer, { countFormat: localize({ key: 'quickInput.countSelected', comment: ['This tells the user how many items are selected in a list of items to select from. The items can be anything.'] }, "{0} Selected") }, this.styles.countBadge); + const inlineActionBar = this._register(new ActionBar(headerContainer, { hoverDelegate: this.options.hoverDelegate })); + inlineActionBar.domNode.classList.add('quick-input-inline-action-bar'); + const okContainer = dom.append(headerContainer, $('.quick-input-action')); const ok = this._register(new Button(okContainer, this.styles.button)); ok.label = localize('ok', "OK"); @@ -321,6 +323,7 @@ export class QuickInputController extends Disposable { description2, widget, rightActionBar, + inlineActionBar, checkAll, inputContainer, filterContainer, @@ -359,7 +362,7 @@ export class QuickInputController extends Disposable { } } - pick>(picks: Promise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { + pick>(picks: Promise[]> | QuickPickInput[], options: IPickOptions = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { type R = (O extends { canPickMany: true } ? T[] : T) | undefined; return new Promise((doResolve, reject) => { let resolve = (result: R) => { @@ -371,7 +374,7 @@ export class QuickInputController extends Disposable { resolve(undefined); return; } - const input = this.createQuickPick(); + const input = this.createQuickPick({ useSeparators: true }); let activeItem: T | undefined; const disposables = [ input, @@ -542,9 +545,11 @@ export class QuickInputController extends Disposable { backButton = backButton; - createQuickPick(): IQuickPick { + createQuickPick(options: { useSeparators: true }): IQuickPick; + createQuickPick(options?: { useSeparators: boolean }): IQuickPick; + createQuickPick(options: { useSeparators: boolean } = { useSeparators: false }): IQuickPick { const ui = this.getUI(true); - return new QuickPick(ui); + return new QuickPick(ui); } createInputBox(): IInputBox { @@ -571,6 +576,7 @@ export class QuickInputController extends Disposable { ui.description2.textContent = ''; dom.reset(ui.widget); ui.rightActionBar.clear(); + ui.inlineActionBar.clear(); ui.checkAll.checked = false; // ui.inputBox.value = ''; Avoid triggering an event. ui.inputBox.placeholder = ''; @@ -580,6 +586,7 @@ export class QuickInputController extends Disposable { ui.count.setCount(0); dom.reset(ui.message); ui.progressBar.stop(); + ui.list.setElements([]); ui.list.matchOnDescription = false; ui.list.matchOnDetail = false; ui.list.matchOnLabel = true; diff --git a/src/vs/platform/quickinput/browser/quickInputService.ts b/src/vs/platform/quickinput/browser/quickInputService.ts index 4727a3f9b52..3c5b30a95e6 100644 --- a/src/vs/platform/quickinput/browser/quickInputService.ts +++ b/src/vs/platform/quickinput/browser/quickInputService.ts @@ -149,7 +149,7 @@ export class QuickInputService extends Themable implements IQuickInputService { }); } - pick>(picks: Promise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { + pick>(picks: Promise[]> | QuickPickInput[], options?: O, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { return this.controller.pick(picks, options, token); } @@ -157,8 +157,10 @@ export class QuickInputService extends Themable implements IQuickInputService { return this.controller.input(options, token); } - createQuickPick(): IQuickPick { - return this.controller.createQuickPick(); + createQuickPick(options: { useSeparators: true }): IQuickPick; + createQuickPick(options?: { useSeparators: boolean }): IQuickPick; + createQuickPick(options: { useSeparators: boolean } = { useSeparators: false }): IQuickPick { + return this.controller.createQuickPick(options); } createInputBox(): IInputBox { diff --git a/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts index 83976c15065..74163bd7e36 100644 --- a/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -7,13 +7,13 @@ import * as dom from 'vs/base/browser/dom'; import { Emitter, Event, EventBufferer, IValueWithChangeEvent } from 'vs/base/common/event'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IObjectTreeElement, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { IObjectTreeElement, ITreeNode, ITreeRenderer, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickItem, QuickPickFocus } from 'vs/platform/quickinput/common/quickInput'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IMatch } from 'vs/base/common/filters'; import { IListAccessibilityProvider, IListStyles } from 'vs/base/browser/ui/list/listWidget'; @@ -37,9 +37,8 @@ import { RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; import { ThrottledDelayer } from 'vs/base/common/async'; import { isCancellationError } from 'vs/base/common/errors'; import type { IHoverWidget, IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; -import { QuickPickFocus } from '../common/quickInput'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { observableValue, observableValueOpts } from 'vs/base/common/observable'; +import { observableValue, observableValueOpts, transaction } from 'vs/base/common/observable'; import { equals } from 'vs/base/common/arrays'; const $ = dom.$; @@ -554,6 +553,12 @@ class QuickPickSeparatorElementRenderer extends BaseQuickInputListRenderer, index: number, data: IQuickInputItemTemplateData): void { const element = node.element; data.element = element; @@ -707,6 +712,7 @@ export class QuickInputTree extends Disposable { // Elements that apply to the current set of elements private readonly _elementDisposable = this._register(new DisposableStore()); private _lastHover: IHoverWidget | undefined; + private _lastQueryString: string | undefined; constructor( private parent: HTMLElement, @@ -727,6 +733,24 @@ export class QuickInputTree extends Disposable { new QuickInputItemDelegate(), [this._itemRenderer, this._separatorRenderer], { + filter: { + filter(element) { + return element.hidden + ? TreeVisibility.Hidden + : element instanceof QuickPickSeparatorElement + ? TreeVisibility.Recurse + : TreeVisibility.Visible; + }, + }, + sorter: { + compare: (element, otherElement) => { + if (!this.sortByLabel || !this._lastQueryString) { + return 0; + } + const normalizedSearchValue = this._lastQueryString.toLowerCase(); + return compareEntries(element, otherElement, normalizedSearchValue); + }, + }, accessibilityProvider: new QuickInputAccessibilityProvider(), setRowLineHeight: false, multipleSelectionSupport: false, @@ -1056,6 +1080,7 @@ export class QuickInputTree extends Disposable { setElements(inputElements: QuickPickItem[]): void { this._elementDisposable.clear(); + this._lastQueryString = undefined; this._inputElements = inputElements; this._hasCheckboxes = this.parent.classList.contains('show-checkboxes'); let currentSeparatorElement: QuickPickSeparatorElement | undefined; @@ -1121,7 +1146,8 @@ export class QuickInputTree extends Disposable { setFocusedElements(items: IQuickPickItem[]) { const elements = items.map(item => this._itemElements.find(e => e.item === item)) - .filter((e): e is QuickPickItemElement => !!e); + .filter((e): e is QuickPickItemElement => !!e) + .filter(e => !e.hidden); this._tree.setFocus(elements); if (items.length > 0) { const focused = this._tree.getFocus()[0]; @@ -1362,6 +1388,7 @@ export class QuickInputTree extends Disposable { } filter(query: string): boolean { + this._lastQueryString = query; if (!(this._sortByLabel || this._matchOnLabel || this._matchOnDescription || this._matchOnDetail)) { this._tree.layout(); return false; @@ -1387,7 +1414,7 @@ export class QuickInputTree extends Disposable { // Filter by value (since we support icons in labels, use $(..) aware fuzzy matching) else { let currentSeparator: IQuickPickSeparator | undefined; - this._elementTree.forEach(element => { + this._itemElements.forEach(element => { let labelHighlights: IMatch[] | undefined; if (this.matchOnLabelMode === 'fuzzy') { labelHighlights = this.matchOnLabel ? matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel)) ?? undefined : undefined; @@ -1418,8 +1445,10 @@ export class QuickInputTree extends Disposable { // we can show the separator unless the list gets sorted by match if (!this.sortByLabel) { - const previous = element.index && this._inputElements[element.index - 1]; - currentSeparator = previous && previous.type === 'separator' ? previous : currentSeparator; + const previous = element.index && this._inputElements[element.index - 1] || undefined; + if (previous?.type === 'separator' && !previous.buttons) { + currentSeparator = previous; + } if (currentSeparator && !element.hidden) { element.separator = currentSeparator; currentSeparator = undefined; @@ -1428,33 +1457,12 @@ export class QuickInputTree extends Disposable { }); } - const shownElements = this._elementTree.filter(element => !element.hidden); - - // Sort by value - if (this.sortByLabel && query) { - const normalizedSearchValue = query.toLowerCase(); - shownElements.sort((a, b) => { - return compareEntries(a, b, normalizedSearchValue); - }); - } - - let currentSeparator: QuickPickSeparatorElement | undefined; - const finalElements = shownElements.reduce((result, element, index) => { - if (element instanceof QuickPickItemElement) { - if (currentSeparator) { - currentSeparator.children.push(element); - } else { - result.push(element); - } - } else if (element instanceof QuickPickSeparatorElement) { - element.children = []; - currentSeparator = element; - result.push(element); - } - return result; - }, new Array()); - - this._setElementsToTree(finalElements); + this._setElementsToTree(this._sortByLabel && query + // We don't render any separators if we're sorting so just render the elements + ? this._itemElements + // Render the full tree + : this._elementTree + ); this._tree.layout(); return true; } @@ -1546,10 +1554,12 @@ export class QuickInputTree extends Disposable { } private _updateCheckedObservables() { - this._allVisibleCheckedObservable.set(this._allVisibleChecked(this._itemElements, false), undefined); - const checkedCount = this._itemElements.filter(element => element.checked).length; - this._checkedCountObservable.set(checkedCount, undefined); - this._checkedElementsObservable.set(this.getCheckedElements(), undefined); + transaction((tx) => { + this._allVisibleCheckedObservable.set(this._allVisibleChecked(this._itemElements, false), tx); + const checkedCount = this._itemElements.filter(element => element.checked).length; + this._checkedCountObservable.set(checkedCount, tx); + this._checkedElementsObservable.set(this.getCheckedElements(), tx); + }); } /** diff --git a/src/vs/platform/quickinput/browser/quickPickPin.ts b/src/vs/platform/quickinput/browser/quickPickPin.ts index 51c8b25d467..d7141ef5f29 100644 --- a/src/vs/platform/quickinput/browser/quickPickPin.ts +++ b/src/vs/platform/quickinput/browser/quickPickPin.ts @@ -18,7 +18,7 @@ const buttonClasses = [pinButtonClass, pinnedButtonClass]; * be removed if @param filterDupliates has been provided. Pin and pinned button events trigger updates to the underlying storage. * Shows the quickpick once formatted. */ -export async function showWithPinnedItems(storageService: IStorageService, storageKey: string, quickPick: IQuickPick, filterDuplicates?: boolean): Promise { +export async function showWithPinnedItems(storageService: IStorageService, storageKey: string, quickPick: IQuickPick, filterDuplicates?: boolean): Promise { const itemsWithoutPinned = quickPick.items; let itemsWithPinned = _formatPinnedItems(storageKey, quickPick, storageService, undefined, filterDuplicates); quickPick.onDidTriggerItemButton(async buttonEvent => { @@ -41,7 +41,7 @@ export async function showWithPinnedItems(storageService: IStorageService, stora quickPick.show(); } -function _formatPinnedItems(storageKey: string, quickPick: IQuickPick, storageService: IStorageService, changedItem?: IQuickPickItem, filterDuplicates?: boolean): QuickPickItem[] { +function _formatPinnedItems(storageKey: string, quickPick: IQuickPick, storageService: IStorageService, changedItem?: IQuickPickItem, filterDuplicates?: boolean): QuickPickItem[] { const formattedItems: QuickPickItem[] = []; let pinnedItems; if (changedItem) { diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index 12b7afa785e..987571327d0 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -129,7 +129,7 @@ export interface IQuickAccessProvider { * @return a disposable that will automatically be disposed when the picker * closes or is replaced by another picker. */ - provide(picker: IQuickPick, token: CancellationToken, options?: IQuickAccessProviderRunOptions): IDisposable; + provide(picker: IQuickPick, token: CancellationToken, options?: IQuickAccessProviderRunOptions): IDisposable; } export interface IQuickAccessProviderHelp { diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index f893e58fbe6..9dcd5049d7b 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -418,7 +418,7 @@ export enum QuickPickFocus { /** * Represents a quick pick control that allows the user to select an item from a list of options. */ -export interface IQuickPick extends IQuickInput { +export interface IQuickPick extends IQuickInput { /** * The type of the quick input. @@ -505,7 +505,7 @@ export interface IQuickPick extends IQuickInput { /** * The items to be displayed in the quick pick. */ - items: ReadonlyArray; + items: O extends { useSeparators: true } ? ReadonlyArray : ReadonlyArray; /** * Whether multiple items can be selected. If so, checkboxes will be rendered. @@ -705,6 +705,18 @@ export interface IInputBox extends IQuickInput { severity: Severity; } +export enum QuickInputButtonLocation { + /** + * In the title bar. + */ + Title = 1, + + /** + * To the right of the input box. + */ + Inline = 2 +} + /** * Represents a button in the quick input UI. */ @@ -728,6 +740,11 @@ export interface IQuickInputButton { * By default, buttons are only visible when hovering over them with the mouse. */ alwaysVisible?: boolean; + /** + * Where the button should be rendered. The default is {@link QuickInputButtonLocation.Title}. + * @note This property is ignored if the button was added to a QuickPickItem. + */ + location?: QuickInputButtonLocation; } /** @@ -854,7 +871,8 @@ export interface IQuickInputService { /** * Provides raw access to the quick pick controller. */ - createQuickPick(): IQuickPick; + createQuickPick(options: { useSeparators: true }): IQuickPick; + createQuickPick(options?: { useSeparators: boolean }): IQuickPick; /** * Provides raw access to the input box controller. diff --git a/src/vs/platform/remote/node/wsl.ts b/src/vs/platform/remote/node/wsl.ts index 4bc71a35f0c..637ccff4fb0 100644 --- a/src/vs/platform/remote/node/wsl.ts +++ b/src/vs/platform/remote/node/wsl.ts @@ -26,7 +26,11 @@ async function testWSLFeatureInstalled(): Promise { const wslExePath = getWSLExecutablePath(); if (wslExePath) { return new Promise(s => { - cp.execFile(wslExePath, ['--status'], err => s(!err)); + try { + cp.execFile(wslExePath, ['--status'], err => s(!err)); + } catch (e) { + s(false); + } }); } } else { diff --git a/src/vs/platform/request/browser/requestService.ts b/src/vs/platform/request/browser/requestService.ts index 70f4e65bcbc..78c34491ff2 100644 --- a/src/vs/platform/request/browser/requestService.ts +++ b/src/vs/platform/request/browser/requestService.ts @@ -40,6 +40,10 @@ export class RequestService extends AbstractRequestService implements IRequestSe return undefined; // not implemented in the web } + async lookupKerberosAuthorization(url: string): Promise { + return undefined; // not implemented in the web + } + async loadCertificates(): Promise { return []; // not implemented in the web } diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index e8574d11f7f..fb732b7f772 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -37,6 +37,7 @@ export interface IRequestService { resolveProxy(url: string): Promise; lookupAuthorization(authInfo: AuthInfo): Promise; + lookupKerberosAuthorization(url: string): Promise; loadCertificates(): Promise; } @@ -96,6 +97,7 @@ export abstract class AbstractRequestService extends Disposable implements IRequ abstract request(options: IRequestOptions, token: CancellationToken): Promise; abstract resolveProxy(url: string): Promise; abstract lookupAuthorization(authInfo: AuthInfo): Promise; + abstract lookupKerberosAuthorization(url: string): Promise; abstract loadCertificates(): Promise; } diff --git a/src/vs/platform/request/common/requestIpc.ts b/src/vs/platform/request/common/requestIpc.ts index b489a52dacc..f6854f5e8f6 100644 --- a/src/vs/platform/request/common/requestIpc.ts +++ b/src/vs/platform/request/common/requestIpc.ts @@ -35,6 +35,7 @@ export class RequestChannel implements IServerChannel { }); case 'resolveProxy': return this.service.resolveProxy(args[0]); case 'lookupAuthorization': return this.service.lookupAuthorization(args[0]); + case 'lookupKerberosAuthorization': return this.service.lookupKerberosAuthorization(args[0]); case 'loadCertificates': return this.service.loadCertificates(); } throw new Error('Invalid call'); @@ -60,6 +61,10 @@ export class RequestChannelClient implements IRequestService { return this.channel.call<{ username: string; password: string } | undefined>('lookupAuthorization', [authInfo]); } + async lookupKerberosAuthorization(url: string): Promise { + return this.channel.call('lookupKerberosAuthorization', [url]); + } + async loadCertificates(): Promise { return this.channel.call('loadCertificates'); } diff --git a/src/vs/platform/request/node/proxy.ts b/src/vs/platform/request/node/proxy.ts index db448e06cc1..141be5bb796 100644 --- a/src/vs/platform/request/node/proxy.ts +++ b/src/vs/platform/request/node/proxy.ts @@ -44,7 +44,21 @@ export async function getProxyAgent(rawRequestURL: string, env: typeof process.e rejectUnauthorized: isBoolean(options.strictSSL) ? options.strictSSL : true, }; - return requestURL.protocol === 'http:' - ? new (await import('http-proxy-agent')).HttpProxyAgent(proxyURL, opts) - : new (await import('https-proxy-agent')).HttpsProxyAgent(proxyURL, opts); + if (requestURL.protocol === 'http:') { + // ESM-comment-begin + const mod = await import('http-proxy-agent'); + // ESM-comment-end + // ESM-uncomment-begin + // const mod = (await import('http-proxy-agent')).default; + // ESM-uncomment-end + return new mod.HttpProxyAgent(proxyURL, opts); + } else { + // ESM-comment-begin + const mod = await import('https-proxy-agent'); + // ESM-comment-end + // ESM-uncomment-begin + // const mod = (await import('https-proxy-agent')).default; + // ESM-uncomment-end + return new mod.HttpsProxyAgent(proxyURL, opts); + } } diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index dd2165c1ce3..f3c57ebbf5c 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -114,6 +114,22 @@ export class RequestService extends AbstractRequestService implements IRequestSe return undefined; // currently not implemented in node } + async lookupKerberosAuthorization(urlStr: string): Promise { + try { + const kerberos = await import('kerberos'); + const url = new URL(urlStr); + const spn = this.configurationService.getValue('http.proxyKerberosServicePrincipal') + || (process.platform === 'win32' ? `HTTP/${url.hostname}` : `HTTP@${url.hostname}`); + this.logService.debug('RequestService#lookupKerberosAuthorization Kerberos authentication lookup', `proxyURL:${url}`, `spn:${spn}`); + const client = await kerberos.initializeClient(spn); + const response = await client.step(''); + return 'Negotiate ' + response; + } catch (err) { + this.logService.debug('RequestService#lookupKerberosAuthorization Kerberos authentication failed', err); + return undefined; + } + } + async loadCertificates(): Promise { const proxyAgent = await import('@vscode/proxy-agent'); return proxyAgent.loadSystemCertificates({ log: this.logService }); @@ -169,7 +185,7 @@ export async function nodeRequest(options: NodeRequestOptions, token: Cancellati stream = res.pipe(createGunzip()); } - resolve({ res, stream: streamToBufferReadableStream(stream) } as IRequestContext); + resolve({ res, stream: streamToBufferReadableStream(stream) } satisfies IRequestContext); } }); diff --git a/src/vs/platform/sign/browser/signService.ts b/src/vs/platform/sign/browser/signService.ts index a9d699bf3b2..b6b08ebf466 100644 --- a/src/vs/platform/sign/browser/signService.ts +++ b/src/vs/platform/sign/browser/signService.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { importAMDNodeModule, resolveAmdNodeModulePath } from 'vs/amdX'; import { WindowIntervalTimer } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { isESM } from 'vs/base/common/amd'; import { memoize } from 'vs/base/common/decorators'; import { FileAccess } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -62,7 +64,7 @@ export class SignService extends AbstractSignService implements ISignService { let [wasm] = await Promise.all([ this.getWasmBytes(), new Promise((resolve, reject) => { - require(['vsda'], resolve, reject); + importAMDNodeModule('vsda', 'rust/web/vsda.js').then(() => resolve(), reject); // todo@connor4312: there seems to be a bug(?) in vscode-loader with // require() not resolving in web once the script loads, so check manually @@ -74,7 +76,6 @@ export class SignService extends AbstractSignService implements ISignService { }).finally(() => checkInterval.dispose()), ]); - const keyBytes = new TextEncoder().encode(this.productService.serverLicense?.join('\n') || ''); for (let i = 0; i + STEP_SIZE < keyBytes.length; i += STEP_SIZE) { const key = await crypto.subtle.importKey('raw', keyBytes.slice(i + IV_SIZE, i + IV_SIZE + KEY_SIZE), { name: 'AES-CBC' }, false, ['decrypt']); @@ -87,7 +88,10 @@ export class SignService extends AbstractSignService implements ISignService { } private async getWasmBytes(): Promise { - const response = await fetch(FileAccess.asBrowserUri('vsda/../vsda_bg.wasm').toString(true)); + const url = isESM + ? resolveAmdNodeModulePath('vsda', 'rust/web/vsda_bg.wasm') + : FileAccess.asBrowserUri('vsda/../vsda_bg.wasm').toString(true); + const response = await fetch(url); if (!response.ok) { throw new Error('error loading vsda'); } diff --git a/src/vs/platform/sign/common/abstractSignService.ts b/src/vs/platform/sign/common/abstractSignService.ts index 6f7c91ba958..838dde91518 100644 --- a/src/vs/platform/sign/common/abstractSignService.ts +++ b/src/vs/platform/sign/common/abstractSignService.ts @@ -36,7 +36,7 @@ export abstract class AbstractSignService implements ISignService { }; } } catch (e) { - // ignore errors silently + console.error(e); } return { id: '', data: value }; } @@ -54,7 +54,7 @@ export abstract class AbstractSignService implements ISignService { try { return (validator.validate(value) === 'ok'); } catch (e) { - // ignore errors silently + console.error(e); return false; } finally { validator.dispose?.(); @@ -65,7 +65,7 @@ export abstract class AbstractSignService implements ISignService { try { return await this.signValue(value); } catch (e) { - // ignore errors silently + console.error(e); } return value; } diff --git a/src/vs/platform/sign/node/signService.ts b/src/vs/platform/sign/node/signService.ts index d07ba9cfbe9..1a2023d6ad1 100644 --- a/src/vs/platform/sign/node/signService.ts +++ b/src/vs/platform/sign/node/signService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { importAMDNodeModule } from 'vs/amdX'; import { AbstractSignService, IVsdaValidator } from 'vs/platform/sign/common/abstractSignService'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -29,6 +30,6 @@ export class SignService extends AbstractSignService implements ISignService { } private vsda(): Promise { - return new Promise((resolve, reject) => require(['vsda'], resolve, reject)); + return importAMDNodeModule('vsda', 'index.js'); } } diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts index dbce8f54458..9e7c183bb45 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts @@ -23,7 +23,7 @@ const enum PromptInputState { * A model of the prompt input state using shell integration and analyzing the terminal buffer. This * may not be 100% accurate but provides a best guess. */ -export interface IPromptInputModel { +export interface IPromptInputModel extends IPromptInputModelState { readonly onDidStartInput: Event; readonly onDidChangeInput: Event; readonly onDidFinishInput: Event; @@ -32,10 +32,6 @@ export interface IPromptInputModel { */ readonly onDidInterrupt: Event; - readonly value: string; - readonly cursorIndex: number; - readonly ghostTextIndex: number; - /** * Gets the prompt input as a user-friendly string where `|` is the cursor position and `[` and * `]` wrap any ghost text. @@ -44,8 +40,26 @@ export interface IPromptInputModel { } export interface IPromptInputModelState { + /** + * The full prompt input include ghost text. + */ readonly value: string; + /** + * The prompt input up to the cursor index, this will always exclude the ghost text. + */ + readonly prefix: string; + /** + * The prompt input from the cursor to the end, this _does not_ include ghost text. + */ + readonly suffix: string; + /** + * The index of the cursor in {@link value}. + */ readonly cursorIndex: number; + /** + * The index of the start of ghost text in {@link value}. This is -1 when there is no ghost + * text. + */ readonly ghostTextIndex: number; } @@ -69,6 +83,8 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { private _value: string = ''; get value() { return this._value; } + get prefix() { return this._value.substring(0, this._cursorIndex); } + get suffix() { return this._value.substring(this._cursorIndex, this._ghostTextIndex === -1 ? undefined : this._ghostTextIndex); } private _cursorIndex: number = 0; get cursorIndex() { return this._cursorIndex; } @@ -462,6 +478,8 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { private _createStateObject(): IPromptInputModelState { return Object.freeze({ value: this._value, + prefix: this.prefix, + suffix: this.suffix, cursorIndex: this._cursorIndex, ghostTextIndex: this._ghostTextIndex }); diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index bef121fbff5..b48cf6306bd 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -149,6 +149,9 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe @debounce(500) private _handleCursorMove() { + if (this._store.isDisposed) { + return; + } // Early versions of conpty do not have real support for an alt buffer, in addition certain // commands such as tsc watch will write to the top of the normal buffer. The following // checks when the cursor has moved while the normal buffer is empty and if it is above the diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index f8e502eb908..c90aca15a20 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -90,6 +90,7 @@ export const enum TerminalSettingId { EnvWindows = 'terminal.integrated.env.windows', EnvironmentChangesIndicator = 'terminal.integrated.environmentChangesIndicator', EnvironmentChangesRelaunch = 'terminal.integrated.environmentChangesRelaunch', + ExperimentalWindowsUseConptyDll = 'terminal.integrated.experimental.windowsUseConptyDll', ShowExitAlert = 'terminal.integrated.showExitAlert', SplitCwd = 'terminal.integrated.splitCwd', WindowsEnableConpty = 'terminal.integrated.windowsEnableConpty', @@ -134,14 +135,16 @@ export const enum PosixShellType { Csh = 'csh', Ksh = 'ksh', Zsh = 'zsh', - Python = 'python' + Python = 'python', + Julia = 'julia' } export const enum WindowsShellType { CommandPrompt = 'cmd', PowerShell = 'pwsh', Wsl = 'wsl', GitBash = 'gitbash', - Python = 'python' + Python = 'python', + Julia = 'julia' } export type TerminalShellType = PosixShellType | WindowsShellType; @@ -661,6 +664,7 @@ export interface ITerminalProcessOptions { nonce: string; }; windowsEnableConpty: boolean; + windowsUseConptyDll: boolean; environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined; workspaceFolder: IWorkspaceFolder | undefined; } diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index ec8182b3c1d..f7295637003 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -15,7 +15,6 @@ import { RequestStore } from 'vs/platform/terminal/common/requestStore'; import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IShellLaunchConfig, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, IFixedTerminalDimensions, IPersistentTerminalProcessLaunchConfig, ICrossVersionSerializedTerminalState, ISerializedTerminalState, ITerminalProcessOptions, IPtyHostLatencyMeasurement } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; -import { Terminal as XtermTerminal } from '@xterm/headless'; import type { ISerializeOptions, SerializeAddon as XtermSerializeAddon } from '@xterm/addon-serialize'; import type { Unicode11Addon as XtermUnicode11Addon } from '@xterm/addon-unicode11'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto } from 'vs/platform/terminal/common/terminalProcess'; @@ -32,6 +31,14 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { join } from 'path'; import { memoize } from 'vs/base/common/decorators'; import * as performance from 'vs/base/common/performance'; +// ESM-comment-begin +import { Terminal as XtermTerminal } from '@xterm/headless'; +// ESM-comment-end +// ESM-uncomment-begin +// import pkg from '@xterm/headless'; +// type XtermTerminal = pkg.Terminal; +// const { Terminal: XtermTerminal } = pkg; +// ESM-uncomment-end export function traceRpc(_target: any, key: string, descriptor: any) { if (typeof descriptor.value !== 'function') { diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 2679ea3683a..b4b28d9ad78 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -74,6 +74,7 @@ const posixShellTypeMap = new Map([ ['sh', PosixShellType.Sh], ['pwsh', PosixShellType.PowerShell], ['python', PosixShellType.Python], + ['julia', PosixShellType.Julia], ['zsh', PosixShellType.Zsh] ]); @@ -153,6 +154,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._properties[ProcessPropertyType.InitialCwd] = this._initialCwd; this._properties[ProcessPropertyType.Cwd] = this._initialCwd; const useConpty = this._options.windowsEnableConpty && process.platform === 'win32' && getWindowsBuildNumber() >= 18309; + const useConptyDll = useConpty && this._options.windowsUseConptyDll; this._ptyOptions = { name, cwd, @@ -161,6 +163,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess cols, rows, useConpty, + useConptyDll, // This option will force conpty to not redraw the whole viewport on launch conptyInheritCursor: useConpty && !!shellLaunchConfig.initialText }; @@ -211,8 +214,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } if (injection.filesToCopy) { for (const f of injection.filesToCopy) { - await fs.promises.mkdir(path.dirname(f.dest), { recursive: true }); try { + await fs.promises.mkdir(path.dirname(f.dest), { recursive: true }); await fs.promises.copyFile(f.source, f.dest); } catch { // Swallow error, this should only happen when multiple users are on the same @@ -401,13 +404,16 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess if (this._store.isDisposed) { return; } - this._currentTitle = ptyProcess.process; + // HACK: The node-pty API can return undefined somehow https://github.com/microsoft/vscode/issues/222323 + this._currentTitle = (ptyProcess.process ?? ''); this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: this._currentTitle }); // If fig is installed it may change the title of the process const sanitizedTitle = this.currentTitle.replace(/ \(figterm\)$/g, ''); if (sanitizedTitle.toLowerCase().startsWith('python')) { this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: PosixShellType.Python }); + } else if (sanitizedTitle.toLowerCase().startsWith('julia')) { + this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: PosixShellType.Julia }); } else { this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: posixShellTypeMap.get(sanitizedTitle) }); } diff --git a/src/vs/platform/terminal/node/windowsShellHelper.ts b/src/vs/platform/terminal/node/windowsShellHelper.ts index e9fcb6f8130..5792455b0ae 100644 --- a/src/vs/platform/terminal/node/windowsShellHelper.ts +++ b/src/vs/platform/terminal/node/windowsShellHelper.ts @@ -143,6 +143,8 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe case 'bash.exe': case 'git-cmd.exe': return WindowsShellType.GitBash; + case 'julia.exe:': + return WindowsShellType.Julia; case 'wsl.exe': case 'ubuntu.exe': case 'ubuntu1804.exe': diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index dd4e198d7d4..06f8aeed6a6 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -12,9 +12,9 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; import { getShellIntegrationInjection, getWindowsBuildNumber, IShellIntegrationConfigInjection } from 'vs/platform/terminal/node/terminalEnvironment'; -const enabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false, nonce: '' }, windowsEnableConpty: true, environmentVariableCollections: undefined, workspaceFolder: undefined }; -const disabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: false, suggestEnabled: false, nonce: '' }, windowsEnableConpty: true, environmentVariableCollections: undefined, workspaceFolder: undefined }; -const winptyProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false, nonce: '' }, windowsEnableConpty: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; +const enabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false, nonce: '' }, windowsEnableConpty: true, windowsUseConptyDll: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; +const disabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: false, suggestEnabled: false, nonce: '' }, windowsEnableConpty: true, windowsUseConptyDll: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; +const winptyProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false, nonce: '' }, windowsEnableConpty: false, windowsUseConptyDll: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; const pwshExe = process.platform === 'win32' ? 'pwsh.exe' : 'pwsh'; const repoRoot = process.platform === 'win32' ? process.cwd()[0].toLowerCase() + process.cwd().substring(1) : process.cwd(); const logService = new NullLogService(); diff --git a/src/vs/platform/theme/browser/defaultStyles.ts b/src/vs/platform/theme/browser/defaultStyles.ts index a0df50920ab..68d93cbc233 100644 --- a/src/vs/platform/theme/browser/defaultStyles.ts +++ b/src/vs/platform/theme/browser/defaultStyles.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IButtonStyles } from 'vs/base/browser/ui/button/button'; import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssVariable, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow, badgeBackground, badgeForeground, breadcrumbsBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, activeContrastBorder, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropOverBackground, listFocusAndSelectionOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, tableColumnsBorder, tableOddRowsBackgroundColor, treeIndentGuidesStroke, asCssVariableWithDefault, editorWidgetBorder, focusBorder, pickerGroupForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, selectBackground, selectBorder, selectForeground, selectListBackground, treeInactiveIndentGuidesStroke, menuBorder, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuSeparatorBackground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listDropBetweenBackground } from 'vs/platform/theme/common/colorRegistry'; +import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssVariable, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow, badgeBackground, badgeForeground, breadcrumbsBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, activeContrastBorder, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropOverBackground, listFocusAndSelectionOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, tableColumnsBorder, tableOddRowsBackgroundColor, treeIndentGuidesStroke, asCssVariableWithDefault, editorWidgetBorder, focusBorder, pickerGroupForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, selectBackground, selectBorder, selectForeground, selectListBackground, treeInactiveIndentGuidesStroke, menuBorder, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuSeparatorBackground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listDropBetweenBackground, radioActiveBackground, radioActiveForeground, radioInactiveBackground, radioInactiveForeground, radioInactiveBorder, radioInactiveHoverBackground, radioActiveBorder } from 'vs/platform/theme/common/colorRegistry'; import { IProgressBarStyles } from 'vs/base/browser/ui/progressbar/progressbar'; import { ICheckboxStyles, IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { IDialogStyles } from 'vs/base/browser/ui/dialog/dialog'; @@ -16,6 +16,7 @@ import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { ISelectBoxStyles } from 'vs/base/browser/ui/selectBox/selectBox'; import { Color } from 'vs/base/common/color'; import { IMenuStyles } from 'vs/base/browser/ui/menu/menu'; +import { IRadioStyles } from 'vs/base/browser/ui/radio/radio'; export type IStyleOverride = { [P in keyof T]?: ColorIdentifier | undefined; @@ -41,6 +42,7 @@ export const defaultKeybindingLabelStyles: IKeybindingLabelStyles = { export function getKeybindingLabelStyles(override: IStyleOverride): IKeybindingLabelStyles { return overrideStyles(override, defaultKeybindingLabelStyles); } + export const defaultButtonStyles: IButtonStyles = { buttonForeground: asCssVariable(buttonForeground), buttonSeparator: asCssVariable(buttonSeparator), @@ -70,6 +72,16 @@ export const defaultToggleStyles: IToggleStyles = { inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground) }; +export const defaultRadioStyles: IRadioStyles = { + activeForeground: asCssVariable(radioActiveForeground), + activeBackground: asCssVariable(radioActiveBackground), + activeBorder: asCssVariable(radioActiveBorder), + inactiveForeground: asCssVariable(radioInactiveForeground), + inactiveBackground: asCssVariable(radioInactiveBackground), + inactiveBorder: asCssVariable(radioInactiveBorder), + inactiveHoverBackground: asCssVariable(radioInactiveHoverBackground), +}; + export function getToggleStyles(override: IStyleOverride): IToggleStyles { return overrideStyles(override, defaultToggleStyles); } diff --git a/src/vs/platform/theme/common/colors/editorColors.ts b/src/vs/platform/theme/common/colors/editorColors.ts index cac6dea162c..cebf9ba8f88 100644 --- a/src/vs/platform/theme/common/colors/editorColors.ts +++ b/src/vs/platform/theme/common/colors/editorColors.ts @@ -426,7 +426,7 @@ export const overviewRulerCommonContentForeground = registerColor('editorOvervie nls.localize('overviewRulerCommonContentForeground', 'Common ancestor overview ruler foreground for inline merge-conflicts.')); export const overviewRulerFindMatchForeground = registerColor('editorOverviewRuler.findMatchForeground', - { dark: '#d186167e', light: '#d186167e', hcDark: '#AB5A00', hcLight: '' }, + { dark: '#d186167e', light: '#d186167e', hcDark: '#AB5A00', hcLight: '#AB5A00' }, nls.localize('overviewRulerFindMatchForeground', 'Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.'), true); export const overviewRulerSelectionHighlightForeground = registerColor('editorOverviewRuler.selectionHighlightForeground', diff --git a/src/vs/platform/theme/common/colors/inputColors.ts b/src/vs/platform/theme/common/colors/inputColors.ts index c79c1d2840b..f31b804f7a1 100644 --- a/src/vs/platform/theme/common/colors/inputColors.ts +++ b/src/vs/platform/theme/common/colors/inputColors.ts @@ -141,6 +141,35 @@ export const buttonSecondaryHoverBackground = registerColor('button.secondaryHov { dark: lighten(buttonSecondaryBackground, 0.2), light: darken(buttonSecondaryBackground, 0.2), hcDark: null, hcLight: null }, nls.localize('buttonSecondaryHoverBackground', "Secondary button background color when hovering.")); +// ------ radio + +export const radioActiveForeground = registerColor('radio.activeForeground', + inputActiveOptionForeground, + nls.localize('radioActiveForeground', "Foreground color of active radio option.")); + +export const radioActiveBackground = registerColor('radio.activeBackground', + inputActiveOptionBackground, + nls.localize('radioBackground', "Background color of active radio option.")); + +export const radioActiveBorder = registerColor('radio.activeBorder', + inputActiveOptionBorder, + nls.localize('radioActiveBorder', "Border color of the active radio option.")); + +export const radioInactiveForeground = registerColor('radio.inactiveForeground', + null, + nls.localize('radioInactiveForeground', "Foreground color of inactive radio option.")); + +export const radioInactiveBackground = registerColor('radio.inactiveBackground', + null, + nls.localize('radioInactiveBackground', "Background color of inactive radio option.")); + +export const radioInactiveBorder = registerColor('radio.inactiveBorder', + { light: transparent(radioActiveForeground, .2), dark: transparent(radioActiveForeground, .2), hcDark: transparent(radioActiveForeground, .4), hcLight: transparent(radioActiveForeground, .2) }, + nls.localize('radioInactiveBorder', "Border color of the inactive radio option.")); + +export const radioInactiveHoverBackground = registerColor('radio.inactiveHoverBackground', + inputActiveOptionHoverBackground, + nls.localize('radioHoverBackground', "Background color of inactive active radio option when hovering.")); // ------ checkbox diff --git a/src/vs/platform/theme/common/colors/listColors.ts b/src/vs/platform/theme/common/colors/listColors.ts index dd5c405199c..8fb7c4a7819 100644 --- a/src/vs/platform/theme/common/colors/listColors.ts +++ b/src/vs/platform/theme/common/colors/listColors.ts @@ -11,7 +11,7 @@ import { registerColor, darken, lighten, transparent, ifDefinedThenElse } from ' // Import the colors we need import { foreground, contrastBorder, activeContrastBorder, focusBorder, iconForeground } from 'vs/platform/theme/common/colors/baseColors'; -import { editorWidgetBackground, editorFindMatchHighlightBorder, editorFindMatchHighlight, widgetShadow } from 'vs/platform/theme/common/colors/editorColors'; +import { editorWidgetBackground, editorFindMatchHighlightBorder, editorFindMatchHighlight, widgetShadow, editorWidgetForeground } from 'vs/platform/theme/common/colors/editorColors'; export const listFocusBackground = registerColor('list.focusBackground', @@ -145,3 +145,21 @@ export const tableColumnsBorder = registerColor('tree.tableColumnsBorder', export const tableOddRowsBackgroundColor = registerColor('tree.tableOddRowsBackground', { dark: transparent(foreground, 0.04), light: transparent(foreground, 0.04), hcDark: null, hcLight: null }, nls.localize('tableOddRowsBackgroundColor', "Background color for odd table rows.")); + +// ------ action list + +export const editorActionListBackground = registerColor('editorActionList.background', + editorWidgetBackground, + nls.localize('editorActionListBackground', "Action List background color.")); + +export const editorActionListForeground = registerColor('editorActionList.foreground', + editorWidgetForeground, + nls.localize('editorActionListForeground', "Action List foreground color.")); + +export const editorActionListFocusForeground = registerColor('editorActionList.focusForeground', + listActiveSelectionForeground, + nls.localize('editorActionListFocusForeground', "Action List foreground color for the focused item.")); + +export const editorActionListFocusBackground = registerColor('editorActionList.focusBackground', + listActiveSelectionBackground, + nls.localize('editorActionListFocusBackground', "Action List background color for the focused item.")); diff --git a/src/vs/platform/userDataProfile/node/userDataProfile.ts b/src/vs/platform/userDataProfile/node/userDataProfile.ts index b5fc85922e9..e8de67d7241 100644 --- a/src/vs/platform/userDataProfile/node/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/node/userDataProfile.ts @@ -93,7 +93,7 @@ export class UserDataProfilesService extends UserDataProfilesReadonlyService imp result[URI.revive(workspace).toString()] = URI.revive(profile).toString(); return result; }, {}); - this.stateService.setItem(UserDataProfilesService.PROFILE_ASSOCIATIONS_KEY, { workspaces }); + this.stateService.setItem(UserDataProfilesService.PROFILE_ASSOCIATIONS_KEY, { workspaces } satisfies StoredProfileAssociations); } const associations = super.getStoredProfileAssociations(); if (!this.stateService.getItem(UserDataProfilesService.PROFILE_ASSOCIATIONS_MIGRATION_KEY, false)) { diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 9f52f0fd29e..b62a1be3db8 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -592,7 +592,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa return { syncResource: this.resource, profile: this.syncResource.profile, remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine: isRemoteDataFromCurrentMachine }; } - async getLastSyncUserData(): Promise { + async getLastSyncUserData(): Promise { let storedLastSyncUserDataStateContent = this.getStoredLastSyncUserDataStateContent(); if (!storedLastSyncUserDataStateContent) { storedLastSyncUserDataStateContent = await this.migrateLastSyncUserData(); @@ -664,7 +664,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa return { ...lastSyncUserDataState, syncData, - } as T; + }; } protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary = {}): Promise { diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 4c698553970..7a28512a252 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -244,7 +244,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs if (remoteChange !== Change.None) { // update remote this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`); - const content = JSON.stringify({ storage: remote.all }); + const content = JSON.stringify({ storage: remote.all }); remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref); this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state.${remote.added.length ? ` Added: ${remote.added}.` : ''}${remote.updated.length ? ` Updated: ${remote.updated}.` : ''}${remote.removed.length ? ` Removed: ${remote.removed}.` : ''}`); } diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 32c4086b550..491dad6ee96 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -148,7 +148,7 @@ export class UserDataSyncStoreClient extends Disposable { private userDataSyncStoreUrl: URI | undefined; private authToken: { token: string; type: string } | undefined; - private readonly commonHeadersPromise: Promise<{ [key: string]: string }>; + private readonly commonHeadersPromise: Promise; private readonly session: RequestsSession; private _onTokenFailed = this._register(new Emitter()); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 3cd97bda585..f83ecab1d9c 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -189,6 +189,7 @@ export class UserDataSyncTestServer implements IRequestService { async resolveProxy(url: string): Promise { return url; } async lookupAuthorization(authInfo: AuthInfo): Promise { return undefined; } + async lookupKerberosAuthorization(url: string): Promise { return undefined; } async loadCertificates(): Promise { return []; } async request(options: IRequestOptions, token: CancellationToken): Promise { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index 03f68a73da9..fefa58adf3c 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -415,6 +415,7 @@ suite('UserDataSyncRequestsSession', () => { async request() { return { res: { headers: {} }, stream: newWriteableBufferStream() }; }, async resolveProxy() { return undefined; }, async lookupAuthorization() { return undefined; }, + async lookupKerberosAuthorization() { return undefined; }, async loadCertificates() { return []; } }; diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index bfa2c0cb42f..3b1cee5f90c 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -18,6 +18,7 @@ import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { deepClone } from 'vs/base/common/objects'; import { isWindows } from 'vs/base/common/platform'; import { isUNCAccessRestrictionsDisabled, getUNCHostAllowlist } from 'vs/base/node/unc'; +import { upcast } from 'vs/base/common/types'; export interface IUtilityProcessConfiguration { @@ -76,6 +77,13 @@ export interface IUtilityProcessConfiguration { * the V8 sandbox. */ readonly forceAllocationsToV8Sandbox?: boolean; + + /** + * HTTP 401 and 407 requests created via electron:net module + * will be redirected to the main process and can be handled + * via the app#login event. + */ + readonly respondToAuthRequestsFromMainProcess?: boolean; } export interface IWindowUtilityProcessConfiguration extends IUtilityProcessConfiguration { @@ -235,20 +243,25 @@ export class UtilityProcess extends Disposable { const execArgv = this.configuration.execArgv ?? []; const allowLoadingUnsignedLibraries = this.configuration.allowLoadingUnsignedLibraries; const forceAllocationsToV8Sandbox = this.configuration.forceAllocationsToV8Sandbox; + const respondToAuthRequestsFromMainProcess = this.configuration.respondToAuthRequestsFromMainProcess; const stdio = 'pipe'; const env = this.createEnv(configuration); this.log('creating new...', Severity.Info); // Fork utility process - this.process = utilityProcess.fork(modulePath, args, { + this.process = utilityProcess.fork(modulePath, args, upcast({ serviceName, env, execArgv, allowLoadingUnsignedLibraries, forceAllocationsToV8Sandbox, + respondToAuthRequestsFromMainProcess, stdio - } as ForkOptions & { forceAllocationsToV8Sandbox?: Boolean }); + })); // Register to events this.registerListeners(this.process, this.configuration, serviceName); diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 2b7ffc4651f..40b1ebf0544 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -5,7 +5,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { PerformanceMark } from 'vs/base/common/performance'; -import { isLinux, isMacintosh, isNative, isWeb, isWindows } from 'vs/base/common/platform'; +import { isLinux, isMacintosh, isNative, isWeb } from 'vs/base/common/platform'; import { URI, UriComponents, UriDto } from 'vs/base/common/uri'; import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -14,6 +14,7 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { FileType } from 'vs/platform/files/common/files'; import { ILoggerResource, LogLevel } from 'vs/platform/log/common/log'; import { PolicyDefinition, PolicyValue } from 'vs/platform/policy/common/policy'; +import product from 'vs/platform/product/common/product'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IAnyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; @@ -160,6 +161,7 @@ export interface IWindowSettings { readonly clickThroughInactive: boolean; readonly newWindowProfile: string; readonly density: IDensitySettings; + readonly experimentalControlOverlay?: boolean; } export interface IDensitySettings { @@ -225,14 +227,23 @@ export function getTitleBarStyle(configurationService: IConfigurationService): T export const DEFAULT_CUSTOM_TITLEBAR_HEIGHT = 35; // includes space for command center export function useWindowControlsOverlay(configurationService: IConfigurationService): boolean { - if (!isWindows || isWeb) { - return false; // only supported on a desktop Windows instance + if (isMacintosh || isWeb) { + return false; // only supported on a Windows/Linux desktop instances } if (hasNativeTitlebar(configurationService)) { return false; // only supported when title bar is custom } + if (isLinux) { + const setting = configurationService.getValue('window.experimentalControlOverlay'); + if (typeof setting === 'boolean') { + return setting; + } + + return product.quality !== 'stable'; // disable by default in stable for now (TODO@bpasero TODO@benibenj flip when custom title is default) + } + // Default to true. return true; } diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 0f626a7ddfc..3e905e3ba66 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import electron from 'electron'; +import electron, { BrowserWindowConstructorOptions } from 'electron'; import { DeferredPromise, RunOnceScheduler, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -32,7 +32,7 @@ import { IApplicationStorageMainService, IStorageMainService } from 'vs/platform import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ThemeIcon } from 'vs/base/common/themables'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { getMenuBarVisibility, IFolderToOpen, INativeWindowConfiguration, IWindowSettings, IWorkspaceToOpen, MenuBarVisibility, hasNativeTitlebar, useNativeFullScreen, useWindowControlsOverlay, DEFAULT_CUSTOM_TITLEBAR_HEIGHT } from 'vs/platform/window/common/window'; +import { getMenuBarVisibility, IFolderToOpen, INativeWindowConfiguration, IWindowSettings, IWorkspaceToOpen, MenuBarVisibility, hasNativeTitlebar, useNativeFullScreen, useWindowControlsOverlay, DEFAULT_CUSTOM_TITLEBAR_HEIGHT, TitlebarStyle } from 'vs/platform/window/common/window'; import { defaultBrowserWindowOptions, IWindowsMainService, OpenContext, WindowStateValidator } from 'vs/platform/windows/electron-main/windows'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; @@ -113,7 +113,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { protected _win: electron.BrowserWindow | null = null; get win() { return this._win; } - protected setWin(win: electron.BrowserWindow): void { + protected setWin(win: electron.BrowserWindow, options?: BrowserWindowConstructorOptions): void { this._win = win; // Window Events @@ -131,13 +131,13 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { this._register(Event.fromNodeEventEmitter(this._win, 'leave-full-screen')(() => this._onDidLeaveFullScreen.fire())); // Sheet Offsets - const useCustomTitleStyle = !hasNativeTitlebar(this.configurationService); + const useCustomTitleStyle = !hasNativeTitlebar(this.configurationService, options?.titleBarStyle === 'hidden' ? TitlebarStyle.CUSTOM : undefined /* unknown */); if (isMacintosh && useCustomTitleStyle) { win.setSheetOffset(isBigSurOrNewer(release()) ? 28 : 22); // offset dialogs by the height of the custom title bar if we have any } // Update the window controls immediately based on cached or default values - if (useCustomTitleStyle && ((isWindows && useWindowControlsOverlay(this.configurationService)) || isMacintosh)) { + if (useCustomTitleStyle && (useWindowControlsOverlay(this.configurationService) || isMacintosh)) { const cachedWindowControlHeight = this.stateService.getItem((BaseWindow.windowControlHeightStateStorageKey)); if (cachedWindowControlHeight) { this.updateWindowControls({ height: cachedWindowControlHeight }); @@ -366,8 +366,8 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { this.stateService.setItem((CodeWindow.windowControlHeightStateStorageKey), options.height); } - // Windows: window control overlay (WCO) - if (isWindows && this.hasWindowControlOverlay) { + // Windows/Linux: window control overlay (WCO) + if (this.hasWindowControlOverlay) { win.setTitleBarOverlay({ color: options.backgroundColor?.trim() === '' ? undefined : options.backgroundColor, symbolColor: options.foregroundColor?.trim() === '' ? undefined : options.foregroundColor, @@ -604,7 +604,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this.windowState = state; this.logService.trace('window#ctor: using window state', state); - const options = instantiationService.invokeFunction(defaultBrowserWindowOptions, this.windowState, { + const options = instantiationService.invokeFunction(defaultBrowserWindowOptions, this.windowState, undefined, { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload.js').fsPath, additionalArguments: [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`], v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none', @@ -616,7 +616,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { mark('code/didCreateCodeBrowserWindow'); this._id = this._win.id; - this.setWin(this._win); + this.setWin(this._win, options); // Apply some state after window creation this.applyState(this.windowState, hasMultipleDisplays); diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 30ed4fe16b0..774ec18be10 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -4,21 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import electron from 'electron'; +import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; +import { join } from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ICodeWindow, IWindowState, WindowMode, defaultWindowState } from 'vs/platform/window/electron-main/window'; -import { IOpenEmptyWindowOptions, IWindowOpenable, IWindowSettings, WindowMinimumSize, hasNativeTitlebar, useNativeFullScreen, useWindowControlsOverlay, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; -import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { join } from 'vs/base/common/path'; import { IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow'; -import { Color } from 'vs/base/common/color'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; +import { IOpenEmptyWindowOptions, IWindowOpenable, IWindowSettings, TitlebarStyle, WindowMinimumSize, hasNativeTitlebar, useNativeFullScreen, useWindowControlsOverlay, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; +import { ICodeWindow, IWindowState, WindowMode, defaultWindowState } from 'vs/platform/window/electron-main/window'; export const IWindowsMainService = createDecorator('windowsMainService'); @@ -115,7 +115,12 @@ export interface IOpenConfiguration extends IBaseOpenConfiguration { export interface IOpenEmptyConfiguration extends IBaseOpenConfiguration { } -export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowState: IWindowState, webPreferences?: electron.WebPreferences): electron.BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } { +export interface IDefaultBrowserWindowOptionsOverrides { + forceNativeTitlebar?: boolean; + disableFullscreen?: boolean; +} + +export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowState: IWindowState, overrides?: IDefaultBrowserWindowOptionsOverrides, webPreferences?: electron.WebPreferences): electron.BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } { const themeMainService = accessor.get(IThemeMainService); const productService = accessor.get(IProductService); const configurationService = accessor.get(IConfigurationService); @@ -161,7 +166,9 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt } } - if (isMacintosh && !useNativeFullScreen(configurationService)) { + if (overrides?.disableFullscreen) { + options.fullscreen = false; + } else if (isMacintosh && !useNativeFullScreen(configurationService)) { options.fullscreenable = false; // enables simple fullscreen mode } @@ -170,7 +177,7 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt options.tabbingIdentifier = productService.nameShort; // this opts in to sierra tabs } - const hideNativeTitleBar = !hasNativeTitlebar(configurationService); + const hideNativeTitleBar = !hasNativeTitlebar(configurationService, overrides?.forceNativeTitlebar ? TitlebarStyle.NATIVE : undefined); if (hideNativeTitleBar) { options.titleBarStyle = 'hidden'; if (!isMacintosh) { diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 070c7ae05d3..5e6279486fc 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -1445,7 +1445,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic userEnv: { ...this.initialUserEnv, ...options.userEnv }, nls: { - // VSCODE_GLOBALS: NLS messages: globalThis._VSCODE_NLS_MESSAGES, language: globalThis._VSCODE_NLS_LANGUAGE }, diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 29aa95e5683..d9bb0122f64 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -696,7 +696,7 @@ export async function createServer(address: string | net.AddressInfo | null, arg let didLogAboutSIGPIPE = false; process.on('SIGPIPE', () => { // See https://github.com/microsoft/vscode-remote-release/issues/6543 - // We would normally install a SIGPIPE listener in bootstrap.js + // We would normally install a SIGPIPE listener in bootstrap-node.js // But in certain situations, the console itself can be in a broken pipe state // so logging SIGPIPE to the console will cause an infinite async loop if (!didLogAboutSIGPIPE) { diff --git a/src/vs/server/node/server.cli.ts b/src/vs/server/node/server.cli.ts index 2eed66f7ee0..fc0f7c259e0 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as _fs from 'fs'; -import * as _url from 'url'; -import * as _cp from 'child_process'; -import * as _http from 'http'; -import * as _os from 'os'; +import * as fs from 'fs'; +import * as url from 'url'; +import * as cp from 'child_process'; +import * as http from 'http'; import { cwd } from 'vs/base/common/process'; import { dirname, extname, resolve, join } from 'vs/base/common/path'; import { parseArgs, buildHelpMessage, buildVersionMessage, OPTIONS, OptionDescriptions, ErrorReporter } from 'vs/platform/environment/node/argv'; @@ -240,8 +239,8 @@ export async function main(desc: ProductDescription, args: string[]): Promise console.log(err)); + const childProcess = cp.fork(join(__dirname, '../../../server-main.js'), cmdLine, { stdio: 'inherit' }); + childProcess.on('error', err => console.log(err)); return; } @@ -270,7 +269,7 @@ export async function main(desc: ProductDescription, args: string[]): Promise process.stdout.write(data)); - cp.stderr.on('data', data => process.stderr.write(data)); + const childProcess = cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: ['inherit', 'pipe', 'pipe'] }); + childProcess.stdout.on('data', data => process.stdout.write(data)); + childProcess.stderr.on('data', data => process.stderr.write(data)); } else { - _cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: 'inherit' }); + cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: 'inherit' }); } } } else { @@ -357,7 +356,7 @@ export async function main(desc: ProductDescription, args: string[]): Promise setTimeout(res, 1000)); } } @@ -376,7 +375,7 @@ function openInBrowser(args: string[], verbose: boolean) { for (const location of args) { try { if (/^(http|https|file):\/\//.test(location)) { - uris.push(_url.parse(location).href); + uris.push(url.parse(location).href); } else { uris.push(pathToURI(location).href); } @@ -406,7 +405,7 @@ function sendToPipe(args: PipeCommand, verbose: boolean): Promise { return; } - const opts: _http.RequestOptions = { + const opts: http.RequestOptions = { socketPath: cliPipe, path: '/', method: 'POST', @@ -416,7 +415,7 @@ function sendToPipe(args: PipeCommand, verbose: boolean): Promise { } }; - const req = _http.request(opts, res => { + const req = http.request(opts, res => { if (res.headers['content-type'] !== 'application/json') { reject('Error in response: Invalid content type: Expected \'application/json\', is: ' + res.headers['content-type']); return; @@ -461,18 +460,18 @@ function fatal(message: string, err: any): void { const preferredCwd = process.env.PWD || cwd(); // prefer process.env.PWD as it does not follow symlinks -function pathToURI(input: string): _url.URL { +function pathToURI(input: string): url.URL { input = input.trim(); input = resolve(preferredCwd, input); - return _url.pathToFileURL(input); + return url.pathToFileURL(input); } function translatePath(input: string, mapFileUri: (input: string) => string, folderURIS: string[], fileURIS: string[]) { const url = pathToURI(input); const mappedUri = mapFileUri(url.href); try { - const stat = _fs.lstatSync(_fs.realpathSync(input)); + const stat = fs.lstatSync(fs.realpathSync(input)); if (stat.isFile()) { fileURIS.push(mappedUri); diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index a8ac6043a51..2abd1e1366c 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -221,7 +221,7 @@ export class WebClientServer { return serveError(req, res, status, text || `Request failed with status ${status}`); } - const responseHeaders: Record = Object.create(null); + const responseHeaders: Record = Object.create(null); const setResponseHeader = (header: string) => { const value = context.res.headers[header]; if (value) { diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 5780ec8797e..c1500981d23 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -19,6 +19,7 @@ import { IAuthenticationUsageService } from 'vs/workbench/services/authenticatio import { getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { CancellationError } from 'vs/base/common/errors'; interface AuthenticationForceNewSessionOptions { detail?: string; @@ -159,6 +160,31 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu return result ?? false; } + private async continueWithIncorrectAccountPrompt(chosenAccountLabel: string, requestedAccountLabel: string): Promise { + const result = await this.dialogService.prompt({ + message: nls.localize('incorrectAccount', "Incorrect account detected"), + detail: nls.localize('incorrectAccountDetail', "The chosen account, {0}, does not match the requested account, {1}.", chosenAccountLabel, requestedAccountLabel), + type: Severity.Warning, + cancelButton: true, + buttons: [ + { + label: nls.localize('keep', 'Keep {0}', chosenAccountLabel), + run: () => chosenAccountLabel + }, + { + label: nls.localize('loginWith', 'Login with {0}', requestedAccountLabel), + run: () => requestedAccountLabel + } + ], + }); + + if (!result.result) { + throw new CancellationError(); + } + + return result.result === chosenAccountLabel; + } + private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { const sessions = await this.authenticationService.getSessions(providerId, scopes, options.account, true); const provider = this.authenticationService.getProvider(providerId); @@ -174,21 +200,21 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu throw new Error('Invalid combination of options. Please remove one of the following: createIfNone, silent'); } + if (options.clearSessionPreference) { + // Clearing the session preference is usually paired with createIfNone, so just remove the preference and + // defer to the rest of the logic in this function to choose the session. + this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); + } + // Check if the sessions we have are valid if (!options.forceNewSession && sessions.length) { if (provider.supportsMultipleAccounts) { - if (options.clearSessionPreference) { - // Clearing the session preference is usually paired with createIfNone, so just remove the preference and - // defer to the rest of the logic in this function to choose the session. - this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); - } else { - // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. - const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); - if (existingSessionPreference) { - const matchingSession = sessions.find(session => session.id === existingSessionPreference); - if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { - return matchingSession; - } + // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. + const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); + if (existingSessionPreference) { + const matchingSession = sessions.find(session => session.id === existingSessionPreference); + if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { + return matchingSession; } } } else if (this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { @@ -212,18 +238,25 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu throw new Error('User did not consent to login.'); } - let session; + let session: AuthenticationSession; if (sessions?.length && !options.forceNewSession) { session = provider.supportsMultipleAccounts && !options.account ? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions) : sessions[0]; } else { - let account: AuthenticationSessionAccount | undefined = options.account; - if (!account) { + let accountToCreate: AuthenticationSessionAccount | undefined = options.account; + if (!accountToCreate) { const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); - account = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate)?.account : undefined; + accountToCreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate)?.account : undefined; } - session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, account }); + + do { + session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, account: accountToCreate }); + } while ( + accountToCreate + && accountToCreate.label !== session.account.label + && !await this.continueWithIncorrectAccountPrompt(session.account.label, accountToCreate.label) + ); } this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); @@ -261,15 +294,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } async $getAccounts(providerId: string): Promise> { - const sessions = await this.authenticationService.getSessions(providerId); - const accounts = new Array(); - const seenAccounts = new Set(); - for (const session of sessions) { - if (!seenAccounts.has(session.account.label)) { - seenAccounts.add(session.account.label); - accounts.push(session.account); - } - } + const accounts = await this.authenticationService.getAccounts(providerId); return accounts; } diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 35126b7998d..3a403ad039c 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -21,11 +21,11 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostChatAgentsShape2, ExtHostContext, IChatParticipantMetadata, IChatProgressDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { ChatAgentLocation, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatContentReference, IChatFollowup, IChatProgress, IChatService, IChatTask, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; @@ -76,6 +76,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA private readonly _agentCompletionProviders = this._register(new DisposableMap()); private readonly _agentIdsToCompletionProviders = this._register(new DisposableMap); + private readonly _chatParticipantDetectionProviders = this._register(new DisposableMap()); + private readonly _pendingProgress = new Map void>(); private readonly _proxy: ExtHostChatAgentsShape2; @@ -172,7 +174,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA disposable = this._chatAgentService.registerDynamicAgent( { id, - name: dynamicProps.name ?? '', // This case is for an API change and can be removed tomorrow + name: dynamicProps.name, description: dynamicProps.description, extensionId: extension, extensionDisplayName: extensionDescription?.displayName ?? extension.value, @@ -298,6 +300,20 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._agentCompletionProviders.deleteAndDispose(handle); this._agentIdsToCompletionProviders.deleteAndDispose(id); } + + $registerChatParticipantDetectionProvider(handle: number): void { + this._chatParticipantDetectionProviders.set(handle, this._chatAgentService.registerChatParticipantDetectionProvider(handle, + { + provideParticipantDetection: async (request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation; participants: IChatParticipantMetadata[] }, token: CancellationToken) => { + return await this._proxy.$detectChatParticipant(handle, request, { history }, options, token); + } + } + )); + } + + $unregisterChatParticipantDetectionProvider(handle: number): void { + this._chatParticipantDetectionProviders.deleteAndDispose(handle); + } } diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index a0f00ce4416..1504c0d2029 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -69,13 +69,13 @@ export class MainThreadCommentThread implements languages.CommentThread { private readonly _onDidChangeLabel = new Emitter(); readonly onDidChangeLabel: Event = this._onDidChangeLabel.event; - private _comments: languages.Comment[] | undefined; + private _comments: ReadonlyArray | undefined; - public get comments(): languages.Comment[] | undefined { + public get comments(): ReadonlyArray | undefined { return this._comments; } - public set comments(newComments: languages.Comment[] | undefined) { + public set comments(newComments: ReadonlyArray | undefined) { this._comments = newComments; this._onDidChangeComments.fire(this._comments); } @@ -204,7 +204,7 @@ export class MainThreadCommentThread implements languages.CommentThread { if (modified('range')) { this._range = changes.range!; } if (modified('label')) { this._label = changes.label; } if (modified('contextValue')) { this._contextValue = changes.contextValue === null ? undefined : changes.contextValue; } - if (modified('comments')) { this._comments = changes.comments; } + if (modified('comments')) { this.comments = changes.comments; } if (modified('collapseState')) { this.initialCollapsibleState = changes.collapseState; } if (modified('canReply')) { this.canReply = changes.canReply!; } if (modified('state')) { this.state = changes.state!; } @@ -212,6 +212,10 @@ export class MainThreadCommentThread implements languages.CommentThread { if (modified('isTemplate')) { this._isTemplate = changes.isTemplate!; } } + hasComments(): boolean { + return !!this.comments && this.comments.length > 0; + } + dispose() { this._isDisposed = true; this._onDidChangeCollapsibleState.dispose(); @@ -642,7 +646,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments provider.updateCommentingRanges(resourceHints); } - async $revealCommentThread(handle: number, commentThreadHandle: number, options: languages.CommentThreadRevealOptions): Promise { + async $revealCommentThread(handle: number, commentThreadHandle: number, commentUniqueIdInThread: number, options: languages.CommentThreadRevealOptions): Promise { const provider = this._commentControllers.get(handle); if (!provider) { @@ -654,7 +658,24 @@ export class MainThreadComments extends Disposable implements MainThreadComments return Promise.resolve(); } - revealCommentThread(this._commentService, this._editorService, this._uriIdentityService, thread, undefined, options.focusReply, undefined, options.preserveFocus); + const comment = thread.comments?.find(comment => comment.uniqueIdInThread === commentUniqueIdInThread); + + revealCommentThread(this._commentService, this._editorService, this._uriIdentityService, thread, comment, options.focusReply, undefined, options.preserveFocus); + } + + async $hideCommentThread(handle: number, commentThreadHandle: number): Promise { + const provider = this._commentControllers.get(handle); + + if (!provider) { + return Promise.resolve(); + } + + const thread = provider.getAllComments().find(thread => thread.commentThreadHandle === commentThreadHandle); + if (!thread || !thread.isDocumentCommentThread()) { + return Promise.resolve(); + } + + thread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; } private registerView(commentsViewAlreadyRegistered: boolean) { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 4aa61aeab45..629af25ae4c 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { createStringDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from 'vs/base/common/dataTransfer'; @@ -321,7 +320,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread selector: selector, provideMultiDocumentHighlights: (model: ITextModel, position: EditorPosition, otherModels: ITextModel[], token: CancellationToken): Promise | undefined> => { return this._proxy.$provideMultiDocumentHighlights(handle, model.uri, position, otherModels.map(model => model.uri), token).then(dto => { - if (isFalsyOrEmpty(dto)) { + // dto should be non-null + non-undefined + // dto length of 0 is valid, just no highlights, pass this through. + if (dto === undefined || dto === null) { return undefined; } const result = new ResourceMap(); @@ -1001,8 +1002,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- mapped edits - $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[]): void { - const provider = new MainThreadMappedEditsProvider(handle, this._proxy, this._uriIdentService); + $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void { + const provider = new MainThreadMappedEditsProvider(displayName, handle, this._proxy, this._uriIdentService); this._registrations.set(handle, this._languageFeaturesService.mappedEditsProvider.register(selector, provider)); } } @@ -1241,6 +1242,7 @@ export class MainThreadDocumentRangeSemanticTokensProvider implements languages. export class MainThreadMappedEditsProvider implements languages.MappedEditsProvider { constructor( + public readonly displayName: string, private readonly _handle: number, private readonly _proxy: ExtHostLanguageFeaturesShape, private readonly _uriService: IUriIdentityService, diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index fbda6ced5a3..1e1a25f966c 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -6,7 +6,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import { ExtHostLanguageModelToolsShape, ExtHostContext, MainContext, MainThreadLanguageModelToolsShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IToolData, ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import { IToolData, ILanguageModelToolsService, IToolResult } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadLanguageModelTools) @@ -29,7 +29,7 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre return Array.from(this._languageModelToolsService.getTools()); } - $invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + $invokeTool(name: string, parameters: any, token: CancellationToken): Promise { return this._languageModelToolsService.invokeTool(name, parameters, token); } diff --git a/src/vs/workbench/api/browser/mainThreadQuickOpen.ts b/src/vs/workbench/api/browser/mainThreadQuickOpen.ts index 001745bf29a..7d8ca24b5d0 100644 --- a/src/vs/workbench/api/browser/mainThreadQuickOpen.ts +++ b/src/vs/workbench/api/browser/mainThreadQuickOpen.ts @@ -8,10 +8,12 @@ import { ExtHostContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, Transf import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; interface QuickInputSession { input: IQuickInput; handlesToItems: Map; + store: DisposableStore; } function reviveIconPathUris(iconPath: { dark: URI; light?: URI | undefined }) { @@ -40,6 +42,9 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { } public dispose(): void { + for (const [_id, session] of this.sessions) { + session.store.dispose(); + } } $show(instance: number, options: IPickOptions, token: CancellationToken): Promise { @@ -121,38 +126,40 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { const sessionId = params.id; let session = this.sessions.get(sessionId); if (!session) { - + const store = new DisposableStore(); const input = params.type === 'quickPick' ? this._quickInputService.createQuickPick() : this._quickInputService.createInputBox(); - input.onDidAccept(() => { + store.add(input); + store.add(input.onDidAccept(() => { this._proxy.$onDidAccept(sessionId); - }); - input.onDidTriggerButton(button => { + })); + store.add(input.onDidTriggerButton(button => { this._proxy.$onDidTriggerButton(sessionId, (button as TransferQuickInputButton).handle); - }); - input.onDidChangeValue(value => { + })); + store.add(input.onDidChangeValue(value => { this._proxy.$onDidChangeValue(sessionId, value); - }); - input.onDidHide(() => { + })); + store.add(input.onDidHide(() => { this._proxy.$onDidHide(sessionId); - }); + })); if (params.type === 'quickPick') { // Add extra events specific for quickpick const quickpick = input as IQuickPick; - quickpick.onDidChangeActive(items => { + store.add(quickpick.onDidChangeActive(items => { this._proxy.$onDidChangeActive(sessionId, items.map(item => (item as TransferQuickPickItem).handle)); - }); - quickpick.onDidChangeSelection(items => { + })); + store.add(quickpick.onDidChangeSelection(items => { this._proxy.$onDidChangeSelection(sessionId, items.map(item => (item as TransferQuickPickItem).handle)); - }); - quickpick.onDidTriggerItemButton((e) => { + })); + store.add(quickpick.onDidTriggerItemButton((e) => { this._proxy.$onDidTriggerItemButton(sessionId, (e.item as TransferQuickPickItem).handle, (e.button as TransferQuickInputButton).handle); - }); + })); } session = { input, - handlesToItems: new Map() + handlesToItems: new Map(), + store }; this.sessions.set(sessionId, session); } @@ -212,7 +219,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { $dispose(sessionId: number): Promise { const session = this.sessions.get(sessionId); if (session) { - session.input.dispose(); + session.store.dispose(); this.sessions.delete(sessionId); } return Promise.resolve(undefined); diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index af9d3f401cf..5801f7845d9 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -6,7 +6,7 @@ import { Barrier } from 'vs/base/common/async'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { observableValue } from 'vs/base/common/observable'; +import { derived, observableValue, observableValueOpts } from 'vs/base/common/observable'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol'; @@ -160,19 +160,11 @@ class MainThreadSCMResource implements ISCMResource { } class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { + readonly currentHistoryItemGroupId = derived(this, reader => this.currentHistoryItemGroup.read(reader)?.id); + readonly currentHistoryItemGroupName = derived(this, reader => this.currentHistoryItemGroup.read(reader)?.name); - private _onDidChangeCurrentHistoryItemGroup = new Emitter(); - readonly onDidChangeCurrentHistoryItemGroup = this._onDidChangeCurrentHistoryItemGroup.event; - - private _currentHistoryItemGroup: ISCMHistoryItemGroup | undefined; - get currentHistoryItemGroup(): ISCMHistoryItemGroup | undefined { return this._currentHistoryItemGroup; } - set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined) { - this._currentHistoryItemGroup = historyItemGroup; - this._onDidChangeCurrentHistoryItemGroup.fire(); - } - - private readonly _currentHistoryItemGroupObs = observableValue(this, undefined); - get currentHistoryItemGroupObs() { return this._currentHistoryItemGroupObs; } + private readonly _currentHistoryItemGroup = observableValueOpts({ owner: this, equalsFn: () => false }, undefined); + get currentHistoryItemGroup() { return this._currentHistoryItemGroup; } constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } @@ -180,6 +172,10 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupId1, historyItemGroupId2, CancellationToken.None); } + async resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds: string[]): Promise { + return this.proxy.$resolveHistoryItemGroupCommonAncestor2(this.handle, historyItemGroupIds, CancellationToken.None); + } + async provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise { const historyItems = await this.proxy.$provideHistoryItems(this.handle, historyItemGroupId, options, CancellationToken.None); return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); @@ -206,7 +202,7 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { } $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined): void { - this._currentHistoryItemGroupObs.set(historyItemGroup, undefined); + this._currentHistoryItemGroup.set(historyItemGroup, undefined); } } @@ -244,7 +240,6 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { get inputBoxTextModel(): ITextModel { return this._inputBoxTextModel; } get contextValue(): string { return this._providerId; } - get historyProvider(): ISCMHistoryProvider | undefined { return this._historyProvider; } get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; } get actionButton(): ISCMActionButtonDescriptor | undefined { return this.features.actionButton ?? undefined; } @@ -260,18 +255,14 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private readonly _commitTemplate = observableValue(this, ''); get commitTemplate() { return this._commitTemplate; } - private readonly _onDidChangeHistoryProvider = new Emitter(); - readonly onDidChangeHistoryProvider: Event = this._onDidChangeHistoryProvider.event; - private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; private _quickDiff: IDisposable | undefined; public readonly isSCM: boolean = true; - private _historyProvider: ISCMHistoryProvider | undefined; - private readonly _historyProviderObs = observableValue(this, undefined); - get historyProviderObs() { return this._historyProviderObs; } + private readonly _historyProvider = observableValue(this, undefined); + get historyProvider() { return this._historyProvider; } constructor( private readonly proxy: ExtHostSCMShape, @@ -322,17 +313,11 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this._quickDiff = undefined; } - if (features.hasHistoryProvider && !this._historyProvider) { + if (features.hasHistoryProvider && !this.historyProvider.get()) { const historyProvider = new MainThreadSCMHistoryProvider(this.proxy, this.handle); - this._historyProviderObs.set(historyProvider, undefined); - - this._historyProvider = historyProvider; - this._onDidChangeHistoryProvider.fire(); - } else if (features.hasHistoryProvider === false && this._historyProvider) { - this._historyProviderObs.set(undefined, undefined); - - this._historyProvider = undefined; - this._onDidChangeHistoryProvider.fire(); + this._historyProvider.set(historyProvider, undefined); + } else if (features.hasHistoryProvider === false && this.historyProvider.get()) { + this._historyProvider.set(undefined, undefined); } } @@ -449,12 +434,11 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } $onDidChangeHistoryProviderCurrentHistoryItemGroup(currentHistoryItemGroup?: SCMHistoryItemGroupDto): void { - if (!this._historyProvider) { + if (!this.historyProvider.get()) { return; } - this._historyProvider.currentHistoryItemGroup = currentHistoryItemGroup ?? undefined; - this._historyProviderObs.get()?.$onDidChangeCurrentHistoryItemGroup(currentHistoryItemGroup); + this._historyProvider.get()?.$onDidChangeCurrentHistoryItemGroup(currentHistoryItemGroup); } toJSON(): any { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts index 21092bb8bfe..3429833aa9c 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts @@ -60,7 +60,7 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma // TerminalShellExecution.createDataStream // Debounce events to reduce the message count - when this listener is disposed the events will be flushed instanceDataListeners.get(instanceId)?.dispose(); - instanceDataListeners.set(instanceId, Event.accumulate(e.instance.onData, 50, this._store)(events => this._proxy.$shellExecutionData(instanceId, events.join()))); + instanceDataListeners.set(instanceId, Event.accumulate(e.instance.onData, 50, this._store)(events => this._proxy.$shellExecutionData(instanceId, events.join('')))); })); // onDidEndTerminalShellExecution diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 1b2a115fca2..5f5a2afc39e 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -7,19 +7,18 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ISettableObservable, transaction } from 'vs/base/common/observable'; +import { ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { IMainThreadTestController, ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; +import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestControllerCapability, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ExtHostContext, ExtHostTestingShape, ILocationDto, ITestControllerPatch, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; @@ -29,8 +28,8 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh private readonly diffListener = this._register(new MutableDisposable()); private readonly testProviderRegistrations = new Map; - canRefresh: MutableObservableValue; + label: ISettableObservable; + capabilities: ISettableObservable; disposable: IDisposable; }>(); @@ -48,10 +47,11 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh provideTestFollowups: (req, token) => this.proxy.$provideTestFollowups(req, token), executeTestFollowup: id => this.proxy.$executeTestFollowup(id), disposeTestFollowups: ids => this.proxy.$disposeTestFollowups(ids), + getTestsRelatedToCode: (uri, position, token) => this.proxy.$getTestsRelatedToCode(uri, position, token), })); - this._register(this.testService.onDidCancelTestRun(({ runId }) => { - this.proxy.$cancelExtensionTestRun(runId); + this._register(this.testService.onDidCancelTestRun(({ runId, taskId }) => { + this.proxy.$cancelExtensionTestRun(runId, taskId); })); this._register(Event.debounce(testProfiles.onDidChange, (_last, e) => e)(() => { @@ -225,20 +225,26 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh /** * @inheritdoc */ - public $registerTestController(controllerId: string, labelStr: string, canRefreshValue: boolean) { + public $registerTestController(controllerId: string, _label: string, _capabilities: TestControllerCapability) { const disposable = new DisposableStore(); - const label = disposable.add(new MutableObservableValue(labelStr)); - const canRefresh = disposable.add(new MutableObservableValue(canRefreshValue)); + const label = observableValue(`${controllerId}.label`, _label); + const capabilities = observableValue(`${controllerId}.cap`, _capabilities); const controller: IMainThreadTestController = { id: controllerId, label, - canRefresh, + capabilities, syncTests: () => this.proxy.$syncTests(), refreshTests: token => this.proxy.$refreshTests(controllerId, token), configureRunProfile: id => this.proxy.$configureRunProfile(controllerId, id), runTests: (reqs, token) => this.proxy.$runControllerTests(reqs, token), startContinuousRun: (reqs, token) => this.proxy.$startContinuousRun(reqs, token), expandTest: (testId, levels) => this.proxy.$expandTest(testId, isFinite(levels) ? levels : -1), + getRelatedCode: (testId, token) => this.proxy.$getCodeRelatedToTest(testId, token).then(locations => + locations.map(l => ({ + uri: URI.revive(l.uri), + range: Range.lift(l.range) + })), + ), }; disposable.add(toDisposable(() => this.testProfiles.removeProfile(controllerId))); @@ -247,7 +253,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh this.testProviderRegistrations.set(controllerId, { instance: controller, label, - canRefresh, + capabilities, disposable }); } @@ -261,13 +267,16 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh return; } - if (patch.label !== undefined) { - controller.label.value = patch.label; - } + transaction(tx => { + if (patch.label !== undefined) { + controller.label.set(patch.label, tx); + } + + if (patch.capabilities !== undefined) { + controller.capabilities.set(patch.capabilities, tx); + } + }); - if (patch.canRefresh !== undefined) { - controller.canRefresh.value = patch.canRefresh; - } } /** diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 09ff17ad867..12441286ddc 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -227,6 +227,10 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return this._requestService.lookupAuthorization(authInfo); } + $lookupKerberosAuthorization(url: string): Promise { + return this._requestService.lookupKerberosAuthorization(url); + } + $loadCertificates(): Promise { return this._requestService.loadCertificates(); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index b6b4f5746bb..43c5387670b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { AsyncIterableObject } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -106,7 +107,7 @@ import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/c import { UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; -import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes'; +import { ExcludeSettingOptions, oldToNewTextSearchResult, TextSearchCompleteMessageType, TextSearchCompleteMessageTypeNew, TextSearchContextNew, TextSearchMatchNew } from 'vs/workbench/services/search/common/searchExtTypes'; import type * as vscode from 'vscode'; export interface IExtensionRegistries { @@ -289,13 +290,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I if (typeof options?.forceNewSession === 'object' && options.forceNewSession.learnMore) { checkProposedApiEnabled(extension, 'authLearnMore'); } - if (options?.account) { - checkProposedApiEnabled(extension, 'authGetSessions'); - } return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, getAccounts(providerId: string) { - checkProposedApiEnabled(extension, 'authGetSessions'); return extHostAuthentication.getAccounts(providerId); }, // TODO: remove this after GHPR and Codespaces move off of it @@ -746,15 +743,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return _asExtensionEvent(extHostTerminalService.onDidExecuteTerminalCommand)(listener, thisArg, disposables); }, onDidChangeTerminalShellIntegration(listener, thisArg?, disposables?) { - checkProposedApiEnabled(extension, 'terminalShellIntegration'); return _asExtensionEvent(extHostTerminalShellIntegration.onDidChangeTerminalShellIntegration)(listener, thisArg, disposables); }, onDidStartTerminalShellExecution(listener, thisArg?, disposables?) { - checkProposedApiEnabled(extension, 'terminalShellIntegration'); return _asExtensionEvent(extHostTerminalShellIntegration.onDidStartTerminalShellExecution)(listener, thisArg, disposables); }, onDidEndTerminalShellExecution(listener, thisArg?, disposables?) { - checkProposedApiEnabled(extension, 'terminalShellIntegration'); return _asExtensionEvent(extHostTerminalShellIntegration.onDidEndTerminalShellExecution)(listener, thisArg, disposables); }, get state() { @@ -966,10 +960,25 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // Note, undefined/null have different meanings on "exclude" return extHostWorkspace.findFiles(include, exclude, maxResults, extension.identifier, token); }, - findFiles2: (filePattern, options?, token?) => { + findFiles2: (filePattern: vscode.GlobPattern, options?: vscode.FindFiles2Options, token?: vscode.CancellationToken): Thenable => { checkProposedApiEnabled(extension, 'findFiles2'); return extHostWorkspace.findFiles2(filePattern, options, extension.identifier, token); }, + findFiles2New: (filePattern: vscode.GlobPattern[], options?: vscode.FindFiles2OptionsNew, token?: vscode.CancellationToken): Thenable => { + checkProposedApiEnabled(extension, 'findFiles2New'); + + const oldOptions = { + exclude: options?.exclude && options.exclude.length > 0 ? options.exclude[0] : undefined, + useDefaultExcludes: !options?.useExcludeSettings || (options?.useExcludeSettings === ExcludeSettingOptions.FilesExclude || options?.useExcludeSettings === ExcludeSettingOptions.SearchAndFilesExclude), + useDefaultSearchExcludes: !options?.useExcludeSettings || (options?.useExcludeSettings === ExcludeSettingOptions.SearchAndFilesExclude), + maxResults: options?.maxResults, + useIgnoreFiles: options?.useIgnoreFiles?.local, + useGlobalIgnoreFiles: options?.useIgnoreFiles?.global, + useParentIgnoreFiles: options?.useIgnoreFiles?.parent, + followSymlinks: options?.followSymlinks, + }; + return extHostWorkspace.findFiles2(filePattern && filePattern.length > 0 ? filePattern[0] : undefined, oldOptions, extension.identifier, token); + }, findTextInFiles: (query: vscode.TextSearchQuery, optionsOrCallback: vscode.FindTextInFilesOptions | ((result: vscode.TextSearchResult) => void), callbackOrToken?: vscode.CancellationToken | ((result: vscode.TextSearchResult) => void), token?: vscode.CancellationToken) => { checkProposedApiEnabled(extension, 'findTextInFiles'); let options: vscode.FindTextInFilesOptions; @@ -986,6 +995,60 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostWorkspace.findTextInFiles(query, options || {}, callback, extension.identifier, token); }, + findTextInFilesNew: (query: vscode.TextSearchQueryNew, options?: vscode.FindTextInFilesOptionsNew, token?: vscode.CancellationToken): vscode.FindTextInFilesResponse => { + checkProposedApiEnabled(extension, 'findTextInFilesNew'); + checkProposedApiEnabled(extension, 'textSearchProviderNew'); + let oldOptions = {}; + + + if (options) { + oldOptions = { + include: options.include && options.include.length > 0 ? options.include[0] : undefined, + exclude: options.exclude && options.exclude.length > 0 ? options.exclude[0] : undefined, + useDefaultExcludes: options.useExcludeSettings === undefined || (options.useExcludeSettings === ExcludeSettingOptions.FilesExclude || options.useExcludeSettings === ExcludeSettingOptions.SearchAndFilesExclude), + useSearchExclude: options.useExcludeSettings === undefined || (options.useExcludeSettings === ExcludeSettingOptions.SearchAndFilesExclude), + maxResults: options.maxResults, + useIgnoreFiles: options.useIgnoreFiles?.local, + useGlobalIgnoreFiles: options.useIgnoreFiles?.global, + useParentIgnoreFiles: options.useIgnoreFiles?.parent, + followSymlinks: options.followSymlinks, + encoding: options.encoding, + previewOptions: options.previewOptions ? { + matchLines: options.previewOptions?.numMatchLines ?? 100, + charsPerLine: options.previewOptions?.charsPerLine ?? 10000, + } : undefined, + beforeContext: options.surroundingContext, + afterContext: options.surroundingContext, + } satisfies vscode.FindTextInFilesOptions & { useSearchExclude?: boolean }; + } + + const complete: Promise = Promise.resolve(undefined); + + const asyncIterable = new AsyncIterableObject(async emitter => { + const callback = async (result: vscode.TextSearchResult) => { + emitter.emitOne(oldToNewTextSearchResult(result)); + return result; + }; + await complete.then(e => { + return extHostWorkspace.findTextInFiles( + query, + oldOptions, + callback, + extension.identifier, + token + ); + }); + }); + + return { + results: asyncIterable, + complete: complete.then((e) => { + return { + limitHit: e?.limitHit ?? false + }; + }), + }; + }, save: (uri) => { return extHostWorkspace.save(uri); }, @@ -1039,6 +1102,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return uriPromise.then(uri => { + extHostLogService.trace(`openTextDocument from ${extension.identifier}`); if (uri.scheme === Schemas.vscodeRemote && !uri.authority) { extHostApiDeprecation.report('workspace.openTextDocument', extension, `A URI of 'vscode-remote' scheme requires an authority.`); } @@ -1134,6 +1198,20 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'textSearchProvider'); return extHostSearch.registerAITextSearchProvider(scheme, provider); }, + registerFileSearchProviderNew: (scheme: string, provider: vscode.FileSearchProviderNew) => { + checkProposedApiEnabled(extension, 'fileSearchProviderNew'); + return { dispose: () => { } }; + }, + registerTextSearchProviderNew: (scheme: string, provider: vscode.TextSearchProviderNew) => { + checkProposedApiEnabled(extension, 'textSearchProviderNew'); + return { dispose: () => { } }; + }, + registerAITextSearchProviderNew: (scheme: string, provider: vscode.AITextSearchProviderNew) => { + // there are some dependencies on textSearchProvider, so we need to check for both + checkProposedApiEnabled(extension, 'aiTextSearchProviderNew'); + checkProposedApiEnabled(extension, 'textSearchProviderNew'); + return { dispose: () => { } }; + }, registerRemoteAuthorityResolver: (authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver) => { checkProposedApiEnabled(extension, 'resolvers'); return extensionService.registerRemoteAuthorityResolver(authorityPrefix, resolver); @@ -1435,6 +1513,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatParticipantPrivate'); return extHostChatAgents2.createDynamicChatAgent(extension, id, dynamicProps, handler); }, + registerChatParticipantDetectionProvider(provider: vscode.ChatParticipantDetectionProvider) { + checkProposedApiEnabled(extension, 'chatParticipantAdditions'); + return extHostChatAgents2.registerChatParticipantDetectionProvider(provider); + }, }; // namespace: lm @@ -1539,6 +1621,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState, CommentThreadState: extHostTypes.CommentThreadState, CommentThreadApplicability: extHostTypes.CommentThreadApplicability, + CommentThreadFocus: extHostTypes.CommentThreadFocus, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, CompletionItemTag: extHostTypes.CompletionItemTag, @@ -1598,6 +1681,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I Position: extHostTypes.Position, ProcessExecution: extHostTypes.ProcessExecution, ProgressLocation: extHostTypes.ProgressLocation, + QuickInputButtonLocation: extHostTypes.QuickInputButtonLocation, QuickInputButtons: extHostTypes.QuickInputButtons, Range: extHostTypes.Range, RelativePattern: extHostTypes.RelativePattern, @@ -1686,6 +1770,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TestResultState: extHostTypes.TestResultState, TestRunRequest: extHostTypes.TestRunRequest, TestMessage: extHostTypes.TestMessage, + TestMessage2: extHostTypes.TestMessage, + TestMessageStackFrame: extHostTypes.TestMessageStackFrame, TestTag: extHostTypes.TestTag, TestRunProfileKind: extHostTypes.TestRunProfileKind, TextSearchCompleteMessageType: TextSearchCompleteMessageType, @@ -1731,12 +1817,15 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseProgressPart: extHostTypes.ChatResponseProgressPart, ChatResponseProgressPart2: extHostTypes.ChatResponseProgressPart2, ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, + ChatResponseReferencePart2: extHostTypes.ChatResponseReferencePart, + ChatResponseCodeCitationPart: extHostTypes.ChatResponseCodeCitationPart, ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatResponseDetectedParticipantPart: extHostTypes.ChatResponseDetectedParticipantPart, ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, + ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, @@ -1753,6 +1842,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I NewSymbolNameTriggerKind: extHostTypes.NewSymbolNameTriggerKind, InlineEdit: extHostTypes.InlineEdit, InlineEditTriggerKind: extHostTypes.InlineEditTriggerKind, + ExcludeSettingOptions: ExcludeSettingOptions, + TextSearchContextNew: TextSearchContextNew, + TextSearchMatchNew: TextSearchMatchNew, + TextSearchCompleteMessageTypeNew: TextSearchCompleteMessageTypeNew, }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7bbab68a618..ad8ed5c219e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -39,6 +39,7 @@ import { IMarkerData } from 'vs/platform/markers/common/markers'; import { IProgressOptions, IProgressStep } from 'vs/platform/progress/common/progress'; import * as quickInput from 'vs/platform/quickinput/common/quickInput'; import { IRemoteConnectionData, TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; @@ -52,10 +53,10 @@ import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; -import { IToolData, IToolDelta } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import { ChatAgentVoteDirection, IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IToolData, IToolDelta, IToolResult } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -65,7 +66,7 @@ import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { IWorkspaceSymbol, NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search'; import { IRawClosedNotebookFileMatch } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechToTextEvent, ITextToSpeechEvent } from 'vs/workbench/contrib/speech/common/speechService'; -import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; +import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestControllerCapability, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { RelatedInformationResult, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; @@ -82,7 +83,6 @@ import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from 'vs/workbench import * as search from 'vs/workbench/services/search/common/search'; import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import type { TerminalShellExecutionCommandLineConfidence } from 'vscode'; -import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; export interface IWorkspaceData extends IStaticWorkspaceData { folders: { uri: UriComponents; name: string; index: number }[]; @@ -149,7 +149,8 @@ export interface MainThreadCommentsShape extends IDisposable { $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; $updateCommentingRanges(handle: number, resourceHints?: languages.CommentingRangeResourceHint): void; - $revealCommentThread(handle: number, commentThreadHandle: number, options: languages.CommentThreadRevealOptions): Promise; + $revealCommentThread(handle: number, commentThreadHandle: number, commentUniqueIdInThread: number, options: languages.CommentThreadRevealOptions): Promise; + $hideCommentThread(handle: number, commentThreadHandle: number): void; } export interface AuthenticationForceNewSessionOptions { @@ -445,7 +446,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $resolvePasteFileData(handle: number, requestId: number, dataId: string): Promise; $resolveDocumentOnDropFileData(handle: number, requestId: number, dataId: string): Promise; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; - $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[]): void; + $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void; } export interface MainThreadLanguagesShape extends IDisposable { @@ -1243,6 +1244,8 @@ export interface IDynamicChatAgentProps { export interface MainThreadChatAgentsShape2 extends IDisposable { $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): void; + $registerChatParticipantDetectionProvider(handle: number): void; + $unregisterChatParticipantDetectionProvider(handle: number): void; $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number, id: string): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; @@ -1283,6 +1286,17 @@ export interface ExtHostChatAgentsShape2 { $provideWelcomeMessage(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined>; $provideSampleQuestions(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise; $releaseSession(sessionId: string): void; + $detectChatParticipant(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise; +} +export interface IChatParticipantMetadata { + participant: string; + command?: string; + description?: string; +} + +export interface IChatParticipantDetectionResult { + participant: string; + command?: string; } export type IChatVariableResolverProgressDto = @@ -1295,8 +1309,8 @@ export interface MainThreadChatVariablesShape extends IDisposable { } export interface MainThreadLanguageModelToolsShape extends IDisposable { - $getTools(): Promise; - $invokeTool(name: string, parameters: any, token: CancellationToken): Promise; + $getTools(): Promise[]>; + $invokeTool(name: string, parameters: any, token: CancellationToken): Promise; $registerTool(id: string): void; $unregisterTool(name: string): void; } @@ -1309,7 +1323,7 @@ export interface ExtHostChatVariablesShape { export interface ExtHostLanguageModelToolsShape { $acceptToolDelta(delta: IToolDelta): Promise; - $invokeTool(id: string, parameters: any, token: CancellationToken): Promise; + $invokeTool(id: string, parameters: any, token: CancellationToken): Promise; } export interface MainThreadUrlsShape extends IDisposable { @@ -1387,6 +1401,7 @@ export interface MainThreadWorkspaceShape extends IDisposable { $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents; name?: string }[]): Promise; $resolveProxy(url: string): Promise; $lookupAuthorization(authInfo: AuthInfo): Promise; + $lookupKerberosAuthorization(url: string): Promise; $loadCertificates(): Promise; $requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; $registerEditSessionIdentityProvider(handle: number, scheme: string): void; @@ -1515,6 +1530,7 @@ export type SCMRawResourceSplices = [ export interface SCMHistoryItemGroupDto { readonly id: string; readonly name: string; + readonly revision?: string; readonly base?: Omit, 'remote'>; readonly remote?: Omit, 'remote'>; } @@ -2332,6 +2348,7 @@ export interface ExtHostSCMShape { $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>; + $resolveHistoryItemGroupCommonAncestor2(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise; } export interface ExtHostQuickDiffShape { @@ -2727,7 +2744,7 @@ export const enum ExtHostTestingResource { export interface ExtHostTestingShape { $runControllerTests(req: IStartControllerTests[], token: CancellationToken): Promise<{ error?: string }[]>; $startContinuousRun(req: ICallProfileRunHandler[], token: CancellationToken): Promise<{ error?: string }[]>; - $cancelExtensionTestRun(runId: string | undefined): void; + $cancelExtensionTestRun(runId: string | undefined, taskId: string | undefined): void; /** Handles a diff of tests, as a result of a subscribeToDiffs() call */ $acceptDiff(diff: TestsDiffOp.Serialized[]): void; /** Expands a test item's children, by the given number of levels. */ @@ -2744,6 +2761,8 @@ export interface ExtHostTestingShape { $syncTests(): Promise; /** Sets the active test run profiles */ $setDefaultRunProfiles(profiles: Record): void; + $getTestsRelatedToCode(uri: UriComponents, position: IPosition, token: CancellationToken): Promise; + $getCodeRelatedToTest(testId: string, token: CancellationToken): Promise; // --- test results: @@ -2772,14 +2791,14 @@ export interface IStringDetails { export interface ITestControllerPatch { label?: string; - canRefresh?: boolean; + capabilities?: TestControllerCapability; } export interface MainThreadTestingShape { // --- test lifecycle: /** Registers that there's a test controller with the given ID */ - $registerTestController(controllerId: string, label: string, canRefresh: boolean): void; + $registerTestController(controllerId: string, label: string, capability: TestControllerCapability): void; /** Updates the label of an existing test controller. */ $updateController(controllerId: string, patch: ITestControllerPatch): void; /** Diposes of the test controller with the given ID */ diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 5535cec0536..6384178b82e 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -88,32 +88,62 @@ const newCommands: ApiCommand[] = [ [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), + new ApiCommand( + 'vscode.experimental.executeDefinitionProvider_recursive', '_executeDefinitionProvider_recursive', 'Execute all definition providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) + ), new ApiCommand( 'vscode.executeTypeDefinitionProvider', '_executeTypeDefinitionProvider', 'Execute all type definition providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), + new ApiCommand( + 'vscode.experimental.executeTypeDefinitionProvider_recursive', '_executeTypeDefinitionProvider_recursive', 'Execute all type definition providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) + ), new ApiCommand( 'vscode.executeDeclarationProvider', '_executeDeclarationProvider', 'Execute all declaration providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), + new ApiCommand( + 'vscode.experimental.executeDeclarationProvider_recursive', '_executeDeclarationProvider_recursive', 'Execute all declaration providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) + ), new ApiCommand( 'vscode.executeImplementationProvider', '_executeImplementationProvider', 'Execute all implementation providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), + new ApiCommand( + 'vscode.experimental.executeImplementationProvider_recursive', '_executeImplementationProvider_recursive', 'Execute all implementation providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) + ), new ApiCommand( 'vscode.executeReferenceProvider', '_executeReferenceProvider', 'Execute all reference providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult('A promise that resolves to an array of Location-instances.', tryMapWith(typeConverters.location.to)) ), + new ApiCommand( + 'vscode.experimental.executeReferenceProvider', '_executeReferenceProvider_recursive', 'Execute all reference providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult('A promise that resolves to an array of Location-instances.', tryMapWith(typeConverters.location.to)) + ), // -- hover new ApiCommand( 'vscode.executeHoverProvider', '_executeHoverProvider', 'Execute all hover providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult('A promise that resolves to an array of Hover-instances.', tryMapWith(typeConverters.Hover.to)) ), + new ApiCommand( + 'vscode.experimental.executeHoverProvider_recursive', '_executeHoverProvider_recursive', 'Execute all hover providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult('A promise that resolves to an array of Hover-instances.', tryMapWith(typeConverters.Hover.to)) + ), // -- selection range new ApiCommand( 'vscode.executeSelectionRangeProvider', '_executeSelectionRangeProvider', 'Execute selection range provider.', diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 8b2209b5363..b366aa2a986 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -10,7 +10,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; -import { Disposable, DisposableMap, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; @@ -93,7 +93,7 @@ class ChatAgentResponseStream { }; Promise.all([progressReporterPromise, task?.(progressReporter)]).then(([handle, res]) => { - if (handle !== undefined && res !== undefined) { + if (handle !== undefined) { this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); } }); @@ -158,6 +158,9 @@ class ChatAgentResponseStream { return this; }, reference(value, iconPath) { + return this.reference2(value, iconPath); + }, + reference2(value, iconPath, options) { throwIfDone(this.reference); if ('variableName' in value) { @@ -176,7 +179,7 @@ class ChatAgentResponseStream { } satisfies IChatContentReference)); } else { // Participant sent a variableName reference but the variable produced no references. Show variable reference with no value - const part = new extHostTypes.ChatResponseReferencePart(value, iconPath); + const part = new extHostTypes.ChatResponseReferencePart(value, iconPath, options); const dto = typeConvert.ChatResponseReferencePart.from(part); references = [dto]; } @@ -187,13 +190,21 @@ class ChatAgentResponseStream { // Something went wrong- that variable doesn't actually exist } } else { - const part = new extHostTypes.ChatResponseReferencePart(value, iconPath); + const part = new extHostTypes.ChatResponseReferencePart(value, iconPath, options); const dto = typeConvert.ChatResponseReferencePart.from(part); _report(dto); } return this; }, + codeCitation(value: vscode.Uri, license: string, snippet: string): void { + throwIfDone(this.codeCitation); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseCodeCitationPart(value, license, snippet); + const dto = typeConvert.ChatResponseCodeCitationPart.from(part); + _report(dto); + }, textEdit(target, edits) { throwIfDone(this.textEdit); checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); @@ -212,11 +223,11 @@ class ChatAgentResponseStream { _report(dto); return this; }, - confirmation(title, message, data) { + confirmation(title, message, data, buttons) { throwIfDone(this.confirmation); checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); - const part = new extHostTypes.ChatResponseConfirmationPart(title, message, data); + const part = new extHostTypes.ChatResponseConfirmationPart(title, message, data, buttons); const dto = typeConvert.ChatResponseConfirmationPart.from(part); _report(dto); return this; @@ -229,14 +240,15 @@ class ChatAgentResponseStream { part instanceof extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart || part instanceof extHostTypes.ChatResponseDetectedParticipantPart || part instanceof extHostTypes.ChatResponseWarningPart || - part instanceof extHostTypes.ChatResponseConfirmationPart + part instanceof extHostTypes.ChatResponseConfirmationPart || + part instanceof extHostTypes.ChatResponseCodeCitationPart ) { checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); } if (part instanceof extHostTypes.ChatResponseReferencePart) { // Ensure variable reference values get fixed up - this.reference(part.value, part.iconPath); + this.reference2(part.value, part.iconPath, part.options); } else { const dto = typeConvert.ChatResponsePart.from(part, that._commandsConverter, that._sessionDisposables); _report(dto); @@ -258,6 +270,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private readonly _agents = new Map(); private readonly _proxy: MainThreadChatAgentsShape2; + private static _participantDetectionProviderIdPool = 0; + private readonly _participantDetectionProviders = new Map(); + private readonly _sessionDisposables: DisposableMap = this._register(new DisposableMap()); private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); @@ -293,44 +308,78 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return agent.apiAgent; } + registerChatParticipantDetectionProvider(provider: vscode.ChatParticipantDetectionProvider): vscode.Disposable { + const handle = ExtHostChatAgents2._participantDetectionProviderIdPool++; + this._participantDetectionProviders.set(handle, provider); + this._proxy.$registerChatParticipantDetectionProvider(handle); + return toDisposable(() => { + this._participantDetectionProviders.delete(handle); + this._proxy.$unregisterChatParticipantDetectionProvider(handle); + }); + } + + async $detectChatParticipant(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise { + const { request, location, history } = await this._createRequest(requestDto, context); + + const provider = this._participantDetectionProviders.get(handle); + if (!provider) { + return undefined; + } + + return provider.provideParticipantDetection( + typeConvert.ChatAgentRequest.to(request, location), + { history }, + { participants: options.participants, location: typeConvert.ChatLocation.to(options.location) }, + token + ); + } + + private async _createRequest(requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }) { + const request = revive(requestDto); + const convertedHistory = await this.prepareHistoryTurns(request.agentId, context); + + // in-place converting for location-data + let location: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined; + if (request.locationData?.type === ChatAgentLocation.Editor) { + // editor data + const document = this._documents.getDocument(request.locationData.document); + location = new extHostTypes.ChatRequestEditorData(document, typeConvert.Selection.to(request.locationData.selection), typeConvert.Range.to(request.locationData.wholeRange)); + + } else if (request.locationData?.type === ChatAgentLocation.Notebook) { + // notebook data + const cell = this._documents.getDocument(request.locationData.sessionInputUri); + location = new extHostTypes.ChatRequestNotebookData(cell); + + } else if (request.locationData?.type === ChatAgentLocation.Terminal) { + // TBD + } + + return { request, location, history: convertedHistory }; + } + async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { throw new Error(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`); } - const request = revive(requestDto); + let stream: ChatAgentResponseStream | undefined; - // Init session disposables - let sessionDisposables = this._sessionDisposables.get(request.sessionId); - if (!sessionDisposables) { - sessionDisposables = new DisposableStore(); - this._sessionDisposables.set(request.sessionId, sessionDisposables); - } - - const stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables); try { - const convertedHistory = await this.prepareHistoryTurns(request.agentId, context); + const { request, location, history } = await this._createRequest(requestDto, context); - // in-place converting for location-data - let location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined; - if (request.locationData?.type === ChatAgentLocation.Editor) { - // editor data - const document = this._documents.getDocument(request.locationData.document); - location2 = new extHostTypes.ChatRequestEditorData(document, typeConvert.Selection.to(request.locationData.selection), typeConvert.Range.to(request.locationData.wholeRange)); - - } else if (request.locationData?.type === ChatAgentLocation.Notebook) { - // notebook data - const cell = this._documents.getDocument(request.locationData.sessionInputUri); - location2 = new extHostTypes.ChatRequestNotebookData(cell); - - } else if (request.locationData?.type === ChatAgentLocation.Terminal) { - // TBD + // Init session disposables + let sessionDisposables = this._sessionDisposables.get(request.sessionId); + if (!sessionDisposables) { + sessionDisposables = new DisposableStore(); + this._sessionDisposables.set(request.sessionId, sessionDisposables); } + stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables); + const task = agent.invoke( - typeConvert.ChatAgentRequest.to(request, location2), - { history: convertedHistory }, + typeConvert.ChatAgentRequest.to(request, location), + { history }, stream.apiObject, token ); @@ -342,7 +391,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } catch (err) { const msg = `result.metadata MUST be JSON.stringify-able. Got error: ${err.message}`; this._logService.error(`[${agent.extension.identifier.value}] [@${agent.id}] ${msg}`, agent.extension); - return { errorDetails: { message: msg }, timings: stream.timings }; + return { errorDetails: { message: msg }, timings: stream?.timings, nextQuestion: result.nextQuestion }; } } let errorDetails: IChatResponseErrorDetails | undefined; @@ -356,7 +405,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS checkProposedApiEnabled(agent.extension, 'chatParticipantPrivate'); } - return { errorDetails, timings: stream.timings, metadata: result?.metadata } satisfies IChatAgentResult; + return { errorDetails, timings: stream?.timings, metadata: result?.metadata, nextQuestion: result?.nextQuestion } satisfies IChatAgentResult; }), token); } catch (e) { this._logService.error(e, agent.extension); @@ -368,7 +417,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return { errorDetails: { message: toErrorMessage(e), responseIsIncomplete: true } }; } finally { - stream.close(); + stream?.close(); } } @@ -383,7 +432,10 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS { ...ehResult, metadata: undefined }; // REQUEST turn - res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentValueReference.to), h.request.agentId)); + const varsWithoutTools = h.request.variables.variables + .filter(v => !v.isTool) + .map(typeConvert.ChatAgentValueReference.to); + res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId)); // RESPONSE turn const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.toContent(r, this._commands.converter))); diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index c84fb4782f9..05761198ee5 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -463,7 +463,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo set label(value: string | undefined) { that.label = value; }, get state(): vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined { return that.state; }, set state(value: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }) { that.state = value; }, - reveal: (options?: vscode.CommentThreadRevealOptions) => that.reveal(options), + reveal: (comment?: vscode.Comment | vscode.CommentThreadRevealOptions, options?: vscode.CommentThreadRevealOptions) => that.reveal(comment, options), + hide: () => that.hide(), dispose: () => { that.dispose(); } @@ -547,9 +548,29 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return; } - async reveal(options?: vscode.CommentThreadRevealOptions): Promise { + async reveal(commentOrOptions?: vscode.Comment | vscode.CommentThreadRevealOptions, options?: vscode.CommentThreadRevealOptions): Promise { checkProposedApiEnabled(this.extensionDescription, 'commentReveal'); - return proxy.$revealCommentThread(this._commentControllerHandle, this.handle, { preserveFocus: false, focusReply: false, ...options }); + let comment: vscode.Comment | undefined; + if (commentOrOptions && (commentOrOptions as vscode.Comment).body !== undefined) { + comment = commentOrOptions as vscode.Comment; + } else { + options = options ?? commentOrOptions as vscode.CommentThreadRevealOptions; + } + let commentToReveal = comment ? this._commentsMap.get(comment) : undefined; + commentToReveal ??= this._commentsMap.get(this._comments[0])!; + let preserveFocus: boolean = true; + let focusReply: boolean = false; + if (options?.focus === types.CommentThreadFocus.Reply) { + focusReply = true; + preserveFocus = false; + } else if (options?.focus === types.CommentThreadFocus.Comment) { + preserveFocus = false; + } + return proxy.$revealCommentThread(this._commentControllerHandle, this.handle, commentToReveal, { preserveFocus, focusReply }); + } + + async hide(): Promise { + return proxy.$hideCommentThread(this._commentControllerHandle, this.handle); } dispose() { @@ -620,11 +641,11 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return this._activeComment; } - private _activeThread: vscode.CommentThread2 | undefined; + private _activeThread: ExtHostCommentThread | undefined; get activeCommentThread(): vscode.CommentThread2 | undefined { checkProposedApiEnabled(this._extension, 'activeComment'); - return this._activeThread; + return this._activeThread?.value; } private _localDisposables: types.Disposable[]; diff --git a/src/vs/workbench/api/common/extHostExtensionActivator.ts b/src/vs/workbench/api/common/extHostExtensionActivator.ts index 2c51f37aed2..d6b20d6103c 100644 --- a/src/vs/workbench/api/common/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/common/extHostExtensionActivator.ts @@ -5,7 +5,7 @@ import type * as vscode from 'vscode'; import * as errors from 'vs/base/common/errors'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ExtensionIdentifier, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions'; import { ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; @@ -119,7 +119,7 @@ export class ActivatedExtension { public readonly activationTimes: ExtensionActivationTimes; public readonly module: IExtensionModule; public readonly exports: IExtensionAPI | undefined; - public readonly subscriptions: IDisposable[]; + public readonly disposable: IDisposable; constructor( activationFailed: boolean, @@ -127,32 +127,32 @@ export class ActivatedExtension { activationTimes: ExtensionActivationTimes, module: IExtensionModule, exports: IExtensionAPI | undefined, - subscriptions: IDisposable[] + disposable: IDisposable ) { this.activationFailed = activationFailed; this.activationFailedError = activationFailedError; this.activationTimes = activationTimes; this.module = module; this.exports = exports; - this.subscriptions = subscriptions; + this.disposable = disposable; } } export class EmptyExtension extends ActivatedExtension { constructor(activationTimes: ExtensionActivationTimes) { - super(false, null, activationTimes, { activate: undefined, deactivate: undefined }, undefined, []); + super(false, null, activationTimes, { activate: undefined, deactivate: undefined }, undefined, Disposable.None); } } export class HostExtension extends ActivatedExtension { constructor() { - super(false, null, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, []); + super(false, null, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, Disposable.None); } } class FailedExtension extends ActivatedExtension { constructor(activationError: Error) { - super(true, activationError, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, []); + super(true, activationError, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, Disposable.None); } } diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 97935e89d08..97ecf09ea56 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -10,7 +10,7 @@ import * as path from 'vs/base/common/path'; import * as performance from 'vs/base/common/performance'; import { originalFSPath, joinPath, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { asPromise, Barrier, IntervalTimer, timeout } from 'vs/base/common/async'; -import { dispose, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; @@ -177,10 +177,10 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._secretState = new ExtHostSecretState(this._extHostContext); this._storagePath = storagePath; - this._instaService = instaService.createChild(new ServiceCollection( + this._instaService = this._store.add(instaService.createChild(new ServiceCollection( [IExtHostStorage, this._storage], [IExtHostSecretState, this._secretState] - )); + ))); this._activator = this._register(new ExtensionsActivator( this._myRegistry, @@ -409,9 +409,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme // clean up subscriptions try { - dispose(extension.subscriptions); + extension.disposable.dispose(); } catch (err) { - this._logService.error(`An error occurred when deactivating the subscriptions for extension '${extensionId.value}':`); + this._logService.error(`An error occurred when disposing the subscriptions for extension '${extensionId.value}':`); this._logService.error(err); } @@ -482,26 +482,26 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._logService.info(`ExtensionService#_doActivateExtension ${extensionDescription.identifier.value}, startup: ${reason.startup}, activationEvent: '${reason.activationEvent}'${extensionDescription.identifier.value !== reason.extensionId.value ? `, root cause: ${reason.extensionId.value}` : ``}`); this._logService.flush(); + const extensionInternalStore = new DisposableStore(); // disposables that follow the extension lifecycle const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ this._loadCommonJSModule(extensionDescription, joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), - this._loadExtensionContext(extensionDescription) + this._loadExtensionContext(extensionDescription, extensionInternalStore) ]).then(values => { performance.mark(`code/extHost/willActivateExtension/${extensionDescription.identifier.value}`); - return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); + return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], extensionInternalStore, activationTimesBuilder); }).then((activatedExtension) => { performance.mark(`code/extHost/didActivateExtension/${extensionDescription.identifier.value}`); return activatedExtension; }); } - private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise { + private _loadExtensionContext(extensionDescription: IExtensionDescription, extensionInternalStore: DisposableStore): Promise { - const lanuageModelAccessInformation = this._extHostLanguageModels.createLanguageModelAccessInformation(extensionDescription); - // TODO: These should probably be disposed when the extension deactivates - const globalState = this._register(new ExtensionGlobalMemento(extensionDescription, this._storage)); - const workspaceState = this._register(new ExtensionMemento(extensionDescription.identifier.value, false, this._storage)); - const secrets = this._register(new ExtensionSecrets(extensionDescription, this._secretState)); + const languageModelAccessInformation = this._extHostLanguageModels.createLanguageModelAccessInformation(extensionDescription); + const globalState = extensionInternalStore.add(new ExtensionGlobalMemento(extensionDescription, this._storage)); + const workspaceState = extensionInternalStore.add(new ExtensionMemento(extensionDescription.identifier.value, false, this._storage)); + const secrets = extensionInternalStore.add(new ExtensionSecrets(extensionDescription, this._secretState)); const extensionMode = extensionDescription.isUnderDevelopment ? (this._initData.environment.extensionTestsLocationURI ? ExtensionMode.Test : ExtensionMode.Development) : ExtensionMode.Production; @@ -527,7 +527,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme workspaceState, secrets, subscriptions: [], - get languageModelAccessInformation() { return lanuageModelAccessInformation; }, + get languageModelAccessInformation() { return languageModelAccessInformation; }, get extensionUri() { return extensionDescription.extensionLocation; }, get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); }, @@ -569,7 +569,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }); } - private static _callActivate(logService: ILogService, extensionId: ExtensionIdentifier, extensionModule: IExtensionModule, context: vscode.ExtensionContext, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + private static _callActivate(logService: ILogService, extensionId: ExtensionIdentifier, extensionModule: IExtensionModule, context: vscode.ExtensionContext, extensionInternalStore: IDisposable, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { // Make sure the extension's surface is not undefined extensionModule = extensionModule || { activate: undefined, @@ -577,7 +577,10 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }; return this._callActivateOptional(logService, extensionId, extensionModule, context, activationTimesBuilder).then((extensionExports) => { - return new ActivatedExtension(false, null, activationTimesBuilder.build(), extensionModule, extensionExports, context.subscriptions); + return new ActivatedExtension(false, null, activationTimesBuilder.build(), extensionModule, extensionExports, toDisposable(() => { + extensionInternalStore.dispose(); + dispose(context.subscriptions); + })); }); } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 0706a87cedb..50ab3ea6729 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -2898,7 +2898,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF registerMappedEditsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider): vscode.Disposable { const handle = this._addNewAdapter(new MappedEditsAdapter(this._documents, provider), extension); - this._proxy.$registerMappedEditsProvider(handle, this._transformDocumentSelector(selector, extension)); + this._proxy.$registerMappedEditsProvider(handle, this._transformDocumentSelector(selector, extension), extension.displayName ?? extension.name); return this._createDisposable(handle); } diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index e588f50ab6d..f383a4ad265 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -5,10 +5,11 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostLanguageModelToolsShape, IMainContext, MainContext, MainThreadLanguageModelToolsShape } from 'vs/workbench/api/common/extHost.protocol'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { IToolData, IToolDelta } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import { IToolData, IToolDelta, IToolResult } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import type * as vscode from 'vscode'; export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { @@ -24,14 +25,15 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape this._proxy.$getTools().then(tools => { for (const tool of tools) { - this._allTools.set(tool.name, tool); + this._allTools.set(tool.name, revive(tool)); } }); } - async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { // Making the round trip here because not all tools were necessarily registered in this EH - return await this._proxy.$invokeTool(name, parameters, token); + const result = await this._proxy.$invokeTool(name, parameters, token); + return typeConvert.LanguageModelToolResult.to(result); } async $acceptToolDelta(delta: IToolDelta): Promise { @@ -49,13 +51,14 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape .map(tool => typeConvert.LanguageModelToolDescription.to(tool)); } - async $invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + async $invokeTool(name: string, parameters: any, token: CancellationToken): Promise { const item = this._registeredTools.get(name); if (!item) { throw new Error(`Unknown tool ${name}`); } - return await item.tool.invoke(parameters, token); + const extensionResult = await item.tool.invoke(parameters, token); + return typeConvert.LanguageModelToolResult.from(extensionResult); } registerTool(extension: IExtensionDescription, name: string, tool: vscode.LanguageModelTool): IDisposable { diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index b850be83f86..3a9075f953b 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -13,7 +13,7 @@ import { ExtHostQuickOpenShape, IMainContext, MainContext, TransferQuickInput, T import { URI } from 'vs/base/common/uri'; import { ThemeIcon, QuickInputButtons, QuickPickItemKind, InputBoxValidationSeverity } from 'vs/workbench/api/common/extHostTypes'; import { isCancellationError } from 'vs/base/common/errors'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { coalesce } from 'vs/base/common/arrays'; import Severity from 'vs/base/common/severity'; import { ThemeIcon as ThemeIconUtils } from 'vs/base/common/themables'; @@ -301,7 +301,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx this._onDidChangeValueEmitter ]; - constructor(protected _extensionId: ExtensionIdentifier, private _onDidDispose: () => void) { + constructor(protected _extension: IExtensionDescription, private _onDidDispose: () => void) { } get title() { @@ -385,6 +385,10 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx } set buttons(buttons: QuickInputButton[]) { + const allowedButtonLocation = isProposedApiEnabled(this._extension, 'quickInputButtonLocation'); + if (!allowedButtonLocation && buttons.some(button => button.location)) { + console.warn(`Extension '${this._extension.identifier.value}' uses a button location which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this._extension.identifier.value}`); + } this._buttons = buttons.slice(); this._handlesToButtons.clear(); buttons.forEach((button, i) => { @@ -397,6 +401,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx ...getIconPathOrClass(button.iconPath), tooltip: button.tooltip, handle: button === QuickInputButtons.Back ? -1 : i, + location: allowedButtonLocation ? button.location : undefined }; }) }); @@ -546,8 +551,8 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private readonly _onDidChangeSelectionEmitter = new Emitter(); private readonly _onDidTriggerItemButtonEmitter = new Emitter>(); - constructor(private extension: IExtensionDescription, onDispose: () => void) { - super(extension.identifier, onDispose); + constructor(extension: IExtensionDescription, onDispose: () => void) { + super(extension, onDispose); this._disposables.push( this._onDidChangeActiveEmitter, this._onDidChangeSelectionEmitter, @@ -569,7 +574,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx this._itemsToHandles.set(item, i); }); - const allowedTooltips = isProposedApiEnabled(this.extension, 'quickPickItemTooltip'); + const allowedTooltips = isProposedApiEnabled(this._extension, 'quickPickItemTooltip'); const pickItems: TransferQuickPickItemOrSeparator[] = []; for (let handle = 0; handle < items.length; handle++) { @@ -578,7 +583,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx pickItems.push({ type: 'separator', label: item.label }); } else { if (item.tooltip && !allowedTooltips) { - console.warn(`Extension '${this.extension.identifier.value}' uses a tooltip which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this.extension.identifier.value}`); + console.warn(`Extension '${this._extension.identifier.value}' uses a tooltip which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this._extension.identifier.value}`); } const icon = (item.iconPath) ? getIconPathOrClass(item.iconPath) : undefined; @@ -712,7 +717,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _validationMessage: string | InputBoxValidationMessage | undefined; constructor(extension: IExtensionDescription, onDispose: () => void) { - super(extension.identifier, onDispose); + super(extension, onDispose); this.update({ type: 'inputBox' }); } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 46f25cb7dd2..b07e8f498b2 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -972,6 +972,11 @@ export class ExtHostSCM implements ExtHostSCMShape { return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupId1, historyItemGroupId2, token) ?? undefined; } + async $resolveHistoryItemGroupCommonAncestor2(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + return await historyProvider?.resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds, token) ?? undefined; + } + async $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; const historyItems = await historyProvider?.provideHistoryItems(historyItemGroupId, options, token); diff --git a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts index 59f391f8b50..06417a27963 100644 --- a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts +++ b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts @@ -11,7 +11,7 @@ import { MainContext, type ExtHostTerminalShellIntegrationShape, type MainThread import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { Emitter, type Event } from 'vs/base/common/event'; -import { isUriComponents, URI } from 'vs/base/common/uri'; +import { URI, type UriComponents } from 'vs/base/common/uri'; import { AsyncIterableObject, Barrier, type AsyncIterableEmitter } from 'vs/base/common/async'; export interface IExtHostTerminalShellIntegration extends ExtHostTerminalShellIntegrationShape { @@ -104,7 +104,7 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH }); } - public $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: URI | undefined): void { + public $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: UriComponents | undefined): void { // Force shellIntegration creation if it hasn't been created yet, this could when events // don't come through on startup if (!this._activeShellIntegrations.has(instanceId)) { @@ -115,7 +115,7 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH confidence: commandLineConfidence, isTrusted }; - this._activeShellIntegrations.get(instanceId)?.startShellExecution(commandLine, cwd); + this._activeShellIntegrations.get(instanceId)?.startShellExecution(commandLine, URI.revive(cwd)); } public $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void { @@ -131,8 +131,8 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH this._activeShellIntegrations.get(instanceId)?.emitData(data); } - public $cwdChange(instanceId: number, cwd: URI | undefined): void { - this._activeShellIntegrations.get(instanceId)?.setCwd(isUriComponents(cwd) ? URI.revive(cwd) : cwd); + public $cwdChange(instanceId: number, cwd: UriComponents | undefined): void { + this._activeShellIntegrations.get(instanceId)?.setCwd(URI.revive(cwd)); } public $closeTerminal(instanceId: number): void { @@ -202,7 +202,8 @@ class InternalTerminalShellIntegration extends Disposable { this._currentExecution.endExecution(undefined); this._onDidRequestEndExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: this._currentExecution.value, exitCode: undefined }); } - const currentExecution = this._currentExecution = new InternalTerminalShellExecution(commandLine, cwd); + // Fallback to the shell integration's cwd as the cwd may not have been restored after a reload + const currentExecution = this._currentExecution = new InternalTerminalShellExecution(commandLine, cwd ?? this._cwd); if (fireEventInMicrotask) { queueMicrotask(() => this._onDidStartTerminalShellExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: currentExecution.value })); } else { diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 1ef9c81d439..d269b34e120 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -14,7 +14,9 @@ import { hash } from 'vs/base/common/hash'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { isDefined } from 'vs/base/common/types'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; +import { IPosition } from 'vs/editor/common/core/position'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -28,7 +30,7 @@ import { FileCoverage, TestRunProfileKind, TestRunRequest } from 'vs/workbench/a import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; -import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; +import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestControllerCapability, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; @@ -37,6 +39,7 @@ interface ControllerInfo { profiles: Map; collection: ExtHostTestItemCollection; extension: IExtensionDescription; + relatedCodeProvider?: vscode.TestRelatedCodeProvider; activeProfiles: Set; } @@ -137,6 +140,23 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { const activeProfiles = new Set(); const proxy = this.proxy; + const getCapability = () => { + let cap = 0; + if (refreshHandler) { + cap |= TestControllerCapability.Refresh; + } + const rcp = info.relatedCodeProvider; + if (rcp) { + if (rcp?.provideRelatedTests) { + cap |= TestControllerCapability.TestRelatedToCode; + } + if (rcp?.provideRelatedCode) { + cap |= TestControllerCapability.CodeRelatedToTest; + } + } + return cap as TestControllerCapability; + }; + const controller: vscode.TestController = { items: collection.root.children, get label() { @@ -152,11 +172,19 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { }, set refreshHandler(value: ((token: CancellationToken) => Thenable | void) | undefined) { refreshHandler = value; - proxy.$updateController(controllerId, { canRefresh: !!value }); + proxy.$updateController(controllerId, { capabilities: getCapability() }); }, get id() { return controllerId; }, + get relatedCodeProvider() { + return info.relatedCodeProvider; + }, + set relatedCodeProvider(value: vscode.TestRelatedCodeProvider | undefined) { + checkProposedApiEnabled(extension, 'testRelatedCode'); + info.relatedCodeProvider = value; + proxy.$updateController(controllerId, { capabilities: getCapability() }); + }, createRunProfile: (label, group, runHandler, isDefault, tag?: vscode.TestTag | undefined, supportsContinuousRun?: boolean) => { // Derive the profile ID from a hash so that the same profile will tend // to have the same hashes, allowing re-run requests to work across reloads. @@ -192,10 +220,10 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { }, }; - proxy.$registerTestController(controllerId, label, !!refreshHandler); + const info: ControllerInfo = { controller, collection, profiles, extension, activeProfiles }; + proxy.$registerTestController(controllerId, label, getCapability()); disposable.add(toDisposable(() => proxy.$unregisterTestController(controllerId))); - const info: ControllerInfo = { controller, collection, profiles, extension, activeProfiles }; this.controllers.set(controllerId, info); disposable.add(toDisposable(() => this.controllers.delete(controllerId))); @@ -249,6 +277,56 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { //#endregion //#region RPC methods + /** + * @inheritdoc + */ + async $getTestsRelatedToCode(uri: UriComponents, _position: IPosition, token: CancellationToken): Promise { + const doc = this.editors.getDocument(URI.revive(uri)); + if (!doc) { + return []; + } + + const position = Convert.Position.to(_position); + const related: string[] = []; + await Promise.all([...this.controllers.values()].map(async (c) => { + let tests: vscode.TestItem[] | undefined | null; + try { + tests = await c.relatedCodeProvider?.provideRelatedTests?.(doc.document, position, token); + } catch (e) { + if (!token.isCancellationRequested) { + this.logService.warn(`Error thrown while providing related tests for ${c.controller.label}`, e); + } + } + + if (tests) { + for (const test of tests) { + related.push(TestId.fromExtHostTestItem(test, c.controller.id).toString()); + } + c.collection.flushDiff(); + } + })); + + return related; + } + + /** + * @inheritdoc + */ + async $getCodeRelatedToTest(testId: string, token: CancellationToken): Promise { + const controller = this.controllers.get(TestId.root(testId)); + if (!controller) { + return []; + } + + const test = controller.collection.tree.get(testId); + if (!test) { + return []; + } + + const locations = await controller.relatedCodeProvider?.provideRelatedCode?.(test.actual, token); + return locations?.map(Convert.location.from) ?? []; + } + /** * @inheritdoc */ @@ -428,11 +506,11 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { /** * Cancels an ongoing test run. */ - public $cancelExtensionTestRun(runId: string | undefined) { + public $cancelExtensionTestRun(runId: string | undefined, taskId: string | undefined) { if (runId === undefined) { this.runTracker.cancelAllRuns(); } else { - this.runTracker.cancelRunById(runId); + this.runTracker.cancelRunById(runId, taskId); } } @@ -521,7 +599,7 @@ const enum TestRunTrackerState { class TestRunTracker extends Disposable { private state = TestRunTrackerState.Running; private running = 0; - private readonly tasks = new Map(); + private readonly tasks = new Map(); private readonly sharedTestIds = new Set(); private readonly cts: CancellationTokenSource; private readonly endEmitter = this._register(new Emitter()); @@ -581,8 +659,10 @@ class TestRunTracker extends Disposable { } /** Requests cancellation of the run. On the second call, forces cancellation. */ - public cancel() { - if (this.state === TestRunTrackerState.Running) { + public cancel(taskId?: string) { + if (taskId) { + this.tasks.get(taskId)?.cts.cancel(); + } else if (this.state === TestRunTrackerState.Running) { this.cts.cancel(); this.state = TestRunTrackerState.Cancelling; } else if (this.state === TestRunTrackerState.Cancelling) { @@ -653,13 +733,15 @@ class TestRunTracker extends Disposable { }; let ended = false; + // tasks are alive for as long as the tracker is alive, so simple this._register is fine: + const cts = this._register(new CancellationTokenSource(this.cts.token)); // one-off map used to associate test items with incrementing IDs in `addCoverage`. // There's no need to include their entire ID, we just want to make sure they're // stable and unique. Normal map is okay since TestRun lifetimes are limited. const run: vscode.TestRun = { isPersisted: this.dto.isPersisted, - token: this.cts.token, + token: cts.token, name, onDidDispose: this.onDidDispose, addCoverage: (coverage) => { @@ -737,8 +819,13 @@ class TestRunTracker extends Disposable { }; this.running++; - this.tasks.set(taskId, { run }); - this.proxy.$startedTestRunTask(runId, { id: taskId, name, running: true }); + this.tasks.set(taskId, { run, cts }); + this.proxy.$startedTestRunTask(runId, { + id: taskId, + ctrlId: this.dto.controllerId, + name: name || this.extension.displayName || this.extension.identifier.value, + running: true, + }); return run; } @@ -843,8 +930,8 @@ export class TestRunCoordinator { /** * Cancels an existing test run via its cancellation token. */ - public cancelRunById(runId: string) { - this.trackedById.get(runId)?.cancel(); + public cancelRunById(runId: string, taskId?: string) { + this.trackedById.get(runId)?.cancel(taskId); } /** diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6fad560a437..e3a474a4955 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -10,7 +10,7 @@ import { createSingleCallFunction } from 'vs/base/common/functional'; import * as htmlContent from 'vs/base/common/htmlContent'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ResourceMap, ResourceSet } from 'vs/base/common/map'; -import { marked } from 'vs/base/common/marked/marked'; +import * as marked from 'vs/base/common/marked/marked'; import { parse, revive } from 'vs/base/common/marshalling'; import { Mimes } from 'vs/base/common/mime'; import { cloneAndChange } from 'vs/base/common/objects'; @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/edit import { IViewBadge } from 'vs/workbench/common/views'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -53,7 +53,7 @@ import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/ed import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; import * as types from './extHostTypes'; -import { IToolData } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import { IToolData, IToolResult } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; export namespace Command { @@ -367,7 +367,7 @@ export namespace MarkdownString { const resUris: { [href: string]: UriComponents } = Object.create(null); res.uris = resUris; - const collectUri = (href: string): string => { + const collectUri = ({ href }: { href: string }): string => { try { let uri = URI.parse(href, true); uri = uri.with({ query: _uriMassage(uri.query, resUris) }); @@ -377,11 +377,16 @@ export namespace MarkdownString { } return ''; }; - const renderer = new marked.Renderer(); - renderer.link = collectUri; - renderer.image = href => typeof href === 'string' ? collectUri(htmlContent.parseHrefAndDimensions(href).href) : ''; - marked(res.value, { renderer }); + marked.marked.walkTokens(marked.marked.lexer(res.value), token => { + if (token.type === 'link') { + collectUri({ href: token.href }); + } else if (token.type === 'image') { + if (typeof token.href === 'string') { + collectUri(htmlContent.parseHrefAndDimensions(token.href)); + } + } + }); return res; } @@ -787,7 +792,7 @@ export namespace SymbolTag { export namespace WorkspaceSymbol { export function from(info: vscode.SymbolInformation): search.IWorkspaceSymbol { - return { + return { name: info.name, kind: SymbolKind.from(info.kind), tags: info.tags && info.tags.map(SymbolTag.from), @@ -966,7 +971,7 @@ export namespace Hover { export namespace EvaluatableExpression { export function from(expression: vscode.EvaluatableExpression): languages.EvaluatableExpression { - return { + return { range: Range.from(expression.range), expression: expression.expression }; @@ -980,24 +985,24 @@ export namespace EvaluatableExpression { export namespace InlineValue { export function from(inlineValue: vscode.InlineValue): languages.InlineValue { if (inlineValue instanceof types.InlineValueText) { - return { + return { type: 'text', range: Range.from(inlineValue.range), text: inlineValue.text - }; + } satisfies languages.InlineValueText; } else if (inlineValue instanceof types.InlineValueVariableLookup) { - return { + return { type: 'variable', range: Range.from(inlineValue.range), variableName: inlineValue.variableName, caseSensitiveLookup: inlineValue.caseSensitiveLookup - }; + } satisfies languages.InlineValueVariableLookup; } else if (inlineValue instanceof types.InlineValueEvaluatableExpression) { - return { + return { type: 'expression', range: Range.from(inlineValue.range), expression: inlineValue.expression - }; + } satisfies languages.InlineValueExpression; } else { throw new Error(`Unknown 'InlineValue' type`); } @@ -1006,28 +1011,28 @@ export namespace InlineValue { export function to(inlineValue: languages.InlineValue): vscode.InlineValue { switch (inlineValue.type) { case 'text': - return { + return { range: Range.to(inlineValue.range), text: inlineValue.text - }; + } satisfies vscode.InlineValueText; case 'variable': - return { + return { range: Range.to(inlineValue.range), variableName: inlineValue.variableName, caseSensitiveLookup: inlineValue.caseSensitiveLookup - }; + } satisfies vscode.InlineValueVariableLookup; case 'expression': - return { + return { range: Range.to(inlineValue.range), expression: inlineValue.expression - }; + } satisfies vscode.InlineValueEvaluatableExpression; } } } export namespace InlineValueContext { export function from(inlineValueContext: vscode.InlineValueContext): extHostProtocol.IInlineValueContextDto { - return { + return { frameId: inlineValueContext.frameId, stoppedLocation: Range.from(inlineValueContext.stoppedLocation) }; @@ -1888,6 +1893,11 @@ export namespace TestMessage { actual: message.actualOutput, contextValue: message.contextValue, location: message.location && ({ range: Range.from(message.location.range), uri: message.location.uri }), + stackTrace: (message as vscode.TestMessage2).stackTrace?.map(s => ({ + label: s.label, + position: s.position && Position.from(s.position), + uri: s.uri && URI.revive(s.uri).toJSON(), + })), }; } @@ -2354,7 +2364,8 @@ export namespace ChatResponseConfirmationPart { kind: 'confirmation', title: part.title, message: part.message, - data: part.data + data: part.data, + buttons: part.buttons }; } } @@ -2505,7 +2516,8 @@ export namespace ChatResponseReferencePart { part.value.value : Location.from(part.value.value as vscode.Location) }, - iconPath + iconPath, + options: part.options }; } @@ -2514,7 +2526,8 @@ export namespace ChatResponseReferencePart { reference: URI.isUri(part.value) ? part.value : Location.from(part.value), - iconPath + iconPath, + options: part.options }; } export function to(part: Dto): vscode.ChatResponseReferencePart { @@ -2534,9 +2547,20 @@ export namespace ChatResponseReferencePart { } } +export namespace ChatResponseCodeCitationPart { + export function from(part: vscode.ChatResponseCodeCitationPart): Dto { + return { + kind: 'codeCitation', + value: part.value, + license: part.license, + snippet: part.snippet + }; + } +} + export namespace ChatResponsePart { - export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseWarningPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2559,6 +2583,8 @@ export namespace ChatResponsePart { return ChatResponseWarningPart.from(part); } else if (part instanceof types.ChatResponseConfirmationPart) { return ChatResponseConfirmationPart.from(part); + } else if (part instanceof types.ChatResponseCodeCitationPart) { + return ChatResponseCodeCitationPart.from(part); } return { @@ -2595,16 +2621,19 @@ export namespace ChatResponsePart { export namespace ChatAgentRequest { export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined): vscode.ChatRequest { + const requestedTools = request.variables.variables.filter(v => v.isTool).map(tool => tool.id); + const variablesWithoutTools = request.variables.variables.filter(v => !v.isTool); return { prompt: request.message, command: request.command, attempt: request.attempt ?? 0, enableCommandDetection: request.enableCommandDetection ?? true, - references: request.variables.variables.map(ChatAgentValueReference.to), + references: variablesWithoutTools.map(ChatAgentValueReference.to), location: ChatLocation.to(request.location), acceptedConfirmationData: request.acceptedConfirmationData, rejectedConfirmationData: request.rejectedConfirmationData, - location2 + location2, + requestedTools }; } } @@ -2669,6 +2698,7 @@ export namespace ChatAgentResult { return { errorDetails: result.errorDetails, metadata: result.metadata, + nextQuestion: result.nextQuestion, }; } } @@ -2699,6 +2729,24 @@ export namespace ChatAgentUserActionEvent { } } +export namespace LanguageModelToolResult { + export function from(result: vscode.LanguageModelToolResult): IToolResult { + return { + ...result, + string: result.toString(), + }; + } + + export function to(result: IToolResult): vscode.LanguageModelToolResult { + const copy: vscode.LanguageModelToolResult = { + ...result, + toString: () => result.string, + }; + delete copy.string; + + return copy; + } +} export namespace TerminalQuickFix { export function from(quickFix: vscode.TerminalQuickFixTerminalCommand | vscode.TerminalQuickFixOpener | vscode.Command, converter: Command.ICommandsConverter, disposables: DisposableStore): extHostProtocol.ITerminalQuickFixTerminalCommandDto | extHostProtocol.ITerminalQuickFixOpenerDto | extHostProtocol.ICommandDto | undefined { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 18c2eefe79d..f01e28b5413 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1236,18 +1236,18 @@ export class Hover { @es5ClassCompat export class VerboseHover extends Hover { - public canIncreaseHover: boolean | undefined; - public canDecreaseHover: boolean | undefined; + public canIncreaseVerbosity: boolean | undefined; + public canDecreaseVerbosity: boolean | undefined; constructor( contents: vscode.MarkdownString | vscode.MarkedString | (vscode.MarkdownString | vscode.MarkedString)[], range?: Range, - canIncreaseHover?: boolean, - canDecreaseHover?: boolean, + canIncreaseVerbosity?: boolean, + canDecreaseVerbosity?: boolean, ) { super(contents, range); - this.canIncreaseHover = canIncreaseHover; - this.canDecreaseHover = canDecreaseHover; + this.canIncreaseVerbosity = canIncreaseVerbosity; + this.canDecreaseVerbosity = canDecreaseVerbosity; } } @@ -3341,6 +3341,11 @@ export enum CommentThreadApplicability { Outdated = 1 } +export enum CommentThreadFocus { + Reply = 1, + Comment = 2 +} + //#endregion //#region Semantic Coloring @@ -3592,6 +3597,11 @@ export class DebugVisualization { //#endregion +export enum QuickInputButtonLocation { + Title = 1, + Inline = 2 +} + @es5ClassCompat export class QuickInputButtons { @@ -4069,9 +4079,11 @@ export class TestMessage implements vscode.TestMessage { public expectedOutput?: string; public actualOutput?: string; public location?: vscode.Location; - /** proposed: */ public contextValue?: string; + /** proposed: */ + public stackTrace?: TestMessageStackFrame[]; + public static diff(message: string | vscode.MarkdownString, expected: string, actual: string) { const msg = new TestMessage(message); msg.expectedOutput = expected; @@ -4087,6 +4099,19 @@ export class TestTag implements vscode.TestTag { constructor(public readonly id: string) { } } +export class TestMessageStackFrame { + /** + * @param label The name of the stack frame + * @param file The file URI of the stack frame + * @param position The position of the stack frame within the file + */ + constructor( + public label: string, + public uri?: vscode.Uri, + public position?: Position, + ) { } +} + //#endregion //#region Test Coverage @@ -4377,10 +4402,13 @@ export class ChatResponseConfirmationPart { title: string; message: string; data: any; - constructor(title: string, message: string, data: any) { + buttons?: string[]; + + constructor(title: string, message: string, data: any, buttons?: string[]) { this.title = title; this.message = message; this.data = data; + this.buttons = buttons; } } @@ -4439,9 +4467,22 @@ export class ChatResponseCommandButtonPart { export class ChatResponseReferencePart { value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }; iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }; - constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }, iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }) { + options?: { status?: { description: string; kind: vscode.ChatResponseReferencePartStatusKind } }; + constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }, iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }, options?: { status?: { description: string; kind: vscode.ChatResponseReferencePartStatusKind } }) { this.value = value; this.iconPath = iconPath; + this.options = options; + } +} + +export class ChatResponseCodeCitationPart { + value: vscode.Uri; + license: string; + snippet: string; + constructor(value: vscode.Uri, license: string, snippet: string) { + this.value = value; + this.license = license; + this.snippet = snippet; } } @@ -4480,6 +4521,12 @@ export enum ChatLocation { Editor = 4, } +export enum ChatResponseReferencePartStatusKind { + Complete = 1, + Partial = 2, + Omitted = 3 +} + export class ChatRequestEditorData implements vscode.ChatRequestEditorData { constructor( readonly document: vscode.TextDocument, diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 61c5b48357c..a25ff3fe913 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -41,6 +41,7 @@ export interface IExtHostWorkspaceProvider { getWorkspaceFolders2(): Promise; resolveProxy(url: string): Promise; lookupAuthorization(authInfo: AuthInfo): Promise; + lookupKerberosAuthorization(url: string): Promise; loadCertificates(): Promise; } @@ -521,7 +522,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac .then(data => Array.isArray(data) ? data.map(d => URI.revive(d)) : []); } - async findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, callback: (result: vscode.TextSearchResult) => void, extensionId: ExtensionIdentifier, token: vscode.CancellationToken = CancellationToken.None): Promise { + async findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions & { useSearchExclude?: boolean }, callback: (result: vscode.TextSearchResult) => void, extensionId: ExtensionIdentifier, token: vscode.CancellationToken = CancellationToken.None): Promise { this._logService.trace(`extHostWorkspace#findTextInFiles: textSearch, extension: ${extensionId.value}, entryPoint: findTextInFiles`); const requestId = this._requestIdProvider.getNext(); @@ -542,6 +543,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac disregardGlobalIgnoreFiles: typeof options.useGlobalIgnoreFiles === 'boolean' ? !options.useGlobalIgnoreFiles : undefined, disregardParentIgnoreFiles: typeof options.useParentIgnoreFiles === 'boolean' ? !options.useParentIgnoreFiles : undefined, disregardExcludeSettings: typeof options.useDefaultExcludes === 'boolean' ? !options.useDefaultExcludes : true, + disregardSearchExcludeSettings: typeof options.useSearchExclude === 'boolean' ? !options.useSearchExclude : true, fileEncoding: options.encoding, maxResults: options.maxResults, previewOptions, @@ -632,6 +634,10 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac return this._proxy.$lookupAuthorization(authInfo); } + lookupKerberosAuthorization(url: string): Promise { + return this._proxy.$lookupKerberosAuthorization(url); + } + loadCertificates(): Promise { return this._proxy.$loadCertificates(); } diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 5ac0ddc7ec9..ec6bb2bd1f4 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -18,6 +18,10 @@ import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { realpathSync } from 'vs/base/node/extpath'; import { ExtHostConsoleForwarder } from 'vs/workbench/api/node/extHostConsoleForwarder'; import { ExtHostDiskFileSystemProvider } from 'vs/workbench/api/node/extHostDiskFileSystemProvider'; +// ESM-uncomment-begin +// import { createRequire } from 'node:module'; +// const require = createRequire(import.meta.url); +// ESM-uncomment-end class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -109,7 +113,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { if (extensionId) { performance.mark(`code/extHost/willLoadExtensionCode/${extensionId}`); } - r = require.__$__nodeRequire(module.fsPath); + r = require.__$__nodeRequire(module.fsPath); } finally { if (extensionId) { performance.mark(`code/extHost/didLoadExtensionCode/${extensionId}`); diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index 0df3f98c980..e6f9cf77053 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// ESM-comment-begin import * as http from 'http'; import * as https from 'https'; import * as tls from 'tls'; import * as net from 'net'; +// ESM-comment-end import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; @@ -19,6 +21,15 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { LogLevel, createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting, ProxyAgentParams, createNetPatch, loadSystemCertificates } from '@vscode/proxy-agent'; import { AuthInfo } from 'vs/platform/request/common/request'; +// ESM-uncomment-begin +// import { createRequire } from 'node:module'; +// const require = createRequire(import.meta.url); +// const http = require('http'); +// const https = require('https'); +// const tls = require('tls'); +// const net = require('net'); +// ESM-uncomment-end + const systemCertificatesV2Default = false; export function connectProxyResolver( @@ -33,7 +44,7 @@ export function connectProxyResolver( const doUseHostProxy = typeof useHostProxy === 'boolean' ? useHostProxy : !initData.remote.isRemote; const params: ProxyAgentParams = { resolveProxy: url => extHostWorkspace.resolveProxy(url), - lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote), + lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote, doUseHostProxy), getProxyURL: () => configProvider.getConfiguration('http').get('proxy'), getProxySupport: () => configProvider.getConfiguration('http').get('proxySupport') || 'off', getNoProxyConfig: () => configProvider.getConfiguration('http').get('noProxy') || [], @@ -148,6 +159,7 @@ async function lookupProxyAuthorization( proxyAuthenticateCache: Record, basicAuthCache: Record, isRemote: boolean, + useHostProxy: boolean, proxyURL: string, proxyAuthenticate: string | string[] | undefined, state: { kerberosRequested?: boolean; basicAuthCacheUsed?: boolean; basicAuthAttempt?: number } @@ -161,8 +173,9 @@ async function lookupProxyAuthorization( const authenticate = Array.isArray(header) ? header : typeof header === 'string' ? [header] : []; sendTelemetry(mainThreadTelemetry, authenticate, isRemote); if (authenticate.some(a => /^(Negotiate|Kerberos)( |$)/i.test(a)) && !state.kerberosRequested) { + state.kerberosRequested = true; + try { - state.kerberosRequested = true; const kerberos = await import('kerberos'); const url = new URL(proxyURL); const spn = configProvider.getConfiguration('http').get('proxyKerberosServicePrincipal') @@ -172,7 +185,15 @@ async function lookupProxyAuthorization( const response = await client.step(''); return 'Negotiate ' + response; } catch (err) { - extHostLogService.error('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err); + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err); + } + + if (isRemote && useHostProxy) { + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication lookup on host', `proxyURL:${proxyURL}`); + const auth = await extHostWorkspace.lookupKerberosAuthorization(proxyURL); + if (auth) { + return 'Negotiate ' + auth; + } } } const basicAuthHeader = authenticate.find(a => /^Basic( |$)/i.test(a)); diff --git a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts index 6b7de3ca716..ec934301cd9 100644 --- a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts @@ -301,7 +301,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 1); const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -322,7 +322,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 2); }); @@ -341,7 +341,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 2); // let [first, second] = value; assert.strictEqual(value[0].uri.authority, 'second'); @@ -362,7 +362,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 1); }); @@ -377,7 +377,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 1); const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -395,7 +395,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 1); const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -413,7 +413,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 1); const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -517,7 +517,7 @@ suite('ExtHostLanguageFeatures', function () { disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { provideDocumentHighlights(): any { - return []; + return undefined; } })); disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, '*', new class implements vscode.DocumentHighlightProvider { @@ -591,7 +591,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, CancellationToken.None); + const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, false, CancellationToken.None); assert.strictEqual(value.length, 2); const [first, second] = value; assert.strictEqual(first.uri.path, '/second'); @@ -607,7 +607,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, CancellationToken.None); + const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, false, CancellationToken.None); assert.strictEqual(value.length, 1); const [item] = value; assert.deepStrictEqual(item.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }); @@ -628,7 +628,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, CancellationToken.None); + const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, false, CancellationToken.None); assert.strictEqual(value.length, 1); }); diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index c4515796d05..4a49c8031a2 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -829,7 +829,8 @@ suite('ExtHost Testing', () => { expected: undefined, contextValue: undefined, actual: undefined, - location: convert.location.from(message1.location) + location: convert.location.from(message1.location), + stackTrace: undefined, }] ]); @@ -846,6 +847,7 @@ suite('ExtHost Testing', () => { expected: undefined, actual: undefined, location: convert.location.from({ uri: test2.uri!, range: test2.range }), + stackTrace: undefined, }] ]); diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index f0d0538ce22..17a9049d064 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -27,11 +27,15 @@ import { TestTextResourcePropertiesService, TestWorkingCopyFileService } from 'v import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { LanguageService } from 'vs/editor/common/services/languageService'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; suite('MainThreadDocumentsAndEditors', () => { @@ -61,12 +65,15 @@ suite('MainThreadDocumentsAndEditors', () => { const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); const themeService = new TestThemeService(); + const instantiationService = new TestInstantiationService(); + instantiationService.set(ILanguageService, disposables.add(new LanguageService())); + instantiationService.set(ILanguageConfigurationService, new TestLanguageConfigurationService()); + instantiationService.set(IUndoRedoService, undoRedoService); modelService = new ModelService( configService, new TestTextResourcePropertiesService(configService), undoRedoService, - disposables.add(new LanguageService()), - new TestLanguageConfigurationService(), + instantiationService ); codeEditorService = new TestCodeEditorService(themeService); textFileService = new class extends mock() { diff --git a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts index 1ca165c1b5f..d83a5ebcf42 100644 --- a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts @@ -16,12 +16,10 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; -import { LanguageService } from 'vs/editor/common/services/languageService'; import { IModelService } from 'vs/editor/common/services/model'; import { ModelService } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -57,6 +55,10 @@ import { ICopyOperation, ICreateFileOperation, ICreateOperation, IDeleteOperatio import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestFileService, TestLifecycleService, TestWorkingCopyService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestContextService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('MainThreadEditors', () => { @@ -80,19 +82,11 @@ suite('MainThreadEditors', () => { createdResources.clear(); deletedResources.clear(); - const configService = new TestConfigurationService(); const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); const themeService = new TestThemeService(); - modelService = new ModelService( - configService, - new TestTextResourcePropertiesService(configService), - undoRedoService, - disposables.add(new LanguageService()), - new TestLanguageConfigurationService(), - ); const services = new ServiceCollection(); services.set(IBulkEditService, new SyncDescriptor(BulkEditService)); @@ -178,8 +172,18 @@ suite('MainThreadEditors', () => { } }); + services.set(ILanguageService, disposables.add(new LanguageService())); + services.set(ILanguageConfigurationService, new TestLanguageConfigurationService()); + const instaService = new InstantiationService(services); + modelService = new ModelService( + configService, + new TestTextResourcePropertiesService(configService), + undoRedoService, + instaService + ); + bulkEdits = instaService.createInstance(MainThreadBulkEdits, SingleProxyRPCProtocol(null)); }); diff --git a/src/vs/workbench/api/test/node/extHostSearch.test.ts b/src/vs/workbench/api/test/node/extHostSearch.test.ts index af20a5e455b..58442641724 100644 --- a/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/src/vs/workbench/api/test/node/extHostSearch.test.ts @@ -16,7 +16,7 @@ import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { MainContext, MainThreadSearchShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration.js'; +import { ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { Range } from 'vs/workbench/api/common/extHostTypes'; import { URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 77574a6338d..bba90710822 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -31,6 +31,7 @@ import { ICommandActionTitle } from 'vs/platform/action/common/action'; import { mainWindow } from 'vs/base/browser/window'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { TitlebarStyle } from 'vs/platform/window/common/window'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; // Register Icons const menubarIcon = registerIcon('menuBar', Codicon.layoutMenubar, localize('menuBarIcon', "Represents the menu bar")); @@ -673,6 +674,48 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 11 }); +// --- Configure Tabs Layout + +export class ConfigureEditorTabsAction extends Action2 { + + static readonly ID = 'workbench.action.configureEditorTabs'; + + constructor() { + super({ + id: ConfigureEditorTabsAction.ID, + title: localize2('configureTabs', "Configure Tabs"), + category: Categories.View, + }); + } + + run(accessor: ServicesAccessor) { + const preferencesService = accessor.get(IPreferencesService); + preferencesService.openSettings({ jsonEditor: false, query: 'workbench.editor tab' }); + } +} +registerAction2(ConfigureEditorTabsAction); + +// --- Configure Editor + +export class ConfigureEditorAction extends Action2 { + + static readonly ID = 'workbench.action.configureEditor'; + + constructor() { + super({ + id: ConfigureEditorAction.ID, + title: localize2('configureEditors', "Configure Editors"), + category: Categories.View, + }); + } + + run(accessor: ServicesAccessor) { + const preferencesService = accessor.get(IPreferencesService); + preferencesService.openSettings({ jsonEditor: false, query: 'workbench.editor' }); + } +} +registerAction2(ConfigureEditorAction); + // --- Toggle Pinned Tabs On Separate Row registerAction2(class extends Action2 { @@ -923,7 +966,7 @@ registerAction2(class extends Action2 { } private async getView(quickInputService: IQuickInputService, viewDescriptorService: IViewDescriptorService, paneCompositePartService: IPaneCompositePartService, viewId?: string): Promise { - const quickPick = quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); quickPick.placeholder = localize('moveFocusedView.selectView', "Select a View to Move"); quickPick.items = this.getViewItems(viewDescriptorService, paneCompositePartService); quickPick.selectedItems = quickPick.items.filter(item => (item as IQuickPickItem).id === viewId) as IQuickPickItem[]; @@ -982,7 +1025,7 @@ class MoveFocusedViewAction extends Action2 { return; } - const quickPick = quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); quickPick.placeholder = localize('moveFocusedView.selectDestination', "Select a Destination for the View"); quickPick.title = localize({ key: 'moveFocusedView.title', comment: ['{0} indicates the title of the view the user has selected to move.'] }, "View: Move {0}", viewDescriptor.name.value); @@ -1368,7 +1411,7 @@ for (const { active } of [...ToggleVisibilityActions, ...MoveSideBarActions, ... registerAction2(class CustomizeLayoutAction extends Action2 { - private _currentQuickPick?: IQuickPick; + private _currentQuickPick?: IQuickPick; constructor() { super({ @@ -1461,7 +1504,7 @@ registerAction2(class CustomizeLayoutAction extends Action2 { const commandService = accessor.get(ICommandService); const quickInputService = accessor.get(IQuickInputService); const keybindingService = accessor.get(IKeybindingService); - const quickPick = quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); this._currentQuickPick = quickPick; quickPick.items = this.getItems(contextKeyService, keybindingService); diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index 9890994cfdf..acf6652d837 100644 --- a/src/vs/workbench/browser/actions/quickAccessActions.ts +++ b/src/vs/workbench/browser/actions/quickAccessActions.ts @@ -163,12 +163,13 @@ registerAction2(class QuickAccessAction extends Action2 { run(accessor: ServicesAccessor): void { const quickInputService = accessor.get(IQuickInputService); + const providerOptions: AnythingQuickAccessProviderRunOptions = { + includeHelp: true, + from: 'commandCenter', + }; quickInputService.quickAccess.show(undefined, { preserveValue: true, - providerOptions: { - includeHelp: true, - from: 'commandCenter', - } as AnythingQuickAccessProviderRunOptions + providerOptions }); } }); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index b8232cb8490..4098ce07f8a 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -12,14 +12,14 @@ import { isWindows, isLinux, isMacintosh, isWeb, isIOS } from 'vs/base/common/pl import { EditorInputCapabilities, GroupIdentifier, isResourceEditorInput, IUntypedEditorInput, pathsToEditors } from 'vs/workbench/common/editor'; import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; -import { Position, Parts, PanelOpensMaximizedOptions, IWorkbenchLayoutService, positionFromString, positionToString, panelOpensMaximizedFromString, PanelAlignment, ActivityBarPosition, LayoutSettings, MULTI_WINDOW_PARTS, SINGLE_WINDOW_PARTS, ZenModeSettings, EditorTabsMode, EditorActionsLocation, shouldShowCustomTitleBar } from 'vs/workbench/services/layout/browser/layoutService'; +import { Position, Parts, PanelOpensMaximizedOptions, IWorkbenchLayoutService, positionFromString, positionToString, panelOpensMaximizedFromString, PanelAlignment, ActivityBarPosition, LayoutSettings, MULTI_WINDOW_PARTS, SINGLE_WINDOW_PARTS, ZenModeSettings, EditorTabsMode, EditorActionsLocation, shouldShowCustomTitleBar, isHorizontal } from 'vs/workbench/services/layout/browser/layoutService'; import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { StartupKind, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { getMenuBarVisibility, IPath, hasNativeTitlebar, hasCustomTitlebar, TitleBarSetting } from 'vs/platform/window/common/window'; +import { getMenuBarVisibility, IPath, hasNativeTitlebar, hasCustomTitlebar, TitleBarSetting, CustomTitleBarVisibility } from 'vs/platform/window/common/window'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -48,7 +48,6 @@ import { AuxiliaryBarPart } from 'vs/workbench/browser/parts/auxiliarybar/auxili import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { CodeWindow, mainWindow } from 'vs/base/browser/window'; -import { CustomTitleBarVisibility } from '../../platform/window/common/window'; //#region Layout Implementation @@ -1266,18 +1265,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const containerDimension = this.getContainerDimension(container); if (container === this.mainContainer) { - const panelPosition = this.getPanelPosition(); - const isColumn = panelPosition === Position.RIGHT || panelPosition === Position.LEFT; + const isPanelHorizontal = isHorizontal(this.getPanelPosition()); const takenWidth = (this.isVisible(Parts.ACTIVITYBAR_PART) ? this.activityBarPartView.minimumWidth : 0) + (this.isVisible(Parts.SIDEBAR_PART) ? this.sideBarPartView.minimumWidth : 0) + - (this.isVisible(Parts.PANEL_PART) && isColumn ? this.panelPartView.minimumWidth : 0) + + (this.isVisible(Parts.PANEL_PART) && !isPanelHorizontal ? this.panelPartView.minimumWidth : 0) + (this.isVisible(Parts.AUXILIARYBAR_PART) ? this.auxiliaryBarPartView.minimumWidth : 0); const takenHeight = (this.isVisible(Parts.TITLEBAR_PART, targetWindow) ? this.titleBarPartView.minimumHeight : 0) + (this.isVisible(Parts.STATUSBAR_PART, targetWindow) ? this.statusBarPartView.minimumHeight : 0) + - (this.isVisible(Parts.PANEL_PART) && !isColumn ? this.panelPartView.minimumHeight : 0); + (this.isVisible(Parts.PANEL_PART) && isPanelHorizontal ? this.panelPartView.minimumHeight : 0); const availableWidth = containerDimension.width - takenWidth; const availableHeight = containerDimension.height - takenHeight; @@ -1546,7 +1544,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Panel Size const panelSize = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) ? this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) - : (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) === Position.BOTTOM ? this.workbenchGrid.getViewSize(this.panelPartView).height : this.workbenchGrid.getViewSize(this.panelPartView).width); + : isHorizontal(this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION)) + ? this.workbenchGrid.getViewSize(this.panelPartView).height + : this.workbenchGrid.getViewSize(this.panelPartView).width; this.stateModel.setInitializationValue(LayoutStateKeys.PANEL_SIZE, panelSize as number); // Auxiliary Bar Size @@ -1635,8 +1635,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.workbenchGrid.resizeView(this.panelPartView, { - width: viewSize.width + (this.getPanelPosition() !== Position.BOTTOM ? sizeChangePxWidth : 0), - height: viewSize.height + (this.getPanelPosition() !== Position.BOTTOM ? 0 : sizeChangePxHeight) + width: viewSize.width + (isHorizontal(this.getPanelPosition()) ? 0 : sizeChangePxWidth), + height: viewSize.height + (isHorizontal(this.getPanelPosition()) ? sizeChangePxHeight : 0) }); break; @@ -1770,8 +1770,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private adjustPartPositions(sideBarPosition: Position, panelAlignment: PanelAlignment, panelPosition: Position): void { // Move activity bar and side bars - const sideBarSiblingToEditor = panelPosition !== Position.BOTTOM || !(panelAlignment === 'center' || (sideBarPosition === Position.LEFT && panelAlignment === 'right') || (sideBarPosition === Position.RIGHT && panelAlignment === 'left')); - const auxiliaryBarSiblingToEditor = panelPosition !== Position.BOTTOM || !(panelAlignment === 'center' || (sideBarPosition === Position.RIGHT && panelAlignment === 'right') || (sideBarPosition === Position.LEFT && panelAlignment === 'left')); + const isPanelVertical = !isHorizontal(panelPosition); + const sideBarSiblingToEditor = isPanelVertical || !(panelAlignment === 'center' || (sideBarPosition === Position.LEFT && panelAlignment === 'right') || (sideBarPosition === Position.RIGHT && panelAlignment === 'left')); + const auxiliaryBarSiblingToEditor = isPanelVertical || !(panelAlignment === 'center' || (sideBarPosition === Position.RIGHT && panelAlignment === 'right') || (sideBarPosition === Position.LEFT && panelAlignment === 'left')); const preMovePanelWidth = !this.isVisible(Parts.PANEL_PART) ? Sizing.Invisible(this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) ?? this.panelPartView.minimumWidth) : this.workbenchGrid.getViewSize(this.panelPartView).width; const preMovePanelHeight = !this.isVisible(Parts.PANEL_PART) ? Sizing.Invisible(this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) ?? this.panelPartView.minimumHeight) : this.workbenchGrid.getViewSize(this.panelPartView).height; const preMoveSideBarSize = !this.isVisible(Parts.SIDEBAR_PART) ? Sizing.Invisible(this.workbenchGrid.getViewCachedVisibleSize(this.sideBarPartView) ?? this.sideBarPartView.minimumWidth) : this.workbenchGrid.getViewSize(this.sideBarPartView).width; @@ -1797,7 +1798,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // We moved all the side parts based on the editor and ignored the panel // Now, we need to put the panel back in the right position when it is next to the editor - if (panelPosition !== Position.BOTTOM) { + if (isPanelVertical) { this.workbenchGrid.moveView(this.panelPartView, preMovePanelWidth, this.editorPartView, panelPosition === Position.LEFT ? Direction.Left : Direction.Right); this.workbenchGrid.resizeView(this.panelPartView, { height: preMovePanelHeight as number, @@ -1824,8 +1825,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi setPanelAlignment(alignment: PanelAlignment, skipLayout?: boolean): void { - // Panel alignment only applies to a panel in the bottom position - if (this.getPanelPosition() !== Position.BOTTOM) { + // Panel alignment only applies to a panel in the top/bottom position + if (!isHorizontal(this.getPanelPosition())) { this.setPanelPosition(Position.BOTTOM); } @@ -1921,7 +1922,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const isMaximized = this.isPanelMaximized(); if (!isMaximized) { if (this.isVisible(Parts.PANEL_PART)) { - if (panelPosition === Position.BOTTOM) { + if (isHorizontal(panelPosition)) { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT, size.height); } else { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH, size.width); @@ -1932,8 +1933,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } else { this.setEditorHidden(false); this.workbenchGrid.resizeView(this.panelPartView, { - width: panelPosition === Position.BOTTOM ? size.width : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH), - height: panelPosition === Position.BOTTOM ? this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT) : size.height + width: isHorizontal(panelPosition) ? size.width : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH), + height: isHorizontal(panelPosition) ? this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT) : size.height }); } @@ -1943,7 +1944,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private panelOpensMaximized(): boolean { // The workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - if (this.getPanelAlignment() !== 'center' && this.getPanelPosition() === Position.BOTTOM) { + if (this.getPanelAlignment() !== 'center' && isHorizontal(this.getPanelPosition())) { return false; } @@ -2021,7 +2022,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi isPanelMaximized(): boolean { // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - return (this.getPanelAlignment() === 'center' || this.getPanelPosition() !== Position.BOTTOM) && !this.isVisible(Parts.EDITOR_PART, mainWindow); + return (this.getPanelAlignment() === 'center' || !isHorizontal(this.getPanelPosition())) && !this.isVisible(Parts.EDITOR_PART, mainWindow); } getSideBarPosition(): Position { @@ -2097,14 +2098,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Save the current size of the panel for the new orthogonal direction // If moving down, save the width of the panel // Otherwise, save the height of the panel - if (position === Position.BOTTOM) { + if (isHorizontal(position)) { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH, size.width); - } else if (positionFromString(oldPositionValue) === Position.BOTTOM) { + } else if (isHorizontal(positionFromString(oldPositionValue))) { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT, size.height); } } - if (position === Position.BOTTOM && this.getPanelAlignment() !== 'center' && editorHidden) { + if (isHorizontal(position) && this.getPanelAlignment() !== 'center' && editorHidden) { this.toggleMaximizedPanel(); editorHidden = false; } @@ -2116,6 +2117,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (position === Position.BOTTOM) { this.workbenchGrid.moveView(this.panelPartView, editorHidden ? size.height : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT), this.editorPartView, Direction.Down); + } else if (position === Position.TOP) { + this.workbenchGrid.moveView(this.panelPartView, editorHidden ? size.height : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT), this.editorPartView, Direction.Up); } else if (position === Position.RIGHT) { this.workbenchGrid.moveView(this.panelPartView, editorHidden ? size.width : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH), this.editorPartView, Direction.Right); } else { @@ -2133,7 +2136,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.setAuxiliaryBarHidden(true); } - if (position === Position.BOTTOM) { + if (isHorizontal(position)) { this.adjustPartPositions(this.getSideBarPosition(), this.getPanelAlignment(), position); } @@ -2242,17 +2245,20 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const auxiliaryBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) ? 0 : nodes.auxiliaryBar.size; const panelSize = this.stateModel.getInitializationValue(LayoutStateKeys.PANEL_SIZE) ? 0 : nodes.panel.size; + const panelPostion = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION); + const sideBarPosition = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_POSITON); + const result = [] as ISerializedNode[]; - if (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) !== Position.BOTTOM) { + if (!isHorizontal(panelPostion)) { result.push(nodes.editor); nodes.editor.size = availableWidth - activityBarSize - sideBarSize - panelSize - auxiliaryBarSize; - if (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) === Position.RIGHT) { + if (panelPostion === Position.RIGHT) { result.push(nodes.panel); } else { result.splice(0, 0, nodes.panel); } - if (this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_POSITON) === Position.LEFT) { + if (sideBarPosition === Position.LEFT) { result.push(nodes.auxiliaryBar); result.splice(0, 0, nodes.sideBar); result.splice(0, 0, nodes.activityBar); @@ -2263,18 +2269,20 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } else { const panelAlignment = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_ALIGNMENT); - const sideBarPosition = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_POSITON); const sideBarNextToEditor = !(panelAlignment === 'center' || (sideBarPosition === Position.LEFT && panelAlignment === 'right') || (sideBarPosition === Position.RIGHT && panelAlignment === 'left')); const auxiliaryBarNextToEditor = !(panelAlignment === 'center' || (sideBarPosition === Position.RIGHT && panelAlignment === 'right') || (sideBarPosition === Position.LEFT && panelAlignment === 'left')); const editorSectionWidth = availableWidth - activityBarSize - (sideBarNextToEditor ? 0 : sideBarSize) - (auxiliaryBarNextToEditor ? 0 : auxiliaryBarSize); + + const editorNodes = this.arrangeEditorNodes({ + editor: nodes.editor, + sideBar: sideBarNextToEditor ? nodes.sideBar : undefined, + auxiliaryBar: auxiliaryBarNextToEditor ? nodes.auxiliaryBar : undefined + }, availableHeight - panelSize, editorSectionWidth); + result.push({ type: 'branch', - data: [this.arrangeEditorNodes({ - editor: nodes.editor, - sideBar: sideBarNextToEditor ? nodes.sideBar : undefined, - auxiliaryBar: auxiliaryBarNextToEditor ? nodes.auxiliaryBar : undefined - }, availableHeight - panelSize, editorSectionWidth), nodes.panel], + data: panelPostion === Position.BOTTOM ? [editorNodes, nodes.panel] : [nodes.panel, editorNodes], size: editorSectionWidth }); @@ -2417,7 +2425,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the panel is visible' }; statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the status bar is visible' }; sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the primary side bar is on the left or right' }; - panelPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the panel is on the bottom, left, or right' }; + panelPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the panel is on the top, bottom, left, or right' }; }; const layoutDescriptor: StartupLayoutEvent = { @@ -2625,7 +2633,7 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === Position.BOTTOM ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? isHorizontal(LayoutStateKeys.PANEL_POSITION.defaultValue)) ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; // Apply all defaults diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index 3a2422d4c99..246ac87fcf6 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -240,14 +240,6 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { return headerArea; } - protected override getToolbarWidth(): number { - if (this.getCompositeBarPosition() === CompositeBarPosition.TOP) { - return 22; - } - - return super.getToolbarWidth(); - } - private headerActionViewItemProvider(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined { if (action.id === ToggleAuxiliaryBarAction.ID) { return this.instantiationService.createInstance(ActionViewItem, undefined, action, options); diff --git a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css index b1e39af1e58..781c090fbe5 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css +++ b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css @@ -52,7 +52,7 @@ .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, .monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { position: absolute; - left: 6px; /* place icon in center */ + left: 5px; /* place icon in center */ } .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index dfb20dc68fb..753f4aa8860 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -69,7 +69,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorHandler } from 'vs/workbench/services/untitled/common/untitledTextEditorHandler'; import { DynamicEditorConfigurations } from 'vs/workbench/browser/parts/editor/editorConfiguration'; -import { EditorActionsDefaultAction, EditorActionsTitleBarAction, HideEditorActionsAction, HideEditorTabsAction, ShowMultipleEditorTabsAction, ShowSingleEditorTabAction, ZenHideEditorTabsAction, ZenShowMultipleEditorTabsAction, ZenShowSingleEditorTabAction } from 'vs/workbench/browser/actions/layoutActions'; +import { ConfigureEditorAction, ConfigureEditorTabsAction, EditorActionsDefaultAction, EditorActionsTitleBarAction, HideEditorActionsAction, HideEditorTabsAction, ShowMultipleEditorTabsAction, ShowSingleEditorTabAction, ZenHideEditorTabsAction, ZenShowMultipleEditorTabsAction, ZenShowSingleEditorTabAction } from 'vs/workbench/browser/actions/layoutActions'; import { ICommandAction } from 'vs/platform/action/common/action'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -385,6 +385,8 @@ MenuRegistry.appendMenuItem(MenuId.EditorActionsPositionSubmenu, { command: { id MenuRegistry.appendMenuItem(MenuId.EditorActionsPositionSubmenu, { command: { id: EditorActionsTitleBarAction.ID, title: localize('titleBar', "Title Bar"), toggled: ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.editor.editorActionsLocation', 'titleBar'), ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.editor.showTabs', 'none'), ContextKeyExpr.equals('config.workbench.editor.editorActionsLocation', 'default'))) }, group: '1_config', order: 20 }); MenuRegistry.appendMenuItem(MenuId.EditorActionsPositionSubmenu, { command: { id: HideEditorActionsAction.ID, title: localize('hidden', "Hidden"), toggled: ContextKeyExpr.equals('config.workbench.editor.editorActionsLocation', 'hidden') }, group: '1_config', order: 30 }); +MenuRegistry.appendMenuItem(MenuId.EditorTabsBarContext, { command: { id: ConfigureEditorTabsAction.ID, title: localize('configureTabs', "Configure Tabs") }, group: '9_configure', order: 10 }); + // Editor Title Context Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITOR_COMMAND_ID, title: localize('close', "Close") }, group: '1_close', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeOthers', "Close Others"), precondition: EditorGroupEditorsCountContext.notEqualsTo('1') }, group: '1_close', order: 20 }); @@ -414,6 +416,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_KEEP_EDI MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_MAXIMIZE_EDITOR_GROUP, title: localize('maximizeGroup', "Maximize Group") }, group: '8_group_operations', order: 5, when: ContextKeyExpr.and(EditorPartMaximizedEditorGroupContext.negate(), EditorPartMultipleEditorGroupsContext) }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_MAXIMIZE_EDITOR_GROUP, title: localize('unmaximizeGroup', "Unmaximize Group") }, group: '8_group_operations', order: 5, when: EditorPartMaximizedEditorGroupContext }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_LOCK_GROUP_COMMAND_ID, title: localize('lockGroup', "Lock Group"), toggled: ActiveEditorGroupLockedContext }, group: '8_group_operations', order: 10, when: IsAuxiliaryEditorPartContext.toNegated() /* already a primary action for aux windows */ }); +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: ConfigureEditorAction.ID, title: localize('configureEditors', "Configure Editors") }, group: '9_configure', order: 10 }); function appendEditorToolItem(primary: ICommandAction, when: ContextKeyExpression | undefined, order: number, alternative?: ICommandAction, precondition?: ContextKeyExpression | undefined): void { const item: IMenuItem = { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index eb9d1534e11..3293be1a2c2 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -38,6 +38,7 @@ import { getActiveDocument } from 'vs/base/browser/dom'; import { ICommandActionTitle } from 'vs/platform/action/common/action'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { resolveCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommandsContext'; +import { IListService } from 'vs/platform/list/browser/listService'; class ExecuteCommandAction extends Action2 { @@ -63,13 +64,13 @@ abstract class AbstractSplitEditorAction extends Action2 { } override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); const direction = this.getDirection(configurationService); - const commandContext = resolveCommandsContext(accessor, args); + const commandContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); - splitEditor(editorGroupService, direction, commandContext); + splitEditor(editorGroupsService, direction, commandContext); } } @@ -1174,7 +1175,7 @@ export class ToggleMaximizeEditorGroupAction extends Action2 { override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { const editorGroupsService = accessor.get(IEditorGroupsService); - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); if (resolvedContext.groupedEditors.length) { editorGroupsService.toggleMaximizeGroup(resolvedContext.groupedEditors[0].group); } @@ -2544,7 +2545,7 @@ abstract class BaseMoveCopyEditorToNewWindowAction extends Action2 { override async run(accessor: ServicesAccessor, ...args: unknown[]) { const editorGroupService = accessor.get(IEditorGroupsService); - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupService, accessor.get(IListService)); if (!resolvedContext.groupedEditors.length) { return; } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 7f137ca5244..10405ef3d50 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -20,7 +20,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { EditorResolution, IEditorOptions, IResourceEditorInput, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IOpenEvent } from 'vs/platform/list/browser/listService'; +import { IListService, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -240,7 +240,7 @@ function registerActiveEditorMoveCopyCommand(): void { } function moveCopyActiveEditorToGroup(isMove: boolean, args: ActiveEditorMoveCopyArguments, control: IVisibleEditorPane, accessor: ServicesAccessor): void { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); const sourceGroup = control.group; @@ -248,49 +248,49 @@ function registerActiveEditorMoveCopyCommand(): void { switch (args.to) { case 'left': - targetGroup = editorGroupService.findGroup({ direction: GroupDirection.LEFT }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ direction: GroupDirection.LEFT }, sourceGroup); if (!targetGroup) { - targetGroup = editorGroupService.addGroup(sourceGroup, GroupDirection.LEFT); + targetGroup = editorGroupsService.addGroup(sourceGroup, GroupDirection.LEFT); } break; case 'right': - targetGroup = editorGroupService.findGroup({ direction: GroupDirection.RIGHT }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ direction: GroupDirection.RIGHT }, sourceGroup); if (!targetGroup) { - targetGroup = editorGroupService.addGroup(sourceGroup, GroupDirection.RIGHT); + targetGroup = editorGroupsService.addGroup(sourceGroup, GroupDirection.RIGHT); } break; case 'up': - targetGroup = editorGroupService.findGroup({ direction: GroupDirection.UP }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ direction: GroupDirection.UP }, sourceGroup); if (!targetGroup) { - targetGroup = editorGroupService.addGroup(sourceGroup, GroupDirection.UP); + targetGroup = editorGroupsService.addGroup(sourceGroup, GroupDirection.UP); } break; case 'down': - targetGroup = editorGroupService.findGroup({ direction: GroupDirection.DOWN }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ direction: GroupDirection.DOWN }, sourceGroup); if (!targetGroup) { - targetGroup = editorGroupService.addGroup(sourceGroup, GroupDirection.DOWN); + targetGroup = editorGroupsService.addGroup(sourceGroup, GroupDirection.DOWN); } break; case 'first': - targetGroup = editorGroupService.findGroup({ location: GroupLocation.FIRST }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ location: GroupLocation.FIRST }, sourceGroup); break; case 'last': - targetGroup = editorGroupService.findGroup({ location: GroupLocation.LAST }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ location: GroupLocation.LAST }, sourceGroup); break; case 'previous': - targetGroup = editorGroupService.findGroup({ location: GroupLocation.PREVIOUS }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ location: GroupLocation.PREVIOUS }, sourceGroup); break; case 'next': - targetGroup = editorGroupService.findGroup({ location: GroupLocation.NEXT }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ location: GroupLocation.NEXT }, sourceGroup); if (!targetGroup) { - targetGroup = editorGroupService.addGroup(sourceGroup, preferredSideBySideGroupDirection(configurationService)); + targetGroup = editorGroupsService.addGroup(sourceGroup, preferredSideBySideGroupDirection(configurationService)); } break; case 'center': - targetGroup = editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)[(editorGroupService.count / 2) - 1]; + targetGroup = editorGroupsService.getGroups(GroupsOrder.GRID_APPEARANCE)[(editorGroupsService.count / 2) - 1]; break; case 'position': - targetGroup = editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)[(args.value ?? 1) - 1]; + targetGroup = editorGroupsService.getGroups(GroupsOrder.GRID_APPEARANCE)[(args.value ?? 1) - 1]; break; } @@ -312,8 +312,8 @@ function registerEditorGroupsLayoutCommands(): void { return; } - const editorGroupService = accessor.get(IEditorGroupsService); - editorGroupService.applyLayout(layout); + const editorGroupsService = accessor.get(IEditorGroupsService); + editorGroupsService.applyLayout(layout); } CommandsRegistry.registerCommand(LAYOUT_EDITOR_GROUPS_COMMAND_ID, (accessor: ServicesAccessor, args: EditorGroupLayout) => { @@ -350,9 +350,9 @@ function registerEditorGroupsLayoutCommands(): void { CommandsRegistry.registerCommand({ id: 'vscode.getEditorLayout', handler: (accessor: ServicesAccessor) => { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); - return editorGroupService.getLayout(); + return editorGroupsService.getLayout(); }, metadata: { description: 'Get Editor Layout', @@ -390,7 +390,7 @@ function registerOpenEditorAPICommands(): void { CommandsRegistry.registerCommand(API_OPEN_EDITOR_COMMAND_ID, async function (accessor: ServicesAccessor, resourceArg: UriComponents | string, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?], label?: string, context?: IOpenEvent) { const editorService = accessor.get(IEditorService); - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const openerService = accessor.get(IOpenerService); const pathService = accessor.get(IPathService); const configurationService = accessor.get(IConfigurationService); @@ -419,7 +419,7 @@ function registerOpenEditorAPICommands(): void { input = { resource, options, label }; } - await editorService.openEditor(input, columnToEditorGroup(editorGroupService, configurationService, column)); + await editorService.openEditor(input, columnToEditorGroup(editorGroupsService, configurationService, column)); } // do not allow to execute commands from here @@ -452,7 +452,7 @@ function registerOpenEditorAPICommands(): void { CommandsRegistry.registerCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, async function (accessor: ServicesAccessor, originalResource: UriComponents, modifiedResource: UriComponents, labelAndOrDescription?: string | { label: string; description: string }, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?], context?: IOpenEvent) { const editorService = accessor.get(IEditorService); - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); const [columnArg, optionsArg] = columnAndOptions ?? []; @@ -473,7 +473,7 @@ function registerOpenEditorAPICommands(): void { label, description, options - }, columnToEditorGroup(editorGroupService, configurationService, column)); + }, columnToEditorGroup(editorGroupsService, configurationService, column)); }); CommandsRegistry.registerCommand(API_OPEN_WITH_EDITOR_COMMAND_ID, async (accessor: ServicesAccessor, resource: UriComponents, id: string, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?]) => { @@ -594,30 +594,30 @@ function registerFocusEditorGroupAtIndexCommands(): void { when: undefined, primary: KeyMod.CtrlCmd | toKeyCode(groupIndex), handler: accessor => { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); // To keep backwards compatibility (pre-grid), allow to focus a group // that does not exist as long as it is the next group after the last // opened group. Otherwise we return. - if (groupIndex > editorGroupService.count) { + if (groupIndex > editorGroupsService.count) { return; } // Group exists: just focus - const groups = editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE); + const groups = editorGroupsService.getGroups(GroupsOrder.GRID_APPEARANCE); if (groups[groupIndex]) { return groups[groupIndex].focus(); } // Group does not exist: create new by splitting the active one of the last group const direction = preferredSideBySideGroupDirection(configurationService); - const lastGroup = editorGroupService.findGroup({ location: GroupLocation.LAST }); + const lastGroup = editorGroupsService.findGroup({ location: GroupLocation.LAST }); if (!lastGroup) { return; } - const newGroup = editorGroupService.addGroup(lastGroup, direction); + const newGroup = editorGroupsService.addGroup(lastGroup, direction); // Focus newGroup.focus(); @@ -654,7 +654,7 @@ function registerFocusEditorGroupAtIndexCommands(): void { } } -export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, resolvedContext: IResolvedEditorCommandsContext): void { +export function splitEditor(editorGroupsService: IEditorGroupsService, direction: GroupDirection, resolvedContext: IResolvedEditorCommandsContext): void { if (!resolvedContext.groupedEditors.length) { return; } @@ -662,7 +662,7 @@ export function splitEditor(editorGroupService: IEditorGroupsService, direction: // Only support splitting from one source group const { group, editors } = resolvedContext.groupedEditors[0]; const preserveFocus = resolvedContext.preserveFocus; - const newGroup = editorGroupService.addGroup(group, direction); + const newGroup = editorGroupsService.addGroup(group, direction); for (const editorToCopy of editors) { // Split editor (if it can be split) @@ -683,7 +683,7 @@ function registerSplitEditorCommands() { { id: SPLIT_EDITOR_RIGHT, direction: GroupDirection.RIGHT } ].forEach(({ id, direction }) => { CommandsRegistry.registerCommand(id, function (accessor, ...args) { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); splitEditor(accessor.get(IEditorGroupsService), direction, resolvedContext); }); }); @@ -729,7 +729,7 @@ function registerCloseEditorCommands() { } // With context: proceed to close editors as instructed - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); const preserveFocus = resolvedContext.preserveFocus; return Promise.all(resolvedContext.groupedEditors.map(async ({ group, editors }) => { @@ -759,7 +759,7 @@ function registerCloseEditorCommands() { when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyW), handler: (accessor, ...args: unknown[]) => { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); return Promise.all(resolvedContext.groupedEditors.map(async ({ group }) => { await group.closeAllEditors({ excludeSticky: true }); })); @@ -773,11 +773,11 @@ function registerCloseEditorCommands() { primary: KeyMod.CtrlCmd | KeyCode.KeyW, win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KeyW] }, handler: (accessor, ...args: unknown[]) => { - const editorGroupService = accessor.get(IEditorGroupsService); - const commandsContext = resolveCommandsContext(accessor, args); + const editorGroupsService = accessor.get(IEditorGroupsService); + const commandsContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); if (commandsContext.groupedEditors.length) { - editorGroupService.removeGroup(commandsContext.groupedEditors[0].group); + editorGroupsService.removeGroup(commandsContext.groupedEditors[0].group); } } }); @@ -788,7 +788,7 @@ function registerCloseEditorCommands() { when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyU), handler: (accessor, ...args: unknown[]) => { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); return Promise.all(resolvedContext.groupedEditors.map(async ({ group }) => { await group.closeEditors({ savedOnly: true, excludeSticky: true }, { preserveFocus: resolvedContext.preserveFocus }); })); @@ -802,7 +802,7 @@ function registerCloseEditorCommands() { primary: undefined, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyT }, handler: (accessor, ...args: unknown[]) => { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); return Promise.all(resolvedContext.groupedEditors.map(async ({ group, editors }) => { const editorsToClose = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).filter(editor => !editors.includes(editor)); @@ -824,7 +824,7 @@ function registerCloseEditorCommands() { when: undefined, primary: undefined, handler: async (accessor, ...args: unknown[]) => { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); if (resolvedContext.groupedEditors.length) { const { group, editors } = resolvedContext.groupedEditors[0]; if (group.activeEditor) { @@ -846,7 +846,7 @@ function registerCloseEditorCommands() { const editorResolverService = accessor.get(IEditorResolverService); const telemetryService = accessor.get(ITelemetryService); - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, editorService, accessor.get(IEditorGroupsService), accessor.get(IListService)); const editorReplacements = new Map(); for (const { group, editors } of resolvedContext.groupedEditors) { @@ -910,15 +910,15 @@ function registerCloseEditorCommands() { }); CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, async (accessor: ServicesAccessor, ...args: unknown[]) => { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); if (resolvedContext.groupedEditors.length) { const { group } = resolvedContext.groupedEditors[0]; await group.closeAllEditors(); - if (group.count === 0 && editorGroupService.getGroup(group.id) /* could be gone by now */) { - editorGroupService.removeGroup(group); // only remove group if it is now empty + if (group.count === 0 && editorGroupsService.getGroup(group.id) /* could be gone by now */) { + editorGroupsService.removeGroup(group); // only remove group if it is now empty } } }); @@ -947,9 +947,9 @@ function registerFocusEditorGroupWihoutWrapCommands(): void { for (const command of commands) { CommandsRegistry.registerCommand(command.id, async (accessor: ServicesAccessor) => { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); - const group = editorGroupService.findGroup({ direction: command.direction }, editorGroupService.activeGroup, false); + const group = editorGroupsService.findGroup({ direction: command.direction }, editorGroupsService.activeGroup, false); group?.focus(); }); } @@ -993,7 +993,7 @@ function registerSplitEditorInGroupCommands(): void { }); } run(accessor: ServicesAccessor, ...args: unknown[]): Promise { - return splitEditorInGroup(accessor, resolveCommandsContext(accessor, args)); + return splitEditorInGroup(accessor, resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService))); } }); @@ -1046,7 +1046,7 @@ function registerSplitEditorInGroupCommands(): void { }); } run(accessor: ServicesAccessor, ...args: unknown[]): Promise { - return joinEditorInGroup(resolveCommandsContext(accessor, args)); + return joinEditorInGroup(resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService))); } }); @@ -1061,7 +1061,7 @@ function registerSplitEditorInGroupCommands(): void { }); } async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); if (!resolvedContext.groupedEditors.length) { return; } @@ -1186,7 +1186,7 @@ function registerOtherEditorCommands(): void { when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.Enter), handler: async (accessor, ...args: unknown[]) => { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); for (const { group, editors } of resolvedContext.groupedEditors) { for (const editor of editors) { group.pinEditor(editor); @@ -1207,7 +1207,7 @@ function registerOtherEditorCommands(): void { }); function setEditorGroupLock(accessor: ServicesAccessor, locked: boolean | undefined, ...args: unknown[]): void { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); const group = resolvedContext.groupedEditors[0]?.group; group?.lock(locked ?? !group.isLocked); } @@ -1262,7 +1262,7 @@ function registerOtherEditorCommands(): void { when: ActiveEditorStickyContext.toNegated(), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), handler: async (accessor, ...args: unknown[]) => { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); for (const { group, editors } of resolvedContext.groupedEditors) { for (const editor of editors) { group.stickEditor(editor); @@ -1278,7 +1278,7 @@ function registerOtherEditorCommands(): void { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.KeyO), handler: async accessor => { const editorService = accessor.get(IEditorService); - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const activeEditor = editorService.activeEditor; const activeTextEditorControl = editorService.activeTextEditorControl; @@ -1294,7 +1294,7 @@ function registerOtherEditorCommands(): void { editor = activeEditor.modified; } - return editorGroupService.activeGroup.openEditor(editor); + return editorGroupsService.activeGroup.openEditor(editor); } }); @@ -1304,7 +1304,7 @@ function registerOtherEditorCommands(): void { when: ActiveEditorStickyContext, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), handler: async (accessor, ...args: unknown[]) => { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); for (const { group, editors } of resolvedContext.groupedEditors) { for (const editor of editors) { group.unstickEditor(editor); @@ -1319,13 +1319,13 @@ function registerOtherEditorCommands(): void { when: undefined, primary: undefined, handler: (accessor, ...args: unknown[]) => { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const quickInputService = accessor.get(IQuickInputService); - const commandsContext = resolveCommandsContext(accessor, args); + const commandsContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); const group = commandsContext.groupedEditors[0]?.group; if (group) { - editorGroupService.activateGroup(group); // we need the group to be active + editorGroupsService.activateGroup(group); // we need the group to be active } return quickInputService.quickAccess.show(ActiveGroupEditorsByMostRecentlyUsedQuickAccess.PREFIX); diff --git a/src/vs/workbench/browser/parts/editor/editorCommandsContext.ts b/src/vs/workbench/browser/parts/editor/editorCommandsContext.ts index ee5957ddfa0..b9fea217efb 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommandsContext.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommandsContext.ts @@ -6,7 +6,6 @@ import { getActiveElement } from 'vs/base/browser/dom'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { URI } from 'vs/base/common/uri'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IListService } from 'vs/platform/list/browser/listService'; import { IEditorCommandsContext, isEditorCommandsContext, IEditorIdentifier, isEditorIdentifier } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -21,10 +20,9 @@ export interface IResolvedEditorCommandsContext { readonly preserveFocus: boolean; } -export function resolveCommandsContext(accessor: ServicesAccessor, commandArgs: unknown[]): IResolvedEditorCommandsContext { - const editorGroupsService = accessor.get(IEditorGroupsService); +export function resolveCommandsContext(commandArgs: unknown[], editorService: IEditorService, editorGroupsService: IEditorGroupsService, listService: IListService): IResolvedEditorCommandsContext { - const commandContext = getCommandsContext(accessor, commandArgs); + const commandContext = getCommandsContext(commandArgs, editorService, editorGroupsService, listService); const preserveFocus = commandContext.length ? commandContext[0].preserveFocus || false : false; const resolvedContext: IResolvedEditorCommandsContext = { groupedEditors: [], preserveFocus }; @@ -60,25 +58,23 @@ export function resolveCommandsContext(accessor: ServicesAccessor, commandArgs: return resolvedContext; } -function getCommandsContext(accessor: ServicesAccessor, commandArgs: unknown[]): IEditorCommandsContext[] { +function getCommandsContext(commandArgs: unknown[], editorService: IEditorService, editorGroupsService: IEditorGroupsService, listService: IListService): IEditorCommandsContext[] { // Figure out if command is executed from a list - const listService = accessor.get(IListService); const list = listService.lastFocusedList; let isListAction = list instanceof List && list.getHTMLElement() === getActiveElement(); // Get editor context for which the command was triggered - let editorContext = getEditorContextFromCommandArgs(accessor, commandArgs, isListAction); + let editorContext = getEditorContextFromCommandArgs(commandArgs, isListAction, editorService, editorGroupsService, listService); // If the editor context can not be determind use the active editor if (!editorContext) { - const editorGroupService = accessor.get(IEditorGroupsService); - const activeGroup = editorGroupService.activeGroup; + const activeGroup = editorGroupsService.activeGroup; const activeEditor = activeGroup.activeEditor; editorContext = { groupId: activeGroup.id, editorIndex: activeEditor ? activeGroup.getIndexOfEditor(activeEditor) : undefined }; isListAction = false; } - const multiEditorContext = getMultiSelectContext(accessor, editorContext, isListAction); + const multiEditorContext = getMultiSelectContext(editorContext, isListAction, editorService, editorGroupsService, listService); // Make sure the command context is the first one in the list return moveCurrentEditorContextToFront(editorContext, multiEditorContext); @@ -106,7 +102,7 @@ function moveCurrentEditorContextToFront(editorContext: IEditorCommandsContext, return multiEditorContext; } -function getEditorContextFromCommandArgs(accessor: ServicesAccessor, commandArgs: unknown[], isListAcion: boolean): IEditorCommandsContext | undefined { +function getEditorContextFromCommandArgs(commandArgs: unknown[], isListAction: boolean, editorService: IEditorService, editorGroupsService: IEditorGroupsService, listService: IListService): IEditorCommandsContext | undefined { // We only know how to extraxt the command context from URI and IEditorCommandsContext arguments const filteredArgs = commandArgs.filter(arg => isEditorCommandsContext(arg) || URI.isUri(arg)); @@ -117,9 +113,6 @@ function getEditorContextFromCommandArgs(accessor: ServicesAccessor, commandArgs } } - const editorService = accessor.get(IEditorService); - const editorGroupsService = accessor.get(IEditorGroupsService); - // Otherwise, try to find the editor group by the URI of the resource for (const uri of filteredArgs as URI[]) { const editorIdentifiers = editorService.findEditors(uri); @@ -130,11 +123,9 @@ function getEditorContextFromCommandArgs(accessor: ServicesAccessor, commandArgs } } - const listService = accessor.get(IListService); - // If there is no context in the arguments, try to find the context from the focused list // if the action was executed from a list - if (isListAcion) { + if (isListAction) { const list = listService.lastFocusedList as List; for (const focusedElement of list.getFocusedElements()) { if (isGroupOrEditor(focusedElement)) { @@ -146,9 +137,7 @@ function getEditorContextFromCommandArgs(accessor: ServicesAccessor, commandArgs return undefined; } -function getMultiSelectContext(accessor: ServicesAccessor, editorContext: IEditorCommandsContext, isListAction: boolean): IEditorCommandsContext[] { - const listService = accessor.get(IListService); - const editorGroupsService = accessor.get(IEditorGroupsService); +function getMultiSelectContext(editorContext: IEditorCommandsContext, isListAction: boolean, editorService: IEditorService, editorGroupsService: IEditorGroupsService, listService: IListService): IEditorCommandsContext[] { // If the action was executed from a list, return all selected editors if (isListAction) { @@ -158,6 +147,15 @@ function getMultiSelectContext(accessor: ServicesAccessor, editorContext: IEdito if (selection.length > 1) { return selection.map(e => groupOrEditorToEditorContext(e, editorContext.preserveFocus, editorGroupsService)); } + + if (selection.length === 0) { + // TODO@benibenj workaround for https://github.com/microsoft/vscode/issues/224050 + // Explainer: the `isListAction` flag can be a false positive in certain cases because + // it will be `true` if the active element is a `List` even if it is part of the editor + // area. The workaround here is to fallback to `isListAction: false` if the list is not + // having any editor or group selected. + return getMultiSelectContext(editorContext, false, editorService, editorGroupsService, listService); + } } // Check editors selected in the group (tabs) else { diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index d785a513798..c305eb57158 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -114,7 +114,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { private readonly _onDidActivateGroup = this._register(new Emitter()); readonly onDidActivateGroup = this._onDidActivateGroup.event; - private readonly _onDidAddGroup = this._register(new Emitter()); + private readonly _onDidAddGroup = this._register(new PauseableEmitter()); readonly onDidAddGroup = this._onDidAddGroup.event; private readonly _onDidRemoveGroup = this._register(new PauseableEmitter()); @@ -134,6 +134,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { private readonly _onDidChangeEditorPartOptions = this._register(new Emitter()); readonly onDidChangeEditorPartOptions = this._onDidChangeEditorPartOptions.event; + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + //#endregion private readonly workspaceMemento = this.getMemento(StorageScope.WORKSPACE, StorageTarget.USER); @@ -1103,6 +1106,10 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { openVerticalPosition = Position.BOTTOM; } + if (e.eventData.clientY < boundingRect.top + proximity) { + openVerticalPosition = Position.TOP; + } + if (horizontalOpenerTimeout && openHorizontalPosition !== lastOpenHorizontalPosition) { clearTimeout(horizontalOpenerTimeout); horizontalOpenerTimeout = undefined; @@ -1352,7 +1359,17 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { private async doApplyState(state: IEditorPartUIState, options?: IEditorGroupViewOptions): Promise { const groups = await this.doPrepareApplyState(); - const resumeEvents = this.disposeGroups(true /* suspress events for the duration of applying state */); + + // Pause add/remove events for groups during the duration of applying the state + // This ensures that we can do this transition atomically with the new state + // being ready when the events are fired. This is important because usually there + // is never the state where no groups are present, but for this transition we + // need to temporarily dispose all groups to restore the new set. + + this._onDidAddGroup.pause(); + this._onDidRemoveGroup.pause(); + + this.disposeGroups(); // MRU this.mostRecentActiveGroups = state.mostRecentActiveGroups; @@ -1361,7 +1378,12 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { try { this.doApplyGridState(state.serializedGrid, state.activeGroup, undefined, options); } finally { - resumeEvents(); + // It is very important to keep this order: first resume the events for + // removed groups and then for added groups. Many listeners may store + // groups in sets by their identifier and groups can have the same + // identifier before and after. + this._onDidRemoveGroup.resume(); + this._onDidAddGroup.resume(); } // Restore editors that were not closed before and are now opened now @@ -1435,13 +1457,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { }; } - private disposeGroups(): void; - private disposeGroups(surpressEvents: boolean): Function; - private disposeGroups(surpressEvents?: boolean): Function | void { - if (surpressEvents) { - this._onDidRemoveGroup.pause(); - } - + private disposeGroups(): void { for (const group of this.groups) { group.dispose(); @@ -1450,14 +1466,13 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { this.groupViews.clear(); this.mostRecentActiveGroups = []; - - if (surpressEvents) { - return () => this._onDidRemoveGroup.resume(); - } } override dispose(): void { + // Event + this._onWillDispose.fire(); + // Forward to all groups this.disposeGroups(); diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 18123131e5b..c04fd7769ac 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions, IEditorPart } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Emitter } from 'vs/base/common/event'; import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { GroupIdentifier } from 'vs/workbench/common/editor'; @@ -22,6 +22,8 @@ import { IAuxiliaryWindowOpenOptions, IAuxiliaryWindowService } from 'vs/workben import { generateUuid } from 'vs/base/common/uuid'; import { ContextKeyValue, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { isHTMLElement } from 'vs/base/browser/dom'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; interface IEditorPartsUIState { readonly auxiliary: IAuxiliaryEditorPartState[]; @@ -70,19 +72,44 @@ export class EditorParts extends MultiWindowParts implements IEditor return this.instantiationService.createInstance(MainEditorPart, this); } + //#region Scoped Instantiation Services + + private readonly mapPartToInstantiationService = new Map(); + + getScopedInstantiationService(part: IEditorPart): IInstantiationService { + if (part === this.mainPart) { + if (!this.mapPartToInstantiationService.has(part.windowId)) { + this.instantiationService.invokeFunction(accessor => { + const editorService = accessor.get(IEditorService); // using `invokeFunction` to get hold of `IEditorService` lazily + + this.mapPartToInstantiationService.set(part.windowId, this._register(this.instantiationService.createChild(new ServiceCollection( + [IEditorService, editorService.createScoped('main', this._store)] + )))); + }); + } + } + + return this.mapPartToInstantiationService.get(part.windowId) ?? this.instantiationService; + } + + //#endregion + //#region Auxiliary Editor Parts - private readonly _onDidCreateAuxiliaryEditorPart = this._register(new Emitter()); + private readonly _onDidCreateAuxiliaryEditorPart = this._register(new Emitter()); readonly onDidCreateAuxiliaryEditorPart = this._onDidCreateAuxiliaryEditorPart.event; async createAuxiliaryEditorPart(options?: IAuxiliaryEditorPartOpenOptions): Promise { const { part, instantiationService, disposables } = await this.instantiationService.createInstance(AuxiliaryEditorPart, this).create(this.getGroupsLabel(this._parts.size), options); + // Keep instantiation service + this.mapPartToInstantiationService.set(part.windowId, instantiationService); + disposables.add(toDisposable(() => this.mapPartToInstantiationService.delete(part.windowId))); + // Events this._onDidAddGroup.fire(part.activeGroup); - const eventDisposables = disposables.add(new DisposableStore()); - this._onDidCreateAuxiliaryEditorPart.fire({ part, instantiationService, disposables: eventDisposables }); + this._onDidCreateAuxiliaryEditorPart.fire(part); return part; } diff --git a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts index 225330438f6..3a69dbbff43 100644 --- a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts +++ b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -60,7 +60,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro ); } - override provide(picker: IQuickPick, token: CancellationToken): IDisposable { + override provide(picker: IQuickPick, token: CancellationToken): IDisposable { // Reset the pick state for this run this.pickState.reset(!!picker.quickNavigate); diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 845160784b1..dcef2cf97d9 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -55,9 +55,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { TabFocus } from 'vs/editor/browser/config/tabFocus'; -import { mainWindow } from 'vs/base/browser/window'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IEditorGroupsService, IEditorPart } from 'vs/workbench/services/editor/common/editorGroupsService'; class SideBySideEditorEncodingSupport implements IEncodingSupport { constructor(private primary: IEncodingSupport, private secondary: IEncodingSupport) { } @@ -886,22 +884,23 @@ export class EditorStatusContribution extends Disposable implements IWorkbenchCo static readonly ID = 'workbench.contrib.editorStatus'; constructor( - @IInstantiationService instantiationService: IInstantiationService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, ) { super(); - // Main Editor Status - const mainInstantiationService = this._register(instantiationService.createChild(new ServiceCollection( - [IEditorService, editorService.createScoped('main', this._store)] - ))); - this._register(mainInstantiationService.createInstance(EditorStatus, mainWindow.vscodeWindowId)); + for (const part of editorGroupService.parts) { + this.createEditorStatus(part); + } - // Auxiliary Editor Status - this._register(editorGroupService.onDidCreateAuxiliaryEditorPart(({ part, instantiationService, disposables }) => { - disposables.add(instantiationService.createInstance(EditorStatus, part.windowId)); - })); + this._register(editorGroupService.onDidCreateAuxiliaryEditorPart(part => this.createEditorStatus(part))); + } + + private createEditorStatus(part: IEditorPart): void { + const disposables = new DisposableStore(); + Event.once(part.onWillDispose)(() => disposables.dispose()); + + const scopedInstantiationService = this.editorGroupService.getScopedInstantiationService(part); + disposables.add(scopedInstantiationService.createInstance(EditorStatus, part.windowId)); } } diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 174997a925d..52f116455cf 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -73,13 +73,9 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { height: 35px; /* matches height of composite container */ - padding: 0 5px; + padding: 0 3px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon, -.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { - font-size: 18px; -} .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon), .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { diff --git a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts index 15a240f9150..f36a4ddea7d 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts @@ -7,8 +7,8 @@ import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; -import { IAccessibleViewService, AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; -import { IAccessibleViewImplentation, alertAccessibleViewFocusChange } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IAccessibleViewService, AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { IAccessibilitySignalService, AccessibilitySignal } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -35,11 +35,9 @@ export class NotificationAccessibleView implements IAccessibleViewImplentation { } commandService.executeCommand('notifications.showList'); let notificationIndex: number | undefined; - let length: number | undefined; const list = listService.lastFocusedList; if (list instanceof WorkbenchList) { notificationIndex = list.indexOf(notification); - length = list.length; } if (notificationIndex === undefined) { return; @@ -54,45 +52,48 @@ export class NotificationAccessibleView implements IAccessibleViewImplentation { } catch { } } } - const message = notification.message.original.toString(); - if (!message) { + + function getContentForNotification(): string | undefined { + const notification = getNotificationFromContext(listService); + const message = notification?.message.original.toString(); + if (!notification) { + return; + } + return notification.source ? localize('notification.accessibleViewSrc', '{0} Source: {1}', message, notification.source) : localize('notification.accessibleView', '{0}', message); + } + const content = getContentForNotification(); + if (!content) { return; } notification.onDidClose(() => accessibleViewService.next()); - return { - id: AccessibleViewProviderId.Notification, - provideContent: () => { - return notification.source ? localize('notification.accessibleViewSrc', '{0} Source: {1}', message, notification.source) : localize('notification.accessibleView', '{0}', message); - }, - onClose(): void { - focusList(); - }, - next(): void { + return new AccessibleContentProvider( + AccessibleViewProviderId.Notification, + { type: AccessibleViewType.View }, + () => content, + () => focusList(), + 'accessibility.verbosity.notification', + undefined, + getActionsFromNotification(notification, accessibilitySignalService), + () => { if (!list) { return; } focusList(); list.focusNext(); - alertAccessibleViewFocusChange(notificationIndex, length, 'next'); - getProvider(); + return getContentForNotification(); }, - previous(): void { + () => { if (!list) { return; } focusList(); list.focusPrevious(); - alertAccessibleViewFocusChange(notificationIndex, length, 'previous'); - getProvider(); + return getContentForNotification(); }, - verbositySettingKey: 'accessibility.verbosity.notification', - options: { type: AccessibleViewType.View }, - actions: getActionsFromNotification(notification, accessibilitySignalService) - }; + ); } return getProvider(); } - dispose() { } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 92b1b45d184..5151fdc18e1 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -312,12 +312,12 @@ export function registerNotificationCommands(center: INotificationsCenterControl picker.canSelectMany = true; picker.placeholder = localize('selectSources', "Select sources to enable all notifications from"); - picker.selectedItems = picker.items.filter(item => (item as INotificationSourceFilter).filter === NotificationsFilter.OFF) as (IQuickPickItem & INotificationSourceFilter)[]; + picker.selectedItems = picker.items.filter(item => item.filter === NotificationsFilter.OFF); picker.show(); disposables.add(picker.onDidAccept(async () => { - for (const item of picker.items as (IQuickPickItem & INotificationSourceFilter)[]) { + for (const item of picker.items) { notificationService.setFilter({ id: item.id, label: item.label, diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 40a5ee28faf..e1c147d8e88 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -17,6 +17,15 @@ border-top-width: 0; /* no border when main editor area is hiden */ } +.monaco-workbench .part.panel.top { + border-bottom-width: 1px; + border-bottom-style: solid; +} + +.monaco-workbench.nomaineditorarea .part.panel.top { + border-bottom-width: 0; /* no border when main editor area is hiden */ +} + .monaco-workbench .part.panel.right { border-left-width: 1px; border-left-style: solid; @@ -81,3 +90,11 @@ display: inline-block; transform: rotate(90deg); } + +/* Rotate icons when panel is on left */ +.monaco-workbench .part.basepanel.top .title-actions .codicon-split-horizontal::before, +.monaco-workbench .part.basepanel.top .global-actions .codicon-panel-maximize::before, +.monaco-workbench .part.basepanel.top .global-actions .codicon-panel-restore::before { + display: inline-block; + transform: rotate(180deg); +} diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 534db0283a7..49d0e197bf6 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -8,7 +8,7 @@ import { localize, localize2 } from 'vs/nls'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuId, MenuRegistry, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, PanelAlignment, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; +import { ActivityBarPosition, isHorizontal, IWorkbenchLayoutService, LayoutSettings, PanelAlignment, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/contextkeys'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { Codicon } from 'vs/base/common/codicons'; @@ -99,6 +99,7 @@ const PositionPanelActionId = { LEFT: 'workbench.action.positionPanelLeft', RIGHT: 'workbench.action.positionPanelRight', BOTTOM: 'workbench.action.positionPanelBottom', + TOP: 'workbench.action.positionPanelTop' }; const AlignPanelActionId = { @@ -136,6 +137,7 @@ function createAlignmentPanelActionConfig(id: string, title: ICommandActionTitle const PositionPanelActionConfigs: PanelActionConfig[] = [ + createPositionPanelActionConfig(PositionPanelActionId.TOP, localize2('positionPanelTop', "Move Panel To Top"), localize('positionPanelTopShort', "Top"), Position.TOP), createPositionPanelActionConfig(PositionPanelActionId.LEFT, localize2('positionPanelLeft', "Move Panel Left"), localize('positionPanelLeftShort', "Left"), Position.LEFT), createPositionPanelActionConfig(PositionPanelActionId.RIGHT, localize2('positionPanelRight', "Move Panel Right"), localize('positionPanelRightShort', "Right"), Position.RIGHT), createPositionPanelActionConfig(PositionPanelActionId.BOTTOM, localize2('positionPanelBottom', "Move Panel To Bottom"), localize('positionPanelBottomShort', "Bottom"), Position.BOTTOM), @@ -158,7 +160,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 4 }); -PositionPanelActionConfigs.forEach(positionPanelAction => { +PositionPanelActionConfigs.forEach((positionPanelAction, index) => { const { id, title, shortLabel, value, when } = positionPanelAction; registerAction2(class extends Action2 { @@ -182,7 +184,7 @@ PositionPanelActionConfigs.forEach(positionPanelAction => { title: shortLabel, toggled: when.negate() }, - order: 5 + order: 5 + index }); }); @@ -280,23 +282,23 @@ registerAction2(class extends Action2 { tooltip: localize('maximizePanel', "Maximize Panel Size"), category: Categories.View, f1: true, - icon: maximizeIcon, + icon: maximizeIcon, // This is being rotated in CSS depending on the panel position // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - precondition: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), PanelPositionContext.notEqualsTo('bottom')), + precondition: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), ContextKeyExpr.and(PanelPositionContext.notEqualsTo('bottom'), PanelPositionContext.notEqualsTo('top'))), toggled: { condition: PanelMaximizedContext, icon: restoreIcon, tooltip: localize('minimizePanel', "Restore Panel Size") }, menu: [{ id: MenuId.PanelTitle, group: 'navigation', order: 1, // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - when: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), PanelPositionContext.notEqualsTo('bottom')) + when: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), ContextKeyExpr.and(PanelPositionContext.notEqualsTo('bottom'), PanelPositionContext.notEqualsTo('top'))) }] }); } run(accessor: ServicesAccessor) { const layoutService = accessor.get(IWorkbenchLayoutService); const notificationService = accessor.get(INotificationService); - if (layoutService.getPanelAlignment() !== 'center' && layoutService.getPanelPosition() === Position.BOTTOM) { + if (layoutService.getPanelAlignment() !== 'center' && isHorizontal(layoutService.getPanelPosition())) { notificationService.warn(localize('panelMaxNotSupported', "Maximizing the panel is only supported when it is center aligned.")); return; } @@ -351,10 +353,6 @@ registerAction2(class extends Action2 { group: 'navigation', order: 2, when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP) - }, { - id: MenuId.AuxiliaryBarHeader, - group: 'navigation', - when: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP) }] }); } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 845274fa940..899d97b4cda 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -112,6 +112,7 @@ export class PanelPart extends AbstractPaneCompositePart { const borderColor = this.getColor(PANEL_BORDER) || this.getColor(contrastBorder) || ''; container.style.borderLeftColor = borderColor; container.style.borderRightColor = borderColor; + container.style.borderBottomColor = borderColor; const title = this.getTitleArea(); if (title) { @@ -166,10 +167,16 @@ export class PanelPart extends AbstractPaneCompositePart { override layout(width: number, height: number, top: number, left: number): void { let dimensions: Dimension; - if (this.layoutService.getPanelPosition() === Position.RIGHT) { - dimensions = new Dimension(width - 1, height); // Take into account the 1px border when layouting - } else { - dimensions = new Dimension(width, height); + switch (this.layoutService.getPanelPosition()) { + case Position.RIGHT: + dimensions = new Dimension(width - 1, height); // Take into account the 1px border when layouting + break; + case Position.TOP: + dimensions = new Dimension(width, height - 1); // Take into account the 1px border when layouting + break; + default: + dimensions = new Dimension(width, height); + break; } // Layout contents diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index d4be01b7f2f..2c94078993b 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -78,7 +78,7 @@ .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, .monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { position: absolute; - left: 6px; /* place icon in center */ + left: 5px; /* place icon in center */ } .monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 28a82a629df..ea8047a243d 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -376,7 +376,7 @@ export abstract class ViewPane extends Pane implements IView { @IThemeService protected themeService: IThemeService, @ITelemetryService protected telemetryService: ITelemetryService, @IHoverService protected readonly hoverService: IHoverService, - protected readonly accessibleViewService?: IAccessibleViewInformationService + protected readonly accessibleViewInformationService?: IAccessibleViewInformationService ) { super({ ...options, ...{ orientation: viewDescriptorService.getViewLocationById(options.id) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL } }); @@ -553,7 +553,7 @@ export abstract class ViewPane extends Pane implements IView { private _getAriaLabel(title: string): string { const viewHasAccessibilityHelpContent = this.viewDescriptorService.getViewDescriptorById(this.id)?.accessibilityHelpContent; - const accessibleViewHasShownForView = this.accessibleViewService?.hasShownAccessibleView(this.id); + const accessibleViewHasShownForView = this.accessibleViewInformationService?.hasShownAccessibleView(this.id); if (!viewHasAccessibilityHelpContent || accessibleViewHasShownForView) { return title; } diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index d782b08c3c2..01df5c12a69 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -39,7 +39,7 @@ import { IAddedViewDescriptorRef, ICustomViewDescriptor, IView, IViewContainerMo import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWorkbenchLayoutService, LayoutSettings, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { isHorizontal, IWorkbenchLayoutService, LayoutSettings } from 'vs/workbench/services/layout/browser/layoutService'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; export const ViewsSubMenu = new MenuId('Views'); @@ -625,8 +625,9 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { case ViewContainerLocation.Sidebar: case ViewContainerLocation.AuxiliaryBar: return Orientation.VERTICAL; - case ViewContainerLocation.Panel: - return this.layoutService.getPanelPosition() === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL; + case ViewContainerLocation.Panel: { + return isHorizontal(this.layoutService.getPanelPosition()) ? Orientation.HORIZONTAL : Orientation.VERTICAL; + } } return Orientation.VERTICAL; diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index e964af6543e..84c49ea97c8 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -133,9 +133,19 @@ export abstract class BaseWindow extends Disposable { continue; // skip over hidden windows (but never over main window) } - const handle = (window as any).vscodeOriginalSetTimeout.apply(this, [handlerFn, timeout, ...args]); + // we track didClear in case the browser does not properly clear the timeout + // this can happen for timeouts on unfocused windows + let didClear = false; + + const handle = (window as any).vscodeOriginalSetTimeout.apply(this, [(...args: unknown[]) => { + if (didClear) { + return; + } + handlerFn(...args); + }, timeout, ...args]); const timeoutDisposable = toDisposable(() => { + didClear = true; (window as any).vscodeOriginalClearTimeout(handle); timeoutDisposables.delete(timeoutDisposable); }); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index fb05198e50b..d83ac066c6e 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -516,9 +516,9 @@ const registry = Registry.as(ConfigurationExtensions.Con }, 'workbench.panel.defaultLocation': { 'type': 'string', - 'enum': ['left', 'bottom', 'right'], + 'enum': ['left', 'bottom', 'top', 'right'], 'default': 'bottom', - 'description': localize('panelDefaultLocation', "Controls the default location of the panel (Terminal, Debug Console, Output, Problems) in a new workspace. It can either show at the bottom, right, or left of the editor area."), + 'description': localize('panelDefaultLocation', "Controls the default location of the panel (Terminal, Debug Console, Output, Problems) in a new workspace. It can either show at the bottom, top, right, or left of the editor area."), }, 'workbench.panel.opensMaximized': { 'type': 'string', diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index b0688133537..a89451b8022 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -50,6 +50,7 @@ import { AccessibilityProgressSignalScheduler } from 'vs/platform/accessibilityS import { setProgressAcccessibilitySignalScheduler } from 'vs/base/browser/ui/progressbar/progressAccessibilitySignal'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { NotificationAccessibleView } from 'vs/workbench/browser/parts/notifications/notificationAccessibleView'; +import { isESM } from 'vs/base/common/amd'; export interface IWorkbenchOptions { @@ -113,7 +114,7 @@ export class Workbench extends Layout { } type AnnotatedError = AnnotatedLoadingError | AnnotatedFactoryError | AnnotatedValidationError; - if (typeof mainWindow.require?.config === 'function') { + if (!isESM && typeof mainWindow.require?.config === 'function') { mainWindow.require.config({ onError: (err: AnnotatedError) => { if (err.phase === 'loading') { diff --git a/src/vs/workbench/common/configuration.ts b/src/vs/workbench/common/configuration.ts index f4aff57ea85..20bd1258ca4 100644 --- a/src/vs/workbench/common/configuration.ts +++ b/src/vs/workbench/common/configuration.ts @@ -272,7 +272,7 @@ export class DynamicWindowConfiguration extends Disposable implements IWorkbench 'default': null, 'enum': [...this.userDataProfilesService.profiles.map(profile => profile.name), null], 'enumItemLabels': [...this.userDataProfilesService.profiles.map(p => ''), localize('active window', "Active Window")], - 'description': localize('newWindowProfile', "Specifies the profile to use when opening a new window. If a profile name is provided, the new window will use that profile. If no profile name is provided, the new window will use the profile of the active window or the default profile if no active window exists."), + 'description': localize('newWindowProfile', "Specifies the profile to use when opening a new window. If a profile name is provided, the new window will use that profile. If no profile name is provided, the new window will use the profile of the active window or the Default profile if no active window exists."), 'scope': ConfigurationScope.APPLICATION, } } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index fa4e47467d2..25f14671a23 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -25,6 +25,8 @@ export const accessibleViewOnLastLine = new RawContextKey('accessibleVi export const accessibleViewCurrentProviderId = new RawContextKey('accessibleViewCurrentProviderId', undefined, undefined); export const accessibleViewInCodeBlock = new RawContextKey('accessibleViewInCodeBlock', undefined, undefined); export const accessibleViewContainsCodeBlocks = new RawContextKey('accessibleViewContainsCodeBlocks', undefined, undefined); +export const accessibleViewHasUnassignedKeybindings = new RawContextKey('accessibleViewHasUnassignedKeybindings', undefined, undefined); +export const accessibleViewHasAssignedKeybindings = new RawContextKey('accessibleViewHasAssignedKeybindings', undefined, undefined); /** * Miscellaneous settings tagged with accessibility and implemented in the accessibility contrib but @@ -58,7 +60,8 @@ export const enum AccessibilityVerbositySettingId { EmptyEditorHint = 'accessibility.verbosity.emptyEditorHint', ReplInputHint = 'accessibility.verbosity.replInputHint', Comments = 'accessibility.verbosity.comments', - DiffEditorActive = 'accessibility.verbosity.diffEditorActive' + DiffEditorActive = 'accessibility.verbosity.diffEditorActive', + Debug = 'accessibility.verbosity.debug', } const baseVerbosityProperty: IConfigurationPropertySchema = { @@ -171,6 +174,10 @@ const configuration: IConfigurationNode = { description: localize('verbosity.diffEditorActive', 'Indicate when a diff editor becomes the active editor.'), ...baseVerbosityProperty }, + [AccessibilityVerbositySettingId.Debug]: { + description: localize('verbosity.debug', 'Provide information about how to access the debug console accessibility help dialog when the debug console or run and debug viewlet is focused. Note that a reload of the window is required for this to take effect.'), + ...baseVerbosityProperty + }, [AccessibilityWorkbenchSettingId.AccessibleViewCloseOnKeyPress]: { markdownDescription: localize('terminal.integrated.accessibleView.closeOnKeyPress', "On keypress, close the Accessible View and focus the element from which it was invoked."), type: 'boolean', @@ -667,6 +674,11 @@ const configuration: IConfigurationNode = { 'description': localize('accessibility.underlineLinks', "Controls whether links should be underlined in the workbench."), 'default': false, }, + 'accessibility.debugWatchVariableAnnouncements': { + 'type': 'boolean', + 'description': localize('accessibility.debugWatchVariableAnnouncements', "Controls whether variable changes should be announced in the debug watch view."), + 'default': true, + }, } }; @@ -751,9 +763,15 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen 'enumItemLabels': languagesSorted.map(key => languages[key].name) }, [AccessibilityVoiceSettingId.AutoSynthesize]: { - 'type': 'boolean', + 'type': 'string', + 'enum': ['on', 'off', 'auto'], + 'enumDescriptions': [ + localize('accessibility.voice.autoSynthesize.on', "Enable the feature. When a screen reader is enabled, note that this will disable aria updates."), + localize('accessibility.voice.autoSynthesize.off', "Disable the feature."), + localize('accessibility.voice.autoSynthesize.auto', "When a screen reader is detected, disable the feature. Otherwise, enable the feature.") + ], 'markdownDescription': localize('autoSynthesize', "Whether a textual response should automatically be read out aloud when speech was used as input. For example in a chat session, a response is automatically synthesized when voice was used as chat request."), - 'default': this.productService.quality !== 'stable', // TODO@bpasero decide on a default + 'default': this.productService.quality !== 'stable' ? 'auto' : 'off', // TODO@bpasero decide on a default 'tags': ['accessibility'] } } @@ -801,14 +819,24 @@ Registry.as(WorkbenchExtensions.ConfigurationMi const delayWarning = getDelaysFromConfig(accessor, 'warningAtPosition'); const volume = getVolumeFromConfig(accessor); const debouncePositionChanges = getDebouncePositionChangesFromConfig(accessor); - return [ - ['accessibility.signalOptions.volume', { value: volume }], - ['accessibility.signalOptions.debouncePositionChanges', { value: debouncePositionChanges }], - ['accessibility.signalOptions.experimental.delays.general', { value: delayGeneral }], - ['accessibility.signalOptions.experimental.delays.errorAtPosition', { value: delayError }], - ['accessibility.signalOptions.experimental.delays.warningAtPosition', { value: delayWarning }], - ['accessibility.signalOptions', { value: undefined }], - ]; + const result: [key: string, { value: any }][] = []; + if (!!volume) { + result.push(['accessibility.signalOptions.volume', { value: volume }]); + } + if (!!delayGeneral) { + result.push(['accessibility.signalOptions.experimental.delays.general', { value: delayGeneral }]); + } + if (!!delayError) { + result.push(['accessibility.signalOptions.experimental.delays.errorAtPosition', { value: delayError }]); + } + if (!!delayWarning) { + result.push(['accessibility.signalOptions.experimental.delays.warningAtPosition', { value: delayWarning }]); + } + if (!!debouncePositionChanges) { + result.push(['accessibility.signalOptions.debouncePositionChanges', { value: debouncePositionChanges }]); + } + result.push(['accessibility.signalOptions', { value: undefined }]); + return result; } }]); @@ -847,6 +875,24 @@ function getDebouncePositionChangesFromConfig(accessor: (key: string) => any): n return accessor('accessibility.signalOptions.debouncePositionChanges') || accessor('accessibility.signalOptions')?.debouncePositionChanges || accessor('accessibility.signals.debouncePositionChanges') || accessor('audioCues.debouncePositionChanges'); } +Registry.as(WorkbenchExtensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: AccessibilityVoiceSettingId.AutoSynthesize, + migrateFn: (value: boolean) => { + let newValue: string | undefined; + if (value === true) { + newValue = 'on'; + } else if (value === false) { + newValue = 'off'; + } else { + return []; + } + return [ + [AccessibilityVoiceSettingId.AutoSynthesize, { value: newValue }], + ]; + } + }]); + Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'accessibility.signals.chatResponsePending', diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index d4bd1f893e8..b3e04ef7839 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -11,7 +11,7 @@ import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { marked } from 'vs/base/common/marked/marked'; +import * as marked from 'vs/base/common/marked/marked'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; @@ -24,7 +24,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; import { localize } from 'vs/nls'; -import { AccessibleViewProviderId, AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol } from 'vs/platform/accessibility/browser/accessibleView'; import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; @@ -40,7 +40,7 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewHasAssignedKeybindings, accessibleViewHasUnassignedKeybindings, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { resolveContentAndKeybindingItems } from 'vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; @@ -51,7 +51,7 @@ const enum DIMENSIONS { MAX_WIDTH = 600 } -export type AccesibleViewContentProvider = AdvancedContentProvider | ExtensionContentProvider; +export type AccesibleViewContentProvider = AccessibleContentProvider | ExtensionContentProvider; interface ICodeBlock { startLine: number; @@ -72,6 +72,9 @@ export class AccessibleView extends Disposable { private _accessibleViewCurrentProviderId: IContextKey; private _accessibleViewInCodeBlock: IContextKey; private _accessibleViewContainsCodeBlocks: IContextKey; + private _hasUnassignedKeybindings: IContextKey; + private _hasAssignedKeybindings: IContextKey; + private _codeBlocks?: ICodeBlock[]; private _inQuickPick: boolean = false; @@ -85,6 +88,9 @@ export class AccessibleView extends Disposable { private _lastProvider: AccesibleViewContentProvider | undefined; + private _viewContainer: HTMLElement | undefined; + + constructor( @IOpenerService private readonly _openerService: IOpenerService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -112,6 +118,8 @@ export class AccessibleView extends Disposable { this._accessibleViewInCodeBlock = accessibleViewInCodeBlock.bindTo(this._contextKeyService); this._accessibleViewContainsCodeBlocks = accessibleViewContainsCodeBlocks.bindTo(this._contextKeyService); this._onLastLine = accessibleViewOnLastLine.bindTo(this._contextKeyService); + this._hasUnassignedKeybindings = accessibleViewHasUnassignedKeybindings.bindTo(this._contextKeyService); + this._hasAssignedKeybindings = accessibleViewHasAssignedKeybindings.bindTo(this._contextKeyService); this._container = document.createElement('div'); this._container.classList.add('accessible-view'); @@ -140,6 +148,7 @@ export class AccessibleView extends Disposable { lineDecorationsWidth: 6, dragAndDrop: false, cursorWidth: 1, + wordWrap: 'off', wrappingStrategy: 'advanced', wrappingIndent: 'none', padding: { top: 2, bottom: 2 }, @@ -156,7 +165,7 @@ export class AccessibleView extends Disposable { } })); this._register(this._configurationService.onDidChangeConfiguration(e => { - if (this._currentProvider instanceof AdvancedContentProvider && e.affectsConfiguration(this._currentProvider.verbositySettingKey)) { + if (this._currentProvider instanceof AccessibleContentProvider && e.affectsConfiguration(this._currentProvider.verbositySettingKey)) { if (this._accessiblityHelpIsShown.get()) { this.show(this._currentProvider); } @@ -187,6 +196,8 @@ export class AccessibleView extends Disposable { this._accessibleViewVerbosityEnabled.reset(); this._accessibleViewGoToSymbolSupported.reset(); this._accessibleViewCurrentProviderId.reset(); + this._hasAssignedKeybindings.reset(); + this._hasUnassignedKeybindings.reset(); } getPosition(id?: AccessibleViewProviderId): Position | undefined { @@ -196,11 +207,17 @@ export class AccessibleView extends Disposable { return this._editorWidget.getPosition() || undefined; } - setPosition(position: Position, reveal?: boolean): void { + setPosition(position: Position, reveal?: boolean, select?: boolean): void { this._editorWidget.setPosition(position); if (reveal) { this._editorWidget.revealPosition(position); } + if (select) { + const lineLength = this._editorWidget.getModel()?.getLineLength(position.lineNumber) ?? 0; + if (lineLength) { + this._editorWidget.setSelection({ startLineNumber: position.lineNumber, startColumn: 1, endLineNumber: position.lineNumber, endColumn: lineLength + 1 }); + } + } } getCodeBlockContext(): ICodeBlockActionContext | undefined { @@ -247,17 +264,17 @@ export class AccessibleView extends Disposable { return; } provider.onOpen?.(); - let viewContainer: HTMLElement | undefined; const delegate: IContextViewDelegate = { getAnchor: () => { return { x: (getActiveWindow().innerWidth / 2) - ((Math.min(this._layoutService.activeContainerDimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH)) / 2), y: this._layoutService.activeContainerOffset.quickPickTop }; }, render: (container) => { - viewContainer = container; - viewContainer.classList.add('accessible-view-container'); + this._viewContainer = container; + this._viewContainer.classList.add('accessible-view-container'); return this._render(provider, container, showAccessibleViewHelp); }, onHide: () => { if (!showAccessibleViewHelp) { this._updateLastProvider(); + this._currentProvider?.dispose(); this._currentProvider = undefined; this._resetContextKeys(); } @@ -276,7 +293,7 @@ export class AccessibleView extends Disposable { if (symbol && this._currentProvider) { this.showSymbol(this._currentProvider, symbol); } - if (provider instanceof AdvancedContentProvider && provider.onDidRequestClearLastProvider) { + if (provider instanceof AccessibleContentProvider && provider.onDidRequestClearLastProvider) { this._register(provider.onDidRequestClearLastProvider((id: string) => { if (this._lastProvider?.options.id === id) { this._lastProvider = undefined; @@ -295,24 +312,32 @@ export class AccessibleView extends Disposable { } if (provider.onDidChangeContent) { this._register(provider.onDidChangeContent(() => { - if (viewContainer) { this._render(provider, viewContainer, showAccessibleViewHelp); } + if (this._viewContainer) { this._render(provider, this._viewContainer, showAccessibleViewHelp); } })); } } previous(): void { - this._currentProvider?.previous?.(); + const newContent = this._currentProvider?.providePreviousContent?.(); + if (!this._currentProvider || !this._viewContainer || !newContent) { + return; + } + this._render(this._currentProvider, this._viewContainer, undefined, newContent); } next(): void { - this._currentProvider?.next?.(); + const newContent = this._currentProvider?.provideNextContent?.(); + if (!this._currentProvider || !this._viewContainer || !newContent) { + return; + } + this._render(this._currentProvider, this._viewContainer, undefined, newContent); } private _verbosityEnabled(): boolean { if (!this._currentProvider) { return false; } - return this._currentProvider instanceof AdvancedContentProvider ? this._configurationService.getValue(this._currentProvider.verbositySettingKey) === true : this._storageService.getBoolean(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${this._currentProvider.id}`, StorageScope.APPLICATION, false); + return this._currentProvider instanceof AccessibleContentProvider ? this._configurationService.getValue(this._currentProvider.verbositySettingKey) === true : this._storageService.getBoolean(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${this._currentProvider.id}`, StorageScope.APPLICATION, false); } goToSymbol(): void { @@ -322,7 +347,10 @@ export class AccessibleView extends Disposable { this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider); } - calculateCodeBlocks(markdown: string): void { + calculateCodeBlocks(markdown?: string): void { + if (!markdown) { + return; + } if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) { return; } @@ -352,7 +380,7 @@ export class AccessibleView extends Disposable { } getSymbols(): IAccessibleViewSymbol[] | undefined { - const provider = this._currentProvider instanceof AdvancedContentProvider ? this._currentProvider : undefined; + const provider = this._currentProvider instanceof AccessibleContentProvider ? this._currentProvider : undefined; if (!this._currentContent || !provider) { return; } @@ -364,7 +392,7 @@ export class AccessibleView extends Disposable { // Symbols haven't been provided and we cannot parse this language return; } - const markdownTokens: marked.TokensList | undefined = marked.lexer(this._currentContent); + const markdownTokens: marked.TokensList | undefined = marked.marked.lexer(this._currentContent); if (!markdownTokens) { return; } @@ -379,10 +407,10 @@ export class AccessibleView extends Disposable { this._openerService.open(URI.parse(this._currentProvider.options.readMoreUrl)); } - configureKeybindings(): void { + configureKeybindings(unassigned: boolean): void { this._inQuickPick = true; const provider = this._updateLastProvider(); - const items = provider?.options?.configureKeybindingItems; + const items = unassigned ? provider?.options?.configureKeybindingItems : provider?.options?.configuredKeybindingItems; if (!items) { return; } @@ -420,12 +448,12 @@ export class AccessibleView extends Disposable { label = token.text; break; case 'list': { - const firstItem = token.items?.[0]; + const firstItem = (token as marked.Tokens.List).items[0]; if (!firstItem) { break; } firstListItem = `- ${firstItem.text}`; - label = token.items?.map(i => i.text).join(', '); + label = (token as marked.Tokens.List).items.map(i => i.text).join(', '); break; } } @@ -464,7 +492,7 @@ export class AccessibleView extends Disposable { } disableHint(): void { - if (!(this._currentProvider instanceof AdvancedContentProvider)) { + if (!(this._currentProvider instanceof AccessibleContentProvider)) { return; } this._configurationService.updateValue(this._currentProvider?.verbositySettingKey, false); @@ -479,50 +507,52 @@ export class AccessibleView extends Disposable { this._accessibleViewIsShown.set(shown); this._accessiblityHelpIsShown.reset(); } - this._accessibleViewSupportsNavigation.set(provider.next !== undefined || provider.previous !== undefined); + this._accessibleViewSupportsNavigation.set(provider.provideNextContent !== undefined || provider.providePreviousContent !== undefined); this._accessibleViewVerbosityEnabled.set(this._verbosityEnabled()); this._accessibleViewGoToSymbolSupported.set(this._goToSymbolsSupported() ? this.getSymbols()?.length! > 0 : false); } - private _render(provider: AccesibleViewContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean): IDisposable { + private _updateContent(provider: AccesibleViewContentProvider, updatedContent?: string): void { + let content = updatedContent ?? provider.provideContent(); + if (provider.options.type === AccessibleViewType.View) { + this._currentContent = content; + this._hasUnassignedKeybindings.reset(); + this._hasAssignedKeybindings.reset(); + return; + } + const readMoreLinkHint = this._readMoreHint(provider); + const disableHelpHint = this._disableVerbosityHint(provider); + const screenReaderModeHint = this._screenReaderModeHint(provider); + const exitThisDialogHint = this._exitDialogHint(provider); + let configureKbHint = ''; + let configureAssignedKbHint = ''; + const resolvedContent = resolveContentAndKeybindingItems(this._keybindingService, screenReaderModeHint + content + readMoreLinkHint + disableHelpHint + exitThisDialogHint); + if (resolvedContent) { + content = resolvedContent.content.value; + if (resolvedContent.configureKeybindingItems) { + provider.options.configureKeybindingItems = resolvedContent.configureKeybindingItems; + this._hasUnassignedKeybindings.set(true); + configureKbHint = this._configureUnassignedKbHint(); + } else { + this._hasAssignedKeybindings.reset(); + } + if (resolvedContent.configuredKeybindingItems) { + provider.options.configuredKeybindingItems = resolvedContent.configuredKeybindingItems; + this._hasAssignedKeybindings.set(true); + configureAssignedKbHint = this._configureAssignedKbHint(); + } else { + this._hasAssignedKeybindings.reset(); + } + } + this._currentContent = content + configureKbHint + configureAssignedKbHint; + } + + private _render(provider: AccesibleViewContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean, updatedContent?: string): IDisposable { this._currentProvider = provider; this._accessibleViewCurrentProviderId.set(provider.id); const verbose = this._verbosityEnabled(); - const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility.", AccessibilityCommandId.AccessibilityHelpOpenHelpLink) : ''; - let disableHelpHint = ''; - if (provider instanceof AdvancedContentProvider && provider.options.type === AccessibleViewType.Help && verbose) { - disableHelpHint = this._getDisableVerbosityHint(); - } - const accessibilitySupport = this._accessibilityService.isScreenReaderOptimized(); - let message = ''; - if (provider.options.type === AccessibleViewType.Help) { - const turnOnMessage = ( - isMacintosh - ? AccessibilityHelpNLS.changeConfigToOnMac - : AccessibilityHelpNLS.changeConfigToOnWinLinux - ); - if (accessibilitySupport && provider instanceof AdvancedContentProvider && provider.verbositySettingKey === AccessibilityVerbositySettingId.Editor) { - message = AccessibilityHelpNLS.auto_on; - message += '\n'; - } else if (!accessibilitySupport) { - message = AccessibilityHelpNLS.auto_off + '\n' + turnOnMessage; - message += '\n'; - } - } - const exitThisDialogHint = verbose && !provider.options.position ? localize('exit', '\n\nExit this dialog (Escape).') : ''; - let content = provider.provideContent(); - if (provider.options.type === AccessibleViewType.Help) { - const resolvedContent = resolveContentAndKeybindingItems(this._keybindingService, content + readMoreLink + disableHelpHint + exitThisDialogHint); - if (resolvedContent) { - content = resolvedContent.content.value; - if (resolvedContent.configureKeybindingItems) { - provider.options.configureKeybindingItems = resolvedContent.configureKeybindingItems; - } - } - } - const newContent = message + content; - this.calculateCodeBlocks(newContent); - this._currentContent = newContent; + this._updateContent(provider, updatedContent); + this.calculateCodeBlocks(this._currentContent); this._updateContextKeys(provider, true); const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus(); this._getTextModel(URI.from({ path: `accessible-view-${provider.id}`, scheme: 'accessible-view', fragment: this._currentContent })).then((model) => { @@ -579,6 +609,8 @@ export class AccessibleView extends Disposable { this._updateContextKeys(provider, false); this._lastProvider = undefined; this._currentContent = undefined; + this._currentProvider?.dispose(); + this._currentProvider = undefined; }; const disposableStore = new DisposableStore(); disposableStore.add(this._editorWidget.onKeyDown((e) => { @@ -593,7 +625,7 @@ export class AccessibleView extends Disposable { e.preventDefault(); e.stopPropagation(); } - if (provider instanceof AdvancedContentProvider) { + if (provider instanceof AccessibleContentProvider) { provider.onKeyDown?.(e); } })); @@ -649,7 +681,7 @@ export class AccessibleView extends Disposable { if (!this._currentProvider) { return false; } - return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || (this._currentProvider instanceof AdvancedContentProvider && !!this._currentProvider.getSymbols?.()); + return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || (this._currentProvider instanceof AccessibleContentProvider && !!this._currentProvider.getSymbols?.()); } private _updateLastProvider(): AccesibleViewContentProvider | undefined { @@ -657,7 +689,7 @@ export class AccessibleView extends Disposable { if (!provider) { return; } - const lastProvider = provider instanceof AdvancedContentProvider ? new AdvancedContentProvider( + const lastProvider = provider instanceof AccessibleContentProvider ? new AccessibleContentProvider( provider.id, provider.options, provider.provideContent.bind(provider), @@ -665,8 +697,8 @@ export class AccessibleView extends Disposable { provider.verbositySettingKey, provider.onOpen?.bind(provider), provider.actions, - provider.next?.bind(provider), - provider.previous?.bind(provider), + provider.provideNextContent?.bind(provider), + provider.providePreviousContent?.bind(provider), provider.onDidChangeContent?.bind(provider), provider.onKeyDown?.bind(provider), provider.getSymbols?.bind(provider), @@ -676,8 +708,8 @@ export class AccessibleView extends Disposable { provider.provideContent.bind(provider), provider.onClose.bind(provider), provider.onOpen?.bind(provider), - provider.next?.bind(provider), - provider.previous?.bind(provider), + provider.provideNextContent?.bind(provider), + provider.providePreviousContent?.bind(provider), provider.actions, provider.onDidChangeContent?.bind(provider), ); @@ -689,26 +721,41 @@ export class AccessibleView extends Disposable { if (!lastProvider) { return; } - - const accessibleViewHelpProvider = { - id: lastProvider.id, - provideContent: () => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._getAccessibleViewHelpDialogContent(this._goToSymbolsSupported()), - onClose: () => { - this._contextViewService.hideContextView(); - // HACK: Delay to allow the context view to hide #207638 - queueMicrotask(() => this.show(lastProvider)); - }, - options: { type: AccessibleViewType.Help }, - verbositySettingKey: lastProvider instanceof AdvancedContentProvider ? lastProvider.verbositySettingKey : undefined - }; + let accessibleViewHelpProvider; + if (lastProvider instanceof AccessibleContentProvider) { + accessibleViewHelpProvider = new AccessibleContentProvider( + lastProvider.id as AccessibleViewProviderId, + { type: AccessibleViewType.Help }, + () => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._accessibleViewHelpDialogContent(this._goToSymbolsSupported()), + () => { + this._contextViewService.hideContextView(); + // HACK: Delay to allow the context view to hide #207638 + queueMicrotask(() => this.show(lastProvider)); + }, + lastProvider.verbositySettingKey as AccessibilityVerbositySettingId + ); + } else { + accessibleViewHelpProvider = new ExtensionContentProvider( + lastProvider.id as AccessibleViewProviderId, + { type: AccessibleViewType.Help }, + () => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._accessibleViewHelpDialogContent(this._goToSymbolsSupported()), + () => { + this._contextViewService.hideContextView(); + // HACK: Delay to allow the context view to hide #207638 + queueMicrotask(() => this.show(lastProvider)); + }, + ); + } this._contextViewService.hideContextView(); // HACK: Delay to allow the context view to hide #186514 - queueMicrotask(() => this.show(accessibleViewHelpProvider, undefined, true)); + if (accessibleViewHelpProvider) { + queueMicrotask(() => this.show(accessibleViewHelpProvider, undefined, true)); + } } - private _getAccessibleViewHelpDialogContent(providerHasSymbols?: boolean): string { - const navigationHint = this._getNavigationHint(); - const goToSymbolHint = this._getGoToSymbolHint(providerHasSymbols); + private _accessibleViewHelpDialogContent(providerHasSymbols?: boolean): string { + const navigationHint = this._navigationHint(); + const goToSymbolHint = this._goToSymbolHint(providerHasSymbols); const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab)."); const chatHints = this._getChatHints(); @@ -732,24 +779,65 @@ export class AccessibleView extends Disposable { if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) { return; } - return [localize('insertAtCursor', " - Insert the code block at the cursor."), - localize('insertIntoNewFile', " - Insert the code block into a new file."), - localize('runInTerminal', " - Run the code block in the terminal.\n")].join('\n'); + return [localize('insertAtCursor', " - Insert the code block at the cursor{0}.", ''), + localize('insertIntoNewFile', " - Insert the code block into a new file{0}.", ''), + localize('runInTerminal', " - Run the code block in the terminal{0}.\n", '')].join('\n'); } - private _getNavigationHint(): string { - return localize('accessibleViewNextPreviousHint', "Show the next item or previous item.", AccessibilityCommandId.ShowNext, AccessibilityCommandId.ShowPrevious); + private _navigationHint(): string { + return localize('accessibleViewNextPreviousHint', "Show the next item{0} or previous item{1}.", ``); } - private _getDisableVerbosityHint(): string { - return localize('acessibleViewDisableHint', "\n\nDisable accessibility verbosity for this feature.", AccessibilityCommandId.DisableVerbosityHint); + private _disableVerbosityHint(provider: AccesibleViewContentProvider): string { + if (provider.options.type === AccessibleViewType.Help && this._verbosityEnabled()) { + return localize('acessibleViewDisableHint', "\nDisable accessibility verbosity for this feature{0}.", ``); + } + return ''; } - private _getGoToSymbolHint(providerHasSymbols?: boolean): string | undefined { + private _goToSymbolHint(providerHasSymbols?: boolean): string | undefined { if (!providerHasSymbols) { return; } - return localize('goToSymbolHint', 'Go to a symbol.', AccessibilityCommandId.GoToSymbol); + return localize('goToSymbolHint', 'Go to a symbol{0}.', ``); + } + + private _configureUnassignedKbHint(): string { + const configureKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureKeybindings)?.getAriaLabel(); + const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Unassigned Keybindings.'; + return localize('configureKb', '\nConfigure keybindings for commands that lack them {0}.', keybindingToConfigureQuickPick); + } + + private _configureAssignedKbHint(): string { + const configureKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureAssignedKeybindings)?.getAriaLabel(); + const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Assigned Keybindings.'; + return localize('configureKbAssigned', '\nConfigure keybindings for commands that already have assignments {0}.', keybindingToConfigureQuickPick); + } + + private _screenReaderModeHint(provider: AccesibleViewContentProvider): string { + const accessibilitySupport = this._accessibilityService.isScreenReaderOptimized(); + let screenReaderModeHint = ''; + const turnOnMessage = ( + isMacintosh + ? AccessibilityHelpNLS.changeConfigToOnMac + : AccessibilityHelpNLS.changeConfigToOnWinLinux + ); + if (accessibilitySupport && provider.id === AccessibleViewProviderId.Editor) { + screenReaderModeHint = AccessibilityHelpNLS.auto_on; + screenReaderModeHint += '\n'; + } else if (!accessibilitySupport) { + screenReaderModeHint = AccessibilityHelpNLS.auto_off + '\n' + turnOnMessage; + screenReaderModeHint += '\n'; + } + return screenReaderModeHint; + } + + private _exitDialogHint(provider: AccesibleViewContentProvider): string { + return this._verbosityEnabled() && !provider.options.position ? localize('exit', '\nExit this dialog (Escape).') : ''; + } + + private _readMoreHint(provider: AccesibleViewContentProvider): string { + return provider.options.readMoreUrl ? localize("openDoc", "\nOpen a browser window with more information related to accessibility{0}.", ``) : ''; } } @@ -771,8 +859,8 @@ export class AccessibleViewService extends Disposable implements IAccessibleView } this._accessibleView.show(provider, undefined, undefined, position); } - configureKeybindings(): void { - this._accessibleView?.configureKeybindings(); + configureKeybindings(unassigned: boolean): void { + this._accessibleView?.configureKeybindings(unassigned); } openHelpLink(): void { this._accessibleView?.openHelpLink(); @@ -815,12 +903,8 @@ export class AccessibleViewService extends Disposable implements IAccessibleView const lastLine = this._accessibleView?.editorWidget.getModel()?.getLineCount(); return lastLine !== undefined && lastLine > 0 ? new Position(lastLine, 1) : undefined; } - setPosition(position: Position, reveal?: boolean): void { - const editorWidget = this._accessibleView?.editorWidget; - editorWidget?.setPosition(position); - if (reveal) { - editorWidget?.revealLine(position.lineNumber); - } + setPosition(position: Position, reveal?: boolean, select?: boolean): void { + this._accessibleView?.setPosition(position, reveal, select); } getCodeBlockContext(): ICodeBlockActionContext | undefined { return this._accessibleView?.getCodeBlockContext(); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index 7aba4fa5baf..67df062dfe0 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -11,7 +11,7 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewHasAssignedKeybindings, accessibleViewHasUnassignedKeybindings, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibleViewProviderId, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; @@ -62,7 +62,6 @@ class AccessibleViewNextCodeBlockAction extends Action2 { mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, }, weight: KeybindingWeight.WorkbenchContrib, }, - // to icon: Codicon.arrowRight, menu: { @@ -233,20 +232,56 @@ class AccessibilityHelpConfigureKeybindingsAction extends Action2 { constructor() { super({ id: AccessibilityCommandId.AccessibilityHelpConfigureKeybindings, - precondition: ContextKeyExpr.and(accessibilityHelpIsShown), + precondition: ContextKeyExpr.and(accessibilityHelpIsShown, accessibleViewHasUnassignedKeybindings), + icon: Codicon.key, keybinding: { primary: KeyMod.Alt | KeyCode.KeyK, weight: KeybindingWeight.WorkbenchContrib }, - title: localize('editor.action.accessibilityHelpConfigureKeybindings', "Accessibility Help Configure Keybindings") + menu: [ + { + id: MenuId.AccessibleView, + group: 'navigation', + order: 3, + when: accessibleViewHasUnassignedKeybindings, + } + ], + title: localize('editor.action.accessibilityHelpConfigureUnassignedKeybindings', "Accessibility Help Configure Unassigned Keybindings") }); } async run(accessor: ServicesAccessor): Promise { - await accessor.get(IAccessibleViewService).configureKeybindings(); + await accessor.get(IAccessibleViewService).configureKeybindings(true); } } registerAction2(AccessibilityHelpConfigureKeybindingsAction); +class AccessibilityHelpConfigureAssignedKeybindingsAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.AccessibilityHelpConfigureAssignedKeybindings, + precondition: ContextKeyExpr.and(accessibilityHelpIsShown, accessibleViewHasAssignedKeybindings), + icon: Codicon.key, + keybinding: { + primary: KeyMod.Alt | KeyCode.KeyA, + weight: KeybindingWeight.WorkbenchContrib + }, + menu: [ + { + id: MenuId.AccessibleView, + group: 'navigation', + order: 4, + when: accessibleViewHasAssignedKeybindings, + } + ], + title: localize('editor.action.accessibilityHelpConfigureAssignedKeybindings', "Accessibility Help Configure Assigned Keybindings") + }); + } + async run(accessor: ServicesAccessor): Promise { + await accessor.get(IAccessibleViewService).configureKeybindings(false); + } +} +registerAction2(AccessibilityHelpConfigureAssignedKeybindingsAction); + class AccessibilityHelpOpenHelpLinkAction extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts index d8d07767246..bc026ec30af 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts @@ -6,7 +6,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -27,12 +27,17 @@ export class AccesibleViewContributions extends Disposable { super(); AccessibleViewRegistry.getImplementations().forEach(impl => { const implementation = (accessor: ServicesAccessor) => { - const provider: AdvancedContentProvider | ExtensionContentProvider | undefined = impl.getProvider(accessor); - if (provider) { + const provider: AccessibleContentProvider | ExtensionContentProvider | undefined = impl.getProvider(accessor); + if (!provider) { + return false; + } + try { accessor.get(IAccessibleViewService).show(provider); return true; + } catch { + provider.dispose(); + return false; } - return false; }; if (impl.type === AccessibleViewType.View) { this._register(AccessibleViewAction.addImplementation(impl.priority, impl.name, implementation, impl.when)); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts index 88bfbd309d4..107f60f28ce 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts @@ -6,35 +6,37 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; -import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -export function resolveContentAndKeybindingItems(keybindingService: IKeybindingService, value?: string): { content: MarkdownString; configureKeybindingItems: IPickerQuickAccessItem[] | undefined } | undefined { +export function resolveContentAndKeybindingItems(keybindingService: IKeybindingService, value?: string): { content: MarkdownString; configureKeybindingItems: IPickerQuickAccessItem[] | undefined; configuredKeybindingItems: IPickerQuickAccessItem[] | undefined } | undefined { if (!value) { return; } const configureKeybindingItems: IPickerQuickAccessItem[] = []; - const matches = value.matchAll(/\.*)\>/gm); + const configuredKeybindingItems: IPickerQuickAccessItem[] = []; + const matches = value.matchAll(/(\[^\<]*)\>)/gm); for (const match of [...matches]) { const commandId = match?.groups?.commandId; let kbLabel; if (match?.length && commandId) { const keybinding = keybindingService.lookupKeybinding(commandId)?.getAriaLabel(); if (!keybinding) { - const configureKb = keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureKeybindings)?.getAriaLabel(); - const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Keybindings.'; - kbLabel = `, configure a keybinding ` + keybindingToConfigureQuickPick; + kbLabel = ` (unassigned keybinding)`; configureKeybindingItems.push({ label: commandId, id: commandId }); } else { kbLabel = ' (' + keybinding + ')'; + configuredKeybindingItems.push({ + label: commandId, + id: commandId + }); } value = value.replace(match[0], kbLabel); } } const content = new MarkdownString(value); content.isTrusted = true; - return { content, configureKeybindingItems: configureKeybindingItems.length ? configureKeybindingItems : undefined }; + return { content, configureKeybindingItems: configureKeybindingItems.length ? configureKeybindingItems : undefined, configuredKeybindingItems: configuredKeybindingItems.length ? configuredKeybindingItems : undefined }; } diff --git a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts index 0e47093d20b..ce6c600768b 100644 --- a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts @@ -39,7 +39,7 @@ export class EditorAccessibilityHelpContribution extends Disposable { } } -class EditorAccessibilityHelpProvider implements IAccessibleViewContentProvider { +class EditorAccessibilityHelpProvider extends Disposable implements IAccessibleViewContentProvider { id = AccessibleViewProviderId.Editor; onClose() { this._editor.focus(); @@ -51,6 +51,7 @@ class EditorAccessibilityHelpProvider implements IAccessibleViewContentProvider @IKeybindingService private readonly _keybindingService: IKeybindingService, @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { + super(); } provideContent(): string { @@ -93,7 +94,11 @@ class EditorAccessibilityHelpProvider implements IAccessibleViewContentProvider } else { content.push(AccessibilityHelpNLS.tabFocusModeOffMsg); } - return content.join('\n\n'); + content.push(AccessibilityHelpNLS.startDebugging); + content.push(AccessibilityHelpNLS.setBreakpoint); + content.push(AccessibilityHelpNLS.debugExecuteSelection); + content.push(AccessibilityHelpNLS.addToWatch); + return content.join('\n'); } } diff --git a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts index f8381417bd8..7c18982222f 100644 --- a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts @@ -57,7 +57,6 @@ function registerAccessibilityHelpAction(keybindingService: IKeybindingService, () => viewsService.openView(viewDescriptor.id, true), ); }, - dispose: () => { }, })); disposableStore.add(keybindingService.onDidUpdateKeybindings(() => { diff --git a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts index d689c02503e..7e84a29bc75 100644 --- a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts +++ b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts @@ -14,5 +14,6 @@ export const enum AccessibilityCommandId { NextCodeBlock = 'editor.action.accessibleViewNextCodeBlock', PreviousCodeBlock = 'editor.action.accessibleViewPreviousCodeBlock', AccessibilityHelpConfigureKeybindings = 'editor.action.accessibilityHelpConfigureKeybindings', + AccessibilityHelpConfigureAssignedKeybindings = 'editor.action.accessibilityHelpConfigureAssignedKeybindings', AccessibilityHelpOpenHelpLink = 'editor.action.accessibilityHelpOpenHelpLink', } diff --git a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts index 8a4be09b66a..f7c3dceac4d 100644 --- a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts +++ b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts @@ -26,10 +26,8 @@ import { IRequestService, asText } from 'vs/platform/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { isWeb } from 'vs/base/common/platform'; -import { isInternalTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; const accountsBadgeConfigKey = 'workbench.accounts.experimental.showEntitlements'; -const chatWelcomeViewConfigKey = 'workbench.chat.experimental.showWelcomeView'; type EntitlementEnablementClassification = { enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the entitlement is enabled' }; @@ -47,7 +45,6 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut private isInitialized = false; private showAccountsBadgeContextKey = new RawContextKey(accountsBadgeConfigKey, false).bindTo(this.contextService); - private showChatWelcomeViewContextKey = new RawContextKey(chatWelcomeViewConfigKey, false).bindTo(this.contextService); private readonly accountsMenuBadgeDisposable = this._register(new MutableDisposable()); constructor( @@ -98,7 +95,6 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut await this.enableEntitlements(e.event.added[0]); } else if (e.providerId === this.productService.gitHubEntitlement!.providerId && e.event.removed?.length) { this.showAccountsBadgeContextKey.set(false); - this.showChatWelcomeViewContextKey.set(false); this.accountsMenuBadgeDisposable.clear(); } })); @@ -156,29 +152,19 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut return; } - const isInternal = isInternalTelemetry(this.productService, this.configurationService); const showAccountsBadge = this.configurationService.inspect(accountsBadgeConfigKey).value ?? false; - const showWelcomeView = this.configurationService.inspect(chatWelcomeViewConfigKey).value ?? false; const [enabled, org] = await this.getEntitlementsInfo(session); - if (enabled) { - if (isInternal && showWelcomeView) { - this.showChatWelcomeViewContextKey.set(true); - this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>(chatWelcomeViewConfigKey, { enabled: true }); - } - if (showAccountsBadge) { - this.createAccountsBadge(org); - this.showAccountsBadgeContextKey.set(showAccountsBadge); - this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>(accountsBadgeConfigKey, { enabled: true }); - } + if (enabled && showAccountsBadge) { + this.createAccountsBadge(org); + this.showAccountsBadgeContextKey.set(showAccountsBadge); + this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>(accountsBadgeConfigKey, { enabled: true }); } } private disableEntitlements() { this.storageService.store(accountsBadgeConfigKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); - this.storageService.store(chatWelcomeViewConfigKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); this.showAccountsBadgeContextKey.set(false); - this.showChatWelcomeViewContextKey.set(false); this.accountsMenuBadgeDisposable.clear(); } @@ -260,17 +246,4 @@ configurationRegistry.registerConfiguration({ } }); -configurationRegistry.registerConfiguration({ - ...applicationConfigurationNodeBase, - properties: { - 'workbench.chat.experimental.showWelcomeView': { - scope: ConfigurationScope.MACHINE, - type: 'boolean', - default: false, - tags: ['experimental'], - description: localize('workbench.chat.showWelcomeView', "When enabled, the chat panel welcome view will be shown.") - } - } -}); - registerWorkbenchContribution2('workbench.contrib.entitlements', EntitlementsContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts index 3c3d4b9aee1..733c0e50037 100644 --- a/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts +++ b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts @@ -113,7 +113,7 @@ export class ManageTrustedExtensionsForAccountAction extends Action2 { } const disposableStore = new DisposableStore(); - const quickPick = disposableStore.add(quickInputService.createQuickPick()); + const quickPick = disposableStore.add(quickInputService.createQuickPick({ useSeparators: true })); quickPick.canSelectMany = true; quickPick.customButton = true; quickPick.customLabel = localize('manageTrustedExtensions.cancel', 'Cancel'); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index e45a95008c3..45a97a0c67a 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -25,13 +25,11 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { AriaRole } from 'vs/base/browser/ui/aria/aria'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; // --- VIEW MODEL @@ -202,9 +200,7 @@ export class BulkEditDataSource implements IAsyncDataSource')); + content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}.', '')); content.push(localize('chat.followUp', 'In the input box, navigate to the suggested follow up question (Shift+Tab) and press Enter to run it.')); content.push(localize('chat.announcement', 'Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.')); - content.push(localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command.')); - content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command.')); - content.push(localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command.')); - content.push(localize('workbench.action.chat.nextFileTree', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command.')); - content.push(localize('workbench.action.chat.clear', 'To clear the request/response list, invoke the Chat Clear command.')); + content.push(localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command{0}.', '')); + content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command{0}.', '')); + content.push(localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command{0}.', '')); + content.push(localize('workbench.action.chat.nextFileTree', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command{0}.', '')); + content.push(localize('workbench.action.chat.clear', 'To clear the request/response list, invoke the Chat Clear command{0}.', '')); } else { content.push(localize('inlineChat.overview', "Inline chat occurs within a code editor and takes into account the current selection. It is useful for making changes to the current editor. For example, fixing diagnostics, documenting or refactoring code. Keep in mind that AI generated code may be incorrect.")); - content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat.")); - content.push(localize('inlineChat.requestHistory', 'In the input box, use and to navigate your request history. Edit input and use enter or the submit button to run a new request.')); - content.push(localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible viewview')); + content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat{0}.", '')); + content.push(localize('inlineChat.requestHistory', 'In the input box, use Show Previous{0} and Show Next{1} to navigate your request history. Edit input and use enter or the submit button to run a new request.', '', '')); + content.push(localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible view{0}.', '')); content.push(localize('inlineChat.contextActions', "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands.")); content.push(localize('inlineChat.fix', "If a fix action is invoked, a response will indicate the problem with the current code. A diff editor will be rendered and can be reached by tabbing.")); - content.push(localize('inlineChat.diff', "Once in the diff editor, enter review mode with. Use up and down arrows to navigate lines with the proposed changes.", AccessibleDiffViewerNext.id)); + content.push(localize('inlineChat.diff', "Once in the diff editor, enter review mode with{0}. Use up and down arrows to navigate lines with the proposed changes.", AccessibleDiffViewerNext.id)); content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more.")); } content.push(localize('chat.signals', "Accessibility Signals can be changed via settings with a prefix of signals.chat. By default, if a request takes more than 4 seconds, you will hear a sound indicating that progress is still occurring.")); - return content.join('\n\n'); + return content.join('\n'); } export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat') { @@ -70,11 +70,11 @@ export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, edi const cachedPosition = inputEditor.getPosition(); inputEditor.getSupportedActions(); const helpText = getAccessibilityHelpText(type); - return { - id: type === 'panelChat' ? AccessibleViewProviderId.Chat : AccessibleViewProviderId.InlineChat, - verbositySettingKey: type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat, - provideContent: () => helpText, - onClose: () => { + return new AccessibleContentProvider( + type === 'panelChat' ? AccessibleViewProviderId.Chat : AccessibleViewProviderId.InlineChat, + { type: AccessibleViewType.Help }, + () => helpText, + () => { if (type === 'panelChat' && cachedPosition) { inputEditor.setPosition(cachedPosition); inputEditor.focus(); @@ -86,6 +86,6 @@ export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, edi } }, - options: { type: AccessibleViewType.Help } - }; + type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat, + ); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 29731511a13..0619ea85f01 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -20,8 +20,7 @@ import { CHAT_VIEW_ID, IChatWidgetService, showChatView } from 'vs/workbench/con import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatDetail, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; @@ -158,7 +157,8 @@ class ChatHistoryAction extends Action2 { picker.items = getPicks(); store.add(picker.onDidTriggerItemButton(context => { if (context.button === openInEditorButton) { - editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { sessionId: context.item.chat.sessionId }, pinned: true } }, ACTIVE_GROUP); + const options: IChatEditorOptions = { target: { sessionId: context.item.chat.sessionId }, pinned: true }; + editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, ACTIVE_GROUP); picker.hide(); } else if (context.button === deleteButton) { chatService.removeHistoryEntry(context.item.chat.sessionId); @@ -258,7 +258,7 @@ export function registerChatActions() { super({ id: 'chat.action.focus', title: localize2('actions.interactiveSession.focus', 'Focus Chat List'), - precondition: ContextKeyExpr.and(CONTEXT_IN_CHAT_INPUT, CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel)), + precondition: ContextKeyExpr.and(CONTEXT_IN_CHAT_INPUT), category: CHAT_CATEGORY, keybinding: [ // On mac, require that the cursor is at the top of the input, to avoid stealing cmd+up to move the cursor to the top diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 378d6857e48..0034369dcdf 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -6,9 +6,9 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { IActiveCodeEditor, ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -40,6 +40,8 @@ import { CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/comm import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import * as strings from 'vs/base/common/strings'; +import { CharCode } from 'vs/base/common/charCode'; export interface IChatCodeBlockActionContext extends ICodeBlockActionContext { element: IChatResponseViewModel; @@ -83,6 +85,168 @@ abstract class ChatCodeBlockAction extends Action2 { abstract runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext): any; } +abstract class InsertCodeBlockAction extends ChatCodeBlockAction { + + override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) { + const editorService = accessor.get(IEditorService); + const textFileService = accessor.get(ITextFileService); + + if (isResponseFiltered(context)) { + // When run from command palette + return; + } + + if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { + return this.handleNotebookEditor(accessor, editorService.activeEditorPane.getControl() as INotebookEditor, context); + } + + let activeEditorControl = editorService.activeTextEditorControl; + if (isDiffEditor(activeEditorControl)) { + activeEditorControl = activeEditorControl.getOriginalEditor().hasTextFocus() ? activeEditorControl.getOriginalEditor() : activeEditorControl.getModifiedEditor(); + } + + if (!isCodeEditor(activeEditorControl)) { + return; + } + + if (!activeEditorControl.hasModel()) { + return; + } + const activeModelUri = activeEditorControl.getModel().uri; + + // Check if model is editable, currently only support untitled and text file + const activeTextModel = textFileService.files.get(activeModelUri) ?? textFileService.untitled.get(activeModelUri); + if (!activeTextModel || activeTextModel.isReadonly()) { + return; + } + + await this.handleTextEditor(accessor, activeEditorControl, context); + } + + private async handleNotebookEditor(accessor: ServicesAccessor, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { + if (!notebookEditor.hasModel()) { + return; + } + + if (notebookEditor.isReadOnly) { + return; + } + + if (notebookEditor.activeCodeEditor?.hasTextFocus()) { + const codeEditor = notebookEditor.activeCodeEditor; + if (codeEditor.hasModel()) { + return this.handleTextEditor(accessor, codeEditor, context); + } + } + + const languageService = accessor.get(ILanguageService); + const focusRange = notebookEditor.getFocus(); + const next = Math.max(focusRange.end - 1, 0); + insertCell(languageService, notebookEditor, next, CellKind.Code, 'below', context.code, true); + this.notifyUserAction(accessor, context); + } + + protected async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { + const activeModel = codeEditor.getModel(); + const range = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); + const text = reindent(codeBlockActionContext.code, activeModel, range.startLineNumber); + return [new ResourceTextEdit(activeModel.uri, { range, text })]; + } + + private async handleTextEditor(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext) { + const bulkEditService = accessor.get(IBulkEditService); + const codeEditorService = accessor.get(ICodeEditorService); + + this.notifyUserAction(accessor, codeBlockActionContext); + const activeModel = codeEditor.getModel(); + + const mappedEdits = await this.computeEdits(accessor, codeEditor, codeBlockActionContext); + + await bulkEditService.apply(mappedEdits); + codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus(); + } + + private notifyUserAction(accessor: ServicesAccessor, context: ICodeBlockActionContext) { + if (isResponseVM(context.element)) { + const chatService = accessor.get(IChatService); + chatService.notifyUserAction({ + agentId: context.element.agent?.id, + command: context.element.slashCommand?.name, + sessionId: context.element.sessionId, + requestId: context.element.requestId, + result: context.element.result, + action: { + kind: 'insert', + codeBlockIndex: context.codeBlockIndex, + totalCharacters: context.code.length, + } + }); + } + } + +} + +function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine: number) { + const newContent = strings.splitLines(codeBlockContent); + if (newContent.length === 0) { + return codeBlockContent; + } + + const formattingOptions = model.getFormattingOptions(); + const codeIndentLevel = computeIndentation(model.getLineContent(seletionStartLine), formattingOptions.tabSize).level; + + const indents = newContent.map(line => computeIndentation(line, formattingOptions.tabSize)); + + // find the smallest indent level in the code block + const newContentIndentLevel = indents.reduce((min, indent, index) => { + if (indent.length !== newContent[index].length) { // ignore empty lines + return Math.min(indent.level, min); + } + return min; + }, Number.MAX_VALUE); + + if (newContentIndentLevel === Number.MAX_VALUE || newContentIndentLevel === codeIndentLevel) { + // all lines are empty or the indent is already correct + return codeBlockContent; + } + const newLines = []; + for (let i = 0; i < newContent.length; i++) { + const { level, length } = indents[i]; + const newLevel = Math.max(0, codeIndentLevel + level - newContentIndentLevel); + const newIndentation = formattingOptions.insertSpaces ? ' '.repeat(formattingOptions.tabSize * newLevel) : '\t'.repeat(newLevel); + newLines.push(newIndentation + newContent[i].substring(length)); + } + return newLines.join('\n'); +} + +// TODO: Merge with `computeIndentLevel` from `vs/editor/common/model/utils.ts` +function computeIndentation(line: string, tabSize: number): { level: number; length: number } { + let nSpaces = 0; + let level = 0; + let i = 0; + let length = 0; + const len = line.length; + while (i < len) { + const chCode = line.charCodeAt(i); + if (chCode === CharCode.Space) { + nSpaces++; + if (nSpaces === tabSize) { + level++; + nSpaces = 0; + length = i + 1; + } + } else if (chCode === CharCode.Tab) { + level++; + nSpaces = 0; + length = i + 1; + } else { + break; + } + i++; + } + return { level, length }; +} + export function registerChatCodeBlockActions() { registerAction2(class CopyCodeBlockAction extends Action2 { constructor() { @@ -94,7 +258,8 @@ export function registerChatCodeBlockActions() { icon: Codicon.copy, menu: { id: MenuId.ChatCodeBlock, - group: 'navigation' + group: 'navigation', + order: 30 } }); } @@ -182,19 +347,20 @@ export function registerChatCodeBlockActions() { return false; }); - registerAction2(class InsertCodeBlockAction extends ChatCodeBlockAction { + registerAction2(class SmartApplyInEditorAction extends InsertCodeBlockAction { constructor() { super({ - id: 'workbench.action.chat.insertCodeBlock', - title: localize2('interactive.insertCodeBlock.label', "Apply in Editor"), + id: 'workbench.action.chat.applyInEditor', + title: localize2('interactive.applyInEditor.label', "Apply in Editor"), precondition: CONTEXT_CHAT_ENABLED, f1: true, category: CHAT_CATEGORY, - icon: Codicon.insert, + icon: Codicon.sparkle, menu: { id: MenuId.ChatCodeBlock, group: 'navigation', - when: CONTEXT_IN_CHAT_SESSION + when: CONTEXT_IN_CHAT_SESSION, + order: 10 }, keybinding: { when: ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), accessibleViewInCodeBlock), @@ -205,117 +371,44 @@ export function registerChatCodeBlockActions() { }); } - override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) { - const editorService = accessor.get(IEditorService); - const textFileService = accessor.get(ITextFileService); + protected override async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { - if (isResponseFiltered(context)) { - // When run from command palette - return; - } - - if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - return this.handleNotebookEditor(accessor, editorService.activeEditorPane.getControl() as INotebookEditor, context); - } - - let activeEditorControl = editorService.activeTextEditorControl; - if (isDiffEditor(activeEditorControl)) { - activeEditorControl = activeEditorControl.getOriginalEditor().hasTextFocus() ? activeEditorControl.getOriginalEditor() : activeEditorControl.getModifiedEditor(); - } - - if (!isCodeEditor(activeEditorControl)) { - return; - } - - const activeModel = activeEditorControl.getModel(); - if (!activeModel) { - return; - } - - // Check if model is editable, currently only support untitled and text file - const activeTextModel = textFileService.files.get(activeModel.uri) ?? textFileService.untitled.get(activeModel.uri); - if (!activeTextModel || activeTextModel.isReadonly()) { - return; - } - - await this.handleTextEditor(accessor, activeEditorControl, activeModel, context); - } - - private async handleNotebookEditor(accessor: ServicesAccessor, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { - if (!notebookEditor.hasModel()) { - return; - } - - if (notebookEditor.isReadOnly) { - return; - } - - if (notebookEditor.activeCodeEditor?.hasTextFocus()) { - const codeEditor = notebookEditor.activeCodeEditor; - const textModel = codeEditor.getModel(); - - if (textModel) { - return this.handleTextEditor(accessor, codeEditor, textModel, context); - } - } - - const languageService = accessor.get(ILanguageService); - const focusRange = notebookEditor.getFocus(); - const next = Math.max(focusRange.end - 1, 0); - insertCell(languageService, notebookEditor, next, CellKind.Code, 'below', context.code, true); - this.notifyUserAction(accessor, context); - } - - private async handleTextEditor(accessor: ServicesAccessor, codeEditor: ICodeEditor, activeModel: ITextModel, codeBlockActionContext: ICodeBlockActionContext) { - this.notifyUserAction(accessor, codeBlockActionContext); - - const bulkEditService = accessor.get(IBulkEditService); - const codeEditorService = accessor.get(ICodeEditorService); const progressService = accessor.get(IProgressService); const notificationService = accessor.get(INotificationService); + const activeModel = codeEditor.getModel(); + const mappedEditsProviders = accessor.get(ILanguageFeaturesService).mappedEditsProvider.ordered(activeModel); - - // try applying workspace edit that was returned by a MappedEditsProvider, else simply insert at selection - - let mappedEdits: WorkspaceEdit | null = null; - if (mappedEditsProviders.length > 0) { // 0th sub-array - editor selections array if there are any selections // 1st sub-array - array with documents used to get the chat reply const docRefs: DocumentContextItem[][] = []; - if (codeEditor.hasModel()) { - const model = codeEditor.getModel(); - const currentDocUri = model.uri; - const currentDocVersion = model.getVersionId(); - const selections = codeEditor.getSelections(); - if (selections.length > 0) { - docRefs.push([ - { - uri: currentDocUri, - version: currentDocVersion, - ranges: selections, - } - ]); - } + const currentDocUri = activeModel.uri; + const currentDocVersion = activeModel.getVersionId(); + const selections = codeEditor.getSelections(); + if (selections.length > 0) { + docRefs.push([ + { + uri: currentDocUri, + version: currentDocVersion, + ranges: selections, + } + ]); } - const usedDocuments = getUsedDocuments(codeBlockActionContext); if (usedDocuments) { docRefs.push(usedDocuments); } const cancellationTokenSource = new CancellationTokenSource(); - try { - mappedEdits = await progressService.withProgress( + const edits = await progressService.withProgress( { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, async progress => { - progress.report({ message: localize('applyCodeBlock.progress', "Applying code block...") }); - for (const provider of mappedEditsProviders) { + progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", provider.displayName) }); const mappedEdits = await provider.provideMappedEdits( activeModel, [codeBlockActionContext.code], @@ -326,51 +419,48 @@ export function registerChatCodeBlockActions() { return mappedEdits; } } - return null; + return undefined; }, () => cancellationTokenSource.cancel() ); + if (edits) { + return edits; + } } catch (e) { notificationService.notify({ severity: Severity.Error, message: localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message) }); + } finally { cancellationTokenSource.dispose(); } - } - - if (mappedEdits) { - console.log('Mapped edits:', mappedEdits); - await bulkEditService.apply(mappedEdits); - } else { - const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); - await bulkEditService.apply([ - new ResourceTextEdit(activeModel.uri, { - range: activeSelection, - text: codeBlockActionContext.code, - }), - ]); - } - codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus(); + // fall back to inserting the code block as is + return super.computeEdits(accessor, codeEditor, codeBlockActionContext); } + }); - private notifyUserAction(accessor: ServicesAccessor, context: ICodeBlockActionContext) { - if (isResponseVM(context.element)) { - const chatService = accessor.get(IChatService); - chatService.notifyUserAction({ - agentId: context.element.agent?.id, - command: context.element.slashCommand?.name, - sessionId: context.element.sessionId, - requestId: context.element.requestId, - result: context.element.result, - action: { - kind: 'insert', - codeBlockIndex: context.codeBlockIndex, - totalCharacters: context.code.length, - } - }); - } + registerAction2(class SmartApplyInEditorAction extends InsertCodeBlockAction { + constructor() { + super({ + id: 'workbench.action.chat.insertCodeBlock', + title: localize2('interactive.insertCodeBlock.label', "Insert At Cursor"), + precondition: CONTEXT_CHAT_ENABLED, + f1: true, + category: CHAT_CATEGORY, + icon: Codicon.insert, + menu: { + id: MenuId.ChatCodeBlock, + group: 'navigation', + when: CONTEXT_IN_CHAT_SESSION, + order: 20 + }, + keybinding: { + when: ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), accessibleViewInCodeBlock), + primary: KeyMod.CtrlCmd | KeyCode.Enter, + mac: { primary: KeyMod.WinCtrl | KeyCode.Enter }, + weight: KeybindingWeight.ExternalExtension + 1 + }, + }); } - }); registerAction2(class InsertIntoNewFileAction extends ChatCodeBlockAction { @@ -385,7 +475,8 @@ export function registerChatCodeBlockActions() { menu: { id: MenuId.ChatCodeBlock, group: 'navigation', - isHiddenByDefault: true + isHiddenByDefault: true, + order: 40, } }); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index ee4b0beba48..0f65c731173 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -7,15 +7,18 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Schemas } from 'vs/base/common/network'; -import { IRange } from 'vs/editor/common/core/range'; +import { compare } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { IRange } from 'vs/editor/common/core/range'; +import { EditorType } from 'vs/editor/common/editorCommon'; import { Command } from 'vs/editor/common/languages'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess'; import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickInputService, IQuickPickItem, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -27,12 +30,10 @@ import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_QUICK_CHAT } f import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EditorType } from 'vs/editor/common/editorCommon'; -import { compare } from 'vs/base/common/strings'; export function registerChatContextActions() { registerAction2(AttachContextAction); @@ -40,7 +41,7 @@ export function registerChatContextActions() { registerAction2(AttachSelectionAction); } -export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem | IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickAccessQuickPickItem; +export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem | IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem; export interface IFileQuickPickItem extends IQuickPickItem { kind: 'file'; @@ -63,6 +64,12 @@ export interface IDynamicVariableQuickPickItem extends IQuickPickItem { command?: Command; } +export interface IToolQuickPickItem extends IQuickPickItem { + kind: 'tool'; + id: string; + name?: string; +} + export interface IStaticVariableQuickPickItem extends IQuickPickItem { kind: 'static'; id: string; @@ -100,7 +107,7 @@ class AttachFileAction extends Action2 { const textEditorService = accessor.get(IEditorService); const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote].includes(activeUri.scheme)) { + if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { variablesService.attachContext('file', activeUri, ChatAgentLocation.Panel); } } @@ -125,7 +132,7 @@ class AttachSelectionAction extends Action2 { const activeEditor = textEditorService.activeTextEditorControl; const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote].includes(activeUri.scheme)) { + if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { const selection = activeEditor?.getSelection(); if (selection) { variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.Panel); @@ -138,12 +145,21 @@ class AttachContextAction extends Action2 { static readonly ID = 'workbench.action.chat.attachContext'; + // used to enable/disable the keybinding and defined menu containment + private static _cdt = ContextKeyExpr.or( + ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), CONTEXT_IN_QUICK_CHAT.isEqualTo(false)), + ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor), ContextKeyExpr.equals('config.chat.experimental.variables.editor', true)), + ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Notebook), ContextKeyExpr.equals('config.chat.experimental.variables.notebook', true)), + ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Terminal), ContextKeyExpr.equals('config.chat.experimental.variables.terminal', true)), + ); + constructor() { super({ id: AttachContextAction.ID, title: localize2('workbench.action.chat.attachContext.label', "Attach Context"), icon: Codicon.attach, category: CHAT_CATEGORY, + precondition: AttachContextAction._cdt, keybinding: { when: CONTEXT_IN_CHAT_INPUT, primary: KeyMod.CtrlCmd | KeyCode.Slash, @@ -151,7 +167,7 @@ class AttachContextAction extends Action2 { }, menu: [ { - when: ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), CONTEXT_IN_QUICK_CHAT.isEqualTo(false)), + when: AttachContextAction._cdt, id: MenuId.ChatExecute, group: 'navigation', }, @@ -185,7 +201,7 @@ class AttachContextAction extends Action2 { value: pick.value, name: `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`, // Apply the original icon with the new name - fullName: `${pick.icon ? `$(${pick.icon.id}) ` : ''}${selection}` + fullName: selection }); } else if ('symbol' in pick && pick.symbol) { // Symbol @@ -218,6 +234,14 @@ class AttachContextAction extends Action2 { name: pick.symbolName!, isDynamic: true }); + } else if ('kind' in pick && pick.kind === 'tool') { + toAttach.push({ + id: pick.id, + name: pick.label, + fullName: pick.label, + value: undefined, + isTool: true + }); } else { // All other dynamic variables and static variables toAttach.push({ @@ -241,6 +265,7 @@ class AttachContextAction extends Action2 { const chatVariablesService = accessor.get(IChatVariablesService); const commandService = accessor.get(ICommandService); const widgetService = accessor.get(IChatWidgetService); + const languageModelToolsService = accessor.get(ILanguageModelToolsService); const context: { widget?: IChatWidget } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; if (!widget) { @@ -250,12 +275,13 @@ class AttachContextAction extends Action2 { const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart); const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; const quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] = []; - for (const variable of chatVariablesService.getVariables()) { + for (const variable of chatVariablesService.getVariables(widget.location)) { if (variable.fullName && (!variable.isSlow || slowSupported)) { quickPickItems.push({ - label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, + label: variable.fullName, name: variable.name, id: variable.id, + iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined, icon: variable.icon }); } @@ -268,10 +294,11 @@ class AttachContextAction extends Action2 { for (const variable of completions) { if (variable.fullName) { quickPickItems.push({ - label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, + label: variable.fullName, id: variable.id, command: variable.command, icon: variable.icon, + iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined, value: variable.value, isDynamic: true, name: variable.name @@ -279,12 +306,29 @@ class AttachContextAction extends Action2 { } } } + } + for (const tool of languageModelToolsService.getTools()) { + if (tool.canBeInvokedManually) { + const item: IToolQuickPickItem = { + kind: 'tool', + label: tool.displayName ?? tool.name, + id: tool.name, + }; + if (ThemeIcon.isThemeIcon(tool.icon)) { + item.iconClass = ThemeIcon.asClassName(tool.icon); + } else if (tool.icon) { + item.iconPath = tool.icon; + } + + quickPickItems.push(item); + } } quickPickItems.push({ - label: localize('chatContext.symbol', '{0} Symbol...', `$(${Codicon.symbolField.id})`), + label: localize('chatContext.symbol', 'Symbol...'), icon: ThemeIcon.fromId(Codicon.symbolField.id), + iconClass: ThemeIcon.asClassName(Codicon.symbolField), prefix: SymbolsQuickAccessProvider.PREFIX }); @@ -306,6 +350,7 @@ class AttachContextAction extends Action2 { } private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[], query: string = '') { + quickInputService.quickAccess.show(query, { enabledProviderPrefixes: [ AnythingQuickAccessProvider.PREFIX, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts b/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts index b4d1afffad3..977cf2f7a14 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts @@ -96,7 +96,8 @@ export function registerChatExportActions() { throw new Error('Invalid chat session data'); } - await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { data }, pinned: true } as IChatEditorOptions }); + const options: IChatEditorOptions = { target: { data }, pinned: true }; + await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }); } catch (err) { throw err; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts index d04e49d6218..0f960b81e71 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts @@ -96,7 +96,7 @@ async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNew const widget = chatView?.widget ?? widgetService.lastFocusedWidget; if (!widget || !('viewId' in widget.viewContext)) { - await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); + await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); return; } @@ -109,7 +109,8 @@ async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNew const viewState = widget.getViewState(); widget.clear(); - await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { sessionId }, pinned: true, viewState: viewState } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); + const options: IChatEditorOptions = { target: { sessionId }, pinned: true, viewState: viewState }; + await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); } async function moveToSidebar(accessor: ServicesAccessor): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 9338bf5b51b..c8bc363bd34 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -15,7 +15,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE, CONTEXT_VOTE_UP_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -37,7 +37,7 @@ export function registerChatTitleActions() { id: MenuId.ChatMessageTitle, group: 'navigation', order: 1, - when: CONTEXT_RESPONSE + when: ContextKeyExpr.and(CONTEXT_RESPONSE, CONTEXT_VOTE_UP_ENABLED) } }); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 9356ca139e5..d1c81e35664 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -34,7 +34,7 @@ import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { agentSlashCommandToMarkdown, agentToMarkdown } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; -import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; +import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView'; import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; @@ -104,7 +104,7 @@ configurationRegistry.registerConfiguration({ 'chat.experimental.variables.editor': { type: 'boolean', description: nls.localize('chat.experimental.variables.editor', "Enables variables for editor chat."), - default: false + default: true }, 'chat.experimental.variables.notebook': { type: 'boolean', @@ -116,6 +116,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.experimental.variables.terminal', "Enables variables for terminal chat."), default: false }, + 'chat.experimental.detectParticipant.enabled': { + type: 'boolean', + description: nls.localize('chat.experimental.detectParticipant.enabled', "Enables chat participant autodetection for panel chat."), + default: false + }, } }); Registry.as(EditorExtensions.EditorPane).registerEditorPane( @@ -228,7 +233,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { } const variables = [ - ...chatVariablesService.getVariables(), + ...chatVariablesService.getVariables(ChatAgentLocation.Panel), { name: 'file', description: nls.localize('file', "Choose a file in the workspace") } ]; const variableText = variables @@ -255,6 +260,7 @@ registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribu workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlashCommandsContribution, LifecyclePhase.Eventually); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); +registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore); registerChatActions(); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 99bb40c2ca4..014af019e00 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -19,6 +19,7 @@ import { ChatAgentLocation, IChatAgentCommand, IChatAgentData } from 'vs/workben import { IChatRequestVariableEntry, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; +import { IChatLocationData } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; @@ -139,8 +140,9 @@ export interface IChatWidget { readonly onDidAcceptInput: Event; readonly onDidHide: Event; readonly onDidSubmitAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>; + readonly onDidChangeAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>; readonly onDidChangeParsedInput: Event; - readonly onDidDeleteContext: Event; + readonly onDidChangeContext: Event<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>; readonly location: ChatAgentLocation; readonly viewContext: IChatWidgetViewContext; readonly viewModel: IChatViewModel | undefined; @@ -150,10 +152,11 @@ export interface IChatWidget { lastSelectedAgent: IChatAgentData | undefined; readonly scopedContextKeyService: IContextKeyService; + getLocationData(): IChatLocationData | undefined; getContrib(id: string): T | undefined; reveal(item: ChatTreeItem): void; focus(item: ChatTreeItem): void; - moveFocus(item: ChatTreeItem, type: 'next' | 'previous'): void; + getSibling(item: ChatTreeItem, type: 'next' | 'previous'): ChatTreeItem | undefined; getFocus(): ChatTreeItem | undefined; setInput(query?: string): void; getInput(): string; diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index 06e0d0e292f..17b9a5e9945 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -12,6 +12,8 @@ import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/cha import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { AccessibilityVoiceSettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; export class ChatAccessibilityService extends Disposable implements IChatAccessibilityService { @@ -22,7 +24,11 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi private _requestId: number = 0; - constructor(@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IInstantiationService private readonly _instantiationService: IInstantiationService) { + constructor( + @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService + ) { super(); } acceptRequest(): number { @@ -41,6 +47,8 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi } const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : ''; const plainTextResponse = renderStringAsPlaintext(new MarkdownString(responseContent)); - status(plainTextResponse + errorDetails); + if (this._configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) !== 'on') { + status(plainTextResponse + errorDetails); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts new file mode 100644 index 00000000000..407e5d75582 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; +import { Emitter } from 'vs/base/common/event'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ResourceLabels } from 'vs/workbench/browser/labels'; +import { URI } from 'vs/base/common/uri'; +import { FileKind } from 'vs/platform/files/common/files'; +import { Range } from 'vs/editor/common/core/range'; +import { basename, dirname } from 'vs/base/common/path'; +import { localize } from 'vs/nls'; +import { ChatResponseReferencePartStatusKind, IChatContentReference } from 'vs/workbench/contrib/chat/common/chatService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + +export class ChatAttachmentsContentPart extends Disposable { + private readonly attachedContextDisposables = this._register(new DisposableStore()); + + private readonly _onDidChangeVisibility = this._register(new Emitter()); + private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); + + constructor( + private readonly variables: IChatRequestVariableEntry[], + private readonly contentReferences: ReadonlyArray = [], + public readonly domNode: HTMLElement = dom.$('.chat-attached-context'), + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IOpenerService private readonly openerService: IOpenerService, + ) { + super(); + + this.initAttachedContext(domNode); + } + + private initAttachedContext(container: HTMLElement) { + dom.clearNode(container); + this.attachedContextDisposables.clear(); + dom.setVisibility(Boolean(this.variables.length), this.domNode); + + this.variables.forEach((attachment) => { + const widget = dom.append(container, dom.$('.chat-attached-context-attachment.show-file-icons')); + const label = this._contextResourceLabels.create(widget, { supportIcons: true }); + const file = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; + const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + + const correspondingContentReference = this.contentReferences.find((ref) => 'variableName' in ref.reference && ref.reference.variableName === attachment.name); + const isAttachmentOmitted = correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted; + const isAttachmentPartialOrOmitted = isAttachmentOmitted || correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial; + + if (file) { + const fileBasename = basename(file.path); + const fileDirname = dirname(file.path); + const friendlyName = `${fileBasename} ${fileDirname}`; + let ariaLabel; + if (isAttachmentOmitted) { + ariaLabel = range ? localize('chat.omittedFileAttachmentWithRange', "Omitted: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.omittedFileAttachment', "Omitted: {0}.", friendlyName); + } else if (isAttachmentPartialOrOmitted) { + ariaLabel = range ? localize('chat.partialFileAttachmentWithRange', "Partially attached: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.partialFileAttachment', "Partially attached: {0}.", friendlyName); + } else { + ariaLabel = range ? localize('chat.fileAttachmentWithRange3', "Attached: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment3', "Attached: {0}.", friendlyName); + } + + label.setFile(file, { + fileKind: FileKind.FILE, + hidePath: true, + range, + title: correspondingContentReference?.options?.status?.description + }); + widget.ariaLabel = ariaLabel; + widget.tabIndex = 0; + widget.style.cursor = 'pointer'; + + this.attachedContextDisposables.add(dom.addDisposableListener(widget, dom.EventType.CLICK, async (e: MouseEvent) => { + dom.EventHelper.stop(e, true); + if (file) { + this.openerService.open( + file, + { + fromUserGesture: true, + editorOptions: { + selection: range + } as any + }); + } + })); + } else { + const attachmentLabel = attachment.fullName ?? attachment.name; + const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; + label.setLabel(withIcon, correspondingContentReference?.options?.status?.description); + + widget.ariaLabel = localize('chat.attachment3', "Attached context: {0}.", attachment.name); + widget.tabIndex = 0; + } + + if (isAttachmentPartialOrOmitted) { + widget.classList.add('warning'); + } + const description = correspondingContentReference?.options?.status?.description; + if (isAttachmentPartialOrOmitted) { + widget.ariaLabel = `${widget.ariaLabel}${description ? ` ${description}` : ''}`; + for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { + const element = label.element.querySelector(selector); + if (element) { + element.classList.add('warning'); + } + } + } + }); + } +} + diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCodeCitationContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCodeCitationContentPart.ts new file mode 100644 index 00000000000..fedc276bdee --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCodeCitationContentPart.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { getCodeCitationsMessage } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatCodeCitations, IChatRendererContent } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +type ChatCodeCitationOpenedClassification = { + owner: 'roblourens'; + comment: 'Indicates when a user opens chat code citations'; +}; + +export class ChatCodeCitationContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + constructor( + citations: IChatCodeCitations, + context: IChatContentPartRenderContext, + @IEditorService private readonly editorService: IEditorService, + @ITelemetryService private readonly telemetryService: ITelemetryService + ) { + super(); + + const label = getCodeCitationsMessage(citations.citations); + const elements = dom.h('.chat-code-citation-message@root', [ + dom.h('span.chat-code-citation-label@label'), + dom.h('.chat-code-citation-button-container@button'), + ]); + elements.label.textContent = label + ' - '; + const button = this._register(new Button(elements.button, { + buttonBackground: undefined, + buttonBorder: undefined, + buttonForeground: undefined, + buttonHoverBackground: undefined, + buttonSecondaryBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryHoverBackground: undefined, + buttonSeparator: undefined + })); + button.label = localize('viewMatches', "View matches"); + this._register(button.onDidClick(() => { + const citationText = `# Code Citations\n\n` + citations.citations.map(c => `## License: ${c.license}\n${c.value.toString()}\n\n\`\`\`\n${c.snippet}\n\`\`\`\n\n`).join('\n'); + this.editorService.openEditor({ resource: undefined, contents: citationText, languageId: 'markdown' }); + this.telemetryService.publicLog2<{}, ChatCodeCitationOpenedClassification>('openedChatCodeCitations'); + })); + this.domNode = elements.root; + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return other.kind === 'codeCitations'; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts index 4159f07c919..a3957178e57 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts @@ -28,10 +28,16 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont super(); const element = context.element; - const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, [ - { label: localize('accept', "Accept"), data: confirmation.data }, - { label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true }, - ])); + const buttons = confirmation.buttons + ? confirmation.buttons.map(button => ({ + label: button, + data: confirmation.data + })) + : [ + { label: localize('accept', "Accept"), data: confirmation.data }, + { label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true }, + ]; + const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, buttons)); confirmationWidget.setShowButtons(!confirmation.isUsed); this._register(confirmationWidget.onDidClick(async e => { @@ -42,6 +48,7 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont { acceptedConfirmationData: [e.data] }; data.agentId = element.agent?.id; data.slashCommand = element.slashCommand?.name; + data.confirmation = e.label; if (await this.chatService.sendRequest(element.sessionId, prompt, data)) { confirmation.isUsed = true; confirmationWidget.setShowButtons(false); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index b1305e8e160..99b28473495 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -11,7 +11,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { matchesSomeScheme, Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; -import { basenameOrAuthority } from 'vs/base/common/resources'; +import { basenameOrAuthority, isEqualAuthority } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -19,30 +19,38 @@ import { FileKind } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { ColorScheme } from 'vs/workbench/browser/web.api'; import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; -import { IChatContentReference, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatResponseReferencePartStatusKind, IChatContentReference, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatRendererContent, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; +import { SETTINGS_AUTHORITY } from 'vs/workbench/services/preferences/common/preferences'; const $ = dom.$; -export class ChatReferencesContentPart extends Disposable implements IChatContentPart { +export interface IChatReferenceListItem extends IChatContentReference { + title?: string; +} + +export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage; + +export class ChatCollapsibleListContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; private readonly _onDidChangeHeight = this._register(new Emitter()); public readonly onDidChangeHeight = this._onDidChangeHeight.event; constructor( - private readonly data: ReadonlyArray, + private readonly data: ReadonlyArray, labelOverride: string | undefined, element: IChatResponseViewModel, - contentReferencesListPool: ContentReferencesListPool, + contentReferencesListPool: CollapsibleListPool, @IOpenerService openerService: IOpenerService, ) { super(); @@ -117,7 +125,8 @@ export class ChatReferencesContentPart extends Disposable implements IChatConten } hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { - return other.kind === 'references' && other.references.length === this.data.length; + return other.kind === 'references' && other.references.length === this.data.length || + other.kind === 'codeCitations' && other.citations.length === this.data.length; } private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { @@ -129,10 +138,10 @@ export class ChatReferencesContentPart extends Disposable implements IChatConten } } -export class ContentReferencesListPool extends Disposable { - private _pool: ResourcePool>; +export class CollapsibleListPool extends Disposable { + private _pool: ResourcePool>; - public get inUse(): ReadonlySet> { + public get inUse(): ReadonlySet> { return this._pool.inUse; } @@ -145,22 +154,22 @@ export class ContentReferencesListPool extends Disposable { this._pool = this._register(new ResourcePool(() => this.listFactory())); } - private listFactory(): WorkbenchList { + private listFactory(): WorkbenchList { const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); const container = $('.chat-used-context-list'); this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); const list = this.instantiationService.createInstance( - WorkbenchList, + WorkbenchList, 'ChatListRenderer', container, - new ContentReferencesListDelegate(), - [this.instantiationService.createInstance(ContentReferencesListRenderer, resourceLabels)], + new CollapsibleListDelegate(), + [this.instantiationService.createInstance(CollapsibleListRenderer, resourceLabels)], { alwaysConsumeMouseWheel: false, accessibilityProvider: { - getAriaLabel: (element: IChatContentReference | IChatWarningMessage) => { + getAriaLabel: (element: IChatCollapsibleListItem) => { if (element.kind === 'warning') { return element.content.value; } @@ -174,10 +183,10 @@ export class ContentReferencesListPool extends Disposable { } }, - getWidgetAriaLabel: () => localize('usedReferences', "Used References") + getWidgetAriaLabel: () => localize('chatCollapsibleList', "Collapsible Chat List") }, dnd: { - getDragURI: (element: IChatContentReference | IChatWarningMessage) => { + getDragURI: (element: IChatCollapsibleListItem) => { if (element.kind === 'warning') { return null; } @@ -199,7 +208,7 @@ export class ContentReferencesListPool extends Disposable { return list; } - get(): IDisposableReference> { + get(): IDisposableReference> { const object = this._pool.get(); let stale = false; return { @@ -213,34 +222,35 @@ export class ContentReferencesListPool extends Disposable { } } -class ContentReferencesListDelegate implements IListVirtualDelegate { - getHeight(element: IChatContentReference): number { +class CollapsibleListDelegate implements IListVirtualDelegate { + getHeight(element: IChatCollapsibleListItem): number { return 22; } - getTemplateId(element: IChatContentReference): string { - return ContentReferencesListRenderer.TEMPLATE_ID; + getTemplateId(element: IChatCollapsibleListItem): string { + return CollapsibleListRenderer.TEMPLATE_ID; } } -interface IChatContentReferenceListTemplate { +interface ICollapsibleListTemplate { label: IResourceLabel; templateDisposables: IDisposable; } -class ContentReferencesListRenderer implements IListRenderer { - static TEMPLATE_ID = 'contentReferencesListRenderer'; - readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; +class CollapsibleListRenderer implements IListRenderer { + static TEMPLATE_ID = 'chatCollapsibleListRenderer'; + readonly templateId: string = CollapsibleListRenderer.TEMPLATE_ID; constructor( private labels: ResourceLabels, @IThemeService private readonly themeService: IThemeService, @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + @IProductService private readonly productService: IProductService, ) { } - renderTemplate(container: HTMLElement): IChatContentReferenceListTemplate { + renderTemplate(container: HTMLElement): ICollapsibleListTemplate { const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); + const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true, supportIcons: true })); return { templateDisposables, label }; } @@ -255,7 +265,7 @@ class ContentReferencesListRenderer implements IListRenderer item.kind === 'textEditGroup')) { this.domNode = $('.interactive-edits-summary', undefined, !element.isComplete - ? localize('editsSummary1', "Making changes...") + ? '' : element.isCanceled ? localize('edits0', "Making changes was aborted.") : localize('editsSummary', "Made changes.")); @@ -75,6 +75,7 @@ export class ChatTextEditContentPart extends Disposable implements IChatContentP this._register(toDisposable(() => { isDisposed = true; cts.dispose(true); + this.ref?.object.clearModel(); })); this.ref = this._register(diffEditorPool.get()); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css index e244f077dd6..bb9d7bc7b7e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css @@ -6,10 +6,13 @@ .chat-confirmation-widget { border: 1px solid var(--vscode-chat-requestBorder); border-radius: 4px; - margin-bottom: 16px; padding: 8px 12px 12px; } +.chat-confirmation-widget:not(:last-child) { + margin-bottom: 16px; +} + .chat-confirmation-widget .chat-confirmation-widget-title { font-weight: 600; } diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index 4f59229ff85..4219221b228 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -9,7 +9,7 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { formatChatQuestion } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; const $ = dom.$; @@ -36,18 +36,9 @@ export class ChatFollowups extends Disposable { return; } - let tooltipPrefix = ''; - if ('agentId' in followup && followup.agentId && followup.agentId !== this.chatAgentService.getDefaultAgent(this.location)?.id) { - const agent = this.chatAgentService.getAgent(followup.agentId); - if (!agent) { - // Refers to agent that doesn't exist - return; - } - - tooltipPrefix += `${chatAgentLeader}${agent.name} `; - if ('subCommand' in followup && followup.subCommand) { - tooltipPrefix += `${chatSubcommandLeader}${followup.subCommand} `; - } + const tooltipPrefix = formatChatQuestion(this.chatAgentService, this.location, '', followup.agentId, followup.subCommand); + if (tooltipPrefix === undefined) { + return; } const baseTitle = followup.kind === 'reply' ? diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 9fcc9fb8e50..7ff228263a2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -26,7 +26,8 @@ import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; +import { MarginHoverController } from 'vs/editor/contrib/hover/browser/marginHoverController'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; @@ -56,7 +57,7 @@ import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chat import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatHistoryEntry, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; -import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; const $ = dom.$; @@ -89,13 +90,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _onDidBlur = this._register(new Emitter()); readonly onDidBlur = this._onDidBlur.event; - private _onDidDeleteContext = this._register(new Emitter()); - readonly onDidDeleteContext = this._onDidDeleteContext.event; + private _onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>()); + readonly onDidChangeContext = this._onDidChangeContext.event; private _onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>()); readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event; - public get attachedContext() { + public get attachedContext(): ReadonlySet { return this._attachedContext; } @@ -149,6 +150,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // private readonly editorOptions: ChatEditorOptions, // TODO this should be used private readonly location: ChatAgentLocation, private readonly options: IChatInputPartOptions, + private readonly getInputState: () => any, @IChatWidgetHistoryService private readonly historyService: IChatWidgetHistoryService, @IModelService private readonly modelService: IModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -234,11 +236,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } showPreviousValue(): void { + const inputState = this.getInputState(); if (this.history.isAtEnd()) { - this.saveCurrentValue(); + this.saveCurrentValue(inputState); } else { - if (!this.history.has({ text: this._inputEditor.getValue(), state: this.history.current().state })) { - this.saveCurrentValue(); + if (!this.history.has({ text: this._inputEditor.getValue(), state: inputState })) { + this.saveCurrentValue(inputState); this.history.resetCursor(); } } @@ -247,11 +250,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } showNextValue(): void { + const inputState = this.getInputState(); if (this.history.isAtEnd()) { return; } else { - if (!this.history.has({ text: this._inputEditor.getValue(), state: this.history.current().state })) { - this.saveCurrentValue(); + if (!this.history.has({ text: this._inputEditor.getValue(), state: inputState })) { + this.saveCurrentValue(inputState); this.history.resetCursor(); } } @@ -288,12 +292,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 }); if (!transient) { - this.saveCurrentValue(); + this.saveCurrentValue(this.getInputState()); } } - private saveCurrentValue(): void { - const newEntry = { text: this._inputEditor.getValue(), state: this.history.current().state }; + private saveCurrentValue(inputState: any): void { + const newEntry = { text: this._inputEditor.getValue(), state: inputState }; this.history.replaceLast(newEntry); } @@ -312,11 +316,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge async acceptInput(isUserQuery?: boolean): Promise { if (isUserQuery) { const userQuery = this._inputEditor.getValue(); - const entry: IChatHistoryEntry = { text: userQuery, state: this.history.current().state }; + const entry: IChatHistoryEntry = { text: userQuery, state: this.getInputState() }; this.history.replaceLast(entry); this.history.add({ text: '' }); } + // Clear attached context, fire event to clear input state, and clear the input editor + this._attachedContext.clear(); this._onDidLoadInputState.fire({}); if (this.accessibilityService.isScreenReaderOptimized() && isMacintosh) { this._acceptInputForVoiceover(); @@ -339,12 +345,26 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._inputEditor.focus(); } - attachContext(...contentReferences: IChatRequestVariableEntry[]): void { - for (const reference of contentReferences) { - this.attachedContext.add(reference); + attachContext(overwrite: boolean, ...contentReferences: IChatRequestVariableEntry[]): void { + const removed = []; + if (overwrite) { + removed.push(...Array.from(this._attachedContext)); + this._attachedContext.clear(); } - this.initAttachedContext(this.attachedContextContainer); + if (contentReferences.length > 0) { + for (const reference of contentReferences) { + this._attachedContext.add(reference); + } + } + + if (removed.length > 0 || contentReferences.length > 0) { + this.initAttachedContext(this.attachedContextContainer); + + if (!overwrite) { + this._onDidChangeContext.fire({ removed, added: contentReferences }); + } + } } render(container: HTMLElement, initialValue: string, widget: IChatWidget) { @@ -384,10 +404,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge insertMode: 'replace', }; options.scrollbar = { ...(options.scrollbar ?? {}), vertical: 'hidden' }; + options.stickyScroll = { enabled: false }; - this._inputEditorElement = dom.append(inputContainer, $('.interactive-input-editor')); + this._inputEditorElement = dom.append(inputContainer, $(chatInputEditorContainerSelector)); const editorOptions = getSimpleCodeEditorWidgetOptions(); - editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([HoverController.ID])); + editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, MarginHoverController.ID])); this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); this._register(this._inputEditor.onDidChangeModelContent(() => { @@ -412,18 +433,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._onDidBlur.fire(); })); - this._register(this._inputEditor.onDidChangeCursorPosition(e => { - const model = this._inputEditor.getModel(); - if (!model) { - return; - } - - const atTop = e.position.column === 1 && e.position.lineNumber === 1; - this.chatCursorAtTop.set(atTop); - - this.historyNavigationBackwardsEnablement.set(atTop); - this.historyNavigationForewardsEnablement.set(e.position.equals(getLastPosition(model))); - })); this.toolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputContainer, this.options.menus.executeToolbar, { telemetrySource: this.options.menus.telemetrySource, @@ -476,9 +485,30 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const lineNumber = this.inputModel.getLineCount(); this._inputEditor.setPosition({ lineNumber, column: this.inputModel.getLineMaxColumn(lineNumber) }); } + + const onDidChangeCursorPosition = () => { + const model = this._inputEditor.getModel(); + if (!model) { + return; + } + + const position = this._inputEditor.getPosition(); + if (!position) { + return; + } + + const atTop = position.column === 1 && position.lineNumber === 1; + this.chatCursorAtTop.set(atTop); + + this.historyNavigationBackwardsEnablement.set(atTop); + this.historyNavigationForewardsEnablement.set(position.equals(getLastPosition(model))); + }; + this._register(this._inputEditor.onDidChangeCursorPosition(e => onDidChangeCursorPosition())); + onDidChangeCursorPosition(); } private initAttachedContext(container: HTMLElement) { + const oldHeight = container.offsetHeight; dom.clearNode(container); this.attachedContextDisposables.clear(); dom.setVisibility(Boolean(this.attachedContext.size), this.attachedContextContainer); @@ -505,7 +535,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge widget.tabIndex = 0; } else { const attachmentLabel = attachment.fullName ?? attachment.name; - label.setLabel(attachmentLabel, undefined); + const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; + label.setLabel(withIcon, undefined); widget.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); widget.tabIndex = 0; @@ -521,7 +552,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachedContextDisposables.add(clearButton); clearButton.icon = Codicon.close; const disp = clearButton.onDidClick((e) => { - this.attachedContext.delete(attachment); + this._attachedContext.delete(attachment); disp.dispose(); // Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click) @@ -533,10 +564,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } this._onDidChangeHeight.fire(); - this._onDidDeleteContext.fire(attachment); + this._onDidChangeContext.fire({ removed: [attachment] }); }); this.attachedContextDisposables.add(disp); }); + + if (oldHeight !== container.offsetHeight) { + this._onDidChangeHeight.fire(); + } } async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { @@ -609,6 +644,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } saveState(): void { + this.saveCurrentValue(this.getInputState()); const inputHistory = [...this.history]; this.historyService.saveHistory(this.location, inputHistory); } @@ -670,3 +706,6 @@ class ChatSubmitDropdownActionItem extends DropdownWithPrimaryActionViewItem { this._register(menu.onDidChange(() => setActions())); } } + +const chatInputEditorContainerSelector = '.interactive-input-editor'; +setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index fb4eae44143..2dbc20d8e56 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -38,14 +38,16 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { ILogService } from 'vs/platform/log/common/log'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatAttachmentsContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart'; +import { ChatCodeCitationContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCodeCitationContentPart'; import { ChatCommandButtonContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart'; import { ChatConfirmationContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart'; import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; import { ChatMarkdownContentPart, EditorPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart'; import { ChatProgressContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart'; -import { ChatReferencesContentPart, ContentReferencesListPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart'; +import { ChatCollapsibleListContentPart, CollapsibleListPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart'; import { ChatTaskContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart'; import { ChatTextEditContentPart, DiffEditorPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart'; import { ChatTreeContentPart, TreePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart'; @@ -54,17 +56,16 @@ import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { ChatCodeBlockContentProvider } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { ChatCodeBlockContentProvider, CodeBlockPart } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatRequestVariableEntry, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatAgentVoteDirection, IChatConfirmation, IChatFollowup, IChatTask, IChatTreeData } from 'vs/workbench/contrib/chat/common/chatService'; -import { IChatReferences, IChatRendererContent, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { ChatAgentVoteDirection, IChatConfirmation, IChatContentReference, IChatFollowup, IChatTask, IChatTreeData } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCodeCitations, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; -import { IChatListItemRendererOptions } from './chat'; const $ = dom.$; @@ -123,7 +124,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { return this._editorPool.inUse(); } @@ -364,6 +365,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { this.updateItemHeight(templateData); })); @@ -795,6 +821,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined, templateData: IChatListItemTemplate) { + return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, undefined); + } + private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { const textEditPart = this.instantiationService.createInstance(ChatTextEditContentPart, chatTextEdit, context, this.rendererOptions, this._diffEditorPool, this._currentLayoutWidth); textEditPart.addDisposable(textEditPart.onDidChangeHeight(() => { diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 9af542462c3..c8207548700 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -7,10 +7,12 @@ import * as dom from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { Lazy } from 'vs/base/common/lazy'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { URI } from 'vs/base/common/uri'; import { Location } from 'vs/editor/common/languages'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -21,11 +23,10 @@ import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; -import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, chatSubcommandLeader, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, chatSubcommandLeader, IParsedChatRequest, IParsedChatRequestPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { contentRefUrl } from '../common/annotations'; -import { Lazy } from 'vs/base/common/lazy'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; /** For rendering slash commands, variables */ const decorationRefUrl = `http://_vscodedecoration_`; @@ -68,6 +69,10 @@ interface ISlashCommandWidgetArgs { command: string; } +interface IDecorationWidgetArgs { + title?: string; +} + export class ChatMarkdownDecorationsRenderer { constructor( @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -79,6 +84,7 @@ export class ChatMarkdownDecorationsRenderer { @IChatService private readonly chatService: IChatService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @ICommandService private readonly commandService: ICommandService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService ) { } convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string { @@ -89,21 +95,28 @@ export class ChatMarkdownDecorationsRenderer { } else if (part instanceof ChatRequestAgentPart) { result += this.instantiationService.invokeFunction(accessor => agentToMarkdown(part.agent, false, accessor)); } else { - const uri = part instanceof ChatRequestDynamicVariablePart && part.data instanceof URI ? - part.data : - undefined; - const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) : - part instanceof ChatRequestAgentPart ? part.agent.id : - ''; - - const text = part.text; - result += `[${text}](${decorationRefUrl}?${title})`; + result += this.genericDecorationToMarkdown(part); } } return result; } + private genericDecorationToMarkdown(part: IParsedChatRequestPart): string { + const uri = part instanceof ChatRequestDynamicVariablePart && part.data instanceof URI ? + part.data : + undefined; + const title = uri ? this.labelService.getUriLabel(uri, { relative: true }) : + part instanceof ChatRequestSlashCommandPart ? part.slashCommand.detail : + part instanceof ChatRequestAgentSubcommandPart ? part.command.description : + part instanceof ChatRequestVariablePart ? (this.chatVariablesService.getVariable(part.variableName)?.description ?? '') : + ''; + + const args: IDecorationWidgetArgs = { title }; + const text = part.text; + return `[${text}](${decorationRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; + } + walkTreeAndAnnotateReferenceLinks(element: HTMLElement): IDisposable { const store = new DisposableStore(); element.querySelectorAll('a').forEach(a => { @@ -136,9 +149,13 @@ export class ChatMarkdownDecorationsRenderer { a); } } else if (href.startsWith(decorationRefUrl)) { - const title = decodeURIComponent(href.slice(decorationRefUrl.length + 1)); + let args: IDecorationWidgetArgs | undefined; + try { + args = JSON.parse(decodeURIComponent(href.slice(decorationRefUrl.length + 1))); + } catch (e) { } + a.parentElement!.replaceChild( - this.renderResourceWidget(a.textContent!, title), + this.renderResourceWidget(a.textContent!, args, store), a); } else if (href.startsWith(contentRefUrl)) { this.renderFileWidget(href, a); @@ -172,7 +189,7 @@ export class ChatMarkdownDecorationsRenderer { this.chatService.sendRequest(widget.viewModel!.sessionId, agent.metadata.sampleRequest ?? '', { location: widget.location, agentId: agent.id }); })); } else { - container = this.renderResourceWidget(nameWithLeader, undefined); + container = this.renderResourceWidget(nameWithLeader, undefined, store); } const agent = this.chatAgentService.getAgent(args.agentId); @@ -232,11 +249,11 @@ export class ChatMarkdownDecorationsRenderer { } - private renderResourceWidget(name: string, title: string | undefined): HTMLElement { + private renderResourceWidget(name: string, args: IDecorationWidgetArgs | undefined, store: DisposableStore): HTMLElement { const container = dom.$('span.chat-resource-widget'); const alias = dom.$('span', undefined, name); - if (title) { - alias.title = title; + if (args?.title) { + store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), container, args.title)); } container.appendChild(alias); diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts index 4f08d298d9c..98e84b995ad 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts @@ -4,9 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { MarkdownRenderOptions, MarkedOptions } from 'vs/base/browser/markdownRenderer'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMarkdownRendererOptions, IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; @@ -56,6 +59,7 @@ export class ChatMarkdownRenderer extends MarkdownRenderer { @ILanguageService languageService: ILanguageService, @IOpenerService openerService: IOpenerService, @ITrustedDomainService private readonly trustedDomainService: ITrustedDomainService, + @IHoverService private readonly hoverService: IHoverService, ) { super(options ?? {}, languageService, openerService); } @@ -79,6 +83,26 @@ export class ChatMarkdownRenderer extends MarkdownRenderer { value: `\n\n${markdown.value}`, } : markdown; - return super.render(mdWithBody, options, markedOptions); + const result = super.render(mdWithBody, options, markedOptions); + return this.attachCustomHover(result); + } + + private attachCustomHover(result: IMarkdownRenderResult): IMarkdownRenderResult { + const store = new DisposableStore(); + result.element.querySelectorAll('a').forEach((element) => { + if (element.title) { + const title = element.title; + element.title = ''; + store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, title)); + } + }); + + return { + element: result.element, + dispose: () => { + result.dispose(); + store.dispose(); + } + }; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index a1c6d7a732d..aa461ef6572 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -3,16 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Action } from 'vs/base/common/actions'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import * as strings from 'vs/base/common/strings'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import * as strings from 'vs/base/common/strings'; import { localize, localize2 } from 'vs/nls'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -21,11 +22,9 @@ import { CHAT_VIEW_ID } from 'vs/workbench/contrib/chat/browser/chat'; import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IRawChatParticipantContribution } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { Action } from 'vs/base/common/actions'; -import { ICommandService } from 'vs/platform/commands/common/commands'; const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatParticipants', @@ -109,82 +108,53 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi }, }); +export class ChatCompatibilityNotifier implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.chatCompatNotifier'; + + constructor( + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @INotificationService notificationService: INotificationService, + @ICommandService commandService: ICommandService + ) { + // It may be better to have some generic UI for this, for any extension that is incompatible, + // but this is only enabled for Copilot Chat now and it needs to be obvious. + extensionsWorkbenchService.queryLocal().then(exts => { + const chat = exts.find(ext => ext.identifier.id === 'github.copilot-chat'); + if (chat?.local?.validations.some(v => v[0] === Severity.Error)) { + notificationService.notify({ + severity: Severity.Error, + message: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."), + actions: { + primary: [ + new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => { + return commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']); + }) + ] + } + }); + } + }); + } +} + export class ChatExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; - private readonly disposables = new DisposableStore(); - private _welcomeViewDescriptor?: IViewDescriptor; private _viewContainer: ViewContainer; private _participantRegistrationDisposables = new DisposableMap(); constructor( @IChatAgentService private readonly _chatAgentService: IChatAgentService, - @IProductService private readonly productService: IProductService, - @IContextKeyService private readonly contextService: IContextKeyService, @ILogService private readonly logService: ILogService, - @INotificationService private readonly notificationService: INotificationService, - @ICommandService private readonly commandService: ICommandService, ) { this._viewContainer = this.registerViewContainer(); - this.registerListeners(); this.handleAndRegisterChatExtensions(); } - private registerListeners() { - this.contextService.onDidChangeContext(e => { - - if (!this.productService.chatWelcomeView) { - return; - } - - const showWelcomeViewConfigKey = 'workbench.chat.experimental.showWelcomeView'; - const keys = new Set([showWelcomeViewConfigKey]); - if (e.affectsSome(keys)) { - const contextKeyExpr = ContextKeyExpr.equals(showWelcomeViewConfigKey, true); - const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); - if (this.contextService.contextMatchesRules(contextKeyExpr)) { - this._welcomeViewDescriptor = { - id: CHAT_VIEW_ID, - name: { original: this.productService.chatWelcomeView.welcomeViewTitle, value: this.productService.chatWelcomeView.welcomeViewTitle }, - containerIcon: this._viewContainer.icon, - ctorDescriptor: new SyncDescriptor(ChatViewPane), - canToggleVisibility: false, - canMoveView: true, - order: 100 - }; - viewsRegistry.registerViews([this._welcomeViewDescriptor], this._viewContainer); - - viewsRegistry.registerViewWelcomeContent(CHAT_VIEW_ID, { - content: this.productService.chatWelcomeView.welcomeViewContent, - }); - } else if (this._welcomeViewDescriptor) { - viewsRegistry.deregisterViews([this._welcomeViewDescriptor], this._viewContainer); - } - } - }, null, this.disposables); - } - private handleAndRegisterChatExtensions(): void { chatParticipantExtensionPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { - // Detect old version of Copilot Chat extension. - // TODO@roblourens remove after one release, after this we will rely on things like the API version - if (extension.value.some(participant => participant.id === 'github.copilot.default' && !participant.fullName)) { - this.notificationService.notify({ - severity: Severity.Error, - message: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."), - actions: { - primary: [ - new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => { - return this.commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']); - }) - ] - } - }); - continue; - } - for (const providerDescriptor of extension.value) { if (!providerDescriptor.name.match(/^[\w0-9_-]+$/)) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with invalid name: ${providerDescriptor.name}. Name must match /^[\\w0-9_-]+$/.`); @@ -254,7 +224,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { for (const extension of delta.removed) { for (const providerDescriptor of extension.value) { - this._participantRegistrationDisposables.deleteAndDispose(getParticipantKey(extension.description.identifier, providerDescriptor.name)); + this._participantRegistrationDisposables.deleteAndDispose(getParticipantKey(extension.description.identifier, providerDescriptor.id)); } } }); diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index 6d682b5d72e..ad6462a3245 100644 --- a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -5,15 +5,15 @@ import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; -import { alertAccessibleViewFocusChange, IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IChatWidgetService, IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatWidgetService, IChatWidget, ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { Disposable } from 'vs/base/common/lifecycle'; export class ChatResponseAccessibleView implements IAccessibleViewImplentation { readonly priority = 100; @@ -22,78 +22,90 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplentation { readonly when = CONTEXT_IN_CHAT_SESSION; getProvider(accessor: ServicesAccessor) { const widgetService = accessor.get(IChatWidgetService); - const codeEditorService = accessor.get(ICodeEditorService); - return resolveProvider(widgetService, codeEditorService, true); - function resolveProvider(widgetService: IChatWidgetService, codeEditorService: ICodeEditorService, initialRender?: boolean) { - const widget = widgetService.lastFocusedWidget; - if (!widget) { - return; - } - const chatInputFocused = initialRender && !!codeEditorService.getFocusedCodeEditor(); - if (initialRender && chatInputFocused) { - widget.focusLastMessage(); - } + const widget = widgetService.lastFocusedWidget; + if (!widget) { + return; + } + const chatInputFocused = widget.hasInputFocus(); + if (chatInputFocused) { + widget.focusLastMessage(); + } - if (!widget) { - return; - } + const verifiedWidget: IChatWidget = widget; + const focusedItem = verifiedWidget.getFocus(); - const verifiedWidget: IChatWidget = widget; - const focusedItem = verifiedWidget.getFocus(); + if (!focusedItem) { + return; + } - if (!focusedItem) { - return; - } + return new ChatResponseAccessibleProvider(verifiedWidget, focusedItem, chatInputFocused); + } +} - widget.focus(focusedItem); - const isWelcome = focusedItem instanceof ChatWelcomeMessageModel; - let responseContent = isResponseVM(focusedItem) ? focusedItem.response.toString() : undefined; - if (isWelcome) { - const welcomeReplyContents = []; - for (const content of focusedItem.content) { - if (Array.isArray(content)) { - welcomeReplyContents.push(...content.map(m => m.message)); - } else { - welcomeReplyContents.push((content as IMarkdownString).value); - } +class ChatResponseAccessibleProvider extends Disposable implements IAccessibleViewContentProvider { + private _focusedItem: ChatTreeItem; + constructor( + private readonly _widget: IChatWidget, + item: ChatTreeItem, + private readonly _chatInputFocused: boolean + ) { + super(); + this._focusedItem = item; + } + + readonly id = AccessibleViewProviderId.Chat; + readonly verbositySettingKey = AccessibilityVerbositySettingId.Chat; + readonly options = { type: AccessibleViewType.View }; + + provideContent(): string { + return this._getContent(this._focusedItem); + } + + private _getContent(item: ChatTreeItem): string { + const isWelcome = item instanceof ChatWelcomeMessageModel; + let responseContent = isResponseVM(item) ? item.response.toString() : ''; + if (isWelcome) { + const welcomeReplyContents = []; + for (const content of item.content) { + if (Array.isArray(content)) { + welcomeReplyContents.push(...content.map(m => m.message)); + } else { + welcomeReplyContents.push((content as IMarkdownString).value); } - responseContent = welcomeReplyContents.join('\n'); } - if (!responseContent && 'errorDetails' in focusedItem && focusedItem.errorDetails) { - responseContent = focusedItem.errorDetails.message; - } - if (!responseContent) { - return; - } - const responses = verifiedWidget.viewModel?.getItems().filter(i => isResponseVM(i)); - const length = responses?.length; - const responseIndex = responses?.findIndex(i => i === focusedItem); + responseContent = welcomeReplyContents.join('\n'); + } + if (!responseContent && 'errorDetails' in item && item.errorDetails) { + responseContent = item.errorDetails.message; + } + return renderMarkdownAsPlaintext(new MarkdownString(responseContent), true); + } - return { - id: AccessibleViewProviderId.Chat, - verbositySettingKey: AccessibilityVerbositySettingId.Chat, - provideContent(): string { return renderMarkdownAsPlaintext(new MarkdownString(responseContent), true); }, - onClose() { - verifiedWidget.reveal(focusedItem); - if (chatInputFocused) { - verifiedWidget.focusInput(); - } else { - verifiedWidget.focus(focusedItem); - } - }, - next() { - verifiedWidget.moveFocus(focusedItem, 'next'); - alertAccessibleViewFocusChange(responseIndex, length, 'next'); - resolveProvider(widgetService, codeEditorService); - }, - previous() { - verifiedWidget.moveFocus(focusedItem, 'previous'); - alertAccessibleViewFocusChange(responseIndex, length, 'previous'); - resolveProvider(widgetService, codeEditorService); - }, - options: { type: AccessibleViewType.View } - }; + onClose(): void { + this._widget.reveal(this._focusedItem); + if (this._chatInputFocused) { + this._widget.focusInput(); + } else { + this._widget.focus(this._focusedItem); } } - dispose() { } + + provideNextContent(): string | undefined { + const next = this._widget.getSibling(this._focusedItem, 'next'); + if (next) { + this._focusedItem = next; + return this._getContent(next); + } + return; + } + + providePreviousContent(): string | undefined { + const previous = this._widget.getSibling(this._focusedItem, 'previous'); + if (previous) { + this._focusedItem = previous; + return this._getContent(previous); + } + return; + } } + diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index cd978afa041..72beab2c568 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -56,7 +56,7 @@ export class ChatVariablesService implements IChatVariablesService { }; jobs.push(data.resolver(prompt.text, part.variableArg, model, variableProgressCallback, token).then(value => { if (value) { - resolvedVariables[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: part.variableName, range: part.range, value, references }; + resolvedVariables[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: part.variableName, range: part.range, value, references, fullName: data.data.fullName, icon: data.data.icon }; } }).catch(onUnexpectedExternalError)); } @@ -80,11 +80,11 @@ export class ChatVariablesService implements IChatVariablesService { }; jobs.push(data.resolver(prompt.text, '', model, variableProgressCallback, token).then(value => { if (value) { - resolvedAttachedContext[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: attachment.name, range: attachment.range, value, references }; + resolvedAttachedContext[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: attachment.name, fullName: attachment.fullName, range: attachment.range, value, references, icon: attachment.icon }; } }).catch(onUnexpectedExternalError)); - } else if (attachment.isDynamic) { - resolvedAttachedContext[i] = { id: attachment.id, name: attachment.name, value: attachment.value }; + } else if (attachment.isDynamic || attachment.isTool) { + resolvedAttachedContext[i] = { ...attachment }; } }); @@ -122,9 +122,12 @@ export class ChatVariablesService implements IChatVariablesService { return this._resolver.get(name.toLowerCase())?.data; } - getVariables(): Iterable> { + getVariables(location: ChatAgentLocation): Iterable> { const all = Iterable.map(this._resolver.values(), data => data.data); - return Iterable.filter(all, data => !data.hidden); + return Iterable.filter(all, data => { + // TODO@jrieken this is improper and should be know from the variable registeration data + return location !== ChatAgentLocation.Editor || !new Set(['selection', 'editor']).has(data.name); + }); } getDynamicVariables(sessionId: string): ReadonlyArray { @@ -161,8 +164,7 @@ export class ChatVariablesService implements IChatVariablesService { return; } - await showChatView(this.viewsService); - const widget = this.chatWidgetService.lastFocusedWidget; + const widget = await showChatView(this.viewsService); if (!widget || !widget.viewModel) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 80022029e40..0cb92990dff 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -99,7 +99,7 @@ export class ChatViewPane extends ViewPane { }; } - private updateModel(model?: IChatModel | undefined, viewState?: IViewPaneState): void { + private updateModel(model?: IChatModel | undefined): void { this.modelDisposables.clear(); model = model ?? (this.chatService.transferredSessionData?.sessionId @@ -109,7 +109,7 @@ export class ChatViewPane extends ViewPane { throw new Error('Could not start chat session'); } - this._widget.setModel(model, { ...(viewState ?? this.viewState) }); + this._widget.setModel(model, { ...this.viewState }); this.viewState.sessionId = model.sessionId; } @@ -179,7 +179,10 @@ export class ChatViewPane extends ViewPane { if (this.widget.viewModel) { this.chatService.clearSession(this.widget.viewModel.sessionId); } - this.updateModel(undefined, { ...this.viewState, inputValue: undefined }); + + // Grab the widget's latest view state because it will be loaded back into the widget + this.updateViewState(); + this.updateModel(undefined); } loadSession(sessionId: string): void { @@ -211,12 +214,16 @@ export class ChatViewPane extends ViewPane { // TODO multiple chat views will overwrite each other this._widget.saveState(); - const widgetViewState = this._widget.getViewState(); - this.viewState.inputValue = widgetViewState.inputValue; - this.viewState.inputState = widgetViewState.inputState; + this.updateViewState(); this.memento.saveMemento(); } super.saveState(); } + + private updateViewState(): void { + const widgetViewState = this._widget.getViewState(); + this.viewState.inputValue = widgetViewState.inputValue; + this.viewState.inputState = widgetViewState.inputState; + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 21ad0513b63..3c7f3459676 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -9,7 +9,7 @@ import { disposableTimeout, timeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { matchesScheme, Schemas } from 'vs/base/common/network'; +import { Schemas } from 'vs/base/common/network'; import { extUri, isEqual } from 'vs/base/common/resources'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -26,7 +26,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions, IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAccessibilityProvider } from 'vs/workbench/contrib/chat/browser/chatAccessibilityProvider'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { ChatListDelegate, ChatListItemRenderer, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; @@ -34,13 +34,12 @@ import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_QUICK_CHAT, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatModelInitState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, formatChatQuestion } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatFollowup, IChatLocationData, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; -import { IChatListItemRendererOptions } from './chat'; const $ = dom.$; @@ -69,8 +68,6 @@ export interface IChatWidgetContrib extends IDisposable { */ getInputState?(): any; - onDidChangeInputState?: Event; - /** * Called with the result of getInputState when navigating input history. */ @@ -88,6 +85,9 @@ export class ChatWidget extends Disposable implements IChatWidget { private readonly _onDidSubmitAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>()); public readonly onDidSubmitAgent = this._onDidSubmitAgent.event; + private _onDidChangeAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>()); + readonly onDidChangeAgent = this._onDidChangeAgent.event; + private _onDidFocus = this._register(new Emitter()); readonly onDidFocus = this._onDidFocus.event; @@ -103,8 +103,8 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidAcceptInput = this._register(new Emitter()); readonly onDidAcceptInput = this._onDidAcceptInput.event; - private _onDidDeleteContext = this._register(new Emitter()); - readonly onDidDeleteContext = this._onDidDeleteContext.event; + private _onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>()); + readonly onDidChangeContext = this._onDidChangeContext.event; private _onDidHide = this._register(new Emitter()); readonly onDidHide = this._onDidHide.event; @@ -223,13 +223,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection)); this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise => { - let resource = input.resource; - - // if trying to open backing documents, actually open the real chat code block doc - if (matchesScheme(resource, Schemas.vscodeCopilotBackingChatCodeBlock)) { - resource = resource.with({ scheme: Schemas.vscodeChatCodeBlock }); - } - + const resource = input.resource; if (resource.scheme !== Schemas.vscodeChatCodeBlock) { return null; } @@ -257,8 +251,8 @@ export class ChatWidget extends Disposable implements IChatWidget { inner.setSelection({ startLineNumber: input.options.selection.startLineNumber, startColumn: input.options.selection.startColumn, - endLineNumber: input.options.selection.startLineNumber ?? input.options.selection.endLineNumber, - endColumn: input.options.selection.startColumn ?? input.options.selection.endColumn + endLineNumber: input.options.selection.endLineNumber ?? input.options.selection.startLineNumber, + endColumn: input.options.selection.endColumn ?? input.options.selection.startColumn }); } return inner; @@ -267,6 +261,9 @@ export class ChatWidget extends Disposable implements IChatWidget { return null; })); } + getLocationData(): IChatLocationData | undefined { + return this._location.resolveData?.(); + } private _lastSelectedAgent: IChatAgentData | undefined; set lastSelectedAgent(agent: IChatAgentData | undefined) { @@ -334,15 +331,6 @@ export class ChatWidget extends Disposable implements IChatWidget { return undefined; } }).filter(isDefined); - - this.contribs.forEach(c => { - if (c.onDidChangeInputState) { - this._register(c.onDidChangeInputState(() => { - const state = this.collectInputState(); - this.inputPart.updateState(state); - })); - } - }); } getContrib(id: string): T | undefined { @@ -357,7 +345,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.inputPart.hasFocus(); } - moveFocus(item: ChatTreeItem, type: 'next' | 'previous'): void { + getSibling(item: ChatTreeItem, type: 'next' | 'previous'): ChatTreeItem | undefined { if (!isResponseVM(item)) { return; } @@ -374,7 +362,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (indexToFocus < 0 || indexToFocus > responseItems.length - 1) { return; } - this.focus(responseItems[indexToFocus]); + return responseItems[indexToFocus]; } clear(): void { @@ -409,7 +397,10 @@ export class ChatWidget extends Disposable implements IChatWidget { // be re-rendered so progressive rendering is restarted, even if the model wasn't updated. `${isResponseVM(element) && element.renderData ? `_${this.visibleChangeCount}` : ''}` + // Re-render once content references are loaded - (isResponseVM(element) ? `_${element.contentReferences.length}` : ''); + (isResponseVM(element) ? `_${element.contentReferences.length}` : '') + + // Rerender request if we got new content references in the response + // since this may change how we render the corresponding attachments in the request + (isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : ''); }, } }); @@ -486,7 +477,9 @@ export class ChatWidget extends Disposable implements IChatWidget { this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(item => { const request = this.chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); if (request) { - this.chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt, location: this.location }).catch(e => this.logService.error('FAILED to rerun request', e)); + this.chatService.resendRequest(request, { + noCommandDetection: true, attempt: request.attempt + 1, location: this.location, locationData: this._location.resolveData?.(), + }).catch(e => this.logService.error('FAILED to rerun request', e)); } })); @@ -580,7 +573,8 @@ export class ChatWidget extends Disposable implements IChatWidget { renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle, menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, - } + }, + () => this.collectInputState() )); this.inputPart.render(container, '', this); @@ -593,7 +587,7 @@ export class ChatWidget extends Disposable implements IChatWidget { }); })); this._register(this.inputPart.onDidFocus(() => this._onDidFocus.fire())); - this._register(this.inputPart.onDidDeleteContext((e) => this._onDidDeleteContext.fire(e))); + this._register(this.inputPart.onDidChangeContext((e) => this._onDidChangeContext.fire(e))); this._register(this.inputPart.onDidAcceptFollowup(e => { if (!this.viewModel) { return; @@ -688,6 +682,11 @@ export class ChatWidget extends Disposable implements IChatWidget { c.setInputState(viewState.inputState?.[c.id]); } }); + this.viewModelDisposables.add(model.onDidChange((e) => { + if (e.kind === 'setAgent') { + this._onDidChangeAgent.fire({ agent: e.agent, slashCommand: e.command }); + } + })); if (this.tree) { this.onDidChangeItems(); @@ -775,14 +774,19 @@ export class ChatWidget extends Disposable implements IChatWidget { }); if (result) { - this.inputPart.attachedContext.clear(); this.inputPart.acceptInput(isUserQuery); this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand }); - this.inputPart.updateState(this.collectInputState()); result.responseCompletePromise.then(() => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; this.chatAccessibilityService.acceptResponse(lastResponse, requestId); + if (lastResponse?.result?.nextQuestion) { + const { prompt, participant, command } = lastResponse.result.nextQuestion; + const question = formatChatQuestion(this.chatAgentService, this.location, prompt, participant, command); + if (question) { + this.input.setValue(question, false); + } + } }); return result.responseCreatedPromise; } @@ -790,16 +794,8 @@ export class ChatWidget extends Disposable implements IChatWidget { return undefined; } - setContext(overwrite: boolean, ...contentReferences: IChatRequestVariableEntry[]) { - if (overwrite) { - this.inputPart.attachedContext.clear(); - } - this.inputPart.attachContext(...contentReferences); - - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } + this.inputPart.attachContext(overwrite, ...contentReferences); } getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { @@ -960,7 +956,6 @@ export class ChatWidget extends Disposable implements IChatWidget { } getViewState(): IChatViewState { - this.inputPart.saveState(); return { inputValue: this.getInput(), inputState: this.collectInputState() }; } } diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 7caa40d9684..f02d89d4599 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -35,7 +35,8 @@ import { BracketMatchingController } from 'vs/editor/contrib/bracketMatching/bro import { ColorDetector } from 'vs/editor/contrib/colorPicker/browser/colorDetector'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; +import { MarginHoverController } from 'vs/editor/contrib/hover/browser/marginHoverController'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import { ViewportSemanticTokensContribution } from 'vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens'; import { SmartSelectController } from 'vs/editor/contrib/smartSelect/browser/smartSelect'; @@ -64,6 +65,7 @@ import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/ import { IMarkdownVulnerability } from '../common/annotations'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { FileKind } from 'vs/platform/files/common/files'; +import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; const $ = dom.$; @@ -146,6 +148,8 @@ export class CodeBlockPart extends Disposable { private readonly disposableStore = this._register(new DisposableStore()); private isDisposed = false; + private resourceContextKey: ResourceContextKey; + constructor( private readonly options: ChatEditorOptions, readonly menuId: MenuId, @@ -160,6 +164,7 @@ export class CodeBlockPart extends Disposable { super(); this.element = $('.interactive-result-code-block'); + this.resourceContextKey = this._register(instantiationService.createInstance(ResourceContextKey)); this.contextKeyService = this._register(contextKeyService.createScoped(this.element)); const scopedInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]))); const editorElement = dom.append(this.element, $('.interactive-result-editor')); @@ -286,7 +291,8 @@ export class CodeBlockPart extends Disposable { ViewportSemanticTokensContribution.ID, BracketMatchingController.ID, SmartSelectController.ID, - HoverController.ID, + ContentHoverController.ID, + MarginHoverController.ID, MessageController.ID, GotoDefinitionAtPositionEditorContribution.ID, ColorDetector.ID @@ -396,7 +402,8 @@ export class CodeBlockPart extends Disposable { } private clearWidgets() { - HoverController.get(this.editor)?.hideContentHover(); + ContentHoverController.get(this.editor)?.hideContentHover(); + MarginHoverController.get(this.editor)?.hideContentHover(); } private async updateEditor(data: ICodeBlockData): Promise { @@ -413,6 +420,7 @@ export class CodeBlockPart extends Disposable { element: data.element, languageId: textModel.getLanguageId() } satisfies ICodeBlockActionContext; + this.resourceContextKey.set(textModel.uri); } private getVulnerabilitiesLabel(): string { @@ -602,7 +610,8 @@ export class CodeCompareBlockPart extends Disposable { ViewportSemanticTokensContribution.ID, BracketMatchingController.ID, SmartSelectController.ID, - HoverController.ID, + ContentHoverController.ID, + MarginHoverController.ID, GotoDefinitionAtPositionEditorContribution.ID, ]) }; @@ -618,9 +627,16 @@ export class CodeCompareBlockPart extends Disposable { diffAlgorithm: 'advanced', readOnly: false, isInEmbeddedEditor: true, - useInlineViewWhenSpaceIsLimited: false, + useInlineViewWhenSpaceIsLimited: true, + experimental: { + useTrueInlineView: true, + }, + renderSideBySideInlineBreakpoint: 300, + renderOverviewRuler: false, + compactMode: true, hideUnchangedRegions: { enabled: true, contextLineCount: 1 }, renderGutterMenu: false, + lineNumbersMinChars: 1, ...options }, { originalEditor: widgetOptions, modifiedEditor: widgetOptions })); } @@ -705,8 +721,10 @@ export class CodeCompareBlockPart extends Disposable { } private clearWidgets() { - HoverController.get(this.diffEditor.getOriginalEditor())?.hideContentHover(); - HoverController.get(this.diffEditor.getModifiedEditor())?.hideContentHover(); + ContentHoverController.get(this.diffEditor.getOriginalEditor())?.hideContentHover(); + ContentHoverController.get(this.diffEditor.getModifiedEditor())?.hideContentHover(); + MarginHoverController.get(this.diffEditor.getOriginalEditor())?.hideContentHover(); + MarginHoverController.get(this.diffEditor.getModifiedEditor())?.hideContentHover(); } private async updateEditor(data: ICodeCompareBlockData, token: CancellationToken): Promise { @@ -779,6 +797,10 @@ export class CodeCompareBlockPart extends Disposable { diffEditor: this.diffEditor, } satisfies ICodeCompareBlockActionContext; } + + clearModel() { + this.diffEditor.setModel(null); + } } export class DefaultChatTextEditor { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts index c15a1f6aee3..c19bc95305f 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatWidget, IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; @@ -13,9 +12,6 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon private _attachedContext = new Set(); - private readonly _onDidChangeInputState = this._register(new Emitter()); - readonly onDidChangeInputState = this._onDidChangeInputState.event; - public static readonly ID = 'chatContextAttachments'; get id() { @@ -25,8 +21,10 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon constructor(readonly widget: IChatWidget) { super(); - this._register(this.widget.onDidDeleteContext((e) => { - this._removeContext(e); + this._register(this.widget.onDidChangeContext((e) => { + if (e.removed) { + this._removeContext(e.removed); + } })); this._register(this.widget.onDidSubmitAgent(() => { @@ -64,12 +62,12 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon } this.widget.setContext(overwrite, ...attachments); - this._onDidChangeInputState.fire(); } - private _removeContext(attachment: IChatRequestVariableEntry) { - this._attachedContext.delete(attachment); - this._onDidChangeInputState.fire(); + private _removeContext(attachments: IChatRequestVariableEntry[]) { + if (attachments.length) { + attachments.forEach(this._attachedContext.delete, this._attachedContext); + } } private _clearAttachedContext() { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 2d8bf253fd0..a81f88f9367 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce } from 'vs/base/common/arrays'; -import { Emitter } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/resources'; @@ -39,9 +38,6 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC return ChatDynamicVariableModel.ID; } - private _onDidChangeInputState = this._register(new Emitter()); - readonly onDidChangeInputState = this._onDidChangeInputState.event; - constructor( private readonly widget: IChatWidget, @ILabelService private readonly labelService: ILabelService, @@ -50,7 +46,6 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC this._register(widget.inputEditor.onDidChangeModelContent(e => { e.changes.forEach(c => { // Don't mutate entries in _variables, since they will be returned from the getter - const originalNumVariables = this._variables.length; this._variables = coalesce(this._variables.map(ref => { const intersection = Range.intersectRanges(ref.range, c.range); if (intersection && !intersection.isEmpty()) { @@ -79,10 +74,6 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC return ref; })); - - if (this._variables.length !== originalNumVariables) { - this._onDidChangeInputState.fire(); - } }); this.updateDecorations(); @@ -105,22 +96,21 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC addReference(ref: IDynamicVariable): void { this._variables.push(ref); this.updateDecorations(); - this._onDidChangeInputState.fire(); } private updateDecorations(): void { - this.widget.inputEditor.setDecorationsByType('chat', dynamicVariableDecorationType, this._variables.map(r => ({ + this.widget.inputEditor.setDecorationsByType('chat', dynamicVariableDecorationType, this._variables.map((r): IDecorationOptions => ({ range: r.range, hoverMessage: this.getHoverForReference(r) }))); } - private getHoverForReference(ref: IDynamicVariable): string | IMarkdownString { + private getHoverForReference(ref: IDynamicVariable): IMarkdownString | undefined { const value = ref.data; if (URI.isUri(value)) { return new MarkdownString(this.labelService.getUriLabel(value, { relative: true })); } else { - return (value as any).toString(); + return undefined; } } } @@ -172,11 +162,10 @@ export class SelectAndInsertFileAction extends Action2 { // This of course assumes that the `files` variable has the behavior that it searches // through files in the workspace. if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) { - options = { - providerOptions: { - additionPicks: [SelectAndInsertFileAction.Item, { type: 'separator' }] - }, + const providerOptions: AnythingQuickAccessProviderRunOptions = { + additionPicks: [SelectAndInsertFileAction.Item, { type: 'separator' }] }; + options = { providerOptions }; } // TODO: have dedicated UX for this instead of using the quick access picker const picks = await quickInputService.quickAccess.pick('', options); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 8910e307eab..8b0484bbe51 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -318,7 +318,7 @@ class BuiltinDynamicCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef); + const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef, true); if (!range) { return null; } @@ -344,12 +344,19 @@ class BuiltinDynamicCompletions extends Disposable { Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BuiltinDynamicCompletions, LifecyclePhase.Eventually); -function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range; varWord: IWordAtPosition | null } | undefined { +function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp, onlyOnWordStart = false): { insert: Range; replace: Range; varWord: IWordAtPosition | null } | undefined { const varWord = getWordAtText(position.column, reg, model.getLineContent(position.lineNumber), 0); if (!varWord && model.getWordUntilPosition(position).word) { // inside a "normal" word return; } + if (varWord && onlyOnWordStart) { + const wordBefore = model.getWordUntilPosition({ lineNumber: position.lineNumber, column: varWord.startColumn }); + if (wordBefore.word) { + // inside a word + return; + } + } let insert: Range; let replace: Range; @@ -394,7 +401,7 @@ class VariableCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, VariableCompletions.VariableNameDef); + const range = computeCompletionRanges(model, position, VariableCompletions.VariableNameDef, true); if (!range) { return null; } @@ -403,7 +410,7 @@ class VariableCompletions extends Disposable { const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); - const variableItems = Array.from(this.chatVariablesService.getVariables()) + const variableItems = Array.from(this.chatVariablesService.getVariables(widget.location)) // This doesn't look at dynamic variables like `file`, where multiple makes sense. .filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name)) .filter(v => !v.isSlow || slowSupported) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 24aafd6c101..b72abe680df 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -242,12 +242,22 @@ class InputEditorSlashCommandMode extends Disposable { private readonly widget: IChatWidget ) { super(); + this._register(this.widget.onDidChangeAgent(e => { + if (e.slashCommand && e.slashCommand.isSticky || !e.slashCommand && e.agent.metadata.isSticky) { + this.repopulateAgentCommand(e.agent, e.slashCommand); + } + })); this._register(this.widget.onDidSubmitAgent(e => { this.repopulateAgentCommand(e.agent, e.slashCommand); })); } private async repopulateAgentCommand(agent: IChatAgentData, slashCommand: IChatAgentCommand | undefined) { + // Make sure we don't repopulate if the user already has something in the input + if (this.widget.inputEditor.getValue().trim()) { + return; + } + let value: string | undefined; if (slashCommand && slashCommand.isSticky) { value = `${chatAgentLeader}${agent.name} ${chatSubcommandLeader}${slashCommand.name} `; diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 23de8d70e1d..6e67ee583a9 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -40,6 +40,12 @@ display: flex; align-items: center; gap: 8px; + + /* + Rendering the avatar icon as round makes it a little larger than the .user container. + Add padding so that the focus outline doesn't run into it, and counteract it with a negative margin so it doesn't actually take up any extra space */ + padding: 2px; + margin: -2px; } .interactive-item-container .header .username { @@ -399,7 +405,7 @@ border-color: var(--vscode-focusBorder); } -.interactive-session .interactive-input-and-execute-toolbar .monaco-editor .mtk1 { +.interactive-input-and-execute-toolbar .monaco-editor .mtk1 { color: var(--vscode-input-foreground); } @@ -482,6 +488,17 @@ color: var(--vscode-interactive-session-foreground); } +.chat-attached-context .chat-attached-context-attachment .monaco-icon-name-container.warning, +.chat-attached-context .chat-attached-context-attachment .monaco-icon-suffix-container.warning, +.chat-used-context-list .monaco-icon-name-container.warning, +.chat-used-context-list .monaco-icon-suffix-container.warning { + color: var(--vscode-notificationsWarningIcon-foreground); +} + +.chat-attached-context .chat-attached-context-attachment.show-file-icons.warning { + border-color: var(--vscode-notificationsWarningIcon-foreground); +} + .chat-notification-widget .chat-warning-codicon .codicon-warning { color: var(--vscode-notificationsWarningIcon-foreground) !important; /* Have to override default styles which apply to all lists */ } @@ -534,6 +551,7 @@ .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .monaco-button.codicon.codicon-close, .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button.codicon.codicon-close { color: var(--vscode-descriptionForeground); + cursor: pointer; } .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .codicon { @@ -547,6 +565,10 @@ flex-wrap: wrap; } +.interactive-session .interactive-item-container.interactive-request .chat-attached-context { + margin-top: -8px; +} + .interactive-session .chat-attached-context .chat-attached-context-attachment { padding: 2px; border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); @@ -555,6 +577,10 @@ max-width: 100%; } +.interactive-session .interactive-item-container.interactive-request .chat-attached-context .chat-attached-context-attachment { + padding-right: 6px; +} + .interactive-session-followups { display: flex; flex-direction: column; @@ -816,3 +842,19 @@ margin-left: 0; margin-top: 1px; } + +.chat-code-citation-label { + opacity: 0.7; + white-space: pre-wrap; +} + +.chat-code-citation-button-container { + display: inline; +} + +.chat-code-citation-button-container .monaco-button { + display: inline; + border: none; + padding: 0; + color: var(--vscode-textLink-foreground); +} diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 08d7013d770..eb1bb9b93fc 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -83,6 +83,21 @@ export interface IChatAgentImplementation { provideSampleQuestions?(location: ChatAgentLocation, token: CancellationToken): ProviderResult; } +export interface IChatParticipantDetectionResult { + participant: string; + command?: string; +} + +export interface IChatParticipantMetadata { + participant: string; + command?: string; + description?: string; +} + +export interface IChatParticipantDetectionProvider { + provideParticipantDetection(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation; participants: IChatParticipantMetadata[] }, token: CancellationToken): Promise; +} + export type IChatAgent = IChatAgentData & IChatAgentImplementation; export interface IChatAgentCommand extends IRawChatCommandContribution { @@ -130,6 +145,12 @@ export interface IChatAgentRequest { rejectedConfirmationData?: any[]; } +export interface IChatQuestion { + readonly prompt: string; + readonly participant?: string; + readonly command?: string; +} + export interface IChatAgentResult { errorDetails?: IChatResponseErrorDetails; timings?: { @@ -138,6 +159,7 @@ export interface IChatAgentResult { }; /** Extra properties that the agent can use to identify a result */ readonly metadata?: { readonly [key: string]: any }; + nextQuestion?: IChatQuestion; } export const IChatAgentService = createDecorator('chatAgentService'); @@ -167,6 +189,8 @@ export interface IChatAgentService { registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise): IDisposable; getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise; + registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable; + detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined>; invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getAgent(id: string): IChatAgentData | undefined; @@ -377,6 +401,53 @@ export class ChatAgentService implements IChatAgentService { return data.impl.provideFollowups(request, result, history, token); } + + private _chatParticipantDetectionProviders = new Map(); + registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider) { + this._chatParticipantDetectionProviders.set(handle, provider); + return toDisposable(() => { + this._chatParticipantDetectionProviders.delete(handle); + }); + } + + async detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> { + // TODO@joyceerhl should we have a selector to be able to narrow down which provider to use + const provider = Iterable.first(this._chatParticipantDetectionProviders.values()); + if (!provider) { + return; + } + + const participants = this.getAgents().reduce((acc, a) => { + acc.push({ participant: a.id, description: undefined }); + for (const command of a.slashCommands) { + acc.push({ participant: a.id, command: command.name, description: undefined }); + } + return acc; + }, []); + + const result = await provider.provideParticipantDetection(request, history, { ...options, participants }, token); + if (!result) { + return; + } + + const agent = this.getAgent(result.participant); + if (!agent) { + // Couldn't find a participant matching the participant detection result + return; + } + + if (!result.command) { + return { agent }; + } + + const command = agent?.slashCommands.find(c => c.name === result.command); + if (!command) { + // Couldn't find a slash command matching the participant detection result + return; + } + + return { agent, command }; + } } export class MergedChatAgent implements IChatAgent { diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index bd51f83ce09..7a17a0dfdf5 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -8,6 +8,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; export const CONTEXT_RESPONSE_VOTE = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); +export const CONTEXT_VOTE_UP_ENABLED = new RawContextKey('chatVoteUpEnabled', false, { type: 'boolean', description: localize('chatVoteUpEnabled', "True when the chat vote up action is enabled.") }); export const CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND = new RawContextKey('chatSessionResponseDetectedAgentOrCommand', false, { type: 'boolean', description: localize('chatSessionResponseDetectedAgentOrCommand', "When the agent or command was automatically detected") }); export const CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING = new RawContextKey('chatResponseSupportsIssueReporting', false, { type: 'boolean', description: localize('chatResponseSupportsIssueReporting', "True when the current chat response supports issue reporting.") }); export const CONTEXT_RESPONSE_FILTERED = new RawContextKey('chatSessionResponseFiltered', false, { type: 'boolean', description: localize('chatResponseFiltered', "True when the chat response was filtered out by the server.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index b7c992fe5ae..c85baeb5551 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { asArray, firstOrDefault } from 'vs/base/common/arrays'; import { DeferredPromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -17,11 +16,12 @@ import { URI, UriComponents, UriDto, isUriComponents } from 'vs/base/common/uri' import { generateUuid } from 'vs/base/common/uuid'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { TextEdit } from 'vs/editor/common/languages'; +import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, ChatAgentVoteDirection, isIUsedContext, IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChatRequestVariableEntry { @@ -33,8 +33,11 @@ export interface IChatRequestVariableEntry { range?: IOffsetRange; value: IChatRequestVariableValue; references?: IChatContentReference[]; + + // TODO are these just a 'kind'? isDynamic?: boolean; isFile?: boolean; + isTool?: boolean; } export interface IChatRequestVariableData { @@ -49,6 +52,7 @@ export interface IChatRequestModel { readonly message: IParsedChatRequest; readonly attempt: number; readonly variableData: IChatRequestVariableData; + readonly confirmation?: string; readonly response?: IChatResponseModel; } @@ -94,6 +98,7 @@ export interface IChatResponseModel { readonly agent?: IChatAgentData; readonly usedContext: IChatUsedContext | undefined; readonly contentReferences: ReadonlyArray; + readonly codeCitations: ReadonlyArray; readonly progressMessages: ReadonlyArray; readonly slashCommand?: IChatAgentCommand; readonly agentOrSlashCommandDetected: boolean; @@ -140,11 +145,16 @@ export class ChatRequestModel implements IChatRequestModel { this._variableData = v; } + public get confirmation(): string | undefined { + return this._confirmation; + } + constructor( private _session: ChatModel, public readonly message: IParsedChatRequest, private _variableData: IChatRequestVariableData, - private _attempt: number = 0 + private _attempt: number = 0, + private _confirmation?: string ) { this.id = 'request_' + ChatRequestModel.nextId++; } @@ -154,8 +164,8 @@ export class ChatRequestModel implements IChatRequestModel { } } -export class Response implements IResponse { - private _onDidChangeValue = new Emitter(); +export class Response extends Disposable implements IResponse { + private _onDidChangeValue = this._register(new Emitter()); public get onDidChangeValue() { return this._onDidChangeValue.event; } @@ -172,12 +182,14 @@ export class Response implements IResponse { */ private _markdownContent = ''; + private _citations: IChatCodeCitation[] = []; get value(): IChatProgressResponseContent[] { return this._responseParts; } constructor(value: IMarkdownString | ReadonlyArray) { + super(); this._responseParts = asArray(value).map((v) => (isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent : 'kind' in v ? v : { kind: 'treeData', treeData: v })); @@ -185,7 +197,7 @@ export class Response implements IResponse { this._updateRepr(true); } - toString(): string { + override toString(): string { return this._responseRepr; } @@ -256,6 +268,11 @@ export class Response implements IResponse { } } + public addCitation(citation: IChatCodeCitation) { + this._citations.push(citation); + this._updateRepr(); + } + private _updateRepr(quiet?: boolean) { this._responseRepr = this._responseParts.map(part => { if (part.kind === 'treeData') { @@ -277,6 +294,8 @@ export class Response implements IResponse { .filter(s => s.length > 0) .join('\n\n'); + this._responseRepr += this._citations.length ? '\n\n' + getCodeCitationsMessage(this._citations) : ''; + this._markdownContent = this._responseParts.map(part => { if (part.kind === 'inlineReference') { return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); @@ -365,6 +384,11 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._contentReferences; } + private readonly _codeCitations: IChatCodeCitation[] = []; + public get codeCitations(): ReadonlyArray { + return this._codeCitations; + } + private readonly _progressMessages: IChatProgressMessage[] = []; public get progressMessages(): ReadonlyArray { return this._progressMessages; @@ -393,7 +417,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._isStale = Array.isArray(_response) && (_response.length !== 0 || isMarkdownString(_response) && _response.value.length !== 0); this._followups = followups ? [...followups] : undefined; - this._response = new Response(_response); + this._response = this._register(new Response(_response)); this._register(this._response.onDidChangeValue(() => this._onDidChange.fire())); this.id = 'response_' + ChatResponseModel.nextId++; } @@ -417,6 +441,12 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } } + applyCodeCitation(progress: IChatCodeCitation) { + this._codeCitations.push(progress); + this._response.addCitation(progress); + this._onDidChange.fire(); + } + setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) { this._agent = agent; this._slashCommand = slashCommand; @@ -508,6 +538,7 @@ export interface ISerializableChatRequestData { /** For backward compat: should be optional */ usedContext?: IChatUsedContext; contentReferences?: ReadonlyArray; + codeCitations?: ReadonlyArray; } export interface IExportableChatData { @@ -524,6 +555,9 @@ export interface ISerializableChatData extends IExportableChatData { sessionId: string; creationDate: number; isImported: boolean; + + /** Indicates that this session was created in this window. Is cleared after the chat has been written to storage once. Needed to sync chat creations/deletions between empty windows. */ + isNew?: boolean; } export function isExportableSessionData(obj: unknown): obj is IExportableChatData { @@ -542,13 +576,22 @@ export function isSerializableSessionData(obj: unknown): obj is ISerializableCha ); } -export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent | IChatRemoveRequestEvent; +export type IChatChangeEvent = + | IChatInitEvent + | IChatAddRequestEvent | IChatChangedRequestEvent | IChatRemoveRequestEvent + | IChatAddResponseEvent + | IChatSetAgentEvent; export interface IChatAddRequestEvent { kind: 'addRequest'; request: IChatRequestModel; } +export interface IChatChangedRequestEvent { + kind: 'changedRequest'; + request: IChatRequestModel; +} + export interface IChatAddResponseEvent { kind: 'addResponse'; response: IChatResponseModel; @@ -578,6 +621,12 @@ export interface IChatRemoveRequestEvent { reason: ChatRequestRemovalReason; } +export interface IChatSetAgentEvent { + kind: 'setAgent'; + agent: IChatAgentData; + command?: IChatAgentCommand; +} + export interface IChatInitEvent { kind: 'initialize'; } @@ -726,15 +775,15 @@ export class ChatModel extends Disposable implements IChatModel { // Port entries from old format const result = 'responseErrorDetails' in raw ? + // eslint-disable-next-line local/code-no-dangerous-type-assertions { errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result; request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, raw.slashCommand, request.id, true, raw.isCanceled, raw.vote, result, raw.followups); if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway? request.response.applyReference(revive(raw.usedContext)); } - if (raw.contentReferences) { - raw.contentReferences.forEach(r => request.response!.applyReference(revive(r))); - } + raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r))); + raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c))); } return request; }); @@ -823,8 +872,8 @@ export class ChatModel extends Disposable implements IChatModel { return this._requests; } - addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand): ChatRequestModel { - const request = new ChatRequestModel(this, message, variableData, attempt); + addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string): ChatRequestModel { + const request = new ChatRequestModel(this, message, variableData, attempt, confirmation); request.response = new ChatResponseModel([], this, chatAgent, slashCommand, request.id); this._requests.push(request); @@ -832,6 +881,11 @@ export class ChatModel extends Disposable implements IChatModel { return request; } + updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) { + request.variableData = variableData; + this._onDidChange.fire({ kind: 'changedRequest', request }); + } + adoptRequest(request: ChatRequestModel): void { // this doesn't use `removeRequest` because it must not dispose the request object @@ -879,7 +933,10 @@ export class ChatModel extends Disposable implements IChatModel { const agent = this.chatAgentService.getAgent(progress.agentId); if (agent) { request.response.setAgent(agent, progress.command); + this._onDidChange.fire({ kind: 'setAgent', agent, command: progress.command }); } + } else if (progress.kind === 'codeCitation') { + request.response.applyCodeCitation(progress); } else { this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`); } @@ -973,7 +1030,8 @@ export class ChatModel extends Disposable implements IChatModel { agent: r.response?.agent ? { ...r.response.agent } : undefined, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, - contentReferences: r.response?.contentReferences + contentReferences: r.response?.contentReferences, + codeCitations: r.response?.codeCitations }; }), }; @@ -1102,3 +1160,15 @@ export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString baseUri: md1.baseUri }; } + +export function getCodeCitationsMessage(citations: ReadonlyArray): string { + if (citations.length === 0) { + return ''; + } + + const licenseTypes = citations.reduce((set, c) => set.add(c.license), new Set()); + const label = licenseTypes.size === 1 ? + localize('codeCitation', "Similar code found with 1 license type", licenseTypes.size) : + localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size); + return label; +} diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 66bc10c2061..67f11571229 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -6,7 +6,7 @@ import { revive } from 'vs/base/common/marshalling'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatAgentCommand, IChatAgentData, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatSlashData } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -194,3 +194,20 @@ export function extractAgentAndCommand(parsed: IParsedChatRequest): { agentPart: const commandPart = parsed.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); return { agentPart, commandPart }; } + +export function formatChatQuestion(chatAgentService: IChatAgentService, location: ChatAgentLocation, prompt: string, participant: string | null = null, command: string | null = null): string | undefined { + let question = ''; + if (participant && participant !== chatAgentService.getDefaultAgent(location)?.id) { + const agent = chatAgentService.getAgent(participant); + if (!agent) { + // Refers to agent that doesn't exist + return undefined; + } + + question += `${chatAgentLeader}${agent.name} `; + if (command) { + question += `${chatSubcommandLeader}${command} `; + } + } + return question + prompt; +} diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index e6492570295..d07026f5b7f 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -109,11 +109,11 @@ export class ChatRequestParser { } // If there is more than one agent with this name, and the user picked it from the suggest widget, then the selected agent should be in the - // context and we use that one. Otherwise just pick the first. + // context and we use that one. const agent = agents.length > 1 && context?.selectedAgent ? context.selectedAgent : - agents[0]; - if (!agent || !agent.locations.includes(location)) { + agents.find((a) => a.locations.includes(location)); + if (!agent) { return; } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index f10564d7828..65653fe84ce 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -75,12 +75,26 @@ export interface IChatContentVariableReference { value?: URI | Location; } +export enum ChatResponseReferencePartStatusKind { + Complete = 1, + Partial = 2, + Omitted = 3 +} + export interface IChatContentReference { reference: URI | Location | IChatContentVariableReference; iconPath?: ThemeIcon | { light: URI; dark?: URI }; + options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }; kind: 'reference'; } +export interface IChatCodeCitation { + value: URI; + license: string; + snippet: string; + kind: 'codeCitation'; +} + export interface IChatContentInlineReference { inlineReference: URI | Location; name?: string; @@ -160,6 +174,7 @@ export interface IChatConfirmation { title: string; message: string; data: any; + buttons?: string[]; isUsed?: boolean; kind: 'confirmation'; } @@ -171,6 +186,7 @@ export type IChatProgress = | IChatUsedContext | IChatContentReference | IChatContentInlineReference + | IChatCodeCitation | IChatAgentDetection | IChatProgressMessage | IChatTask @@ -332,6 +348,11 @@ export interface IChatSendRequestOptions { /** The target agent ID can be specified with this property instead of using @ in 'message' */ agentId?: string; slashCommand?: string; + + /** + * The label of the confirmation action that was selected. + */ + confirmation?: string; } export const IChatService = createDecorator('IChatService'); diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index b18e59ac6b4..f01c96c083c 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -10,18 +10,21 @@ import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; -import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_VOTE_UP_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; @@ -30,6 +33,7 @@ import { ChatServiceTelemetry } from 'vs/workbench/contrib/chat/common/chatServi import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const serializedChatKey = 'interactive.sessions'; @@ -50,8 +54,11 @@ type ChatProviderInvokedEvent = { requestType: 'string' | 'followup' | 'slashCommand'; chatSessionId: string; agent: string; + agentExtensionId: string | undefined; slashCommand: string | undefined; location: ChatAgentLocation; + citations: number; + numCodeBlocks: number; }; type ChatProviderInvokedClassification = { @@ -61,21 +68,41 @@ type ChatProviderInvokedClassification = { requestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of request that the user made.' }; chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A random ID for the session.' }; agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of agent used.' }; + agentExtensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that contributed the agent.' }; slashCommand?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of slashCommand used.' }; - location?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location at which chat request was made.' }; + location: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location at which chat request was made.' }; + citations: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of public code citations that were returned with the response.' }; + numCodeBlocks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of code blocks in the response.' }; owner: 'roblourens'; comment: 'Provides insight into the performance of Chat agents.'; }; const maxPersistedSessions = 25; +class CancellableRequest implements IDisposable { + constructor( + public readonly cancellationTokenSource: CancellationTokenSource, + public requestId?: string | undefined + ) { } + + dispose() { + this.cancellationTokenSource.dispose(); + } + + cancel() { + this.cancellationTokenSource.cancel(); + } +} + export class ChatService extends Disposable implements IChatService { declare _serviceBrand: undefined; private readonly _sessionModels = this._register(new DisposableMap()); - private readonly _pendingRequests = this._register(new DisposableMap()); + private readonly _pendingRequests = this._register(new DisposableMap()); private _persistedSessions: ISerializableChatsData; + /** Just for empty windows, need to enforce that a chat was deleted, even though other windows still have it */ + private _deletedChatIds = new Set(); private _transferredSessionData: IChatTransferredSessionData | undefined; public get transferredSessionData(): IChatTransferredSessionData | undefined { @@ -101,11 +128,15 @@ export class ChatService extends Disposable implements IChatService { @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IWorkbenchAssignmentService workbenchAssignmentService: IWorkbenchAssignmentService, + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry); - const sessionData = storageService.get(serializedChatKey, StorageScope.WORKSPACE, ''); + const isEmptyWindow = !workspaceContextService.getWorkspace().folders.length; + const sessionData = storageService.get(serializedChatKey, isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, ''); if (sessionData) { this._persistedSessions = this.deserializeChats(sessionData); const countsForLog = Object.keys(this._persistedSessions).length; @@ -125,6 +156,10 @@ export class ChatService extends Disposable implements IChatService { } this._register(storageService.onWillSaveState(() => this.saveState())); + + const voteUpEnabled = CONTEXT_VOTE_UP_ENABLED.bindTo(contextKeyService); + workbenchAssignmentService.getTreatment('chatVoteUpEnabled') + .then(value => voteUpEnabled.set(!!value)); } isEnabled(location: ChatAgentLocation): boolean { @@ -132,26 +167,85 @@ export class ChatService extends Disposable implements IChatService { } private saveState(): void { - let allSessions: (ChatModel | ISerializableChatData)[] = Array.from(this._sessionModels.values()) + const liveChats = Array.from(this._sessionModels.values()) .filter(session => session.initialLocation === ChatAgentLocation.Panel) .filter(session => session.getRequests().length > 0); - allSessions = allSessions.concat( - Object.values(this._persistedSessions) - .filter(session => !this._sessionModels.has(session.sessionId)) - .filter(session => session.requests.length)); - allSessions.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0)); - allSessions = allSessions.slice(0, maxPersistedSessions); - if (allSessions.length) { - this.trace('onWillSaveState', `Persisting ${allSessions.length} sessions`); + + const isEmptyWindow = !this.workspaceContextService.getWorkspace().folders.length; + if (isEmptyWindow) { + this.syncEmptyWindowChats(liveChats); + } else { + let allSessions: (ChatModel | ISerializableChatData)[] = liveChats; + allSessions = allSessions.concat( + Object.values(this._persistedSessions) + .filter(session => !this._sessionModels.has(session.sessionId)) + .filter(session => session.requests.length)); + allSessions.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0)); + allSessions = allSessions.slice(0, maxPersistedSessions); + if (allSessions.length) { + this.trace('onWillSaveState', `Persisting ${allSessions.length} sessions`); + } + + const serialized = JSON.stringify(allSessions); + + if (allSessions.length) { + this.trace('onWillSaveState', `Persisting ${serialized.length} chars`); + } + + this.storageService.store(serializedChatKey, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE); } - const serialized = JSON.stringify(allSessions); + this._deletedChatIds.clear(); + } - if (allSessions.length) { - this.trace('onWillSaveState', `Persisting ${serialized.length} chars`); + private syncEmptyWindowChats(thisWindowChats: ChatModel[]): void { + // Note- an unavoidable race condition exists here. If there are multiple empty windows open, and the user quits the application, then the focused + // window may lose active chats, because all windows are reading and writing to storageService at the same time. This can't be fixed without some + // kind of locking, but in reality, the focused window will likely have run `saveState` at some point, like on a window focus change, and it will + // generally be fine. + const sessionData = this.storageService.get(serializedChatKey, StorageScope.APPLICATION, ''); + + const originalPersistedSessions = this._persistedSessions; + let persistedSessions: ISerializableChatsData; + if (sessionData) { + persistedSessions = this.deserializeChats(sessionData); + const countsForLog = Object.keys(persistedSessions).length; + if (countsForLog > 0) { + this.trace('constructor', `Restored ${countsForLog} persisted sessions`); + } + } else { + persistedSessions = {}; } - this.storageService.store(serializedChatKey, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE); + this._deletedChatIds.forEach(id => delete persistedSessions[id]); + + // Has the chat in this window been updated, and then closed? Overwrite the old persisted chats. + Object.values(originalPersistedSessions).forEach(session => { + const persistedSession = persistedSessions[session.sessionId]; + if (persistedSession && session.requests.length > persistedSession.requests.length) { + // We will add a 'modified date' at some point, but comparing the number of requests is good enough + persistedSessions[session.sessionId] = session; + } else if (!persistedSession && session.isNew) { + // This session was created in this window, and hasn't been persisted yet + session.isNew = false; + persistedSessions[session.sessionId] = session; + } + }); + + this._persistedSessions = persistedSessions; + + // Add this window's active chat models to the set to persist. + // Having the same session open in two empty windows at the same time can lead to data loss, this is acceptable + const allSessions: Record = { ...this._persistedSessions }; + for (const chat of thisWindowChats) { + allSessions[chat.sessionId] = chat; + } + + let sessionsList = Object.values(allSessions); + sessionsList.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0)); + sessionsList = sessionsList.slice(0, maxPersistedSessions); + const data = JSON.stringify(sessionsList); + this.storageService.store(serializedChatKey, data, StorageScope.APPLICATION, StorageTarget.MACHINE); } notifyUserAction(action: IChatUserActionEvent): void { @@ -244,11 +338,13 @@ export class ChatService extends Disposable implements IChatService { } removeHistoryEntry(sessionId: string): void { + this._deletedChatIds.add(sessionId); delete this._persistedSessions[sessionId]; this.saveState(); } clearAllHistoryEntries(): void { + Object.values(this._persistedSessions).forEach(session => this._deletedChatIds.add(session.sessionId)); this._persistedSessions = {}; this.saveState(); } @@ -458,9 +554,12 @@ export class ChatService extends Disposable implements IChatService { result: 'cancelled', requestType, agent: agentPart?.agent.id ?? '', + agentExtensionId: agentPart?.agent.extensionId.value ?? '', slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, chatSessionId: model.sessionId, location, + citations: request?.response?.codeCitations.length ?? 0, + numCodeBlocks: getCodeBlocks(request.response?.response.toString() ?? '').length }); model.cancelRequest(request); @@ -470,35 +569,64 @@ export class ChatService extends Disposable implements IChatService { let rawResult: IChatAgentResult | null | undefined; let agentOrCommandFollowups: Promise | undefined = undefined; + if (agentPart || (defaultAgent && !commandPart)) { - const agent = (agentPart?.agent ?? defaultAgent)!; - await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); - const history = getHistoryEntriesFromModel(model, agentPart?.agent.id); + const prepareChatAgentRequest = async (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel) => { + const initVariableData: IChatRequestVariableData = { variables: [] }; + request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation); - const initVariableData: IChatRequestVariableData = { variables: [] }; - request = model.addRequest(parsedRequest, initVariableData, attempt, agent, agentSlashCommandPart?.command); - completeResponseCreated(); - const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, options?.attachedContext, model, progressCallback, token); - request.variableData = variableData; + // Variables may have changed if the agent and slash command changed, so resolve them again even if we already had a chatRequest + const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, options?.attachedContext, model, progressCallback, token); + model.updateRequest(request, variableData); + const promptTextResult = getPromptText(request.message); + const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack - const promptTextResult = getPromptText(request.message); - const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack - - const requestProps: IChatAgentRequest = { - sessionId, - requestId: request.id, - agentId: agent.id, - message: promptTextResult.message, - command: agentSlashCommandPart?.command.name, - variables: updatedVariableData, - enableCommandDetection, - attempt, - location, - locationData: options?.locationData, - acceptedConfirmationData: options?.acceptedConfirmationData, - rejectedConfirmationData: options?.rejectedConfirmationData, + return { + sessionId, + requestId: request.id, + agentId: agent.id, + message: promptTextResult.message, + command: command?.name, + variables: updatedVariableData, + enableCommandDetection, + attempt, + location, + locationData: options?.locationData, + acceptedConfirmationData: options?.acceptedConfirmationData, + rejectedConfirmationData: options?.rejectedConfirmationData, + } satisfies IChatAgentRequest; }; + let detectedAgent: IChatAgentData | undefined; + let detectedCommand: IChatAgentCommand | undefined; + if (this.configurationService.getValue('chat.experimental.detectParticipant.enabled') && !agentPart && !commandPart && enableCommandDetection) { + // We have no agent or command to scope history with, pass the full history to the participant detection provider + const defaultAgentHistory = getHistoryEntriesFromModel(model, defaultAgent.id); + + // Prepare the request object that we will send to the participant detection provider + const chatAgentRequest = await prepareChatAgentRequest(defaultAgent, agentSlashCommandPart?.command, enableCommandDetection); + + const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token); + if (result) { + // Update the response in the ChatModel to reflect the detected agent and command + request.response?.setAgent(result.agent, result.command); + detectedAgent = result.agent; + detectedCommand = result.command; + } + } + + const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!; + const command = detectedCommand ?? agentSlashCommandPart?.command; + await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); + + // Recompute history in case the agent or command changed + const history = getHistoryEntriesFromModel(model, agent.id); + const requestProps = await prepareChatAgentRequest(agent, command, !detectedAgent && enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */); + const pendingRequest = this._pendingRequests.get(sessionId); + if (pendingRequest && !pendingRequest.requestId) { + pendingRequest.requestId = requestProps.requestId; + } + completeResponseCreated(); const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); rawResult = agentResult; agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); @@ -538,15 +666,19 @@ export class ChatService extends Disposable implements IChatService { rawResult.errorDetails && gotProgress ? 'errorWithOutput' : rawResult.errorDetails ? 'error' : 'success'; + const commandForTelemetry = agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command; this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { timeToFirstProgress: rawResult.timings?.firstProgress, totalTime: rawResult.timings?.totalElapsed, result, requestType, agent: agentPart?.agent.id ?? '', - slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, + agentExtensionId: agentPart?.agent.extensionId.value ?? '', + slashCommand: commandForTelemetry, chatSessionId: model.sessionId, - location + location, + citations: request.response?.codeCitations.length ?? 0, + numCodeBlocks: getCodeBlocks(request.response?.response.toString() ?? '').length }); model.setResponse(request, rawResult); completeResponseCreated(); @@ -556,6 +688,7 @@ export class ChatService extends Disposable implements IChatService { if (agentOrCommandFollowups) { agentOrCommandFollowups.then(followups => { model.setFollowups(request, followups); + this._chatServiceTelemetry.retrievedFollowups(agentPart?.agent.id ?? '', commandForTelemetry, followups?.length ?? 0); }); } } @@ -567,9 +700,12 @@ export class ChatService extends Disposable implements IChatService { result, requestType, agent: agentPart?.agent.id ?? '', + agentExtensionId: agentPart?.agent.extensionId.value ?? '', slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, chatSessionId: model.sessionId, - location + location, + citations: 0, + numCodeBlocks: 0 }); this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`); if (request) { @@ -583,7 +719,7 @@ export class ChatService extends Disposable implements IChatService { } }; const rawResponsePromise = sendRequestInternal(); - this._pendingRequests.set(model.sessionId, source); + this._pendingRequests.set(model.sessionId, new CancellableRequest(source)); rawResponsePromise.finally(() => { this._pendingRequests.deleteAndDispose(model.sessionId); }); @@ -601,6 +737,12 @@ export class ChatService extends Disposable implements IChatService { await model.waitForInitialization(); + const pendingRequest = this._pendingRequests.get(sessionId); + if (pendingRequest?.requestId === requestId) { + pendingRequest.cancel(); + this._pendingRequests.deleteAndDispose(sessionId); + } + model.removeRequest(requestId); } @@ -621,6 +763,7 @@ export class ChatService extends Disposable implements IChatService { if (request.response && !request.response.isComplete) { const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionId); if (cts) { + cts.requestId = request.id; this._pendingRequests.set(target.sessionId, cts); } } @@ -670,7 +813,9 @@ export class ChatService extends Disposable implements IChatService { if (model.initialLocation === ChatAgentLocation.Panel) { // Turn all the real objects into actual JSON, otherwise, calling 'revive' may fail when it tries to // assign values to properties that are getters- microsoft/vscode-copilot-release#1233 - this._persistedSessions[sessionId] = JSON.parse(JSON.stringify(model)); + const sessionData: ISerializableChatData = JSON.parse(JSON.stringify(model)); + sessionData.isNew = true; + this._persistedSessions[sessionId] = sessionData; } this._sessionModels.deleteAndDispose(sessionId); @@ -701,3 +846,26 @@ export class ChatService extends Disposable implements IChatService { this.trace('transferChatSession', `Transferred session ${model.sessionId} to workspace ${toWorkspace.toString()}`); } } + +function getCodeBlocks(text: string): string[] { + const lines = text.split('\n'); + const codeBlockLanguages: string[] = []; + + let codeBlockState: undefined | { readonly delimiter: string; readonly languageId: string }; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (codeBlockState) { + if (new RegExp(`^\\s*${codeBlockState.delimiter}\\s*$`).test(line)) { + codeBlockLanguages.push(codeBlockState.languageId); + codeBlockState = undefined; + } + } else { + const match = line.match(/^(\s*)(`{3,}|~{3,})(\w*)/); + if (match) { + codeBlockState = { delimiter: match[2], languageId: match[3] }; + } + } + } + return codeBlockLanguages; +} diff --git a/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts b/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts index ec8d5aa339e..ff4adf69d86 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts @@ -63,6 +63,18 @@ type ChatCommandClassification = { comment: 'Provides insight into the usage of Chat features.'; }; +type ChatFollowupEvent = { + agentId: string; + command: string | undefined; +}; + +type ChatFollowupClassification = { + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + type ChatTerminalEvent = { languageId: string; agentId: string; @@ -77,6 +89,20 @@ type ChatTerminalClassification = { comment: 'Provides insight into the usage of Chat features.'; }; +type ChatFollowupsRetrievedEvent = { + agentId: string; + command: string | undefined; + numFollowups: number; +}; + +type ChatFollowupsRetrievedClassification = { + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' }; + numFollowups: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of followup prompts returned by the agent.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + export class ChatServiceTelemetry { constructor( @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -116,6 +142,19 @@ export class ChatServiceTelemetry { agentId: action.agentId ?? '', command: action.command, }); + } else if (action.action.kind === 'followUp') { + this.telemetryService.publicLog2('chatFollowupClicked', { + agentId: action.agentId ?? '', + command: action.command, + }); } } + + retrievedFollowups(agentId: string, command: string | undefined, numFollowups: number): void { + this.telemetryService.publicLog2('chatFollowupsRetrieved', { + agentId, + command, + numFollowups, + }); + } } diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 1df71e988eb..8f8e394ae65 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -23,7 +23,6 @@ export interface IChatVariableData { description: string; modelDescription?: string; isSlow?: boolean; - hidden?: boolean; canTakeArgument?: boolean; } @@ -44,7 +43,7 @@ export interface IChatVariablesService { registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable; hasVariable(name: string): boolean; getVariable(name: string): IChatVariableData | undefined; - getVariables(): Iterable>; + getVariables(location: ChatAgentLocation): Iterable>; getDynamicVariables(sessionId: string): ReadonlyArray; // should be its own service? attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation): void; diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index e4e742afcd5..6cb97bed2af 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -6,18 +6,19 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; -import { marked } from 'vs/base/common/marked/marked'; +import * as marked from 'vs/base/common/marked/marked'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/annotations'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModelInitState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModelInitState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatRequestVariableEntry, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatCodeCitation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; +import { hash } from 'vs/base/common/hash'; export function isRequestVM(item: unknown): item is IChatRequestViewModel { return !!item && typeof item === 'object' && 'message' in item; @@ -68,7 +69,10 @@ export interface IChatRequestViewModel { readonly message: IParsedChatRequest | IChatFollowup; readonly messageText: string; readonly attempt: number; + readonly variables: IChatRequestVariableEntry[]; currentRenderedHeight: number | undefined; + readonly contentReferences?: ReadonlyArray; + readonly confirmation?: string; } export interface IChatResponseMarkdownRenderData { @@ -123,10 +127,18 @@ export interface IChatReferences { kind: 'references'; } +/** + * Content type for citations used during rendering, not in the model + */ +export interface IChatCodeCitations { + citations: ReadonlyArray; + kind: 'codeCitations'; +} + /** * Type for content parts rendered by IChatListRenderer */ -export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences; +export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences | IChatCodeCitations; export interface IChatLiveUpdateData { firstWordTime: number; @@ -151,6 +163,7 @@ export interface IChatResponseViewModel { readonly response: IResponse; readonly usedContext: IChatUsedContext | undefined; readonly contentReferences: ReadonlyArray; + readonly codeCitations: ReadonlyArray; readonly progressMessages: ReadonlyArray; readonly isComplete: boolean; readonly isCanceled: boolean; @@ -293,38 +306,13 @@ export class ChatViewModel extends Disposable implements IChatViewModel { } let codeBlockIndex = 0; - const renderer = new marked.Renderer(); - renderer.code = (value, languageId) => { - languageId ??= ''; - this.codeBlockModelCollection.update(this._model.sessionId, model, codeBlockIndex++, { text: value, languageId }); - return ''; - }; - - marked.parse(this.ensureFencedCodeBlocksTerminated(content), { renderer }); - } - - /** - * Marked doesn't consistently render fenced code blocks that aren't terminated. - * - * Try to close them ourselves to workaround this. - */ - private ensureFencedCodeBlocksTerminated(content: string): string { - const lines = content.split('\n'); - let inCodeBlock = false; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.startsWith('```')) { - inCodeBlock = !inCodeBlock; + marked.walkTokens(marked.lexer(content), token => { + if (token.type === 'code') { + const lang = token.lang || ''; + const text = token.text; + this.codeBlockModelCollection.update(this._model.sessionId, model, codeBlockIndex++, { text, languageId: lang }); } - } - - // If we're still in a code block at the end of the content, add a closing fence - if (inCodeBlock) { - lines.push('```'); - } - - return lines.join('\n'); + }); } } @@ -334,7 +322,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { } get dataId() { - return this.id + `_${ChatModelInitState[this._model.session.initState]}`; + return this.id + `_${ChatModelInitState[this._model.session.initState]}_${hash(this.variables)}`; } get sessionId() { @@ -361,6 +349,18 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.attempt; } + get variables() { + return this._model.variableData.variables; + } + + get contentReferences() { + return this._model.response?.contentReferences; + } + + get confirmation() { + return this._model.confirmation; + } + currentRenderedHeight: number | undefined; constructor( @@ -431,6 +431,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.contentReferences; } + get codeCitations(): ReadonlyArray { + return this._model.codeCitations; + } + get progressMessages(): ReadonlyArray { return this._model.progressMessages; } diff --git a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts index b81d391186e..c1989d4f997 100644 --- a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts +++ b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts @@ -10,13 +10,43 @@ export interface IWordCountResult { isFullString: boolean; } +const r = String.raw; + +/** + * Matches `[text](link title?)` or `[text]( title?)` + * + * Taken from vscode-markdown-languageservice + */ +const linkPattern = + r`(? + /**/r`(?:` + + /*****/r`[^\[\]\\]|` + // Non-bracket chars, or... + /*****/r`\\.|` + // Escaped char, or... + /*****/r`\[[^\[\]]*\]` + // Matched bracket pair + /**/r`)*` + + r`\])` + // <-- close prefix match + + // Destination + r`(\(\s*)` + // Pre href + /**/r`(` + + /*****/r`[^\s\(\)<](?:[^\s\(\)]|\([^\s\(\)]*?\))*|` + // Link without whitespace, or... + /*****/r`<(?:\\[<>]|[^<>])+>` + // In angle brackets + /**/r`)` + + + // Title + /**/r`\s*(?:"[^"]*"|'[^']*'|\([^\(\)]*\))?\s*` + + r`\)`; + export function getNWords(str: string, numWordsToCount: number): IWordCountResult { // This regex matches each word and skips over whitespace and separators. A word is: // A markdown link // One chinese character // One or more + - =, handled so that code like "a=1+2-3" is broken up better // One or more characters that aren't whitepace or any of the above - const allWordMatches = Array.from(str.matchAll(/\[([^\]]+)\]\(([^)]+)\)|\p{sc=Han}|=+|\++|-+|[^\s\|\p{sc=Han}|=|\+|\-]+/gu)); + const allWordMatches = Array.from(str.matchAll(new RegExp(linkPattern + r`|\p{sc=Han}|=+|\++|-+|[^\s\|\p{sc=Han}|=|\+|\-]+`, 'gu'))); const targetWords = allWordMatches.slice(0, numWordsToCount); diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index a1c70e6d1a4..ad5866a8fa4 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -3,18 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Emitter, Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export interface IToolData { name: string; + icon?: { dark: URI; light?: URI } | ThemeIcon; displayName?: string; description: string; - parametersSchema?: Object; + parametersSchema?: IJSONSchema; + canBeInvokedManually?: boolean; } interface IToolEntry { @@ -22,8 +27,13 @@ interface IToolEntry { impl?: IToolImpl; } +export interface IToolResult { + [contentType: string]: any; + string: string; +} + export interface IToolImpl { - invoke(parameters: any, token: CancellationToken): Promise; + invoke(parameters: any, token: CancellationToken): Promise; } export const ILanguageModelToolsService = createDecorator('ILanguageModelToolsService'); @@ -39,7 +49,7 @@ export interface ILanguageModelToolsService { registerToolData(toolData: IToolData): IDisposable; registerToolImplementation(name: string, tool: IToolImpl): IDisposable; getTools(): Iterable>; - invokeTool(name: string, parameters: any, token: CancellationToken): Promise; + invokeTool(name: string, parameters: any, token: CancellationToken): Promise; } export class LanguageModelToolsService implements ILanguageModelToolsService { @@ -89,7 +99,7 @@ export class LanguageModelToolsService implements ILanguageModelToolsService { return Iterable.map(this._tools.values(), i => i.data); } - async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { let tool = this._tools.get(name); if (!tool) { throw new Error(`Tool ${name} was not contributed`); diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index 5cc34e72faf..0c5802da757 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -6,18 +6,22 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { DisposableMap } from 'vs/base/common/lifecycle'; +import { joinPath } from 'vs/base/common/resources'; +import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import { ILanguageModelToolsService, IToolData } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; interface IRawToolContribution { name: string; + icon?: string | { light: string; dark: string }; displayName?: string; description: string; parametersSchema?: IJSONSchema; + canBeInvokedManually?: boolean; } const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ @@ -52,6 +56,29 @@ const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.r description: localize('parametersSchema', "A JSON schema for the parameters this tool accepts."), type: 'object', $ref: 'http://json-schema.org/draft-07/schema#' + }, + canBeInvokedManually: { + description: localize('canBeInvokedManually', "Whether this tool can be invoked manually by the user through the chat UX."), + type: 'boolean' + }, + icon: { + description: localize('icon', "An icon that represents this tool. Either a file path, an object with file paths for dark and light themes, or a theme icon reference, like `\\$(zap)`"), + anyOf: [{ + type: 'string' + }, + { + type: 'object', + properties: { + light: { + description: localize('icon.light', 'Icon path when a light theme is used'), + type: 'string' + }, + dark: { + description: localize('icon.dark', 'Icon path when a dark theme is used'), + type: 'string' + } + } + }] } } } @@ -73,14 +100,32 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri ) { languageModelToolsExtensionPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { - for (const tool of extension.value) { - if (!tool.name || !tool.description) { - logService.warn(`Invalid tool contribution from ${extension.description.identifier.value}: ${JSON.stringify(tool)}`); + for (const rawTool of extension.value) { + if (!rawTool.name || !rawTool.description) { + logService.warn(`Invalid tool contribution from ${extension.description.identifier.value}: ${JSON.stringify(rawTool)}`); continue; } + const rawIcon = rawTool.icon; + let icon: IToolData['icon'] | undefined; + if (typeof rawIcon === 'string') { + icon = ThemeIcon.fromString(rawIcon) ?? { + dark: joinPath(extension.description.extensionLocation, rawIcon), + light: joinPath(extension.description.extensionLocation, rawIcon) + }; + } else if (rawIcon) { + icon = { + dark: joinPath(extension.description.extensionLocation, rawIcon.dark), + light: joinPath(extension.description.extensionLocation, rawIcon.light) + }; + } + + const tool = { + ...rawTool, + icon + }; const disposable = languageModelToolsService.registerToolData(tool); - this._registrationDisposables.set(toToolKey(extension.description.identifier, tool.name), disposable); + this._registrationDisposables.set(toToolKey(extension.description.identifier, rawTool.name), disposable); } } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 58586530c70..15e2584f498 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -388,11 +388,8 @@ class VoiceChatSessions { if (!response) { return; } - - if ( - !this.accessibilityService.isScreenReaderOptimized() && // do not auto synthesize when screen reader is active - this.configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) === true - ) { + const autoSynthesize = this.configurationService.getValue<'on' | 'off' | 'auto'>(AccessibilityVoiceSettingId.AutoSynthesize); + if (autoSynthesize === 'on' || autoSynthesize === 'auto' && !this.accessibilityService.isScreenReaderOptimized()) { let context: IVoiceChatSessionController | 'focused'; if (controller.context === 'inline') { // TODO@bpasero this is ugly, but the lightweight inline chat turns into diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap index d704b7b322d..b9ca8267b0c 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap @@ -1,4 +1,4 @@
    -
  1. hello test text
  2. +
  3. hello test text
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index 78fe7781692..9b85a97b4d7 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -91,7 +91,8 @@ ], kind: "usedContext" }, - contentReferences: [ ] + contentReferences: [ ], + codeCitations: [ ] } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index be8bb0fed92..421835ade92 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -91,7 +91,8 @@ ], kind: "usedContext" }, - contentReferences: [ ] + contentReferences: [ ], + codeCitations: [ ] } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index a749780f183..d0b833b4c19 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -75,7 +75,8 @@ }, slashCommand: undefined, usedContext: undefined, - contentReferences: [ ] + contentReferences: [ ], + codeCitations: [ ] } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index 81efbdca4b6..3c0f0bcc656 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -154,10 +154,10 @@ suite('ChatModel', () => { }); suite('Response', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const store = ensureNoDisposablesAreLeakedInTestSuite(); test('mergeable markdown', async () => { - const response = new Response([]); + const response = store.add(new Response([])); response.updateContent({ content: new MarkdownString('markdown1'), kind: 'markdownContent' }); response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' }); await assertSnapshot(response.value); @@ -166,7 +166,7 @@ suite('Response', () => { }); test('not mergeable markdown', async () => { - const response = new Response([]); + const response = store.add(new Response([])); const md1 = new MarkdownString('markdown1'); md1.supportHtml = true; response.updateContent({ content: md1, kind: 'markdownContent' }); @@ -175,7 +175,7 @@ suite('Response', () => { }); test('inline reference', async () => { - const response = new Response([]); + const response = store.add(new Response([])); response.updateContent({ content: new MarkdownString('text before'), kind: 'markdownContent' }); response.updateContent({ inlineReference: URI.parse('https://microsoft.com'), kind: 'inlineReference' }); response.updateContent({ content: new MarkdownString('text after'), kind: 'markdownContent' }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index df32dfed2f4..da76add7eca 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -9,6 +9,8 @@ import { URI } from 'vs/base/common/uri'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -26,6 +28,8 @@ import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/ import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/mockChatVariables'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; +import { NullWorkbenchAssignmentService } from 'vs/workbench/services/assignment/test/common/nullAssignmentService'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -73,6 +77,7 @@ suite('ChatService', () => { setup(async () => { instantiationService = testDisposables.add(new TestInstantiationService(new ServiceCollection( [IChatVariablesService, new MockChatVariablesService()], + [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()] ))); instantiationService.stub(IStorageService, storageService = testDisposables.add(new TestStorageService())); instantiationService.stub(ILogService, new NullLogService()); @@ -82,6 +87,7 @@ suite('ChatService', () => { instantiationService.stub(IViewsService, new TestExtensionService()); instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IChatService, new MockChatService()); chatAgentService = instantiationService.createInstance(ChatAgentService); diff --git a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts index 665c2e386b1..a013abded58 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts @@ -38,6 +38,10 @@ suite('ChatWordCounter', () => { ['[hello](https://example.com) world', 2, '[hello](https://example.com) world'], ['oh [hello](https://example.com "title") world', 1, 'oh'], ['oh [hello](https://example.com "title") world', 2, 'oh [hello](https://example.com "title")'], + // Parens in link destination + ['[hello](https://example.com?()) world', 1, '[hello](https://example.com?())'], + // Escaped brackets in link text + ['[he \\[l\\] \\]lo](https://example.com?()) world', 1, '[he \\[l\\] \\]lo](https://example.com?())'], ]; cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 2ad2f91c6b1..a1257cad616 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -12,7 +12,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { ProviderResult } from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatParticipantDetectionProvider } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatProgress, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChatService'; @@ -52,6 +52,12 @@ suite('VoiceChat', () => { ]; class TestChatAgentService implements IChatAgentService { + registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable { + throw new Error('Method not implemented.'); + } + detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> { + throw new Error('Method not implemented.'); + } _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } diff --git a/src/vs/workbench/contrib/codeActions/browser/codeActions.contribution.ts b/src/vs/workbench/contrib/codeActions/browser/codeActions.contribution.ts index afcf9915643..b8158d25544 100644 --- a/src/vs/workbench/contrib/codeActions/browser/codeActions.contribution.ts +++ b/src/vs/workbench/contrib/codeActions/browser/codeActions.contribution.ts @@ -11,7 +11,7 @@ import { CodeActionsExtensionPoint, codeActionsExtensionPointDescriptor } from ' import { DocumentationExtensionPoint, documentationExtensionPointDescriptor } from 'vs/workbench/contrib/codeActions/common/documentationExtensionPoint'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { CodeActionsContribution, editorConfiguration } from './codeActionsContribution'; +import { CodeActionsContribution, editorConfiguration, notebookEditorConfiguration } from './codeActionsContribution'; import { CodeActionDocumentationContribution } from './documentationContribution'; const codeActionsExtensionPoint = ExtensionsRegistry.registerExtensionPoint(codeActionsExtensionPointDescriptor); @@ -20,6 +20,9 @@ const documentationExtensionPoint = ExtensionsRegistry.registerExtensionPoint(Extensions.Configuration) .registerConfiguration(editorConfiguration); +Registry.as(Extensions.Configuration) + .registerConfiguration(notebookEditorConfiguration); + class WorkbenchConfigurationContribution { constructor( @IInstantiationService instantiationService: IInstantiationService, diff --git a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts index 17e6217a6a9..3f075cb5eeb 100644 --- a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts +++ b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts @@ -35,6 +35,21 @@ const createCodeActionsAutoSave = (description: string): IJSONSchema => { }; }; +const createNotebookCodeActionsAutoSave = (description: string): IJSONSchema => { + return { + type: ['string', 'boolean'], + enum: ['explicit', 'never', true, false], + enumDescriptions: [ + nls.localize('explicit', 'Triggers Code Actions only when explicitly saved.'), + nls.localize('never', 'Never triggers Code Actions on save.'), + nls.localize('explicitBoolean', 'Triggers Code Actions only when explicitly saved. This value will be deprecated in favor of "explicit".'), + nls.localize('neverBoolean', 'Triggers Code Actions only when explicitly saved. This value will be deprecated in favor of "never".') + ], + default: 'explicit', + description: description + }; +}; + const codeActionsOnSaveSchema: IConfigurationPropertySchema = { oneOf: [ @@ -66,6 +81,37 @@ export const editorConfiguration = Object.freeze({ } }); +const notebookCodeActionsOnSaveSchema: IConfigurationPropertySchema = { + oneOf: [ + { + type: 'object', + additionalProperties: { + type: 'string' + }, + }, + { + type: 'array', + items: { type: 'string' } + } + ], + markdownDescription: nls.localize('notebook.codeActionsOnSave', 'Run a series of Code Actions for a notebook on save. Code Actions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `"notebook.source.organizeImports": "explicit"`'), + type: 'object', + additionalProperties: { + type: ['string', 'boolean'], + enum: ['explicit', 'never', true, false], + // enum: ['explicit', 'always', 'never'], -- autosave support needs to be built first + // nls.localize('always', 'Always triggers Code Actions on save, including autosave, focus, and window change events.'), + }, + default: {} +}; + +export const notebookEditorConfiguration = Object.freeze({ + ...editorConfigurationBaseNode, + properties: { + 'notebook.codeActionsOnSave': notebookCodeActionsOnSaveSchema + } +}); + export class CodeActionsContribution extends Disposable implements IWorkbenchContribution { private _contributedCodeActions: CodeActionsExtensionPoint[] = []; @@ -81,7 +127,6 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon super(); // TODO: @justschen caching of code actions based on extensions loaded: https://github.com/microsoft/vscode/issues/216019 - languageFeatures.codeActionProvider.onDidChange(() => { this.updateSettingsFromCodeActionProviders(); this.updateConfigurationSchemaFromContribs(); @@ -114,23 +159,29 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon private updateConfigurationSchema(codeActionContributions: readonly CodeActionsExtensionPoint[]) { const newProperties: IJSONSchemaMap = {}; + const newNotebookProperties: IJSONSchemaMap = {}; for (const [sourceAction, props] of this.getSourceActions(codeActionContributions)) { this.settings.add(sourceAction); newProperties[sourceAction] = createCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", props.title)); + newNotebookProperties[sourceAction] = createNotebookCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", props.title)); } codeActionsOnSaveSchema.properties = newProperties; + notebookCodeActionsOnSaveSchema.properties = newNotebookProperties; Registry.as(Extensions.Configuration) .notifyConfigurationSchemaUpdated(editorConfiguration); } private updateConfigurationSchemaFromContribs() { const properties: IJSONSchemaMap = { ...codeActionsOnSaveSchema.properties }; + const notebookProperties: IJSONSchemaMap = { ...notebookCodeActionsOnSaveSchema.properties }; for (const codeActionKind of this.settings) { if (!properties[codeActionKind]) { properties[codeActionKind] = createCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", codeActionKind)); + notebookProperties[codeActionKind] = createNotebookCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", codeActionKind)); } } codeActionsOnSaveSchema.properties = properties; + notebookCodeActionsOnSaveSchema.properties = notebookProperties; Registry.as(Extensions.Configuration) .notifyConfigurationSchemaUpdated(editorConfiguration); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts index ef4ce7693a1..2f3564d2cb8 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts @@ -7,7 +7,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from 'vs/editor/browser/widget/diffEditor/commands'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { localize } from 'vs/nls'; -import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -36,13 +36,13 @@ export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation return; } - const switchSides = localize('msg3', "Run the command Diff Editor: Switch Side to toggle between the original and modified editors."); + const switchSides = localize('msg3', "Run the command Diff Editor: Switch Side{0} to toggle between the original and modified editors.", ''); const diffEditorActiveAnnouncement = localize('msg5', "The setting, accessibility.verbosity.diffEditorActive, controls if a diff editor announcement is made when it becomes the active editor."); const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; const content = [ localize('msg1', "You are in a diff editor."), - localize('msg2', "View the next or previous diff in diff review mode, which is optimized for screen readers.", AccessibleDiffViewerNext.id, AccessibleDiffViewerPrev.id), + localize('msg2', "View the next{0} or previous{1} diff in diff review mode, which is optimized for screen readers.", '', ''), switchSides, diffEditorActiveAnnouncement, localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), @@ -51,15 +51,12 @@ export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation if (commentCommandInfo) { content.push(commentCommandInfo); } - return { - id: AccessibleViewProviderId.DiffEditor, - verbositySettingKey: AccessibilityVerbositySettingId.DiffEditor, - provideContent: () => content.join('\n\n'), - onClose: () => { - codeEditor.focus(); - }, - options: { type: AccessibleViewType.Help } - }; + return new AccessibleContentProvider( + AccessibleViewProviderId.DiffEditor, + { type: AccessibleViewType.Help }, + () => content.join('\n'), + () => codeEditor.focus(), + AccessibilityVerbositySettingId.DiffEditor, + ); } - dispose() { } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 2a40f9d9413..ab35c780ddc 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -123,7 +123,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#endregion - protected override provideWithoutTextEditor(picker: IQuickPick): IDisposable { + protected override provideWithoutTextEditor(picker: IQuickPick): IDisposable { if (this.canPickWithOutlineService()) { return this.doGetOutlinePicks(picker); } @@ -134,7 +134,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return this.editorService.activeEditorPane ? this.outlineService.canCreateOutline(this.editorService.activeEditorPane) : false; } - private doGetOutlinePicks(picker: IQuickPick): IDisposable { + private doGetOutlinePicks(picker: IQuickPick): IDisposable { const pane = this.editorService.activeEditorPane; if (!pane) { return Disposable.None; diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index d04fd459483..b7e5e721a28 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; @@ -30,6 +30,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; import { SaveReason } from 'vs/workbench/common/editor'; import { getModifiedRanges } from 'vs/workbench/contrib/format/browser/formatModified'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ITextFileEditorModel, ITextFileSaveParticipant, ITextFileSaveParticipantContext, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -267,13 +269,53 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant { } } -class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { +class CodeActionOnSaveParticipant extends Disposable implements ITextFileSaveParticipant { constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - ) { } + @IHostService private readonly hostService: IHostService, + @IEditorService private readonly editorService: IEditorService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + ) { + super(); + + this._register(this.hostService.onDidChangeFocus(() => { this.triggerCodeActionsCommand(); })); + this._register(this.editorService.onDidActiveEditorChange(() => { this.triggerCodeActionsCommand(); })); + } + + private async triggerCodeActionsCommand() { + if (this.configurationService.getValue('editor.codeActions.triggerOnFocusChange') && this.configurationService.getValue('files.autoSave') === 'afterDelay') { + const model = this.codeEditorService.getActiveCodeEditor()?.getModel(); + if (!model) { + return undefined; + } + + const settingsOverrides = { overrideIdentifier: model.getLanguageId(), resource: model.uri }; + const setting = this.configurationService.getValue<{ [kind: string]: string | boolean } | string[]>('editor.codeActionsOnSave', settingsOverrides); + + if (!setting) { + return undefined; + } + + if (Array.isArray(setting)) { + return undefined; + } + + const settingItems: string[] = Object.keys(setting).filter(x => setting[x] && setting[x] === 'always' && CodeActionKind.Source.contains(new HierarchicalKind(x))); + + const cancellationTokenSource = new CancellationTokenSource(); + + const codeActionKindList = []; + for (const item of settingItems) { + codeActionKindList.push(new HierarchicalKind(item)); + } + + // run code actions based on what is found from setting === 'always', no exclusions. + await this.applyOnSaveActions(model, codeActionKindList, [], Progress.None, cancellationTokenSource.token); + } + } async participate(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { if (!model.textEditorModel) { diff --git a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts index f7660482b85..912b9a49299 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts @@ -13,6 +13,9 @@ import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEdito import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { selectionBackground, inputBackground, inputForeground, editorSelectionBackground } from 'vs/platform/theme/common/colorRegistry'; export function getSimpleEditorOptions(configurationService: IConfigurationService): IEditorOptions { return { @@ -60,3 +63,35 @@ export function getSimpleCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { ]) }; } + +/** + * Should be called to set the styling on editors that are appearing as just input boxes + * @param editorContainerSelector An element selector that will match the container of the editor + */ +export function setupSimpleEditorSelectionStyling(editorContainerSelector: string): IDisposable { + // Override styles in selections.ts + return registerThemingParticipant((theme, collector) => { + const selectionBackgroundColor = theme.getColor(selectionBackground); + + if (selectionBackgroundColor) { + // Override inactive selection bg + const inputBackgroundColor = theme.getColor(inputBackground); + if (inputBackgroundColor) { + collector.addRule(`${editorContainerSelector} .monaco-editor-background { background-color: ${inputBackgroundColor}; } `); + collector.addRule(`${editorContainerSelector} .monaco-editor .selected-text { background-color: ${inputBackgroundColor.transparent(0.4)}; }`); + } + + // Override selected fg + const inputForegroundColor = theme.getColor(inputForeground); + if (inputForegroundColor) { + collector.addRule(`${editorContainerSelector} .monaco-editor .view-line span.inline-selected-text { color: ${inputForegroundColor}; }`); + } + + collector.addRule(`${editorContainerSelector} .monaco-editor .focused .selected-text { background-color: ${selectionBackgroundColor}; }`); + } else { + // Use editor selection color if theme has not set a selection background color + collector.addRule(`${editorContainerSelector} .monaco-editor .focused .selected-text { background-color: ${theme.getColor(editorSelectionBackground)}; }`); + } + }); + +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index e06219499e2..64bfec8be9d 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -3,42 +3,41 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./suggestEnabledInput'; import { $, Dimension, append } from 'vs/base/browser/dom'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; +import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import { Widget } from 'vs/base/browser/ui/widget'; import { Emitter, Event } from 'vs/base/common/event'; +import { HistoryNavigator } from 'vs/base/common/history'; import { KeyCode } from 'vs/base/common/keyCodes'; import { mixin } from 'vs/base/common/objects'; import { isMacintosh } from 'vs/base/common/platform'; import { URI as uri } from 'vs/base/common/uri'; +import 'vs/css!./suggestEnabledInput'; +import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModel } from 'vs/editor/common/model'; +import { ensureValidWordDefinition, getWordAtText } from 'vs/editor/common/core/wordHelper'; import * as languages from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IModelService } from 'vs/editor/common/services/model'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ColorIdentifier, asCssVariable, asCssVariableWithDefault, editorSelectionBackground, inputBackground, inputBorder, inputForeground, inputPlaceholderForeground, selectionBackground } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; -import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; -import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; -import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; -import { HistoryNavigator } from 'vs/base/common/history'; -import { registerAndCreateHistoryNavigationContext, IHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; -import { IHistoryNavigationWidget } from 'vs/base/browser/history'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; -import { ensureValidWordDefinition, getWordAtText } from 'vs/editor/common/core/wordHelper'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IHistoryNavigationContext, registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ColorIdentifier, asCssVariable, asCssVariableWithDefault, inputBackground, inputBorder, inputForeground, inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; +import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; +import { getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; export interface SuggestResultsProvider { /** @@ -462,34 +461,7 @@ export class ContextScopedSuggestEnabledInputWithHistory extends SuggestEnabledI } } -// Override styles in selections.ts -registerThemingParticipant((theme, collector) => { - const selectionBackgroundColor = theme.getColor(selectionBackground); - - if (selectionBackgroundColor) { - // Override inactive selection bg - const inputBackgroundColor = theme.getColor(inputBackground); - if (inputBackgroundColor) { - collector.addRule(`.suggest-input-container .monaco-editor .selected-text { background-color: ${inputBackgroundColor.transparent(0.4)}; }`); - } - - // Override selected fg - const inputForegroundColor = theme.getColor(inputForeground); - if (inputForegroundColor) { - collector.addRule(`.suggest-input-container .monaco-editor .view-line span.inline-selected-text { color: ${inputForegroundColor}; }`); - } - - const backgroundColor = theme.getColor(inputBackground); - if (backgroundColor) { - collector.addRule(`.suggest-input-container .monaco-editor-background { background-color: ${backgroundColor}; } `); - } - collector.addRule(`.suggest-input-container .monaco-editor .focused .selected-text { background-color: ${selectionBackgroundColor}; }`); - } else { - // Use editor selection color if theme has not set a selection background color - collector.addRule(`.suggest-input-container .monaco-editor .focused .selected-text { background-color: ${theme.getColor(editorSelectionBackground)}; }`); - } -}); - +setupSimpleEditorSelectionStyling('.suggest-input-container'); function getSuggestEnabledInputOptions(ariaLabel?: string): IEditorOptions { return { diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index fe3632fb8d0..1bd9688ea69 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -23,6 +23,7 @@ import { EncodedTokenizationResult, IState, ITokenizationSupport, TokenizationRe import { NullState } from 'vs/editor/common/languages/nullTokenize'; import { MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { ITextModel } from 'vs/editor/common/model'; +import { FileAccess } from 'vs/base/common/network'; function getIRange(range: IRange): IRange { return { @@ -56,7 +57,7 @@ function registerLanguageConfiguration(instantiationService: TestInstantiationSe let configPath: string; switch (languageId) { case LanguageId.TypeScript: - configPath = path.join('extensions', 'typescript-basics', 'language-configuration.json'); + configPath = FileAccess.asFileUri('vs/workbench/contrib/codeEditor/test/node/language-configuration.json').fsPath; break; default: throw new Error('Unknown languageId'); diff --git a/src/vs/workbench/contrib/codeEditor/test/node/language-configuration.json b/src/vs/workbench/contrib/codeEditor/test/node/language-configuration.json new file mode 100644 index 00000000000..25a23685738 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/test/node/language-configuration.json @@ -0,0 +1,250 @@ +{ + // Note that this file should stay in sync with 'javascript-language-basics/javascript-language-configuration.json' + "comments": { + "lineComment": "//", + "blockComment": [ + "/*", + "*/" + ] + }, + "brackets": [ + [ + "${", + "}" + ], + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] + ], + "autoClosingPairs": [ + { + "open": "{", + "close": "}" + }, + { + "open": "[", + "close": "]" + }, + { + "open": "(", + "close": ")" + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "\"", + "close": "\"", + "notIn": [ + "string" + ] + }, + { + "open": "`", + "close": "`", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "/**", + "close": " */", + "notIn": [ + "string" + ] + } + ], + "surroundingPairs": [ + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "'", + "'" + ], + [ + "\"", + "\"" + ], + [ + "`", + "`" + ], + [ + "<", + ">" + ] + ], + "colorizedBracketPairs": [ + [ + "(", + ")" + ], + [ + "[", + "]" + ], + [ + "{", + "}" + ], + [ + "<", + ">" + ] + ], + "autoCloseBefore": ";:.,=}])>` \n\t", + "folding": { + "markers": { + "start": "^\\s*//\\s*#?region\\b", + "end": "^\\s*//\\s*#?endregion\\b" + } + }, + "wordPattern": { + "pattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\@\\~\\!\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>/\\?\\s]+)", + }, + "indentationRules": { + "decreaseIndentPattern": { + "pattern": "^\\s*[\\}\\]\\)].*$" + }, + "increaseIndentPattern": { + "pattern": "^.*(\\{[^}]*|\\([^)]*|\\[[^\\]]*)$" + }, + // e.g. * ...| or */| or *-----*/| + "unIndentedLinePattern": { + "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$|^(\\t|[ ])*[ ]\\*/\\s*$|^(\\t|[ ])*\\*([ ]([^\\*]|\\*(?!/))*)?$" + }, + "indentNextLinePattern": { + "pattern": "^((.*=>\\s*)|((.*[^\\w]+|\\s*)(if|while|for)\\s*\\(.*\\)\\s*))$" + } + }, + "onEnterRules": [ + { + // e.g. /** | */ + "beforeText": { + "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$" + }, + "afterText": { + "pattern": "^\\s*\\*/$" + }, + "action": { + "indent": "indentOutdent", + "appendText": " * " + } + }, + { + // e.g. /** ...| + "beforeText": { + "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$" + }, + "action": { + "indent": "none", + "appendText": " * " + } + }, + { + // e.g. * ...| + "beforeText": { + "pattern": "^(\\t|[ ])*\\*([ ]([^\\*]|\\*(?!/))*)?$" + }, + "previousLineText": { + "pattern": "(?=^(\\s*(/\\*\\*|\\*)).*)(?=(?!(\\s*\\*/)))" + }, + "action": { + "indent": "none", + "appendText": "* " + } + }, + { + // e.g. */| + "beforeText": { + "pattern": "^(\\t|[ ])*[ ]\\*/\\s*$" + }, + "action": { + "indent": "none", + "removeText": 1 + }, + }, + { + // e.g. *-----*/| + "beforeText": { + "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$" + }, + "action": { + "indent": "none", + "removeText": 1 + }, + }, + { + "beforeText": { + "pattern": "^\\s*(\\bcase\\s.+:|\\bdefault:)$" + }, + "afterText": { + "pattern": "^(?!\\s*(\\bcase\\b|\\bdefault\\b))" + }, + "action": { + "indent": "indent" + } + }, + { + // Decrease indentation after single line if/else if/else, for, or while + "previousLineText": "^\\s*(((else ?)?if|for|while)\\s*\\(.*\\)\\s*|else\\s*)$", + // But make sure line doesn't have braces or is not another if statement + "beforeText": "^\\s+([^{i\\s]|i(?!f\\b))", + "action": { + "indent": "outdent" + } + }, + // Indent when pressing enter from inside () + { + "beforeText": "^.*\\([^\\)]*$", + "afterText": "^\\s*\\).*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, + // Indent when pressing enter from inside {} + { + "beforeText": "^.*\\{[^\\}]*$", + "afterText": "^\\s*\\}.*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, + // Indent when pressing enter from inside [] + { + "beforeText": "^.*\\[[^\\]]*$", + "afterText": "^\\s*\\].*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, + ] +} diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 0a73f27023a..4f50bfb1085 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -118,7 +118,7 @@ export class CommentReply extends Disposable { this.expandReplyArea(); } else if (hasExistingComments) { this.createReplyButton(this.commentEditor, this.form); - } else if (focus && (!this._commentThread.comments || this._commentThread.comments.length === 0)) { + } else if (focus && (this._commentThread.comments && this._commentThread.comments.length === 0)) { this.expandReplyArea(); } this._error = dom.append(this.form, dom.$('.validation-error.hidden')); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index 7156f465fa5..bc62a581bf8 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -70,6 +70,12 @@ export class CommentThreadBody extends D this._commentsElement.focus(); } + ensureFocusIntoNewEditingComment() { + if (this._commentElements.length === 1 && this._commentElements[0].isEditing) { + this._commentElements[0].setFocus(true); + } + } + async display() { this._commentsElement = dom.append(this.container, dom.$('div.comments-container')); this._commentsElement.setAttribute('role', 'presentation'); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index 8333654958e..d8bcd057107 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, ActionRunner } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import * as languages from 'vs/editor/common/languages'; import { IRange } from 'vs/editor/common/core/range'; @@ -26,7 +26,11 @@ import { MarshalledCommentThread } from 'vs/workbench/common/comments'; const collapseIcon = registerIcon('review-comment-collapse', Codicon.chevronUp, nls.localize('collapseIcon', 'Icon to collapse a review comment.')); const COLLAPSE_ACTION_CLASS = 'expand-review-action ' + ThemeIcon.asClassName(collapseIcon); +const DELETE_ACTION_CLASS = 'expand-review-action ' + ThemeIcon.asClassName(Codicon.trashcan); +function threadHasComments(comments: ReadonlyArray | undefined): comments is ReadonlyArray { + return !!comments && comments.length > 0; +} export class CommentThreadHeader extends Disposable { private _headElement: HTMLElement; @@ -63,7 +67,17 @@ export class CommentThreadHeader extends Disposable { this._register(this._actionbarWidget); - this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => this._delegate.collapse()); + const collapseClass = threadHasComments(this._commentThread.comments) ? COLLAPSE_ACTION_CLASS : DELETE_ACTION_CLASS; + this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), collapseClass, true, () => this._delegate.collapse()); + if (!threadHasComments(this._commentThread.comments)) { + const commentsChanged: MutableDisposable = this._register(new MutableDisposable()); + commentsChanged.value = this._commentThread.onDidChangeComments(() => { + if (threadHasComments(this._commentThread.comments)) { + this._collapseAction.class = COLLAPSE_ACTION_CLASS; + commentsChanged.clear(); + } + }); + } const menu = this._commentMenus.getCommentThreadTitleActions(this._contextKeyService); this._register(menu); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 521455bb77a..4129171b360 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -353,6 +353,10 @@ export class CommentThreadWidget extends } } + ensureFocusIntoNewEditingComment() { + this._body.ensureFocusIntoNewEditingComment(); + } + focusCommentEditor() { this._commentReply?.expandReplyAreaAndFocusCommentEditor(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 2c909a2ed5e..5fcfb5a4ef6 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -187,34 +187,18 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } public reveal(commentUniqueId?: number, focus: CommentWidgetFocus = CommentWidgetFocus.None) { + this.makeVisible(commentUniqueId, focus); + const comment = this._commentThread.comments?.find(comment => comment.uniqueIdInThread === commentUniqueId); + this.commentService.setActiveCommentAndThread(this.uniqueOwner, { thread: this._commentThread, comment }); + } + + private _expandAndShowZoneWidget() { if (!this._isExpanded) { this.show(this.arrowPosition(this._commentThread.range), 2); } + } - if (commentUniqueId !== undefined) { - const height = this.editor.getLayoutInfo().height; - const coords = this._commentThreadWidget.getCommentCoords(commentUniqueId); - if (coords) { - let scrollTop: number = 1; - if (this._commentThread.range) { - const commentThreadCoords = coords.thread; - const commentCoords = coords.comment; - scrollTop = this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top; - } - this.editor.setScrollTop(scrollTop); - if (focus === CommentWidgetFocus.Widget) { - this._commentThreadWidget.focus(); - } else if (focus === CommentWidgetFocus.Editor) { - this._commentThreadWidget.focusCommentEditor(); - } - return; - } - } - const rangeToReveal = this._commentThread.range - ? new Range(this._commentThread.range.startLineNumber, this._commentThread.range.startColumn, this._commentThread.range.endLineNumber + 1, 1) - : new Range(1, 1, 1, 1); - - this.editor.revealRangeInCenter(rangeToReveal); + private _setFocus(focus: CommentWidgetFocus) { if (focus === CommentWidgetFocus.Widget) { this._commentThreadWidget.focus(); } else if (focus === CommentWidgetFocus.Editor) { @@ -222,6 +206,41 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } + private _goToComment(commentUniqueId: number, focus: CommentWidgetFocus) { + const height = this.editor.getLayoutInfo().height; + const coords = this._commentThreadWidget.getCommentCoords(commentUniqueId); + if (coords) { + let scrollTop: number = 1; + if (this._commentThread.range) { + const commentThreadCoords = coords.thread; + const commentCoords = coords.comment; + scrollTop = this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top; + } + this.editor.setScrollTop(scrollTop); + this._setFocus(focus); + } else { + this._goToThread(focus); + } + } + + private _goToThread(focus: CommentWidgetFocus) { + const rangeToReveal = this._commentThread.range + ? new Range(this._commentThread.range.startLineNumber, this._commentThread.range.startColumn, this._commentThread.range.endLineNumber + 1, 1) + : new Range(1, 1, 1, 1); + + this.editor.revealRangeInCenter(rangeToReveal); + this._setFocus(focus); + } + + public makeVisible(commentUniqueId?: number, focus: CommentWidgetFocus = CommentWidgetFocus.None) { + this._expandAndShowZoneWidget(); + + if (commentUniqueId !== undefined) { + this._goToComment(commentUniqueId, focus); + } + this._goToThread(focus); + } + public getPendingComments(): { newComment: string | undefined; edits: { [key: number]: string } } { return { newComment: this._commentThreadWidget.getPendingComment(), @@ -300,8 +319,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; } - public expand() { + public expand(setActive?: boolean) { this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; + if (setActive) { + this.commentService.setActiveCommentAndThread(this.uniqueOwner, { thread: this._commentThread }); + } } public getGlyphPosition(): number { @@ -311,14 +333,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget return 0; } - toggleExpand() { - if (this._isExpanded) { - this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; - } else { - this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; - } - } - async update(commentThread: languages.CommentThread) { if (this._commentThread !== commentThread) { this._commentThreadDisposables.forEach(disposable => disposable.dispose()); @@ -371,7 +385,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget // If this is a new comment thread awaiting user input then we need to reveal it. if (shouldReveal) { - this.reveal(); + this.makeVisible(); } this.bindCommentThreadListeners(); @@ -401,6 +415,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThreadDisposables.push(this._commentThread.onDidChangeCollapsibleState(state => { if (state === languages.CommentThreadCollapsibleState.Expanded && !this._isExpanded) { this.show(this.arrowPosition(this._commentThread.range), 2); + this._commentThreadWidget.ensureFocusIntoNewEditingComment(); return; } diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 1c247dec349..e9ec6e6afaa 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -13,21 +13,23 @@ import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCo import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { Disposable } from 'vs/base/common/lifecycle'; + export namespace CommentAccessibilityHelpNLS { export const intro = nls.localize('intro', "The editor contains commentable range(s). Some useful commands include:"); - export const tabFocus = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus{0}", ``); + export const tabFocus = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus{0}.", ``); export const commentCommands = nls.localize('commentCommands', "Some useful comment commands include:"); export const escape = nls.localize('escape', "- Dismiss Comment (Escape)"); - export const nextRange = nls.localize('next', "- Go to Next Commenting Range{0}", ``); - export const previousRange = nls.localize('previous', "- Go to Previous Commenting Range{0}", ``); - export const nextCommentThread = nls.localize('nextCommentThreadKb', "- Go to Next Comment Thread{0}", ``); - export const previousCommentThread = nls.localize('previousCommentThreadKb', "- Go to Previous Comment Thread{0}", ``); - export const addComment = nls.localize('addCommentNoKb', "- Add Comment on Current Selection{0}", ``); - export const submitComment = nls.localize('submitComment', "- Submit Comment{0}", ``); + export const nextRange = nls.localize('next', "- Go to Next Commenting Range{0}.", ``); + export const previousRange = nls.localize('previous', "- Go to Previous Commenting Range{0}.", ``); + export const nextCommentThread = nls.localize('nextCommentThreadKb', "- Go to Next Comment Thread{0}.", ``); + export const previousCommentThread = nls.localize('previousCommentThreadKb', "- Go to Previous Comment Thread{0}.", ``); + export const addComment = nls.localize('addCommentNoKb', "- Add Comment on Current Selection{0}.", ``); + export const submitComment = nls.localize('submitComment', "- Submit Comment{0}.", ``); } -export class CommentsAccessibilityHelpProvider implements IAccessibleViewContentProvider { +export class CommentsAccessibilityHelpProvider extends Disposable implements IAccessibleViewContentProvider { id = AccessibleViewProviderId.Comments; verbositySettingKey: AccessibilityVerbositySettingId = AccessibilityVerbositySettingId.Comments; options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; @@ -48,5 +50,4 @@ export class CommentsAccessibilityHelp implements IAccessibleViewImplentation { getProvider(accessor: ServicesAccessor) { return accessor.get(IInstantiationService).createInstance(CommentsAccessibilityHelpProvider); } - dispose() { } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts index 2fdac52458d..907cd886376 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts @@ -6,82 +6,81 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { IMenuService } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { COMMENTS_VIEW_ID, CommentsMenus } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; -import { CommentsPanel, CONTEXT_KEY_HAS_COMMENTS } from 'vs/workbench/contrib/comments/browser/commentsView'; +import { CommentsPanel, CONTEXT_KEY_COMMENT_FOCUSED } from 'vs/workbench/contrib/comments/browser/commentsView'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export class CommentsAccessibleView extends Disposable implements IAccessibleViewImplentation { readonly priority = 90; readonly name = 'comment'; - readonly when = CONTEXT_KEY_HAS_COMMENTS; + readonly when = CONTEXT_KEY_COMMENT_FOCUSED; readonly type = AccessibleViewType.View; getProvider(accessor: ServicesAccessor) { const contextKeyService = accessor.get(IContextKeyService); const viewsService = accessor.get(IViewsService); const menuService = accessor.get(IMenuService); const commentsView = viewsService.getActiveViewWithId(COMMENTS_VIEW_ID); - if (!commentsView) { + const focusedCommentNode = commentsView?.focusedCommentNode; + if (!commentsView || !focusedCommentNode) { return; } const menus = this._register(new CommentsMenus(menuService)); menus.setContextKeyService(contextKeyService); - function resolveProvider() { - if (!commentsView) { - return; - } - - const commentNode = commentsView.focusedCommentNode; - const content = commentsView.focusedCommentInfo?.toString(); - if (!commentNode || !content) { - return; - } - const menuActions = [...menus.getResourceContextActions(commentNode)].filter(i => i.enabled); - const actions = menuActions.map(action => { - return { - ...action, - run: () => { - commentsView.focus(); - action.run({ - thread: commentNode.thread, - $mid: MarshalledId.CommentThread, - commentControlHandle: commentNode.controllerHandle, - commentThreadHandle: commentNode.threadHandle, - }); - } - }; - }); - return { - id: AccessibleViewProviderId.Notification, - provideContent: () => { - return content; - }, - onClose(): void { - commentsView.focus(); - }, - next(): void { - commentsView.focus(); - commentsView.focusNextNode(); - resolveProvider(); - }, - previous(): void { - commentsView.focus(); - commentsView.focusPreviousNode(); - resolveProvider(); - }, - verbositySettingKey: AccessibilityVerbositySettingId.Comments, - options: { type: AccessibleViewType.View }, - actions - }; - } - return resolveProvider(); + return new CommentsAccessibleContentProvider(commentsView, focusedCommentNode, menus); } constructor() { super(); } } + +class CommentsAccessibleContentProvider extends Disposable implements IAccessibleViewContentProvider { + constructor( + private readonly _commentsView: CommentsPanel, + private readonly _focusedCommentNode: any, + private readonly _menus: CommentsMenus, + ) { + super(); + } + readonly id = AccessibleViewProviderId.Comments; + readonly verbositySettingKey = AccessibilityVerbositySettingId.Comments; + readonly options = { type: AccessibleViewType.View }; + public actions = [...this._menus.getResourceContextActions(this._focusedCommentNode)].filter(i => i.enabled).map(action => { + return { + ...action, + run: () => { + this._commentsView.focus(); + action.run({ + thread: this._focusedCommentNode.thread, + $mid: MarshalledId.CommentThread, + commentControlHandle: this._focusedCommentNode.controllerHandle, + commentThreadHandle: this._focusedCommentNode.threadHandle, + }); + } + }; + }); + provideContent(): string { + const commentNode = this._commentsView.focusedCommentNode; + const content = this._commentsView.focusedCommentInfo?.toString(); + if (!commentNode || !content) { + throw new Error('Comment tree is focused but no comment is selected'); + } + return content; + } + onClose(): void { + this._commentsView.focus(); + } + provideNextContent(): string | undefined { + this._commentsView.focusNextNode(); + return this.provideContent(); + } + providePreviousContent(): string | undefined { + this._commentsView.focusPreviousNode(); + return this.provideContent(); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 09aeddd25e6..34d258d02c7 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -46,6 +46,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { threadHasMeaningfulComments } from 'vs/workbench/contrib/comments/browser/commentsModel'; export const ID = 'editor.contrib.review'; @@ -869,8 +870,7 @@ export class CommentController implements IEditorContribution { const pendingCommentText = (this._pendingNewCommentCache[uniqueOwner] && this._pendingNewCommentCache[uniqueOwner][thread.threadId]) ?? continueOnCommentText; const pendingEdits = this._pendingEditsCache[uniqueOwner] && this._pendingEditsCache[uniqueOwner][thread.threadId]; - const isThreadTemplateOrEmpty = (thread.isTemplate || (!thread.comments || (thread.comments.length === 0))); - const shouldReveal = thread.canReply && isThreadTemplateOrEmpty && (!thread.editorId || (thread.editorId === editorId)); + const shouldReveal = thread.canReply && thread.isTemplate && (!thread.comments || (thread.comments.length === 0)) && (!thread.editorId || (thread.editorId === editorId)); await this.displayCommentThread(uniqueOwner, thread, shouldReveal, pendingCommentText, pendingEdits); this._commentInfos.filter(info => info.uniqueOwner === uniqueOwner)[0].threads.push(thread); this.tryUpdateReservedSpace(); @@ -1016,7 +1016,7 @@ export class CommentController implements IEditorContribution { } private async openCommentsView(thread: languages.CommentThread) { - if (thread.comments && (thread.comments.length > 0)) { + if (thread.comments && (thread.comments.length > 0) && threadHasMeaningfulComments(thread)) { const openViewState = this.configurationService.getValue(COMMENTS_SECTION).openView; if (openViewState === 'file') { return this.viewsService.openView(COMMENTS_VIEW_ID); @@ -1098,7 +1098,7 @@ export class CommentController implements IEditorContribution { const existingCommentsAtLine = this._commentWidgets.filter(widget => widget.getGlyphPosition() === (commentRange ? commentRange.endLineNumber : 0)); if (existingCommentsAtLine.length) { const allExpanded = existingCommentsAtLine.every(widget => widget.expanded); - existingCommentsAtLine.forEach(allExpanded ? widget => widget.collapse() : widget => widget.expand()); + existingCommentsAtLine.forEach(allExpanded ? widget => widget.collapse() : widget => widget.expand(true)); this.processNextThreadToAdd(); return; } else { diff --git a/src/vs/workbench/contrib/comments/browser/commentsModel.ts b/src/vs/workbench/contrib/comments/browser/commentsModel.ts index d0701d5f344..624db1d5235 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsModel.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsModel.ts @@ -9,6 +9,12 @@ import { CommentThread } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { Disposable } from 'vs/base/common/lifecycle'; +import { isMarkdownString } from 'vs/base/common/htmlContent'; + +export function threadHasMeaningfulComments(thread: CommentThread): boolean { + return !!thread.comments && !!thread.comments.length && thread.comments.some(comment => isMarkdownString(comment.body) ? comment.body.value.length > 0 : comment.body.length > 0); + +} export interface ICommentsModel { hasCommentThreads(): boolean; @@ -38,9 +44,6 @@ export class CommentsModel extends Disposable implements ICommentsModel { return resource; }).flat(); }).flat(); - this._resourceCommentThreads.sort((a, b) => { - return a.resource.toString() > b.resource.toString() ? 1 : -1; - }); } public setCommentThreads(uniqueOwner: string, owner: string, ownerLabel: string, commentThreads: CommentThread[]): void { @@ -116,7 +119,14 @@ export class CommentsModel extends Disposable implements ICommentsModel { } public hasCommentThreads(): boolean { - return !!this._resourceCommentThreads.length; + // There's a resource with at least one thread + return !!this._resourceCommentThreads.length && this._resourceCommentThreads.some(resource => { + // At least one of the threads in the the resource has comments + return (resource.commentThreads.length > 0) && resource.commentThreads.some(thread => { + // At least one of the comments in the thread is not empty + return threadHasMeaningfulComments(thread.thread); + }); + }); } public getMessage(): string { diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 7f0dfde0e64..29100b80901 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -76,7 +76,7 @@ interface ICommentThreadTemplateData { disposables: IDisposable[]; } -class CommentsModelVirualDelegate implements IListVirtualDelegate { +class CommentsModelVirtualDelegate implements IListVirtualDelegate { private static readonly RESOURCE_ID = 'resource-with-comments'; private static readonly COMMENT_ID = 'comment-node'; @@ -90,10 +90,10 @@ class CommentsModelVirualDelegate implements IListVirtualDelegate('commentsView.hasComments', false); export const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey('commentsView.someCommentsExpanded', false); +export const CONTEXT_KEY_COMMENT_FOCUSED = new RawContextKey('commentsView.commentFocused', false); const VIEW_STORAGE_ID = 'commentsViewState'; -function createResourceCommentsIterator(model: ICommentsModel): Iterable> { - return Iterable.map(model.resourceCommentThreads, m => { - const CommentNodeIt = Iterable.from(m.commentThreads); - const children = Iterable.map(CommentNodeIt, r => ({ element: r })); +type CommentsTreeNode = CommentsModel | ResourceWithCommentThreads | CommentNode; - return { element: m, children }; - }); +function createResourceCommentsIterator(model: ICommentsModel): Iterable> { + const result: ITreeElement[] = []; + + for (const m of model.resourceCommentThreads) { + const children = []; + for (const r of m.commentThreads) { + if (threadHasMeaningfulComments(r.thread)) { + children.push({ element: r }); + } + } + if (children.length > 0) { + result.push({ element: m, children }); + } + } + return result; } export class CommentsPanel extends FilterViewPane implements ICommentsView { @@ -59,6 +70,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { private totalComments: number = 0; private readonly hasCommentsContextKey: IContextKey; private readonly someCommentsExpandedContextKey: IContextKey; + private readonly commentsFocusedContextKey: IContextKey; private readonly filter: Filter; readonly filters: CommentsFilters; @@ -136,7 +148,8 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { @ITelemetryService telemetryService: ITelemetryService, @IHoverService hoverService: IHoverService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IPathService private readonly pathService: IPathService ) { const stateMemento = new Memento(VIEW_STORAGE_ID, storageService); const viewState = stateMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -152,12 +165,14 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); this.hasCommentsContextKey = CONTEXT_KEY_HAS_COMMENTS.bindTo(contextKeyService); this.someCommentsExpandedContextKey = CONTEXT_KEY_SOME_COMMENTS_EXPANDED.bindTo(contextKeyService); + this.commentsFocusedContextKey = CONTEXT_KEY_COMMENT_FOCUSED.bindTo(contextKeyService); this.stateMemento = stateMemento; this.viewState = viewState; this.filters = this._register(new CommentsFilters({ showResolved: this.viewState['showResolved'] !== false, showUnresolved: this.viewState['showUnresolved'] !== false, + sortBy: this.viewState['sortBy'], }, this.contextKeyService)); this.filter = new Filter(new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved)); @@ -165,6 +180,9 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { if (event.showResolved || event.showUnresolved) { this.updateFilter(); } + if (event.sortBy) { + this.refresh(); + } })); this._register(this.filterWidget.onDidChangeFilterText(() => this.updateFilter())); } @@ -174,6 +192,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this.viewState['filterHistory'] = this.filterWidget.getHistory(); this.viewState['showResolved'] = this.filters.showResolved; this.viewState['showUnresolved'] = this.filters.showUnresolved; + this.viewState['sortBy'] = this.filters.sortBy; this.stateMemento.saveMemento(); super.saveState(); } @@ -267,10 +286,10 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } } - private async renderComments(): Promise { + private renderComments(): void { this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads()); this.renderMessage(); - await this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel)); + this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel)); } public collapseAll() { @@ -388,8 +407,30 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { overrideStyles: this.getLocationBasedColors().listOverrideStyles, selectionNavigation: true, filter: this.filter, + sorter: { + compare: (a: CommentsTreeNode, b: CommentsTreeNode) => { + if (a instanceof CommentsModel || b instanceof CommentsModel) { + return 0; + } + if (this.filters.sortBy === CommentsSortOrder.UpdatedAtDescending) { + return a.lastUpdatedAt > b.lastUpdatedAt ? -1 : 1; + } else if (this.filters.sortBy === CommentsSortOrder.ResourceAscending) { + if (a instanceof ResourceWithCommentThreads && b instanceof ResourceWithCommentThreads) { + const workspaceScheme = this.pathService.defaultUriScheme; + if ((a.resource.scheme !== b.resource.scheme) && (a.resource.scheme === workspaceScheme || b.resource.scheme === workspaceScheme)) { + // Workspace scheme should always come first + return b.resource.scheme === workspaceScheme ? 1 : -1; + } + return a.resource.toString() > b.resource.toString() ? 1 : -1; + } else if (a instanceof CommentNode && b instanceof CommentNode && a.thread.range && b.thread.range) { + return a.thread.range?.startLineNumber > b.thread.range?.startLineNumber ? 1 : -1; + } + } + return 0; + }, + }, keyboardNavigationLabelProvider: { - getKeyboardNavigationLabel: (item: CommentsModel | ResourceWithCommentThreads | CommentNode) => { + getKeyboardNavigationLabel: (item: CommentsTreeNode) => { return undefined; } }, @@ -423,6 +464,8 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this._register(this.tree.onDidChangeCollapseState(() => { this.updateSomeCommentsExpanded(); })); + this._register(this.tree.onDidFocus(() => this.commentsFocusedContextKey.set(true))); + this._register(this.tree.onDidBlur(() => this.commentsFocusedContextKey.set(false))); } private openFile(element: any, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): void { @@ -444,11 +487,8 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } if (this.isVisible()) { this.hasCommentsContextKey.set(this.commentService.commentsModel.hasCommentThreads()); - - this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads()); this.cachedFilterStats = undefined; - this.renderMessage(); - this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel)); + this.renderComments(); if (this.tree.getSelection().length === 0 && this.commentService.commentsModel.hasCommentThreads()) { const firstComment = this.commentService.commentsModel.resourceCommentThreads[0].commentThreads[0]; diff --git a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index b850d68ac5c..8fedca8e845 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts @@ -10,24 +10,34 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments'; -import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { COMMENTS_VIEW_ID } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; import { viewFilterSubmenu } from 'vs/workbench/browser/parts/views/viewFilter'; +import { Codicon } from 'vs/base/common/codicons'; + +export const enum CommentsSortOrder { + ResourceAscending = 'resourceAscending', + UpdatedAtDescending = 'updatedAtDescending', +} + const CONTEXT_KEY_SHOW_RESOLVED = new RawContextKey('commentsView.showResolvedFilter', true); const CONTEXT_KEY_SHOW_UNRESOLVED = new RawContextKey('commentsView.showUnResolvedFilter', true); +const CONTEXT_KEY_SORT_BY = new RawContextKey('commentsView.sortBy', CommentsSortOrder.ResourceAscending); export interface CommentsFiltersChangeEvent { showResolved?: boolean; showUnresolved?: boolean; + sortBy?: CommentsSortOrder; } interface CommentsFiltersOptions { showResolved: boolean; showUnresolved: boolean; + sortBy: CommentsSortOrder; } export class CommentsFilters extends Disposable { @@ -39,6 +49,7 @@ export class CommentsFilters extends Disposable { super(); this._showResolved.set(options.showResolved); this._showUnresolved.set(options.showUnresolved); + this._sortBy.set(options.sortBy); } private readonly _showUnresolved = CONTEXT_KEY_SHOW_UNRESOLVED.bindTo(this.contextKeyService); @@ -63,6 +74,16 @@ export class CommentsFilters extends Disposable { } } + private _sortBy = CONTEXT_KEY_SORT_BY.bindTo(this.contextKeyService); + get sortBy(): CommentsSortOrder { + return this._sortBy.get()!; + } + set sortBy(sortBy: CommentsSortOrder) { + if (this._sortBy.get() !== sortBy) { + this._sortBy.set(sortBy); + this._onDidChange.fire({ sortBy }); + } + } } registerAction2(class extends ViewAction { @@ -168,3 +189,64 @@ registerAction2(class extends ViewAction { view.filters.showResolved = !view.filters.showResolved; } }); + +const commentSortSubmenu = new MenuId('submenu.filter.commentSort'); +MenuRegistry.appendMenuItem(viewFilterSubmenu, { + submenu: commentSortSubmenu, + title: localize('comment sorts', "Sort By"), + group: '2_sort', + icon: Codicon.history, + when: ContextKeyExpr.equals('view', COMMENTS_VIEW_ID), +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleSortByUpdatedAt`, + title: localize('toggle sorting by updated at', "Updated Time"), + category: localize('comments', "Comments"), + icon: Codicon.history, + viewId: COMMENTS_VIEW_ID, + toggled: { + condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.UpdatedAtDescending), + title: localize('sorting by updated at', "Updated Time"), + }, + menu: { + id: commentSortSubmenu, + group: 'navigation', + order: 1, + isHiddenByDefault: false, + }, + }); + } + + async runInView(serviceAccessor: ServicesAccessor, view: ICommentsView): Promise { + view.filters.sortBy = CommentsSortOrder.UpdatedAtDescending; + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleSortByResource`, + title: localize('toggle sorting by resource', "File"), + category: localize('comments', "Comments"), + icon: Codicon.history, + viewId: COMMENTS_VIEW_ID, + toggled: { + condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.ResourceAscending), + title: localize('sorting by file', "File"), + }, + menu: { + id: commentSortSubmenu, + group: 'navigation', + order: 0, + isHiddenByDefault: false, + }, + }); + } + + async runInView(serviceAccessor: ServicesAccessor, view: ICommentsView): Promise { + view.filters.sortBy = CommentsSortOrder.ResourceAscending; + } +}); diff --git a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts index bad0ff2cab0..6c29539e694 100644 --- a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts +++ b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts @@ -36,7 +36,8 @@ import { LinkDetector } from 'vs/editor/contrib/links/browser/links'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { MenuId } from 'vs/platform/actions/common/actions'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; +import { MarginHoverController } from 'vs/editor/contrib/hover/browser/marginHoverController'; export const ctxCommentEditorFocused = new RawContextKey('commentEditorFocused', false); export const MIN_EDITOR_HEIGHT = 5 * 18; @@ -78,7 +79,8 @@ export class SimpleCommentEditor extends CodeEditorWidget { DropIntoEditorController.ID, LinkDetector.ID, MessageController.ID, - HoverController.ID, + ContentHoverController.ID, + MarginHoverController.ID, SelectionClipboardContributionID, InlineCompletionsController.ID, CodeActionController.ID, @@ -137,6 +139,7 @@ export class SimpleCommentEditor extends CodeEditorWidget { autoClosingBrackets: configurationService.getValue('editor.autoClosingBrackets'), quickSuggestions: false, accessibilitySupport: configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'), + fontFamily: configurationService.getValue('editor.fontFamily'), }; } } @@ -144,7 +147,7 @@ export class SimpleCommentEditor extends CodeEditorWidget { export function calculateEditorHeight(parentEditor: LayoutableEditor, editor: ICodeEditor, currentHeight: number): number { const layoutInfo = editor.getLayoutInfo(); const lineHeight = editor.getOption(EditorOption.lineHeight); - const contentHeight = (editor._getViewModel()?.getLineCount()! * lineHeight) ?? editor.getContentHeight(); // Can't just call getContentHeight() because it returns an incorrect, large, value when the editor is first created. + const contentHeight = (editor._getViewModel()?.getLineCount()! * lineHeight); // Can't just call getContentHeight() because it returns an incorrect, large, value when the editor is first created. if ((contentHeight > layoutInfo.height) || (contentHeight < layoutInfo.height && currentHeight > MIN_EDITOR_HEIGHT)) { const linesToAdd = Math.ceil((contentHeight - layoutInfo.height) / lineHeight); diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index fbf25f6c06b..716985b311b 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -42,6 +42,23 @@ export class CommentNode { hasReply(): boolean { return this.replies && this.replies.length !== 0; } + + private _lastUpdatedAt: string | undefined; + + get lastUpdatedAt(): string { + if (this._lastUpdatedAt === undefined) { + let updatedAt = this.comment.timestamp || ''; + if (this.replies.length) { + const reply = this.replies[this.replies.length - 1]; + const replyUpdatedAt = reply.lastUpdatedAt; + if (replyUpdatedAt > updatedAt) { + updatedAt = replyUpdatedAt; + } + } + this._lastUpdatedAt = updatedAt; + } + return this._lastUpdatedAt; + } } export class ResourceWithCommentThreads { @@ -71,5 +88,25 @@ export class ResourceWithCommentThreads { return commentNodes[0]; } + + private _lastUpdatedAt: string | undefined; + + get lastUpdatedAt() { + if (this._lastUpdatedAt === undefined) { + let updatedAt = ''; + // Return result without cahcing as we expect data to arrive later + if (!this.commentThreads.length) { + return updatedAt; + } + for (const thread of this.commentThreads) { + const threadUpdatedAt = thread.lastUpdatedAt; + if (threadUpdatedAt && threadUpdatedAt > updatedAt) { + updatedAt = threadUpdatedAt; + } + } + this._lastUpdatedAt = updatedAt; + } + return this._lastUpdatedAt; + } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 04b79c589ec..e2fa81aee13 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -132,7 +132,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ priority: contributedEditor.priority, }, { - singlePerResource: () => !this.getCustomEditorCapabilities(contributedEditor.id)?.supportsMultipleEditorsPerDocument ?? true + singlePerResource: () => !(this.getCustomEditorCapabilities(contributedEditor.id)?.supportsMultipleEditorsPerDocument ?? false) }, { createEditorInput: ({ resource }, group) => { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index d29256d9072..2baa0a915c5 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -504,7 +504,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi if (!clz) { continue; } - const hasSomeActionableCodicon = !(clz.includes('codicon-') || clz.startsWith('coverage-deco-')) || clz.includes('codicon-testing-') || clz.includes('codicon-merge-') || clz.includes('codicon-arrow-') || clz.includes('codicon-loading') || clz.includes('codicon-fold') || clz.includes('codicon-inline-chat'); + const hasSomeActionableCodicon = !(clz.includes('codicon-') || clz.startsWith('coverage-deco-')) || clz.includes('codicon-testing-') || clz.includes('codicon-merge-') || clz.includes('codicon-arrow-') || clz.includes('codicon-loading') || clz.includes('codicon-fold') || clz.includes('codicon-gutter-lightbulb') || clz.includes('codicon-lightbulb-sparkle'); if (hasSomeActionableCodicon) { return false; } @@ -811,61 +811,66 @@ class InlineBreakpointWidget implements IContentWidget, IDisposable { } registerThemingParticipant((theme, collector) => { + const scope = '.monaco-editor .glyph-margin-widgets, .monaco-workbench .debug-breakpoints, .monaco-workbench .disassembly-view'; const debugIconBreakpointColor = theme.getColor(debugIconBreakpointForeground); if (debugIconBreakpointColor) { - collector.addRule(` - ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.regular)}`).join(',\n ')}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointUnsupported)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointHint)}:not([class*='codicon-debug-breakpoint']):not([class*='codicon-debug-stackframe']), - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after { - color: ${debugIconBreakpointColor} !important; - } - `); + collector.addRule(`${scope} { + ${icons.allBreakpoints.map(b => `${ThemeIcon.asCSSSelector(b.regular)}`).join(',\n ')}, + ${ThemeIcon.asCSSSelector(icons.debugBreakpointUnsupported)}, + ${ThemeIcon.asCSSSelector(icons.debugBreakpointHint)}:not([class*='codicon-debug-breakpoint']):not([class*='codicon-debug-stackframe']), + ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after, + ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after { + color: ${debugIconBreakpointColor} !important; + } + }`); - collector.addRule(` - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.pending)} { - color: ${debugIconBreakpointColor} !important; - font-size: 12px !important; - } - `); + collector.addRule(`${scope} { + ${ThemeIcon.asCSSSelector(icons.breakpoint.pending)} { + color: ${debugIconBreakpointColor} !important; + font-size: 12px !important; + } + }`); } const debugIconBreakpointDisabledColor = theme.getColor(debugIconBreakpointDisabledForeground); if (debugIconBreakpointDisabledColor) { - collector.addRule(` - ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.disabled)}`).join(',\n ')} { - color: ${debugIconBreakpointDisabledColor}; - } - `); + collector.addRule(`${scope} { + ${icons.allBreakpoints.map(b => ThemeIcon.asCSSSelector(b.disabled)).join(',\n ')} { + color: ${debugIconBreakpointDisabledColor}; + } + }`); } const debugIconBreakpointUnverifiedColor = theme.getColor(debugIconBreakpointUnverifiedForeground); if (debugIconBreakpointUnverifiedColor) { - collector.addRule(` - ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.unverified)}`).join(',\n ')} { - color: ${debugIconBreakpointUnverifiedColor}; - } - `); + collector.addRule(`${scope} { + ${icons.allBreakpoints.map(b => ThemeIcon.asCSSSelector(b.unverified)).join(',\n ')} { + color: ${debugIconBreakpointUnverifiedColor}; + } + }`); } const debugIconBreakpointCurrentStackframeForegroundColor = theme.getColor(debugIconBreakpointCurrentStackframeForeground); if (debugIconBreakpointCurrentStackframeForegroundColor) { collector.addRule(` - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStackframe)}, .monaco-editor .debug-top-stack-frame-column { color: ${debugIconBreakpointCurrentStackframeForegroundColor} !important; } + ${scope} { + ${ThemeIcon.asCSSSelector(icons.debugStackframe)} { + color: ${debugIconBreakpointCurrentStackframeForegroundColor} !important; + } + } `); } const debugIconBreakpointStackframeFocusedColor = theme.getColor(debugIconBreakpointStackframeForeground); if (debugIconBreakpointStackframeFocusedColor) { - collector.addRule(` - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)} { - color: ${debugIconBreakpointStackframeFocusedColor} !important; - } - `); + collector.addRule(`${scope} { + ${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)} { + color: ${debugIconBreakpointStackframeFocusedColor} !important; + } + }`); } }); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 11a82d72c3a..c002c37848f 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -6,6 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Button } from 'vs/base/browser/ui/button/button'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -34,6 +35,7 @@ import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -109,6 +111,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi @IKeybindingService private readonly keybindingService: IKeybindingService, @ILabelService private readonly labelService: ILabelService, @ITextModelService private readonly textModelService: ITextModelService, + @IHoverService private readonly hoverService: IHoverService ) { super(editor, { showFrame: true, showArrow: false, frameWidth: 1, isAccessible: true }); @@ -204,12 +207,12 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi protected _fillContainer(container: HTMLElement): void { this.setCssClass('breakpoint-widget'); - const selectBox = new SelectBox([ + const selectBox = new SelectBox([ { text: nls.localize('expression', "Expression") }, { text: nls.localize('hitCount', "Hit Count") }, { text: nls.localize('logMessage', "Log Message") }, { text: nls.localize('triggeredBy', "Wait for Breakpoint") }, - ], this.context, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') }); + ] satisfies ISelectOptionItem[], this.context, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') }); this.selectContainer = $('.breakpoint-select-container'); selectBox.render(dom.append(container, this.selectContainer)); selectBox.onDidSelect(e => { @@ -221,6 +224,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.createModesInput(container); this.inputContainer = $('.inputContainer'); + this.toDispose.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.inputContainer, this.placeholder)); this.createBreakpointInput(dom.append(container, this.inputContainer)); this.input.getModel().setValue(this.getInputValue(this.breakpoint)); diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 6c3aeb1d666..70240432101 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -48,19 +48,28 @@ const FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { color: themeColorFromId(focusedStackFrameColor) } }; -const TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = { +export const TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = { description: 'top-stack-frame-decoration', isWholeLine: true, className: 'debug-top-stack-frame-line', stickiness }; -const FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { +export const FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { description: 'focused-stack-frame-decoration', isWholeLine: true, className: 'debug-focused-stack-frame-line', stickiness }; +export const makeStackFrameColumnDecoration = (noCharactersBefore: boolean): IModelDecorationOptions => ({ + description: 'top-stack-frame-inline-decoration', + before: { + content: '\uEB8B', + inlineClassName: noCharactersBefore ? 'debug-top-stack-frame-column start-of-line' : 'debug-top-stack-frame-column', + inlineClassNameAffectsLetterSpacing: true + }, +}); + export function createDecorationsForStackFrame(stackFrame: IStackFrame, isFocusedSession: boolean, noCharactersBefore: boolean): IModelDeltaDecoration[] { // only show decorations for the currently focused thread. const result: IModelDeltaDecoration[] = []; @@ -85,14 +94,7 @@ export function createDecorationsForStackFrame(stackFrame: IStackFrame, isFocuse if (stackFrame.range.startColumn > 1) { result.push({ - options: { - description: 'top-stack-frame-inline-decoration', - before: { - content: '\uEB8B', - inlineClassName: noCharactersBefore ? 'debug-top-stack-frame-column start-of-line' : 'debug-top-stack-frame-column', - inlineClassNameAffectsLetterSpacing: true - }, - }, + options: makeStackFrameColumnDecoration(noCharactersBefore), range: columnUntilEOLRange }); } diff --git a/src/vs/workbench/contrib/debug/browser/callStackWidget.ts b/src/vs/workbench/contrib/debug/browser/callStackWidget.ts new file mode 100644 index 00000000000..19fddadc59f --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/callStackWidget.ts @@ -0,0 +1,648 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { assertNever } from 'vs/base/common/assert'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { autorun, autorunWithStore, derived, IObservable, ISettableObservable, observableValue } from 'vs/base/common/observable'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { Constants } from 'vs/base/common/uint'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import 'vs/css!./media/callStackWidget'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; +import { Location } from 'vs/editor/common/languages'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize, localize2 } from 'vs/nls'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { makeStackFrameColumnDecoration, TOP_STACK_FRAME_DECORATION } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + + +export class CallStackFrame { + constructor( + public readonly name: string, + public readonly source?: URI, + public readonly line = 1, + public readonly column = 1, + ) { } +} + +export class SkippedCallFrames { + constructor( + public readonly label: string, + public readonly load: (token: CancellationToken) => Promise, + ) { } +} + +export abstract class CustomStackFrame { + public readonly showHeader = observableValue('CustomStackFrame.showHeader', true); + public abstract readonly height: IObservable; + public abstract readonly label: string; + public icon?: ThemeIcon; + public abstract render(container: HTMLElement): IDisposable; + public renderActions?(container: HTMLElement): IDisposable; +} + +export type AnyStackFrame = SkippedCallFrames | CallStackFrame | CustomStackFrame; + +interface IFrameLikeItem { + readonly collapsed: ISettableObservable; + readonly height: IObservable; +} + +class WrappedCallStackFrame extends CallStackFrame implements IFrameLikeItem { + public readonly editorHeight = observableValue('WrappedCallStackFrame.height', 100); + public readonly collapsed = observableValue('WrappedCallStackFrame.collapsed', false); + + public readonly height = derived(reader => { + return this.collapsed.read(reader) ? HEADER_HEIGHT : HEADER_HEIGHT + this.editorHeight.read(reader); + }); + + constructor(original: CallStackFrame) { + super(original.name, original.source, original.line, original.column); + } +} + +class WrappedCustomStackFrame implements IFrameLikeItem { + public readonly collapsed = observableValue('WrappedCallStackFrame.collapsed', false); + + public readonly height = derived(reader => { + const headerHeight = this.original.showHeader.read(reader) ? HEADER_HEIGHT : 0; + return this.collapsed.read(reader) ? headerHeight : headerHeight + this.original.height.read(reader); + }); + + constructor(public readonly original: CustomStackFrame) { } +} + +type ListItem = WrappedCallStackFrame | SkippedCallFrames | WrappedCustomStackFrame; + +const WIDGET_CLASS_NAME = 'multiCallStackWidget'; + +/** + * A reusable widget that displays a call stack as a series of editors. Note + * that this both used in debug's exception widget as well as in the testing + * call stack view. + */ +export class CallStackWidget extends Disposable { + private readonly list: WorkbenchList; + private readonly layoutEmitter = this._register(new Emitter()); + private readonly currentFramesDs = this._register(new DisposableStore()); + private cts?: CancellationTokenSource; + + constructor( + container: HTMLElement, + containingEditor: ICodeEditor | undefined, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + container.classList.add(WIDGET_CLASS_NAME); + this._register(toDisposable(() => container.classList.remove(WIDGET_CLASS_NAME))); + + this.list = this._register(instantiationService.createInstance( + WorkbenchList, + 'TestResultStackWidget', + container, + new StackDelegate(), + [ + instantiationService.createInstance(FrameCodeRenderer, containingEditor, this.layoutEmitter.event), + instantiationService.createInstance(MissingCodeRenderer), + instantiationService.createInstance(CustomRenderer), + instantiationService.createInstance(SkippedRenderer, (i) => this.loadFrame(i)), + ], + { + multipleSelectionSupport: false, + mouseSupport: false, + keyboardSupport: false, + accessibilityProvider: instantiationService.createInstance(StackAccessibilityProvider), + } + ) as WorkbenchList); + } + + /** Replaces the call frames display in the view. */ + public setFrames(frames: AnyStackFrame[]): void { + // cancel any existing load + this.currentFramesDs.clear(); + this.cts = new CancellationTokenSource(); + this._register(toDisposable(() => this.cts!.dispose(true))); + + this.list.splice(0, this.list.length, this.mapFrames(frames)); + } + + public layout(height?: number, width?: number): void { + this.list.layout(height, width); + this.layoutEmitter.fire(); + } + + private async loadFrame(replacing: SkippedCallFrames): Promise { + if (!this.cts) { + return; + } + + const frames = await replacing.load(this.cts.token); + if (this.cts.token.isCancellationRequested) { + return; + } + + const index = this.list.indexOf(replacing); + this.list.splice(index, 1, this.mapFrames(frames)); + } + + private mapFrames(frames: AnyStackFrame[]): ListItem[] { + const result: ListItem[] = []; + for (const frame of frames) { + if (frame instanceof SkippedCallFrames) { + result.push(frame); + continue; + } + + const wrapped = frame instanceof CustomStackFrame + ? new WrappedCustomStackFrame(frame) : new WrappedCallStackFrame(frame); + result.push(wrapped); + + this.currentFramesDs.add(autorun(reader => { + const height = wrapped.height.read(reader); + const idx = this.list.indexOf(wrapped); + if (idx !== -1) { + this.list.updateElementHeight(idx, height); + } + })); + } + + return result; + } +} + +class StackAccessibilityProvider implements IListAccessibilityProvider { + constructor(@ILabelService private readonly labelService: ILabelService) { } + + getAriaLabel(e: ListItem): string | IObservable | null { + if (e instanceof SkippedCallFrames) { + return e.label; + } + + if (e instanceof WrappedCustomStackFrame) { + return e.original.label; + } + + if (e instanceof CallStackFrame) { + if (e.source && e.line) { + return localize({ + comment: ['{0} is an extension-defined label, then line number and filename'], + key: 'stackTraceLabel', + }, '{0}, line {1} in {2}', e.name, e.line, this.labelService.getUriLabel(e.source, { relative: true })); + } + + return e.name; + } + + assertNever(e); + } + getWidgetAriaLabel(): string { + return localize('stackTrace', 'Stack Trace'); + } +} + +class StackDelegate implements IListVirtualDelegate { + getHeight(element: ListItem): number { + if (element instanceof CallStackFrame || element instanceof WrappedCustomStackFrame) { + return element.height.get(); + } + if (element instanceof SkippedCallFrames) { + return 50; + } + + assertNever(element); + } + + getTemplateId(element: ListItem): string { + if (element instanceof CallStackFrame) { + return element.source ? FrameCodeRenderer.templateId : MissingCodeRenderer.templateId; + } + if (element instanceof SkippedCallFrames) { + return SkippedRenderer.templateId; + } + if (element instanceof WrappedCustomStackFrame) { + return CustomRenderer.templateId; + } + + assertNever(element); + } +} + +interface IStackTemplateData extends IAbstractFrameRendererTemplateData { + editor: CodeEditorWidget; + toolbar: MenuWorkbenchToolBar; +} + +const editorOptions: IEditorOptions = { + scrollBeyondLastLine: false, + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden', + handleMouseWheel: false, + useShadows: false, + }, + overviewRulerLanes: 0, + fixedOverflowWidgets: true, + overviewRulerBorder: false, + stickyScroll: { enabled: false }, + minimap: { enabled: false }, + readOnly: true, + automaticLayout: false, +}; + +const makeFrameElements = () => dom.h('div.multiCallStackFrame', [ + dom.h('div.header@header', [ + dom.h('div.collapse-button@collapseButton'), + dom.h('div.title.show-file-icons@title'), + dom.h('div.actions@actions'), + ]), + + dom.h('div.editorParent', [ + dom.h('div.editorContainer@editor'), + ]) +]); + +const HEADER_HEIGHT = 32; + +interface IAbstractFrameRendererTemplateData { + container: HTMLElement; + label: ResourceLabel; + elements: ReturnType; + decorations: string[]; + collapse: Button; + elementStore: DisposableStore; + templateStore: DisposableStore; +} + +abstract class AbstractFrameRenderer implements IListRenderer { + public abstract templateId: string; + + constructor( + @IInstantiationService protected readonly instantiationService: IInstantiationService, + ) { } + + renderTemplate(container: HTMLElement): T { + const elements = makeFrameElements(); + container.appendChild(elements.root); + + + const templateStore = new DisposableStore(); + container.classList.add('multiCallStackFrameContainer'); + templateStore.add(toDisposable(() => { + container.classList.remove('multiCallStackFrameContainer'); + elements.root.remove(); + })); + + const label = templateStore.add(this.instantiationService.createInstance(ResourceLabel, elements.title, {})); + + const collapse = templateStore.add(new Button(elements.collapseButton, {})); + + const contentId = generateUuid(); + elements.editor.id = contentId; + elements.editor.role = 'region'; + elements.collapseButton.setAttribute('aria-controls', contentId); + + return this.finishRenderTemplate({ + container, + decorations: [], + elements, + label, + collapse, + elementStore: templateStore.add(new DisposableStore()), + templateStore, + }); + } + + protected abstract finishRenderTemplate(data: IAbstractFrameRendererTemplateData): T; + + renderElement(element: ListItem, index: number, template: T, height: number | undefined): void { + const { elementStore } = template; + elementStore.clear(); + const item = element as IFrameLikeItem; + + this.setupCollapseButton(item, template); + } + + private setupCollapseButton(item: IFrameLikeItem, { elementStore, elements, collapse }: T) { + elementStore.add(autorun(reader => { + collapse.element.className = ''; + const collapsed = item.collapsed.read(reader); + collapse.icon = collapsed ? Codicon.chevronRight : Codicon.chevronDown; + collapse.element.ariaExpanded = String(!collapsed); + elements.root.classList.toggle('collapsed', collapsed); + })); + elementStore.add(collapse.onDidClick(() => { + item.collapsed.set(!item.collapsed.get(), undefined); + })); + } + + disposeElement(element: ListItem, index: number, templateData: T, height: number | undefined): void { + templateData.elementStore.clear(); + } + + disposeTemplate(templateData: T): void { + templateData.templateStore.dispose(); + } +} + +const CONTEXT_LINES = 2; + +/** Renderer for a normal stack frame where code is available. */ +class FrameCodeRenderer extends AbstractFrameRenderer { + public static readonly templateId = 'f'; + + public readonly templateId = FrameCodeRenderer.templateId; + + constructor( + private readonly containingEditor: ICodeEditor | undefined, + private readonly onLayout: Event, + @ITextModelService private readonly modelService: ITextModelService, + @ICodeEditorService private readonly editorService: ICodeEditorService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(instantiationService); + } + + protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IStackTemplateData { + const editor = this.containingEditor + ? this.instantiationService.createInstance( + EmbeddedCodeEditorWidget, + data.elements.editor, + editorOptions, + { isSimpleWidget: true }, + this.containingEditor, + ) + : this.instantiationService.createInstance( + CodeEditorWidget, + data.elements.editor, + editorOptions, + { isSimpleWidget: true }, + ); + + data.templateStore.add(editor); + + const toolbar = data.templateStore.add(this.instantiationService.createInstance(MenuWorkbenchToolBar, data.elements.actions, MenuId.DebugCallStackToolbar, { + menuOptions: { shouldForwardArgs: true }, + actionViewItemProvider: (action, options) => createActionViewItem(this.instantiationService, action, options), + })); + + return { ...data, editor, toolbar }; + } + + override renderElement(element: ListItem, index: number, template: IStackTemplateData, height: number | undefined): void { + super.renderElement(element, index, template, height); + + const { elementStore, editor } = template; + + const item = element as WrappedCallStackFrame; + const uri = item.source!; + + template.label.element.setFile(uri); + template.elements.title.role = 'link'; + elementStore.add(dom.addDisposableListener(template.elements.title, 'click', e => { + this.editorService.openCodeEditor({ + resource: uri, + options: { + selection: Range.fromPositions({ + column: item.column ?? 1, + lineNumber: item.line ?? 1, + }), + selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport, + }, + }, this.containingEditor || null, e.ctrlKey || e.metaKey); + })); + + const cts = new CancellationTokenSource(); + elementStore.add(toDisposable(() => cts.dispose(true))); + this.modelService.createModelReference(uri).then(reference => { + if (cts.token.isCancellationRequested) { + return reference.dispose(); + } + + elementStore.add(reference); + editor.setModel(reference.object.textEditorModel); + this.setupEditorAfterModel(item, template); + this.setupEditorLayout(item, template); + }); + } + + private setupEditorLayout(item: WrappedCallStackFrame, { elementStore, container, editor }: IStackTemplateData) { + const layout = () => { + const prev = editor.getContentHeight(); + editor.layout({ width: container.clientWidth, height: prev }); + + const next = editor.getContentHeight(); + if (next !== prev) { + editor.layout({ width: container.clientWidth, height: next }); + } + + item.editorHeight.set(next, undefined); + }; + elementStore.add(editor.onDidChangeModelDecorations(layout)); + elementStore.add(editor.onDidChangeModelContent(layout)); + elementStore.add(editor.onDidChangeModelOptions(layout)); + elementStore.add(this.onLayout(layout)); + layout(); + } + + private setupEditorAfterModel(item: WrappedCallStackFrame, template: IStackTemplateData): void { + const range = Range.fromPositions({ + column: item.column ?? 1, + lineNumber: item.line ?? 1, + }); + + template.toolbar.context = { uri: item.source, range }; + + template.editor.setHiddenAreas([ + Range.fromPositions( + { column: 1, lineNumber: 1 }, + { column: 1, lineNumber: Math.max(1, item.line - CONTEXT_LINES - 1) }, + ), + Range.fromPositions( + { column: 1, lineNumber: item.line + CONTEXT_LINES + 1 }, + { column: 1, lineNumber: Constants.MAX_SAFE_SMALL_INTEGER }, + ), + ]); + + template.editor.changeDecorations(accessor => { + for (const d of template.decorations) { + accessor.removeDecoration(d); + } + template.decorations.length = 0; + + const beforeRange = range.setStartPosition(range.startLineNumber, 1); + const hasCharactersBefore = !!template.editor.getModel()?.getValueInRange(beforeRange).trim(); + const decoRange = range.setEndPosition(range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER); + + template.decorations.push(accessor.addDecoration( + decoRange, + makeStackFrameColumnDecoration(!hasCharactersBefore), + )); + template.decorations.push(accessor.addDecoration( + decoRange, + TOP_STACK_FRAME_DECORATION, + )); + }); + + item.editorHeight.set(template.editor.getContentHeight(), undefined); + } +} + +interface IMissingTemplateData { + container: HTMLElement; +} + +/** Renderer for a call frame that's missing a URI */ +class MissingCodeRenderer implements IListRenderer { + public static readonly templateId = 'm'; + public readonly templateId = MissingCodeRenderer.templateId; + + renderTemplate(container: HTMLElement): IMissingTemplateData { + return { container }; + } + + renderElement(element: ListItem, index: number, templateData: IMissingTemplateData, height: number | undefined): void { + templateData.container.innerText = (element as CallStackFrame).name; + } + + disposeTemplate(templateData: IMissingTemplateData): void { + dom.clearNode(templateData.container); + } +} + +interface IMissingTemplateData { + container: HTMLElement; +} + +/** Renderer for a call frame that's missing a URI */ +class CustomRenderer extends AbstractFrameRenderer { + public static readonly templateId = 'c'; + public readonly templateId = CustomRenderer.templateId; + + protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IAbstractFrameRendererTemplateData { + return data; + } + + override renderElement(element: ListItem, index: number, template: IAbstractFrameRendererTemplateData, height: number | undefined): void { + super.renderElement(element, index, template, height); + + const item = element as WrappedCustomStackFrame; + const { elementStore, container, label } = template; + + label.element.setResource({ name: item.original.label }, { icon: item.original.icon }); + + elementStore.add(autorun(reader => { + template.elements.header.style.display = item.original.showHeader.read(reader) ? '' : 'none'; + })); + + elementStore.add(autorunWithStore((reader, store) => { + if (!item.collapsed.read(reader)) { + store.add(item.original.render(container)); + } + })); + + const actions = item.original.renderActions?.(template.elements.actions); + if (actions) { + elementStore.add(actions); + } + } +} + +interface ISkippedTemplateData { + button: Button; + current?: SkippedCallFrames; + store: DisposableStore; +} + +/** Renderer for a button to load more call frames */ +class SkippedRenderer implements IListRenderer { + public static readonly templateId = 's'; + public readonly templateId = SkippedRenderer.templateId; + + constructor( + private readonly loadFrames: (fromItem: SkippedCallFrames) => Promise, + @INotificationService private readonly notificationService: INotificationService, + ) { } + + renderTemplate(container: HTMLElement): ISkippedTemplateData { + const store = new DisposableStore(); + const button = new Button(container, { title: '', ...defaultButtonStyles }); + const data: ISkippedTemplateData = { button, store }; + + store.add(button); + store.add(button.onDidClick(() => { + if (!data.current || !button.enabled) { + return; + } + + button.enabled = false; + this.loadFrames(data.current).catch(e => { + this.notificationService.error(localize('failedToLoadFrames', 'Failed to load stack frames: {0}', e.message)); + }); + })); + + return data; + } + + renderElement(element: ListItem, index: number, templateData: ISkippedTemplateData, height: number | undefined): void { + const cast = element as SkippedCallFrames; + templateData.button.enabled = true; + templateData.button.label = cast.label; + templateData.current = cast; + } + + disposeTemplate(templateData: ISkippedTemplateData): void { + templateData.store.dispose(); + } +} + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'callStackWidget.goToFile', + title: localize2('goToFile', 'Open File'), + icon: Codicon.goToFile, + menu: { + id: MenuId.DebugCallStackToolbar, + order: 22, + group: 'navigation', + }, + }); + } + + async run(accessor: ServicesAccessor, { uri, range }: Location): Promise { + const editorService = accessor.get(IEditorService); + await editorService.openEditor({ + resource: uri, + options: { + selection: range, + selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport, + }, + }); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index b85da3db794..c909d645ca9 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -11,6 +11,7 @@ import 'vs/css!./media/debug.contribution'; import 'vs/css!./media/debugHover'; import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import * as nls from 'vs/nls'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ICommandActionTitle, Icon } from 'vs/platform/action/common/action'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; @@ -21,13 +22,14 @@ import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/pl import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from 'vs/workbench/common/contributions'; import { EditorExtensions } from 'vs/workbench/common/editor'; import { IViewContainersRegistry, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from 'vs/workbench/common/views'; import { BreakpointEditorContribution } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; import { BreakpointsView } from 'vs/workbench/contrib/debug/browser/breakpointsView'; import { CallStackEditorContribution } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView'; +import { ReplAccessibleView } from 'vs/workbench/contrib/debug/browser/replAccessibleView'; import { registerColors } from 'vs/workbench/contrib/debug/browser/debugColors'; import { ADD_CONFIGURATION_ID, ADD_TO_WATCH_ID, ADD_TO_WATCH_LABEL, CALLSTACK_BOTTOM_ID, CALLSTACK_BOTTOM_LABEL, CALLSTACK_DOWN_ID, CALLSTACK_DOWN_LABEL, CALLSTACK_TOP_ID, CALLSTACK_TOP_LABEL, CALLSTACK_UP_ID, CALLSTACK_UP_LABEL, CONTINUE_ID, CONTINUE_LABEL, COPY_EVALUATE_PATH_ID, COPY_EVALUATE_PATH_LABEL, COPY_STACK_TRACE_ID, COPY_VALUE_ID, COPY_VALUE_LABEL, DEBUG_COMMAND_CATEGORY, DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, DEBUG_QUICK_ACCESS_PREFIX, DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, EDIT_EXPRESSION_COMMAND_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, NEXT_DEBUG_CONSOLE_ID, NEXT_DEBUG_CONSOLE_LABEL, OPEN_LOADED_SCRIPTS_LABEL, PAUSE_ID, PAUSE_LABEL, PREV_DEBUG_CONSOLE_ID, PREV_DEBUG_CONSOLE_LABEL, REMOVE_EXPRESSION_COMMAND_ID, RESTART_FRAME_ID, RESTART_LABEL, RESTART_SESSION_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SELECT_DEBUG_CONSOLE_ID, SELECT_DEBUG_CONSOLE_LABEL, SELECT_DEBUG_SESSION_ID, SELECT_DEBUG_SESSION_LABEL, SET_EXPRESSION_COMMAND_ID, SHOW_LOADED_SCRIPTS_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_INTO_TARGET_ID, STEP_INTO_TARGET_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL, TERMINATE_THREAD_ID, TOGGLE_INLINE_BREAKPOINT_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { DebugConsoleQuickAccess } from 'vs/workbench/contrib/debug/browser/debugConsoleQuickAccess'; @@ -56,6 +58,11 @@ import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassem import { COPY_NOTEBOOK_VARIABLE_VALUE_ID, COPY_NOTEBOOK_VARIABLE_VALUE_LABEL } from 'vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariableCommands'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import './debugSettingMigration'; +import { ReplAccessibilityHelp } from 'vs/workbench/contrib/debug/browser/replAccessibilityHelp'; +import { ReplAccessibilityAnnouncer } from 'vs/workbench/contrib/debug/common/replAccessibilityAnnouncer'; +import { RunAndDebugAccessibilityHelp } from 'vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp'; +import { DebugWatchAccessibilityAnnouncer } from 'vs/workbench/contrib/debug/common/debugAccessibilityAnnouncer'; const debugCategory = nls.localize('debugCategory', "Debug"); registerColors(); @@ -616,9 +623,15 @@ configurationRegistry.registerConfiguration({ description: nls.localize('debug.disassemblyView.showSourceCode', "Show Source Code in Disassembly View.") }, 'debug.autoExpandLazyVariables': { - type: 'boolean', - default: false, - description: nls.localize('debug.autoExpandLazyVariables', "Automatically show values for variables that are lazily resolved by the debugger, such as getters.") + type: 'string', + enum: ['auto', 'on', 'off'], + default: 'auto', + enumDescriptions: [ + nls.localize('debug.autoExpandLazyVariables.auto', "When in screen reader optimized mode, automatically expand lazy variables."), + nls.localize('debug.autoExpandLazyVariables.on', "Always automatically expand lazy variables."), + nls.localize('debug.autoExpandLazyVariables.off', "Never automatically expand lazy variables.") + ], + description: nls.localize('debug.autoExpandLazyVariables', "Controls whether variables that are lazily resolved, such as getters, are automatically resolved and expanded by the debugger.") }, 'debug.enableStatusBarColor': { type: 'boolean', @@ -632,3 +645,9 @@ configurationRegistry.registerConfiguration({ } } }); + +AccessibleViewRegistry.register(new ReplAccessibleView()); +AccessibleViewRegistry.register(new ReplAccessibilityHelp()); +AccessibleViewRegistry.register(new RunAndDebugAccessibilityHelp()); +registerWorkbenchContribution2(ReplAccessibilityAnnouncer.ID, ReplAccessibilityAnnouncer, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(DebugWatchAccessibilityAnnouncer.ID, DebugWatchAccessibilityAnnouncer, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 50a1b351fc2..d92bd1ec955 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -24,6 +24,9 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; const $ = dom.$; @@ -49,7 +52,8 @@ export class StartDebugActionViewItem extends BaseActionViewItem { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IContextViewService contextViewService: IContextViewService, @IKeybindingService private readonly keybindingService: IKeybindingService, - @IHoverService private readonly hoverService: IHoverService + @IHoverService private readonly hoverService: IHoverService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(context, action, options); this.toDispose = []; @@ -80,7 +84,7 @@ export class StartDebugActionViewItem extends BaseActionViewItem { const title = this.action.label + keybindingLabel; this.toDispose.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.start, title)); this.start.setAttribute('role', 'button'); - this.start.ariaLabel = title; + this._setAriaLabel(title); this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.CLICK, () => { this.start.blur(); @@ -261,6 +265,21 @@ export class StartDebugActionViewItem extends BaseActionViewItem { this.selectBox.setOptions(this.debugOptions.map((data, index): ISelectOptionItem => ({ text: data.label, isDisabled: disabledIdxs.indexOf(index) !== -1 })), this.selected); } + + private _setAriaLabel(title: string): void { + let ariaLabel = title; + let keybinding: string | undefined; + const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Debug); + if (verbose) { + keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp, this.contextKeyService)?.getLabel() ?? undefined; + } + if (keybinding) { + ariaLabel = nls.localize('commentLabelWithKeybinding', "{0}, use ({1}) for accessibility help", ariaLabel, keybinding); + } else { + ariaLabel = nls.localize('commentLabelWithKeybindingNoKeybinding', "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding.", ariaLabel); + } + this.start.ariaLabel = ariaLabel; + } } export class FocusSessionActionViewItem extends SelectActionViewItem { diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 423316207be..f5ba671db0d 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; @@ -27,6 +28,7 @@ import { CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE, IAdapte import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { breakpointsExtPoint, debuggersExtPoint, launchSchema, presentationSchema } from 'vs/workbench/contrib/debug/common/debugSchemas'; import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; +import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -49,6 +51,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { private readonly _onDidDebuggersExtPointRead = new Emitter(); private breakpointContributions: Breakpoints[] = []; private debuggerWhenKeys = new Set(); + private taskLabels: string[] = []; /** Extensions that were already active before any debugger activation events */ private earlyActivatedExtensions: Set | undefined; @@ -66,7 +69,8 @@ export class AdapterManager extends Disposable implements IAdapterManager { @IContextKeyService private readonly contextKeyService: IContextKeyService, @ILanguageService private readonly languageService: ILanguageService, @IDialogService private readonly dialogService: IDialogService, - @ILifecycleService private readonly lifecycleService: ILifecycleService + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @ITaskService private readonly tasksService: ITaskService, ) { super(); this.adapterDescriptorFactories = []; @@ -85,12 +89,22 @@ export class AdapterManager extends Disposable implements IAdapterManager { this._register(this.onDidDebuggersExtPointRead(() => { this.debugExtensionsAvailable.set(this.debuggers.length > 0); })); + + // generous debounce since this will end up calling `resolveTask` internally + const updateTaskScheduler = this._register(new RunOnceScheduler(() => this.updateTaskLabels(), 5000)); + + this._register(Event.any(tasksService.onDidChangeTaskConfig, tasksService.onDidChangeTaskProviders)(() => { + updateTaskScheduler.cancel(); + updateTaskScheduler.schedule(); + })); this.lifecycleService.when(LifecyclePhase.Eventually) .then(() => this.debugExtensionsAvailable.set(this.debuggers.length > 0)); // If no extensions with a debugger contribution are loaded this._register(delegate.onDidNewSession(s => { this.usedDebugTypes.add(s.configuration.type); })); + + updateTaskScheduler.schedule(); } private registerListeners(): void { @@ -137,7 +151,14 @@ export class AdapterManager extends Disposable implements IAdapterManager { }); } - private updateDebugAdapterSchema(): void { + private updateTaskLabels() { + this.tasksService.getKnownTasks().then(tasks => { + this.taskLabels = tasks.map(task => task._label); + this.updateDebugAdapterSchema(); + }); + } + + private updateDebugAdapterSchema() { // update the schema to include all attributes, snippets and types from extensions. const items = (launchSchema.properties!['configurations'].items); const taskSchema = TaskDefinitionRegistry.getJsonSchema(); @@ -160,7 +181,8 @@ export class AdapterManager extends Disposable implements IAdapterManager { }], default: '', defaultSnippets: [{ body: { task: '', type: '' } }], - description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts.") + description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts."), + examples: this.taskLabels, }, 'postDebugTask': { anyOf: [taskSchema, { @@ -168,7 +190,8 @@ export class AdapterManager extends Disposable implements IAdapterManager { }], default: '', defaultSnippets: [{ body: { task: '', type: '' } }], - description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.") + description: nls.localize('debugPostDebugTask', "Task to run after debug session ends."), + examples: this.taskLabels, }, 'presentation': presentationSchema, 'internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA, diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 7db66075458..1db501d89d3 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -36,6 +36,7 @@ import { showLoadedScriptMenu } from 'vs/workbench/contrib/debug/common/loadedSc import { showDebugSessionMenu } from 'vs/workbench/contrib/debug/browser/debugSessionPicker'; import { TEXT_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { ILocalizedString } from 'vs/platform/action/common/action'; +import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; export const ADD_CONFIGURATION_ID = 'debug.addConfiguration'; export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint'; @@ -1009,7 +1010,11 @@ MenuRegistry.appendMenuItem(MenuId.EditorContext, { title: nls.localize('addInlineBreakpoint', "Add Inline Breakpoint"), category: DEBUG_COMMAND_CATEGORY }, - when: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, PanelFocusContext.toNegated(), EditorContextKeys.editorTextFocus), + when: ContextKeyExpr.and( + CONTEXT_IN_DEBUG_MODE, + PanelFocusContext.toNegated(), + EditorContextKeys.editorTextFocus, + CONTEXT_IN_CHAT_SESSION.toNegated()), group: 'debug', order: 1 }); diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 7151a0ffa5d..ed6d6ecef5f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -20,6 +20,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import { ILogService } from 'vs/platform/log/common/log'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -72,7 +73,8 @@ export class ConfigurationManager implements IConfigurationManager { @IExtensionService private readonly extensionService: IExtensionService, @IHistoryService private readonly historyService: IHistoryService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @ILogService private readonly logService: ILogService, ) { this.configProviders = []; this.toDispose = [this._onDidChangeConfigurationProviders]; @@ -257,7 +259,18 @@ export class ConfigurationManager implements IConfigurationManager { disposables.add(input.onDidHide(() => resolve(undefined))); }); - const nestedPicks = await Promise.all(picks); + let nestedPicks: IDynamicPickItem[][]; + try { + // This await invokes the extension providers, which might fail due to several reasons, + // therefore we gate this logic under a try/catch to prevent leaving the Debug Tab + // selector in a borked state. + nestedPicks = await Promise.all(picks); + } catch (err) { + this.logService.error(err); + disposables.dispose(); + return; + } + const items = nestedPicks.flat(); input.items = items; diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index 4f613027aaf..63792065199 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -23,6 +23,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { PanelFocusContext } from 'vs/workbench/common/contextkeys'; +import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView'; import { DisassemblyView } from 'vs/workbench/contrib/debug/browser/disassemblyView'; import { Repl } from 'vs/workbench/contrib/debug/browser/repl'; @@ -304,7 +305,12 @@ export class RunToCursorAction extends EditorAction { id: RunToCursorAction.ID, label: RunToCursorAction.LABEL.value, alias: 'Debug: Run to Cursor', - precondition: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, PanelFocusContext.toNegated(), ContextKeyExpr.or(EditorContextKeys.editorTextFocus, CONTEXT_DISASSEMBLY_VIEW_FOCUS)), + precondition: ContextKeyExpr.and( + CONTEXT_DEBUGGERS_AVAILABLE, + PanelFocusContext.toNegated(), + ContextKeyExpr.or(EditorContextKeys.editorTextFocus, CONTEXT_DISASSEMBLY_VIEW_FOCUS), + CONTEXT_IN_CHAT_SESSION.negate() + ), contextMenuOpts: { group: 'debug', order: 2, @@ -345,7 +351,10 @@ export class SelectionToReplAction extends EditorAction { id: SelectionToReplAction.ID, label: SelectionToReplAction.LABEL.value, alias: 'Debug: Evaluate in Console', - precondition: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus), + precondition: ContextKeyExpr.and( + CONTEXT_IN_DEBUG_MODE, + EditorContextKeys.editorTextFocus, + CONTEXT_IN_CHAT_SESSION.negate()), contextMenuOpts: { group: 'debug', order: 0 @@ -385,7 +394,10 @@ export class SelectionToWatchExpressionsAction extends EditorAction { id: SelectionToWatchExpressionsAction.ID, label: SelectionToWatchExpressionsAction.LABEL.value, alias: 'Debug: Add to Watch', - precondition: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus), + precondition: ContextKeyExpr.and( + CONTEXT_IN_DEBUG_MODE, + EditorContextKeys.editorTextFocus, + CONTEXT_IN_CHAT_SESSION.negate()), contextMenuOpts: { group: 'debug', order: 1 diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 5e3d5e9d583..96c0952b0f2 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -37,7 +37,7 @@ import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops } from 'vs/e import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IModelService } from 'vs/editor/common/services/model'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; import * as nls from 'vs/nls'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; @@ -375,7 +375,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { return; } - const hoverController = this.editor.getContribution(HoverController.ID); + const hoverController = this.editor.getContribution(ContentHoverController.ID); hoverController?.hideContentHover(); this.editor.updateOptions({ hover: { enabled: false } }); @@ -389,7 +389,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { } private showEditorHover(position: Position, focus: boolean) { - const hoverController = this.editor.getContribution(HoverController.ID); + const hoverController = this.editor.getContribution(ContentHoverController.ID); const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); // enable the editor hover, otherwise the content controller will see it // as disabled and hide it on the first mouse move (#193149) diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 79c3cc8c123..612530c75ed 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -45,6 +45,7 @@ import { isDefined } from 'vs/base/common/types'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; @@ -84,7 +85,7 @@ export class DebugSession implements IDebugSession, IDisposable { private readonly _onDidProgressEnd = new Emitter(); private readonly _onDidInvalidMemory = new Emitter(); - private readonly _onDidChangeREPLElements = new Emitter(); + private readonly _onDidChangeREPLElements = new Emitter(); private _name: string | undefined; private readonly _onDidChangeName = new Emitter(); @@ -117,6 +118,7 @@ export class DebugSession implements IDebugSession, IDisposable { @ILogService private readonly logService: ILogService, @ITestService private readonly testService: ITestService, @ITestResultService testResultService: ITestResultService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, ) { this._options = options || {}; this.parentSession = this._options.parentSession; @@ -128,7 +130,7 @@ export class DebugSession implements IDebugSession, IDisposable { const toDispose = this.globalDisposables; const replListener = toDispose.add(new MutableDisposable()); - replListener.value = this.repl.onDidChangeElements(() => this._onDidChangeREPLElements.fire()); + replListener.value = this.repl.onDidChangeElements((e) => this._onDidChangeREPLElements.fire(e)); if (lifecycleService) { toDispose.add(lifecycleService.onWillShutdown(() => { this.shutdown(); @@ -176,7 +178,7 @@ export class DebugSession implements IDebugSession, IDisposable { // remove its parent, if it's still running if (!this.hasSeparateRepl() && this.raw?.isInShutdown === false) { this.repl = this.repl.clone(); - replListener.value = this.repl.onDidChangeElements(() => this._onDidChangeREPLElements.fire()); + replListener.value = this.repl.onDidChangeElements((e) => this._onDidChangeREPLElements.fire(e)); this.parentSession = undefined; } })); @@ -238,7 +240,9 @@ export class DebugSession implements IDebugSession, IDisposable { get autoExpandLazyVariables(): boolean { // This tiny helper avoids converting the entire debug model to use service injection - return this.configurationService.getValue('debug').autoExpandLazyVariables; + const screenReaderOptimized = this.accessibilityService.isScreenReaderOptimized(); + const value = this.configurationService.getValue('debug').autoExpandLazyVariables; + return value === 'auto' && screenReaderOptimized || value === 'on'; } setConfiguration(configuration: { resolved: IConfig; unresolved: IConfig | undefined }) { @@ -291,7 +295,7 @@ export class DebugSession implements IDebugSession, IDisposable { return this._onDidEndAdapter.event; } - get onDidChangeReplElements(): Event { + get onDidChangeReplElements(): Event { return this._onDidChangeREPLElements.event; } @@ -1370,7 +1374,7 @@ export class DebugSession implements IDebugSession, IDisposable { } const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; - if (!focusedStackFrame || !isFrameDeemphasized(focusedStackFrame)) { + if (!focusedStackFrame || isFrameDeemphasized(focusedStackFrame)) { // The top stack frame can be deemphesized so try to focus again #68616 focus(); } diff --git a/src/vs/workbench/contrib/debug/browser/debugSessionPicker.ts b/src/vs/workbench/contrib/debug/browser/debugSessionPicker.ts index 74ca56b6439..bc2417b216d 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSessionPicker.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSessionPicker.ts @@ -21,7 +21,7 @@ export async function showDebugSessionMenu(accessor: ServicesAccessor, selectAnd const commandService = accessor.get(ICommandService); const localDisposableStore = new DisposableStore(); - const quickPick = quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); localDisposableStore.add(quickPick); quickPick.matchOnLabel = quickPick.matchOnDescription = quickPick.matchOnDetail = quickPick.sortByLabel = false; quickPick.placeholder = nls.localize('moveFocusedView.selectView', 'Search debug sessions by name'); diff --git a/src/vs/workbench/contrib/debug/browser/debugSettingMigration.ts b/src/vs/workbench/contrib/debug/browser/debugSettingMigration.ts new file mode 100644 index 00000000000..cee1d887598 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugSettingMigration.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; + +Registry.as(Extensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: 'debug.autoExpandLazyVariables', + migrateFn: (value: boolean) => { + let newValue: string | undefined; + if (value === true) { + newValue = 'on'; + } else if (value === false) { + newValue = 'off'; + } + return [ + ['debug.autoExpandLazyVariables', { value: newValue }], + ]; + } + }]); diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 15de8bc4e40..d9c1fdc9217 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -3,35 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { Action } from 'vs/base/common/actions'; +import { disposableTimeout } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { createErrorWithActions } from 'vs/base/common/errorMessage'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import severity from 'vs/base/common/severity'; -import { Event } from 'vs/base/common/event'; -import { Markers } from 'vs/workbench/contrib/markers/common/markers'; -import { ITaskService, ITaskSummary } from 'vs/workbench/contrib/tasks/common/taskService'; +import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; -import { ITaskEvent, TaskEventKind, ITaskIdentifier, Task } from 'vs/workbench/contrib/tasks/common/tasks'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { createErrorWithActions } from 'vs/base/common/errorMessage'; -import { Action } from 'vs/base/common/actions'; +import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; +import { Markers } from 'vs/workbench/contrib/markers/common/markers'; +import { ConfiguringTask, CustomTask, ITaskEvent, ITaskIdentifier, Task, TaskEventKind } from 'vs/workbench/contrib/tasks/common/tasks'; +import { ITaskService, ITaskSummary } from 'vs/workbench/contrib/tasks/common/taskService'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -function once(match: (e: ITaskEvent) => boolean, event: Event): Event { - return (listener, thisArgs = null, disposables?) => { - const result = event(e => { - if (match(e)) { - result.dispose(); - return listener.call(thisArgs, e); - } - }, null, disposables); - return result; - }; -} +const onceFilter = (event: Event, filter: (e: ITaskEvent) => boolean) => Event.once(Event.filter(event, filter)); export const enum TaskRunResult { Failure, @@ -39,10 +33,17 @@ export const enum TaskRunResult { } const DEBUG_TASK_ERROR_CHOICE_KEY = 'debug.taskerrorchoice'; +const ABORT_LABEL = nls.localize('abort', "Abort"); +const DEBUG_ANYWAY_LABEL = nls.localize({ key: 'debugAnyway', comment: ['&& denotes a mnemonic'] }, "&&Debug Anyway"); +const DEBUG_ANYWAY_LABEL_NO_MEMO = nls.localize('debugAnywayNoMemo', "Debug Anyway"); -export class DebugTaskRunner { +interface IRunnerTaskSummary extends ITaskSummary { + cancelled?: boolean; +} - private canceled = false; +export class DebugTaskRunner implements IDisposable { + + private globalCancellation = new CancellationTokenSource(); constructor( @ITaskService private readonly taskService: ITaskService, @@ -51,18 +52,26 @@ export class DebugTaskRunner { @IViewsService private readonly viewsService: IViewsService, @IDialogService private readonly dialogService: IDialogService, @IStorageService private readonly storageService: IStorageService, - @ICommandService private readonly commandService: ICommandService + @ICommandService private readonly commandService: ICommandService, + @IProgressService private readonly progressService: IProgressService, ) { } cancel(): void { - this.canceled = true; + this.globalCancellation.dispose(true); + this.globalCancellation = new CancellationTokenSource(); } - async runTaskAndCheckErrors(root: IWorkspaceFolder | IWorkspace | undefined, taskId: string | ITaskIdentifier | undefined): Promise { + public dispose(): void { + this.globalCancellation.dispose(true); + } + + async runTaskAndCheckErrors( + root: IWorkspaceFolder | IWorkspace | undefined, + taskId: string | ITaskIdentifier | undefined, + ): Promise { try { - this.canceled = false; - const taskSummary = await this.runTask(root, taskId); - if (this.canceled || (taskSummary && taskSummary.exitCode === undefined)) { + const taskSummary = await this.runTask(root, taskId, this.globalCancellation.token); + if (taskSummary && (taskSummary.exitCode === undefined || taskSummary.cancelled)) { // User canceled, either debugging, or the prelaunch task return TaskRunResult.Failure; } @@ -101,7 +110,7 @@ export class DebugTaskRunner { message, buttons: [ { - label: nls.localize({ key: 'debugAnyway', comment: ['&& denotes a mnemonic'] }, "&&Debug Anyway"), + label: DEBUG_ANYWAY_LABEL, run: () => DebugChoice.DebugAnyway }, { @@ -110,7 +119,7 @@ export class DebugTaskRunner { } ], cancelButton: { - label: nls.localize('abort', "Abort"), + label: ABORT_LABEL, run: () => DebugChoice.Cancel }, checkbox: { @@ -182,7 +191,7 @@ export class DebugTaskRunner { } } - async runTask(root: IWorkspace | IWorkspaceFolder | undefined, taskId: string | ITaskIdentifier | undefined): Promise { + async runTask(root: IWorkspace | IWorkspaceFolder | undefined, taskId: string | ITaskIdentifier | undefined, token = this.globalCancellation.token): Promise { if (!taskId) { return Promise.resolve(null); } @@ -200,23 +209,42 @@ export class DebugTaskRunner { // If a task is missing the problem matcher the promise will never complete, so we need to have a workaround #35340 let taskStarted = false; + const store = new DisposableStore(); const getTaskKey = (t: Task) => t.getKey() ?? t.getMapKey(); const taskKey = getTaskKey(task); - const inactivePromise: Promise = new Promise((c) => once(e => { - // When a task isBackground it will go inactive when it is safe to launch. - // But when a background task is terminated by the user, it will also fire an inactive event. - // This means that we will not get to see the real exit code from running the task (undefined when terminated by the user). - // Catch the ProcessEnded event here, which occurs before inactive, and capture the exit code to prevent this. - return (e.kind === TaskEventKind.Inactive - || (e.kind === TaskEventKind.ProcessEnded && e.exitCode === undefined)) - && getTaskKey(e.__task) === taskKey; - }, this.taskService.onDidStateChange)(e => { - taskStarted = true; - c(e.kind === TaskEventKind.ProcessEnded ? { exitCode: e.exitCode } : null); - })); + const inactivePromise: Promise = new Promise((resolve) => store.add( + onceFilter(this.taskService.onDidStateChange, e => { + // When a task isBackground it will go inactive when it is safe to launch. + // But when a background task is terminated by the user, it will also fire an inactive event. + // This means that we will not get to see the real exit code from running the task (undefined when terminated by the user). + // Catch the ProcessEnded event here, which occurs before inactive, and capture the exit code to prevent this. + return (e.kind === TaskEventKind.Inactive + || (e.kind === TaskEventKind.ProcessEnded && e.exitCode === undefined)) + && getTaskKey(e.__task) === taskKey; + })(e => { + taskStarted = true; + resolve(e.kind === TaskEventKind.ProcessEnded ? { exitCode: e.exitCode } : null); + }), + )); - const promise: Promise = this.taskService.getActiveTasks().then(async (tasks): Promise => { + store.add( + onceFilter(this.taskService.onDidStateChange, e => ((e.kind === TaskEventKind.Active) || (e.kind === TaskEventKind.DependsOnStarted)) && getTaskKey(e.__task) === taskKey + )(() => { + // Task is active, so everything seems to be fine, no need to prompt after 10 seconds + // Use case being a slow running task should not be prompted even though it takes more than 10 seconds + taskStarted = true; + }) + ); + + const didAcquireInput = store.add(new Emitter()); + store.add(onceFilter( + this.taskService.onDidStateChange, + e => (e.kind === TaskEventKind.AcquiredInput) && getTaskKey(e.__task) === taskKey + )(() => didAcquireInput.fire())); + + const taskDonePromise: Promise = this.taskService.getActiveTasks().then(async (tasks): Promise => { if (tasks.find(t => getTaskKey(t) === taskKey)) { + didAcquireInput.fire(); // Check that the task isn't busy and if it is, wait for it const busyTasks = await this.taskService.getBusyTasks(); if (busyTasks.find(t => getTaskKey(t) === taskKey)) { @@ -226,11 +254,7 @@ export class DebugTaskRunner { // task is already running and isn't busy - nothing to do. return Promise.resolve(null); } - once(e => ((e.kind === TaskEventKind.Active) || (e.kind === TaskEventKind.DependsOnStarted)) && getTaskKey(e.__task) === taskKey, this.taskService.onDidStateChange)(() => { - // Task is active, so everything seems to be fine, no need to prompt after 10 seconds - // Use case being a slow running task should not be prompted even though it takes more than 10 seconds - taskStarted = true; - }); + const taskPromise = this.taskService.run(task); if (task.configurationProperties.isBackground) { return inactivePromise; @@ -239,28 +263,59 @@ export class DebugTaskRunner { return taskPromise.then(x => x ?? null); }); - return new Promise((c, e) => { - const waitForInput = new Promise(resolve => once(e => (e.kind === TaskEventKind.AcquiredInput) && getTaskKey(e.__task) === taskKey, this.taskService.onDidStateChange)(() => { - resolve(); + const result = new Promise((resolve, reject) => { + taskDonePromise.then(result => { + taskStarted = true; + resolve(result); + }, error => reject(error)); + + store.add(token.onCancellationRequested(() => { + resolve({ exitCode: undefined, cancelled: true }); + this.taskService.terminate(task).catch(() => { }); })); - promise.then(result => { - taskStarted = true; - c(result); - }, error => e(error)); - - waitForInput.then(() => { + // Start the timeouts once a terminal has been acquired + store.add(didAcquireInput.event(() => { const waitTime = task.configurationProperties.isBackground ? 5000 : 10000; - setTimeout(() => { + // Error shown if there's a background task with no problem matcher that doesn't exit quickly + store.add(disposableTimeout(() => { if (!taskStarted) { - const errorMessage = typeof taskId === 'string' - ? nls.localize('taskNotTrackedWithTaskId', "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined.", taskId) - : nls.localize('taskNotTracked', "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined.", JSON.stringify(taskId)); - e({ severity: severity.Error, message: errorMessage }); + const errorMessage = nls.localize('taskNotTracked', "The task '{0}' has not exited and doesn't have a 'problemMatcher' defined. Make sure to define a problem matcher for watch tasks.", typeof taskId === 'string' ? taskId : JSON.stringify(taskId)); + reject({ severity: severity.Error, message: errorMessage }); } - }, waitTime); - }); + }, waitTime)); + + // Notification shown on any task taking a while to resolve + store.add(disposableTimeout(() => { + const message = nls.localize('runningTask', "Waiting for preLaunchTask '{0}'...", task.configurationProperties.name); + const buttons = [DEBUG_ANYWAY_LABEL_NO_MEMO, ABORT_LABEL]; + const canConfigure = task instanceof CustomTask || task instanceof ConfiguringTask; + if (canConfigure) { + buttons.splice(1, 0, nls.localize('configureTask', "Configure Task")); + } + + this.progressService.withProgress( + { location: ProgressLocation.Notification, title: message, buttons }, + () => result.catch(() => { }), + (choice) => { + if (choice === undefined) { + // no-op, keep waiting + } else if (choice === 0) { // debug anyway + resolve({ exitCode: 0 }); + } else { // abort or configure + resolve({ exitCode: undefined, cancelled: true }); + this.taskService.terminate(task).catch(() => { }); + if (canConfigure && choice === 1) { // configure + this.taskService.openConfig(task as CustomTask); + } + } + } + ); + }, 10_000)); + })); }); + + return result.finally(() => store.dispose()); } } diff --git a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index 92f139a3721..62923aedc28 100644 --- a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -247,6 +247,8 @@ export class DisassemblyView extends EditorPane { } )) as WorkbenchTable; + this._disassembledInstructions.domNode.classList.add('disassembly-view'); + if (this.focusedInstructionReference) { this.reloadDisassembly(this.focusedInstructionReference, 0); } @@ -661,8 +663,7 @@ class BreakpointRenderer implements ITableRenderer; private menu: IMenu; + private replDataSource: IAsyncDataSource | undefined; + private findIsOpen: boolean = false; constructor( options: IViewPaneOptions, @@ -128,10 +133,10 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { @ICodeEditorService codeEditorService: ICodeEditorService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IContextMenuService contextMenuService: IContextMenuService, - @IConfigurationService configurationService: IConfigurationService, + @IConfigurationService protected override readonly configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, @IEditorService private readonly editorService: IEditorService, - @IKeybindingService keybindingService: IKeybindingService, + @IKeybindingService protected override readonly keybindingService: IKeybindingService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, @IHoverService hoverService: IHoverService, @@ -232,7 +237,9 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.filter.filterQuery = this.filterWidget.getFilterText(); if (this.tree) { this.tree.refilter(); - revealLastElement(this.tree); + if (!this.findIsOpen || this.filterWidget.hasFocus()) { + revealLastElement(this.tree); + } } })); } @@ -345,6 +352,10 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.filterWidget.focus(); } + openFind(): void { + this.tree?.openFind(); + } + private setMode(): void { if (!this.isVisible()) { return; @@ -522,10 +533,26 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.tree?.collapseAll(); } + getDebugSession(): IDebugSession | undefined { + return this.tree?.getInput(); + } + getReplInput(): CodeEditorWidget { return this.replInput; } + getReplDataSource(): IAsyncDataSource | undefined { + return this.replDataSource; + } + + getFocusedElement(): IReplElement | undefined { + return this.tree?.getFocus()?.[0]; + } + + focusTree(): void { + this.tree?.domFocus(); + } + override focus(): void { super.focus(); setTimeout(() => this.replInput.focus(), 0); @@ -624,6 +651,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { const wordWrap = this.configurationService.getValue('debug').console.wordWrap; this.treeContainer.classList.toggle('word-wrap', wordWrap); const linkDetector = this.instantiationService.createInstance(LinkDetector); + this.replDataSource = new ReplDataSource(); const tree = this.tree = >this.instantiationService.createInstance( WorkbenchAsyncDataTree, @@ -638,14 +666,13 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { new ReplEvaluationResultsRenderer(linkDetector, this.hoverService), new ReplRawObjectsRenderer(linkDetector, this.hoverService), ], - // https://github.com/microsoft/TypeScript/issues/32526 - new ReplDataSource() satisfies IAsyncDataSource, + this.replDataSource, { filter: this.filter, accessibilityProvider: new ReplAccessibilityProvider(), identityProvider, mouseSupport: false, - findWidgetEnabled: false, + findWidgetEnabled: true, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e.toString(true) }, horizontalScrolling: !wordWrap, setRowLineHeight: false, @@ -670,11 +697,16 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { })); this._register(tree.onContextMenu(e => this.onContextMenu(e))); + this._register(tree.onDidChangeFindOpenState((open) => this.findIsOpen = open)); + let lastSelectedString: string; this._register(tree.onMouseClick(() => { + if (this.findIsOpen) { + return; + } const selection = dom.getWindow(this.treeContainer).getSelection(); if (!selection || selection.type !== 'Range' || lastSelectedString === selection.toString()) { - // only focus the input if the user is not currently selecting. + // only focus the input if the user is not currently selecting and find isn't open. this.replInput.focus(); } lastSelectedString = selection ? selection.toString() : ''; @@ -702,7 +734,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { options.suggest = { showStatusBar: true }; const config = this.configurationService.getValue('debug'); options.acceptSuggestionOnEnter = config.console.acceptSuggestionOnEnter === 'on' ? 'on' : 'off'; - options.ariaLabel = localize('debugConsole', "Debug Console"); + options.ariaLabel = this.getAriaLabel(); this.replInput = this.scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, options, getSimpleCodeEditorWidgetOptions()); @@ -725,6 +757,21 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this._register(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.BLUR, () => this.replInputContainer.classList.remove('synthetic-focus'))); } + private getAriaLabel(): string { + let ariaLabel = localize('debugConsole', "Debug Console"); + if (!this.configurationService.getValue(AccessibilityVerbositySettingId.Debug)) { + return ariaLabel; + } + const keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getAriaLabel(); + if (keybinding) { + ariaLabel = localize('commentLabelWithKeybinding', "{0}, use ({1}) for accessibility help", ariaLabel, keybinding); + } else { + ariaLabel = localize('commentLabelWithKeybindingNoKeybinding', "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding.", ariaLabel); + } + + return ariaLabel; + } + private onContextMenu(e: ITreeContextMenuEvent): void { const actions: IAction[] = []; createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, actions); @@ -861,8 +908,8 @@ class AcceptReplInputAction extends EditorAction { constructor() { super({ id: 'repl.action.acceptInput', - label: localize({ key: 'actions.repl.acceptInput', comment: ['Apply input from the debug console input box'] }, "REPL Accept Input"), - alias: 'REPL Accept Input', + label: localize({ key: 'actions.repl.acceptInput', comment: ['Apply input from the debug console input box'] }, "Debug Console: Accept Input"), + alias: 'Debug Console: Accept Input', precondition: CONTEXT_IN_DEBUG_REPL, kbOpts: { kbExpr: EditorContextKeys.textInputFocus, @@ -879,25 +926,57 @@ class AcceptReplInputAction extends EditorAction { } } -class FilterReplAction extends EditorAction { +class FilterReplAction extends ViewAction { constructor() { super({ + viewId: REPL_VIEW_ID, id: 'repl.action.filter', - label: localize('repl.action.filter', "REPL Focus Content to Filter"), - alias: 'REPL Filter', + title: localize('repl.action.filter', "Debug Console: Focus Filter"), precondition: CONTEXT_IN_DEBUG_REPL, - kbOpts: { - kbExpr: EditorContextKeys.textInputFocus, + keybinding: [{ + when: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.KeyF, weight: KeybindingWeight.EditorContrib - } + }] }); } - run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { - const repl = getReplView(accessor.get(IViewsService)); - repl?.focusFilter(); + runInView(accessor: ServicesAccessor, repl: Repl): void | Promise { + repl.focusFilter(); + } +} + + +class FindReplAction extends ViewAction { + + constructor() { + super({ + viewId: REPL_VIEW_ID, + id: 'repl.action.find', + title: localize('repl.action.find', "Debug Console: Focus Find"), + precondition: ContextKeyExpr.or(CONTEXT_IN_DEBUG_REPL, ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view')), + keybinding: [{ + when: ContextKeyExpr.or(CONTEXT_IN_DEBUG_REPL, ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view')), + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyF, + weight: KeybindingWeight.EditorContrib + }], + icon: Codicon.search, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyExpr.equals('view', REPL_VIEW_ID), + order: 15 + }, { + id: MenuId.DebugConsoleContext, + group: 'z_commands', + order: 25 + }], + }); + } + + runInView(accessor: ServicesAccessor, view: Repl): void | Promise { + view.openFind(); } } @@ -923,7 +1002,8 @@ class ReplCopyAllAction extends EditorAction { registerEditorAction(AcceptReplInputAction); registerEditorAction(ReplCopyAllAction); -registerEditorAction(FilterReplAction); +registerAction2(FilterReplAction); +registerAction2(FindReplAction); class SelectReplActionViewItem extends FocusSessionActionViewItem { @@ -939,7 +1019,7 @@ class SelectReplActionViewItem extends FocusSessionActionViewItem { } } -function getReplView(viewsService: IViewsService): Repl | undefined { +export function getReplView(viewsService: IViewsService): Repl | undefined { return viewsService.getActiveViewWithId(REPL_VIEW_ID) as Repl ?? undefined; } @@ -998,7 +1078,15 @@ registerAction2(class extends ViewAction { id: MenuId.DebugConsoleContext, group: 'z_commands', order: 20 - }] + }], + keybinding: [{ + primary: 0, + mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyK }, + // Weight is higher than work workbench contributions so the keybinding remains + // highest priority when chords are registered afterwards + weight: KeybindingWeight.WorkbenchContrib + 1, + when: ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view') + }], }); } diff --git a/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts b/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts new file mode 100644 index 00000000000..4cf2efbc9ca --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { getReplView, Repl } from 'vs/workbench/contrib/debug/browser/repl'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { localize } from 'vs/nls'; + +export class ReplAccessibilityHelp implements IAccessibleViewImplentation { + priority = 120; + name = 'replHelp'; + when = ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view'); + type: AccessibleViewType = AccessibleViewType.Help; + getProvider(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const replView = getReplView(viewsService); + if (!replView) { + return undefined; + } + return new ReplAccessibilityHelpProvider(replView); + } +} + +class ReplAccessibilityHelpProvider extends Disposable implements IAccessibleViewContentProvider { + public readonly id = AccessibleViewProviderId.ReplHelp; + public readonly verbositySettingKey = AccessibilityVerbositySettingId.Debug; + public readonly options = { type: AccessibleViewType.Help }; + private _treeHadFocus = false; + constructor(private readonly _replView: Repl) { + super(); + this._treeHadFocus = !!_replView.getFocusedElement(); + } + + public onClose(): void { + if (this._treeHadFocus) { + return this._replView.focusTree(); + } + this._replView.getReplInput().focus(); + } + + public provideContent(): string { + return [ + localize('repl.help', "The debug console is a Read-Eval-Print-Loop that allows you to evaluate expressions and run commands and can be focused with{0}.", ''), + localize('repl.output', "The debug console output can be navigated to from the input field with the Focus Previous Widget command{0}.", ''), + localize('repl.input', "The debug console input can be navigated to from the output with the Focus Next Widget command{0}.", ''), + localize('repl.history', "The debug console output history can be navigated with the up and down arrow keys."), + localize('repl.accessibleView', "The Open Accessible View command{0} will allow character by character navigation of the console output.", ''), + localize('repl.showRunAndDebug', "The Show Run and Debug view command{0} will open the Run and Debug view and provides more information about debugging.", ''), + localize('repl.clear', "The Debug: Clear Console command{0} will clear the console output.", ''), + localize('repl.lazyVariables', "The setting `debug.expandLazyVariables` controls whether variables are evaluated automatically. This is enabled by default when using a screen reader."), + ].join('\n'); + } +} + diff --git a/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts b/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts new file mode 100644 index 00000000000..891598edb4b --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IReplElement } from 'vs/workbench/contrib/debug/common/debug'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { getReplView, Repl } from 'vs/workbench/contrib/debug/browser/repl'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Position } from 'vs/editor/common/core/position'; + +export class ReplAccessibleView implements IAccessibleViewImplentation { + priority = 70; + name = 'debugConsole'; + when = ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view'); + type: AccessibleViewType = AccessibleViewType.View; + getProvider(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const accessibleViewService = accessor.get(IAccessibleViewService); + const replView = getReplView(viewsService); + if (!replView) { + return undefined; + } + + const focusedElement = replView.getFocusedElement(); + return new ReplOutputAccessibleViewProvider(replView, focusedElement, accessibleViewService); + } +} + +class ReplOutputAccessibleViewProvider extends Disposable implements IAccessibleViewContentProvider { + public readonly id = AccessibleViewProviderId.Repl; + private _content: string | undefined; + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + private readonly _onDidResolveChildren: Emitter = this._register(new Emitter()); + public readonly onDidResolveChildren: Event = this._onDidResolveChildren.event; + + public readonly verbositySettingKey = AccessibilityVerbositySettingId.Debug; + public readonly options = { + type: AccessibleViewType.View + }; + + private _elementPositionMap: Map = new Map(); + private _treeHadFocus = false; + + constructor( + private readonly _replView: Repl, + private readonly _focusedElement: IReplElement | undefined, + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService) { + super(); + this._treeHadFocus = !!_focusedElement; + } + public provideContent(): string { + const debugSession = this._replView.getDebugSession(); + if (!debugSession) { + return 'No debug session available.'; + } + const elements = debugSession.getReplElements(); + if (!elements.length) { + return 'No output in the debug console.'; + } + if (!this._content) { + this._updateContent(elements); + } + // Content is loaded asynchronously, so we need to check if it's available or fallback to the elements that are already available. + return this._content ?? elements.map(e => e.toString(true)).join('\n'); + } + + public onClose(): void { + this._content = undefined; + this._elementPositionMap.clear(); + if (this._treeHadFocus) { + return this._replView.focusTree(); + } + this._replView.getReplInput().focus(); + } + + public onOpen(): void { + // Children are resolved async, so we need to update the content when they are resolved. + this._register(this.onDidResolveChildren(() => { + this._onDidChangeContent.fire(); + queueMicrotask(() => { + if (this._focusedElement) { + const position = this._elementPositionMap.get(this._focusedElement.getId()); + if (position) { + this._accessibleViewService.setPosition(position, true); + } + } + }); + })); + } + + private async _updateContent(elements: IReplElement[]) { + const dataSource = this._replView.getReplDataSource(); + if (!dataSource) { + return; + } + let line = 1; + const content: string[] = []; + for (const e of elements) { + content.push(e.toString().replace(/\n/g, '')); + this._elementPositionMap.set(e.getId(), new Position(line, 1)); + line++; + if (dataSource.hasChildren(e)) { + const childContent: string[] = []; + const children = await dataSource.getChildren(e); + for (const child of children) { + const id = child.getId(); + if (!this._elementPositionMap.has(id)) { + // don't overwrite parent position + this._elementPositionMap.set(id, new Position(line, 1)); + } + childContent.push(' ' + child.toString()); + line++; + } + content.push(childContent.join('\n')); + } + } + + this._content = content.join('\n'); + this._onDidResolveChildren.fire(); + } +} diff --git a/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts b/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts new file mode 100644 index 00000000000..01b912abf1e --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; +import { FocusedViewContext, SidebarFocusContext } from 'vs/workbench/common/contextkeys'; +import { BREAKPOINTS_VIEW_ID, CALLSTACK_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, VARIABLES_VIEW_ID, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; + +export class RunAndDebugAccessibilityHelp implements IAccessibleViewImplentation { + priority = 120; + name = 'runAndDebugHelp'; + when = ContextKeyExpr.or( + ContextKeyExpr.and(ContextKeyExpr.equals('activeViewlet', 'workbench.view.debug'), SidebarFocusContext), + ContextKeyExpr.equals(FocusedViewContext.key, VARIABLES_VIEW_ID), + ContextKeyExpr.equals(FocusedViewContext.key, WATCH_VIEW_ID), + ContextKeyExpr.equals(FocusedViewContext.key, CALLSTACK_VIEW_ID), + ContextKeyExpr.equals(FocusedViewContext.key, LOADED_SCRIPTS_VIEW_ID), + ContextKeyExpr.equals(FocusedViewContext.key, BREAKPOINTS_VIEW_ID) + ); + type: AccessibleViewType = AccessibleViewType.Help; + getProvider(accessor: ServicesAccessor) { + return new RunAndDebugAccessibilityHelpProvider(accessor.get(ICommandService), accessor.get(IViewsService)); + } +} + +class RunAndDebugAccessibilityHelpProvider extends Disposable implements IAccessibleViewContentProvider { + public readonly id = AccessibleViewProviderId.RunAndDebug; + public readonly verbositySettingKey = AccessibilityVerbositySettingId.Debug; + public readonly options = { type: AccessibleViewType.Help }; + private _focusedView: string | undefined; + constructor( + @ICommandService private readonly _commandService: ICommandService, + @IViewsService private readonly _viewsService: IViewsService + ) { + super(); + this._focusedView = this._viewsService.getFocusedViewName(); + } + + public onClose(): void { + switch (this._focusedView) { + case 'Watch': + this._commandService.executeCommand('workbench.debug.action.focusWatchView'); + break; + case 'Variables': + this._commandService.executeCommand('workbench.debug.action.focusVariablesView'); + break; + case 'Call Stack': + this._commandService.executeCommand('workbench.debug.action.focusCallStackView'); + break; + case 'Breakpoints': + this._commandService.executeCommand('workbench.debug.action.focusBreakpointsView'); + break; + default: + this._commandService.executeCommand('workbench.view.debug'); + } + } + + public provideContent(): string { + return [ + localize('debug.showRunAndDebug', "The Show Run and Debug view command{0} will open the current view.", ''), + localize('debug.startDebugging', "The Debug: Start Debugging command{0} will start a debug session.", ''), + AccessibilityHelpNLS.setBreakpoint, + AccessibilityHelpNLS.addToWatch, + localize('onceDebugging', "Once debugging, the following commands will be available:"), + localize('debug.restartDebugging', "- Debug: Restart Debugging command{0} will restart the current debug session.", ''), + localize('debug.stopDebugging', "- Debug: Stop Debugging command{0} will stop the current debugging session.", ''), + localize('debug.continue', "- Debug: Continue command{0} will continue execution until the next breakpoint.", ''), + localize('debug.stepInto', "- Debug: Step Into command{0} will step into the next function call.", ''), + localize('debug.stepOver', "- Debug: Step Over command{0} will step over the current function call.", ''), + localize('debug.stepOut', "- Debug: Step Out command{0} will step out of the current function call.", ''), + localize('debug.views', 'The debug viewlet is comprised of several views that can be focused with the following commands or navigated to via tab then arrow keys:'), + localize('debug.focusBreakpoints', "- Debug: Focus Breakpoints View command{0} will focus the breakpoints view.", ''), + localize('debug.focusCallStack', "- Debug: Focus Call Stack View command{0} will focus the call stack view.", ''), + localize('debug.focusVariables', "- Debug: Focus Variables View command{0} will focus the variables view.", ''), + localize('debug.focusWatch', "- Debug: Focus Watch View command{0} will focus the watch view.", ''), + localize('debug.help', "The debug console is a Read-Eval-Print-Loop that allows you to evaluate expressions and run commands and can be focused with{0}.", ''), + localize('debug.watchSetting', "The setting {0} controls whether watch variable changes are announced.", 'accessibility.debugWatchVariableAnnouncements'), + ].join('\n'); + } +} + diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 41f1a55b557..96785a6403e 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -401,7 +401,7 @@ export interface IDebugSession extends ITreeElement { // session events readonly onDidEndAdapter: Event; readonly onDidChangeState: Event; - readonly onDidChangeReplElements: Event; + readonly onDidChangeReplElements: Event; // DA capabilities readonly capabilities: DebugProtocol.Capabilities; @@ -743,7 +743,14 @@ export interface IDebugModel extends ITreeElement { getBreakpointModes(forBreakpointType: 'source' | 'exception' | 'data' | 'instruction'): DebugProtocol.BreakpointMode[]; onDidChangeBreakpoints: Event; onDidChangeCallStack: Event; + /** + * The expression has been added, removed, or repositioned. + */ onDidChangeWatchExpressions: Event; + /** + * The expression's value has changed. + */ + onDidChangeWatchExpressionValue: Event; fetchCallstack(thread: IThread, levels?: number): Promise; } @@ -792,7 +799,7 @@ export interface IDebugConfiguration { disassemblyView: { showSourceCode: boolean; }; - autoExpandLazyVariables: boolean; + autoExpandLazyVariables: 'auto' | 'off' | 'on'; enableStatusBarColor: boolean; showVariableTypes: boolean; } diff --git a/src/vs/workbench/contrib/debug/common/debugAccessibilityAnnouncer.ts b/src/vs/workbench/contrib/debug/common/debugAccessibilityAnnouncer.ts new file mode 100644 index 00000000000..4c8bfe340e6 --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/debugAccessibilityAnnouncer.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; +import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Expression } from 'vs/workbench/contrib/debug/common/debugModel'; + +export class DebugWatchAccessibilityAnnouncer extends Disposable implements IWorkbenchContribution { + static ID = 'workbench.contrib.debugWatchAccessibilityAnnouncer'; + private readonly _listener: MutableDisposable = this._register(new MutableDisposable()); + constructor( + @IDebugService private readonly _debugService: IDebugService, + @ILogService private readonly _logService: ILogService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IConfigurationService private readonly _configurationService: IConfigurationService + ) { + super(); + this._setListener(); + this._register(_configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('accessibility.debugWatchVariableAnnouncements')) { + this._setListener(); + } + })); + } + + private _setListener(): void { + const value = this._configurationService.getValue('accessibility.debugWatchVariableAnnouncements'); + if (value && !this._listener.value) { + this._listener.value = this._debugService.getModel().onDidChangeWatchExpressionValue((e) => { + if (!e || e.value === Expression.DEFAULT_VALUE) { + return; + } + + // TODO: get user feedback, perhaps setting to configure verbosity + whether value, name, neither, or both are announced + this._accessibilityService.alert(`${e.name} = ${e.value}`); + this._logService.trace(`debugAccessibilityAnnouncerValueChanged ${e.name} ${e.value}`); + }); + } else { + this._listener.clear(); + } + } +} diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index f9c67ec8958..036074f519a 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -10,7 +10,7 @@ import { VSBuffer, decodeBase64, encodeBase64 } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { stringHash } from 'vs/base/common/hash'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; import { mixin } from 'vs/base/common/objects'; import { autorun } from 'vs/base/common/observable'; import * as resources from 'vs/base/common/resources'; @@ -298,6 +298,9 @@ export class Expression extends ExpressionContainer implements IExpression { public available: boolean; + private readonly _onDidChangeValue = new Emitter(); + public readonly onDidChangeValue: Event = this._onDidChangeValue.event; + constructor(public name: string, id = generateUuid()) { super(undefined, undefined, 0, id); this.available = false; @@ -309,7 +312,11 @@ export class Expression extends ExpressionContainer implements IExpression { } async evaluate(session: IDebugSession | undefined, stackFrame: IStackFrame | undefined, context: string, keepLazyVars?: boolean, location?: IDebugEvaluatePosition): Promise { + const hadDefaultValue = this.value === Expression.DEFAULT_VALUE; this.available = await this.evaluateExpression(this.name, session, stackFrame, context, keepLazyVars, location); + if (hadDefaultValue || this.valueChanged) { + this._onDidChangeValue.fire(this); + } } override toString(): string { @@ -1403,12 +1410,14 @@ export class DebugModel extends Disposable implements IDebugModel { private readonly _onDidChangeBreakpoints = this._register(new Emitter()); private readonly _onDidChangeCallStack = this._register(new Emitter()); private readonly _onDidChangeWatchExpressions = this._register(new Emitter()); + private readonly _onDidChangeWatchExpressionValue = this._register(new Emitter()); private readonly _breakpointModes = new Map(); private breakpoints!: Breakpoint[]; private functionBreakpoints!: FunctionBreakpoint[]; private exceptionBreakpoints!: ExceptionBreakpoint[]; private dataBreakpoints!: DataBreakpoint[]; private watchExpressions!: Expression[]; + private watchExpressionChangeListeners: DisposableMap = this._register(new DisposableMap()); private instructionBreakpoints: InstructionBreakpoint[]; constructor( @@ -1434,6 +1443,10 @@ export class DebugModel extends Disposable implements IDebugModel { this.instructionBreakpoints = []; this.sessions = []; + + for (const we of this.watchExpressions) { + this.watchExpressionChangeListeners.set(we.getId(), we.onDidChangeValue((e) => this._onDidChangeWatchExpressionValue.fire(e))); + } } getId(): string { @@ -1497,6 +1510,10 @@ export class DebugModel extends Disposable implements IDebugModel { return this._onDidChangeWatchExpressions.event; } + get onDidChangeWatchExpressionValue(): Event { + return this._onDidChangeWatchExpressionValue.event; + } + rawUpdate(data: IRawModelUpdate): void { const session = this.sessions.find(p => p.getId() === data.sessionId); if (session) { @@ -2003,6 +2020,7 @@ export class DebugModel extends Disposable implements IDebugModel { addWatchExpression(name?: string): IExpression { const we = new Expression(name || ''); + this.watchExpressionChangeListeners.set(we.getId(), we.onDidChangeValue((e) => this._onDidChangeWatchExpressionValue.fire(e))); this.watchExpressions.push(we); this._onDidChangeWatchExpressions.fire(we); @@ -2020,6 +2038,11 @@ export class DebugModel extends Disposable implements IDebugModel { removeWatchExpressions(id: string | null = null): void { this.watchExpressions = id ? this.watchExpressions.filter(we => we.getId() !== id) : []; this._onDidChangeWatchExpressions.fire(undefined); + if (!id) { + this.watchExpressionChangeListeners.clearAndDisposeAll(); + return; + } + this.watchExpressionChangeListeners.deleteAndDispose(id); } moveWatchExpression(id: string, position: number): void { diff --git a/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts b/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts index 5e9bb1fa33e..1784d05a1d8 100644 --- a/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts +++ b/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts @@ -35,7 +35,7 @@ export async function showLoadedScriptMenu(accessor: ServicesAccessor) { const labelService = accessor.get(ILabelService); const localDisposableStore = new DisposableStore(); - const quickPick = quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); localDisposableStore.add(quickPick); quickPick.matchOnLabel = quickPick.matchOnDescription = quickPick.matchOnDetail = quickPick.sortByLabel = false; quickPick.placeholder = nls.localize('moveFocusedView.selectView', "Search loaded scripts by name"); diff --git a/src/vs/workbench/contrib/debug/common/replAccessibilityAnnouncer.ts b/src/vs/workbench/contrib/debug/common/replAccessibilityAnnouncer.ts new file mode 100644 index 00000000000..9199bd55245 --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/replAccessibilityAnnouncer.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; + +export class ReplAccessibilityAnnouncer extends Disposable implements IWorkbenchContribution { + static ID = 'debug.replAccessibilityAnnouncer'; + constructor( + @IDebugService debugService: IDebugService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @ILogService logService: ILogService + ) { + super(); + const viewModel = debugService.getViewModel(); + this._register(viewModel.onDidFocusSession((session) => { + if (!session) { + return; + } + this._register(session.onDidChangeReplElements((element) => { + if (!element || !('originalExpression' in element)) { + // element was removed or hasn't been resolved yet + return; + } + const value = element.toString(); + accessibilityService.status(value); + logService.trace('ReplAccessibilityAnnouncer#onDidChangeReplElements', element.originalExpression + ': ' + value); + })); + })); + } +} diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index 6556849e5e9..91be45421b1 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -260,7 +260,7 @@ export interface INewReplElementData { export class ReplModel { private replElements: IReplElement[] = []; - private readonly _onDidChangeElements = new Emitter(); + private readonly _onDidChangeElements = new Emitter(); readonly onDidChangeElements = this._onDidChangeElements.event; constructor(private readonly configurationService: IConfigurationService) { } @@ -306,7 +306,7 @@ export class ReplModel { if (!previousElement.value.endsWith('\n') && !previousElement.value.endsWith('\r\n') && previousElement.count === 1) { this.replElements[this.replElements.length - 1] = new ReplOutputElement( session, getUniqueId(), previousElement.value + output, sev, source); - this._onDidChangeElements.fire(); + this._onDidChangeElements.fire(undefined); return; } } @@ -337,14 +337,13 @@ export class ReplModel { this.replElements.splice(0, this.replElements.length - MAX_REPL_LENGTH); } } - - this._onDidChangeElements.fire(); + this._onDidChangeElements.fire(newElement); } removeReplExpressions(): void { if (this.replElements.length > 0) { this.replElements = []; - this._onDidChangeElements.fire(); + this._onDidChangeElements.fire(undefined); } } diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index 4275f9e0a89..43f92b4552f 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -222,13 +222,26 @@ export class ExecutableDebugAdapter extends StreamDebugAdapter { throw new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter.")); } } else { + let spawnCommand = command; + let spawnArgs = args; const spawnOptions: cp.SpawnOptions = { env: env }; if (options.cwd) { spawnOptions.cwd = options.cwd; } - this.serverProcess = cp.spawn(command, args, spawnOptions); + if (platform.isWindows && (command.endsWith('.bat') || command.endsWith('.cmd'))) { + // https://github.com/microsoft/vscode/issues/224184 + spawnOptions.shell = true; + spawnCommand = `"${command}"`; + spawnArgs = args.map(a => { + a = a.replace(/"/g, '\\"'); // Escape existing double quotes with \ + // Wrap in double quotes + return `"${a}"`; + }); + } + + this.serverProcess = cp.spawn(spawnCommand, spawnArgs, spawnOptions); } this.serverProcess.on('error', err => { diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index e4473594e4c..e5da4f5392d 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -10,6 +10,7 @@ import { Constants } from 'vs/base/common/uint'; import { generateUuid } from 'vs/base/common/uuid'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; +import { TestAccessibilityService } from 'vs/platform/accessibility/test/common/testAccessibilityService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -41,7 +42,7 @@ export function createTestSession(model: DebugModel, name = 'mockSession', optio } }; } - } as IDebugService, undefined!, undefined!, new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }), undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService(), undefined!, undefined!); + } as IDebugService, undefined!, undefined!, new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }), undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService(), undefined!, undefined!, new TestAccessibilityService()); } function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame; secondStackFrame: StackFrame } { @@ -445,7 +446,7 @@ suite('Debug - CallStack', () => { override get state(): State { return State.Stopped; } - }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService(), undefined!, undefined!); + }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService(), undefined!, undefined!, new TestAccessibilityService()); disposables.add(session); const runningSession = createTestSession(model); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts index 6cde637373c..d6b8d2fc244 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts @@ -61,7 +61,8 @@ suite('debugConfigurationManager', () => { new TestExtensionService(), new TestHistoryService(), new UriIdentityService(fileService), - new ContextKeyService(configurationService)); + new ContextKeyService(configurationService), + new NullLogService()); }); teardown(() => disposables.dispose()); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 464a4794def..2cbd91f7e6d 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -258,7 +258,7 @@ export class MockSession implements IDebugSession { } removeReplExpressions(): void { } - get onDidChangeReplElements(): Event { + get onDidChangeReplElements(): Event { throw new Error('not implemented'); } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index deb9b7e7548..09272cd2e8b 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -960,7 +960,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } private async pickContinueEditSessionDestination(): Promise { - const quickPick = this.quickInputService.createQuickPick(); + const quickPick = this.quickInputService.createQuickPick({ useSeparators: true }); const workspaceContext = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? this.contextService.getWorkspace().folders[0].name diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index f612c22c3e3..a453b5f8676 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -311,7 +311,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes * Prompts the user to pick an authentication option for storing and getting edit sessions. */ private async getAccountPreference(reason: 'read' | 'write'): Promise { - const quickpick = this.quickInputService.createQuickPick(); + const quickpick = this.quickInputService.createQuickPick({ useSeparators: true }); quickpick.ok = false; quickpick.placeholder = reason === 'read' ? localize('choose account read placeholder', "Select an account to restore your working changes from the cloud") : localize('choose account placeholder', "Select an account to store your working changes in the cloud"); quickpick.ignoreFocusOut = true; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index d3109d7f7d9..5b54f2c850b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -471,9 +471,16 @@ export class ExtensionEditor extends EditorPane { const currentOptions: IExtensionEditorOptions | undefined = this.options; super.setOptions(options); this.updatePreReleaseVersionContext(); + if (this.input && this.template && currentOptions?.showPreReleaseVersion !== options?.showPreReleaseVersion) { this.render((this.input as ExtensionsInput).extension, this.template, !!options?.preserveFocus); + return; } + + if (options?.tab) { + this.template?.navbar.switch(options.tab); + } + } private updatePreReleaseVersionContext(): void { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 697fafb73d8..60aa8d8ae66 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -641,7 +641,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: MenuId.ViewContainerTitle, order: 5, group: '1_updates', - when: enableAutoUpdateWhenCondition + when: ContextKeyExpr.and(ContextKeyExpr.equals('viewContainer', VIEWLET_ID), enableAutoUpdateWhenCondition) }, { id: MenuId.CommandPalette, }], @@ -658,7 +658,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: MenuId.ViewContainerTitle, order: 5, group: '1_updates', - when: disableAutoUpdateWhenCondition + when: ContextKeyExpr.and(ContextKeyExpr.equals('viewContainer', VIEWLET_ID), disableAutoUpdateWhenCondition) }, { id: MenuId.CommandPalette, }], diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 3f7e240ced1..c5d00d34e75 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -855,12 +855,14 @@ export class UninstallAction extends ExtensionAction { await this.extensionsWorkbenchService.uninstall(this.extension); alert(localize('uninstallExtensionComplete', "Please reload Visual Studio Code to complete the uninstallation of the extension {0}.", this.extension.displayName)); } catch (error) { - this.dialogService.error(getErrorMessage(error)); + if (!isCancellationError(error)) { + this.dialogService.error(getErrorMessage(error)); + } } } } -abstract class AbstractUpdateAction extends ExtensionAction { +export class UpdateAction extends ExtensionAction { private static readonly EnabledClass = `${this.LABEL_ACTION_CLASS} prominent update`; private static readonly DisabledClass = `${this.EnabledClass} disabled`; @@ -868,15 +870,21 @@ abstract class AbstractUpdateAction extends ExtensionAction { private readonly updateThrottler = new Throttler(); constructor( - id: string, label: string | undefined, - protected readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + private readonly verbose: boolean, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IDialogService private readonly dialogService: IDialogService, + @IOpenerService private readonly openerService: IOpenerService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { - super(id, label, AbstractUpdateAction.DisabledClass, false); + super(`extensions.update`, localize('update', "Update"), UpdateAction.DisabledClass, false); this.update(); } update(): void { this.updateThrottler.queue(() => this.computeAndUpdateEnablement()); + if (this.extension) { + this.label = this.verbose ? localize('update to', "Update to v{0}", this.extension.latestVersion) : localize('update', "Update"); + } } private async computeAndUpdateEnablement(): Promise { @@ -895,31 +903,45 @@ abstract class AbstractUpdateAction extends ExtensionAction { const isInstalled = this.extension.state === ExtensionState.Installed; this.enabled = canInstall && isInstalled && this.extension.outdated; - this.class = this.enabled ? AbstractUpdateAction.EnabledClass : AbstractUpdateAction.DisabledClass; - } -} - -export class UpdateAction extends AbstractUpdateAction { - - constructor( - private readonly verbose: boolean, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - ) { - super(`extensions.update`, localize('update', "Update"), extensionsWorkbenchService); - } - - override update(): void { - super.update(); - if (this.extension) { - this.label = this.verbose ? localize('update to', "Update to v{0}", this.extension.latestVersion) : localize('update', "Update"); - } + this.class = this.enabled ? UpdateAction.EnabledClass : UpdateAction.DisabledClass; } override async run(): Promise { if (!this.extension) { return; } + + const consent = await this.extensionsWorkbenchService.shouldRequireConsentToUpdate(this.extension); + if (consent) { + const { result } = await this.dialogService.prompt<'update' | 'review' | 'cancel'>({ + type: 'warning', + title: localize('updateExtensionConsentTitle', "Update {0} Extension", this.extension.displayName), + message: localize('updateExtensionConsent', "{0}\n\nWould you like to update the extension?", consent), + buttons: [{ + label: localize('update', "Update"), + run: () => 'update' + }, { + label: localize('review', "Review"), + run: () => 'review' + }, { + label: localize('cancel', "Cancel"), + run: () => 'cancel' + }] + }); + if (result === 'cancel') { + return; + } + if (result === 'review') { + if (this.extension.hasChangelog()) { + return this.extensionsWorkbenchService.open(this.extension, { tab: ExtensionEditorTab.Changelog }); + } + if (this.extension.repository) { + return this.openerService.open(this.extension.repository); + } + return this.extensionsWorkbenchService.open(this.extension); + } + } + alert(localize('updateExtensionStart', "Updating extension {0} to version {1} started.", this.extension.displayName, this.extension.latestVersion)); return this.install(this.extension); } @@ -2413,8 +2435,8 @@ export class ExtensionStatusAction extends ExtensionAction { updateWhenCounterExtensionChanges: boolean = true; - private _status: ExtensionStatus | undefined; - get status(): ExtensionStatus | undefined { return this._status; } + private _status: ExtensionStatus[] = []; + get status(): ExtensionStatus[] { return this._status; } private readonly _onDidChangeStatus = this._register(new Emitter()); readonly onDidChangeStatus = this._onDidChangeStatus.event; @@ -2480,6 +2502,23 @@ export class ExtensionStatusAction extends ExtensionAction { return; } + if (this.extension.outdated && this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension)) { + const message = await this.extensionsWorkbenchService.shouldRequireConsentToUpdate(this.extension); + if (message) { + const markdown = new MarkdownString(); + markdown.appendMarkdown(`${message} `); + markdown.appendMarkdown( + localize('auto update message', "Please [review the extension]({0}) and update it manually.", + this.extension.hasChangelog() + ? URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Changelog]))}`).toString() + : this.extension.repository + ? this.extension.repository + : URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id]))}`).toString() + )); + this.updateStatus({ icon: warningIcon, message: markdown }, true); + } + } + if (this.extension.gallery && this.extension.state === ExtensionState.Uninstalled && !await this.extensionsWorkbenchService.canInstall(this.extension)) { if (this.extensionManagementServerService.localExtensionManagementServer || this.extensionManagementServerService.remoteExtensionManagementServer) { const targetPlatform = await (this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.getTargetPlatform() : this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.getTargetPlatform()); @@ -2681,24 +2720,43 @@ export class ExtensionStatusAction extends ExtensionAction { } private updateStatus(status: ExtensionStatus | undefined, updateClass: boolean): void { - if (this._status === status) { - return; + if (status) { + if (this._status.some(s => s.message.value === status.message.value && s.icon?.id === status.icon?.id)) { + return; + } + } else { + if (this._status.length === 0) { + return; + } + this._status = []; } - if (this._status && status && this._status.message === status.message && this._status.icon?.id === status.icon?.id) { - return; + + if (status) { + this._status.push(status); + this._status.sort((a, b) => + b.icon === trustIcon ? -1 : + a.icon === trustIcon ? 1 : + b.icon === errorIcon ? -1 : + a.icon === errorIcon ? 1 : + b.icon === warningIcon ? -1 : + a.icon === warningIcon ? 1 : + b.icon === infoIcon ? -1 : + a.icon === infoIcon ? 1 : + 0 + ); } - this._status = status; + if (updateClass) { - if (this._status?.icon === errorIcon) { + if (status?.icon === errorIcon) { this.class = `${ExtensionStatusAction.CLASS} extension-status-error ${ThemeIcon.asClassName(errorIcon)}`; } - else if (this._status?.icon === warningIcon) { + else if (status?.icon === warningIcon) { this.class = `${ExtensionStatusAction.CLASS} extension-status-warning ${ThemeIcon.asClassName(warningIcon)}`; } - else if (this._status?.icon === infoIcon) { + else if (status?.icon === infoIcon) { this.class = `${ExtensionStatusAction.CLASS} extension-status-info ${ThemeIcon.asClassName(infoIcon)}`; } - else if (this._status?.icon === trustIcon) { + else if (status?.icon === trustIcon) { this.class = `${ExtensionStatusAction.CLASS} ${ThemeIcon.asClassName(trustIcon)}`; } else { @@ -2709,7 +2767,7 @@ export class ExtensionStatusAction extends ExtensionAction { } override async run(): Promise { - if (this._status?.icon === trustIcon) { + if (this._status[0]?.icon === trustIcon) { return this.commandService.executeCommand('workbench.trust.manage'); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 95c05e762e5..b9f501d547a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -255,6 +255,6 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = const verifiedPublisherIconColor = theme.getColor(extensionVerifiedPublisherIconColor); if (verifiedPublisherIconColor) { const disabledVerifiedPublisherIconColor = verifiedPublisherIconColor.transparent(.5).makeOpaque(WORKBENCH_BACKGROUND(theme)); - collector.addRule(`.extensions-list .monaco-list .monaco-list-row.disabled .author .verified-publisher ${ThemeIcon.asCSSSelector(verifiedPublisherThemeIcon)} { color: ${disabledVerifiedPublisherIconColor}; }`); + collector.addRule(`.extensions-list .monaco-list .monaco-list-row.disabled:not(.selected) .author .verified-publisher ${ThemeIcon.asCSSSelector(verifiedPublisherThemeIcon)} { color: ${disabledVerifiedPublisherIconColor}; }`); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 933788b162f..f2eea7c616d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { isCancellationError, getErrorMessage } from 'vs/base/common/errors'; import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; -import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy, InstallExtensionInfo, ExtensionGalleryErrorCode, ExtensionGalleryError } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService, EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions, getExtensionDependencies } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -897,10 +897,16 @@ export class ExtensionsListView extends ViewPane { } } if (galleryExtensions.length) { - const extensions = await this.extensionsWorkbenchService.getExtensions(galleryExtensions.map(id => ({ id })), { source: options.source }, token); - for (const extension of extensions) { - if (extension.gallery && !extension.deprecationInfo && (await this.extensionManagementService.canInstall(extension.gallery))) { - result.push(extension); + try { + const extensions = await this.extensionsWorkbenchService.getExtensions(galleryExtensions.map(id => ({ id })), { source: options.source }, token); + for (const extension of extensions) { + if (extension.gallery && !extension.deprecationInfo && (await this.extensionManagementService.canInstall(extension.gallery))) { + result.push(extension); + } + } + } catch (error) { + if (!resourceExtensions.length || !this.isOfflineError(error)) { + throw error; } } } @@ -1067,7 +1073,7 @@ export class ExtensionsListView extends ViewPane { if (count === 0 && this.isBodyVisible()) { if (error) { - if (isOfflineError(error)) { + if (this.isOfflineError(error)) { this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(Severity.Warning); this.bodyTemplate.messageBox.textContent = localize('offline error', "Unable to search the Marketplace when offline, please check your network connection."); } else { @@ -1085,6 +1091,13 @@ export class ExtensionsListView extends ViewPane { this.updateSize(); } + private isOfflineError(error: Error): boolean { + if (error instanceof ExtensionGalleryError) { + return error.code === ExtensionGalleryErrorCode.Offline; + } + return isOfflineError(error); + } + protected updateSize() { if (this.options.flexibleHeight) { this.maximumBodySize = this.list?.model.length ? Number.POSITIVE_INFINITY : 0; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 4b53d91cbc5..23b84778def 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -561,7 +561,18 @@ export class ExtensionHoverWidget extends ExtensionWidget { }, focus); }, placement: 'element' - }, this.options.target, { markdown: () => Promise.resolve(this.getHoverMarkdown()), markdownNotSupportedFallback: undefined }); + }, + this.options.target, + { + markdown: () => Promise.resolve(this.getHoverMarkdown()), + markdownNotSupportedFallback: undefined + }, + { + appearance: { + showHoverHint: true + } + } + ); } } @@ -641,7 +652,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { const runtimeState = this.extension.runtimeState; const recommendationMessage = this.getRecommendationMessage(this.extension); - if (extensionRuntimeStatus || extensionStatus || runtimeState || recommendationMessage || preReleaseMessage) { + if (extensionRuntimeStatus || extensionStatus.length || runtimeState || recommendationMessage || preReleaseMessage) { markdown.appendMarkdown(`---`); markdown.appendText(`\n`); @@ -667,11 +678,11 @@ export class ExtensionHoverWidget extends ExtensionWidget { } } - if (extensionStatus) { - if (extensionStatus.icon) { - markdown.appendMarkdown(`$(${extensionStatus.icon.id}) `); + for (const status of extensionStatus) { + if (status.icon) { + markdown.appendMarkdown(`$(${status.icon.id}) `); } - markdown.appendMarkdown(extensionStatus.message.value); + markdown.appendMarkdown(status.message.value); if (this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.extension.local) { markdown.appendMarkdown(` [${localize('dependencies', "Show Dependencies")}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Dependencies]))}`)})`); } @@ -756,12 +767,18 @@ export class ExtensionStatusWidget extends ExtensionWidget { const disposables = new DisposableStore(); this.renderDisposables.value = disposables; const extensionStatus = this.extensionStatusAction.status; - if (extensionStatus) { + if (extensionStatus.length) { const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); - if (extensionStatus.icon) { - markdown.appendMarkdown(`$(${extensionStatus.icon.id}) `); + for (let i = 0; i < extensionStatus.length; i++) { + const status = extensionStatus[i]; + if (status.icon) { + markdown.appendMarkdown(`$(${status.icon.id}) `); + } + markdown.appendMarkdown(status.message.value); + if (i < extensionStatus.length - 1) { + markdown.appendText(`\n`); + } } - markdown.appendMarkdown(extensionStatus.message.value); const rendered = disposables.add(renderMarkdown(markdown, { actionHandler: { callback: (content) => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 78abd2bffab..584743f41d1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -16,7 +16,8 @@ import { IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallOperation, WEB_EXTENSION_TAG, InstallExtensionResult, IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX, - InstallOptions, IProductVersion + InstallOptions, IProductVersion, + UninstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension, extensionsConfigurationNodeBase } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -42,7 +43,7 @@ import { FileAccess } from 'vs/base/common/network'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { IUserDataAutoSyncService, IUserDataSyncEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { isBoolean, isString, isUndefined } from 'vs/base/common/types'; +import { isBoolean, isDefined, isString, isUndefined } from 'vs/base/common/types'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IExtensionService, IExtensionsStatus, toExtension, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { isWeb, language } from 'vs/base/common/platform'; @@ -79,14 +80,15 @@ type ExtensionsLoadClassification = { export class Extension implements IExtension { public enablementState: EnablementState = EnablementState.EnabledGlobally; - public readonly resourceExtension: IResourceExtension | undefined; + + private galleryResourcesCache = new Map(); constructor( private stateProvider: IExtensionStateProvider, private runtimeStateProvider: IExtensionStateProvider, public readonly server: IExtensionManagementServer | undefined, public local: ILocalExtension | undefined, - public gallery: IGalleryExtension | undefined, + private _gallery: IGalleryExtension | undefined, private readonly resourceExtensionInfo: { resourceExtension: IResourceExtension; isWorkspaceScoped: boolean } | undefined, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -94,7 +96,32 @@ export class Extension implements IExtension { @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService ) { - this.resourceExtension = resourceExtensionInfo?.resourceExtension; + } + + get resourceExtension(): IResourceExtension | undefined { + if (this.resourceExtensionInfo) { + return this.resourceExtensionInfo.resourceExtension; + } + if (this.local?.isWorkspaceScoped) { + return { + type: 'resource', + identifier: this.local.identifier, + location: this.local.location, + manifest: this.local.manifest, + changelogUri: this.local.changelogUrl, + readmeUri: this.local.readmeUrl, + }; + } + return undefined; + } + + get gallery(): IGalleryExtension | undefined { + return this._gallery; + } + + set gallery(gallery: IGalleryExtension | undefined) { + this._gallery = gallery; + this.galleryResourcesCache.clear(); } get type(): ExtensionType { @@ -361,11 +388,7 @@ export class Extension implements IExtension { } if (this.gallery) { - if (this.gallery.assets.manifest) { - return this.galleryService.getManifest(this.gallery, token); - } - this.logService.error(nls.localize('Manifest is not found', "Manifest is not found"), this.identifier.id); - return null; + return this.getGalleryManifest(token); } if (this.resourceExtension) { @@ -375,6 +398,25 @@ export class Extension implements IExtension { return null; } + async getGalleryManifest(token: CancellationToken = CancellationToken.None): Promise { + if (this.gallery) { + let cache = this.galleryResourcesCache.get('manifest'); + if (!cache) { + if (this.gallery.assets.manifest) { + this.galleryResourcesCache.set('manifest', cache = this.galleryService.getManifest(this.gallery, token) + .catch(e => { + this.galleryResourcesCache.delete('manifest'); + throw e; + })); + } else { + this.logService.error(nls.localize('Manifest is not found', "Manifest is not found"), this.identifier.id); + } + } + return cache; + } + return null; + } + hasReadme(): boolean { if (this.local && this.local.readmeUrl) { return true; @@ -1010,7 +1052,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.setEnabledAutoUpdateExtensions([]); this.setDisabledAutoUpdateExtensions([]); this._onChange.fire(undefined); - this.extensionManagementService.resetPinnedStateForAllUserExtensions(!isAutoUpdateEnabled); + this.updateExtensionsPinnedState(!isAutoUpdateEnabled); } if (isAutoUpdateEnabled) { this.eventuallyAutoUpdateExtensions(); @@ -1139,6 +1181,13 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } + private updateExtensionsPinnedState(pinned: boolean): Promise { + return this.progressService.withProgress({ + location: ProgressLocation.Extensions, + title: nls.localize('updatingExtensions', "Updating Extensions Auto Update State"), + }, () => this.extensionManagementService.resetPinnedStateForAllUserExtensions(pinned)); + } + private reset(): void { for (const task of this.tasksInProgress) { task.cancel(); @@ -1787,7 +1836,17 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private async autoUpdateExtensions(): Promise { - const toUpdate = this.outdated.filter(e => !e.local?.pinned && this.shouldAutoUpdateExtension(e)); + const toUpdate: IExtension[] = []; + for (const extension of this.outdated) { + if (!this.shouldAutoUpdateExtension(extension)) { + continue; + } + if (await this.shouldRequireConsentToUpdate(extension)) { + continue; + } + toUpdate.push(extension); + } + if (!toUpdate.length) { return; } @@ -1858,6 +1917,35 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return false; } + async shouldRequireConsentToUpdate(extension: IExtension): Promise { + if (!extension.outdated) { + return; + } + + if (extension.local?.manifest.main || extension.local?.manifest.browser) { + return; + } + + if (!extension.gallery) { + return; + } + + if (isDefined(extension.gallery.properties?.executesCode)) { + if (!extension.gallery.properties.executesCode) { + return; + } + } else { + const manifest = extension instanceof Extension + ? await extension.getGalleryManifest() + : await this.galleryService.getManifest(extension.gallery, CancellationToken.None); + if (!manifest?.main && !manifest?.browser) { + return; + } + } + + return nls.localize('consentRequiredToUpdate', "The update for {0} extension introduces executable code, which is not present in the currently installed version.", extension.displayName); + } + isAutoUpdateEnabledFor(extensionOrPublisher: IExtension | string): boolean { if (isString(extensionOrPublisher)) { if (EXTENSION_IDENTIFIER_REGEX.test(extensionOrPublisher)) { @@ -1881,10 +1969,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (isString(extensionOrPublisher)) { throw new Error('Expected extension, found publisher string'); } - if (!extensionOrPublisher.local) { - throw new Error('Only installed extensions can be pinned'); - } - const disabledAutoUpdateExtensions = this.getDisabledAutoUpdateExtensions(); const extensionId = extensionOrPublisher.identifier.id.toLowerCase(); const extensionIndex = disabledAutoUpdateExtensions.indexOf(extensionId); @@ -1899,7 +1983,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } this.setDisabledAutoUpdateExtensions(disabledAutoUpdateExtensions); - if (enable && extensionOrPublisher.pinned) { + if (enable && extensionOrPublisher.local && extensionOrPublisher.pinned) { await this.extensionManagementService.updateMetadata(extensionOrPublisher.local, { pinned: false }); } this._onChange.fire(extensionOrPublisher); @@ -2232,18 +2316,71 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return this.promptAndSetEnablement(extensions, enablementState); } - uninstall(extension: IExtension): Promise { - const ext = extension.local ? extension : this.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]; - const toUninstall: ILocalExtension | null = ext && ext.local ? ext.local : null; - - if (!toUninstall) { - return Promise.reject(new Error('Missing local')); + async uninstall(e: IExtension): Promise { + const extension = e.local ? e : this.local.find(local => areSameExtensions(local.identifier, e.identifier)); + if (!extension?.local) { + throw new Error('Missing local'); } + + const extensionsToUninstall: UninstallExtensionInfo[] = [{ extension: extension.local }]; + const dependents: IExtension[] = []; + for (const local of this.local) { + if (local === extension) { + continue; + } + if (!local.local) { + continue; + } + if (local.dependencies.length === 0) { + continue; + } + if (extension.extensionPack.some(id => areSameExtensions({ id }, local.identifier))) { + continue; + } + if (dependents.some(d => d.extensionPack.some(id => areSameExtensions({ id }, local.identifier)))) { + continue; + } + if (local.dependencies.some(dep => areSameExtensions(extension.identifier, { id: dep }))) { + dependents.push(local); + extensionsToUninstall.push({ extension: local.local }); + } + } + + if (dependents.length) { + const { result } = await this.dialogService.prompt({ + title: nls.localize('uninstallDependents', "Uninstall Extension with Dependents"), + type: Severity.Warning, + message: this.getErrorMessageForUninstallingAnExtensionWithDependents(extension, dependents), + buttons: [{ + label: nls.localize('uninstallAll', "Uninstall All"), + run: () => true + }], + cancelButton: { + run: () => false + } + }); + if (!result) { + throw new CancellationError(); + } + } + return this.withProgress({ location: ProgressLocation.Extensions, title: nls.localize('uninstallingExtension', 'Uninstalling extension....'), - source: `${toUninstall.identifier.id}` - }, () => this.extensionManagementService.uninstall(toUninstall).then(() => undefined)); + source: `${extension.identifier.id}` + }, () => this.extensionManagementService.uninstallExtensions(extensionsToUninstall).then(() => undefined)); + } + + private getErrorMessageForUninstallingAnExtensionWithDependents(extension: IExtension, dependents: IExtension[]): string { + if (dependents.length === 1) { + return nls.localize('singleDependentUninstallError', "Cannot uninstall '{0}' extension alone. '{1}' extension depends on this. Do you want to uninstall all these extensions?", extension.displayName, dependents[0].displayName); + } + if (dependents.length === 2) { + return nls.localize('twoDependentsUninstallError', "Cannot uninstall '{0}' extension alone. '{1}' and '{2}' extensions depend on this. Do you want to uninstall all these extensions?", + extension.displayName, dependents[0].displayName, dependents[1].displayName); + } + return nls.localize('multipleDependentsUninstallError', "Cannot uninstall '{0}' extension alone. '{1}', '{2}' and other extensions depend on this. Do you want to uninstall all these extensions?", + extension.displayName, dependents[0].displayName, dependents[1].displayName); } reinstall(extension: IExtension): Promise { @@ -2428,30 +2565,29 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } - private checkAndSetEnablement(extensions: IExtension[], otherExtensions: IExtension[], enablementState: EnablementState): Promise { + private async checkAndSetEnablement(extensions: IExtension[], otherExtensions: IExtension[], enablementState: EnablementState): Promise { const allExtensions = [...extensions, ...otherExtensions]; const enable = enablementState === EnablementState.EnabledGlobally || enablementState === EnablementState.EnabledWorkspace; if (!enable) { for (const extension of extensions) { const dependents = this.getDependentsAfterDisablement(extension, allExtensions, this.local); if (dependents.length) { - return new Promise((resolve, reject) => { - this.notificationService.prompt(Severity.Error, this.getDependentsErrorMessage(extension, allExtensions, dependents), [ - { - label: nls.localize('disable all', 'Disable All'), - run: async () => { - try { - await this.checkAndSetEnablement(dependents, [extension], enablementState); - resolve(); - } catch (error) { - reject(error); - } - } - } - ], { - onCancel: () => reject(new CancellationError()) - }); + const { result } = await this.dialogService.prompt({ + title: nls.localize('disableDependents', "Disable Extension with Dependents"), + type: Severity.Warning, + message: this.getDependentsErrorMessageForDisablement(extension, allExtensions, dependents), + buttons: [{ + label: nls.localize('disable all', 'Disable All'), + run: () => true + }], + cancelButton: { + run: () => false + } }); + if (!result) { + throw new CancellationError(); + } + await this.checkAndSetEnablement(dependents, [extension], enablementState); } } } @@ -2506,7 +2642,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension }); } - private getDependentsErrorMessage(extension: IExtension, allDisabledExtensions: IExtension[], dependents: IExtension[]): string { + private getDependentsErrorMessageForDisablement(extension: IExtension, allDisabledExtensions: IExtension[], dependents: IExtension[]): string { for (const e of [extension, ...allDisabledExtensions]) { const dependentsOfTheExtension = dependents.filter(d => d.dependencies.some(id => areSameExtensions({ id }, e.identifier))); if (dependentsOfTheExtension.length) { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 985b5511c05..30bbba041ad 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -192,14 +192,6 @@ font-weight: 600; } -.monaco-list-row.disabled .extension-list-item > .details > .footer > .author > .publisher-name{ - color: var(--vscode-disabledForeground); -} - -.monaco-list-row.disabled.selected .extension-list-item > .details > .footer > .author > .publisher-name{ - color: unset; -} - .monaco-list-row.selected .extension-list-item > .details > .footer > .author > .publisher-name{ color: unset; } @@ -235,12 +227,16 @@ min-width: 0; } -.monaco-list-row.disabled .extension-list-item { +.monaco-list-row.disabled:not(.selected) .extension-list-item > .details > .footer > .author > .publisher-name { color: var(--vscode-disabledForeground); } -.monaco-list-row.disabled.selected .extension-list-item .details .header .name, -.monaco-list-row.disabled.selected .extension-list-item .details .description { +.monaco-list-row.disabled:not(.selected) .extension-list-item { + color: var(--vscode-disabledForeground); +} + +.monaco-list-row.disabled:not(.selected) .extension-list-item .details .header .name, +.monaco-list-row.disabled:not(.selected) .extension-list-item .details .description { color: unset; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index 552e9cf54c2..a4514bd07c0 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -144,10 +144,10 @@ max-width: 100px; } -.monaco-workbench.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .icon-container > .icon, -.monaco-workbench.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .icon-container > .icon, -.monaco-workbench.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .header-container .codicon, -.monaco-workbench.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .header-container .codicon { +.monaco-workbench.vs .extensions-viewlet > .extensions .monaco-list-row.disabled:not(.selected) > .extension-list-item > .icon-container > .icon, +.monaco-workbench.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled:not(.selected) > .extension-list-item > .icon-container > .icon, +.monaco-workbench.vs .extensions-viewlet > .extensions .monaco-list-row.disabled:not(.selected) > .extension-list-item > .details > .header-container .codicon, +.monaco-workbench.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled:not(.selected) > .extension-list-item > .details > .header-container .codicon { opacity: 0.5; } diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index 586c9f83b2f..c604e3162b9 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -55,6 +55,8 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { this._register(this.fileService.watch(this.uriIdentityService.extUri.joinPath(folder.uri, WORKSPACE_EXTENSIONS_FOLDER))); } + this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.onDidChangeWorkspaceExtensionsScheduler.schedule())); + this._register(this.fileService.onDidFilesChange(e => { if (this.contextService.getWorkspace().folders.some(folder => e.affects(this.uriIdentityService.extUri.joinPath(folder.uri, WORKSPACE_EXTENSIONS_FOLDER), FileChangeType.ADDED, FileChangeType.DELETED)) diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index c619e3a0b57..db54e378743 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -138,6 +138,7 @@ export interface IExtensionsWorkbenchService { setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise; isAutoUpdateEnabledFor(extensionOrPublisher: IExtension | string): boolean; updateAutoUpdateEnablementFor(extensionOrPublisher: IExtension | string, enable: boolean): Promise; + shouldRequireConsentToUpdate(extension: IExtension): Promise; open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise; updateAutoUpdateValue(value: AutoUpdateConfigurationValue): Promise; getAutoUpdateValue(): AutoUpdateConfigurationValue; diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts index 4ba4300bff2..47325650b92 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts @@ -4,37 +4,43 @@ *--------------------------------------------------------------------------------------------*/ import { Action } from 'vs/base/common/actions'; +import { Disposable } from 'vs/base/common/lifecycle'; import { randomPort } from 'vs/base/common/ports'; import * as nls from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INativeHostService } from 'vs/platform/native/common/native'; import { IProductService } from 'vs/platform/product/common/productService'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensionHostKind'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; export class DebugExtensionHostAction extends Action { static readonly ID = 'workbench.extensions.action.debugExtensionHost'; - static readonly LABEL = nls.localize('debugExtensionHost', "Start Debugging Extension Host"); + static readonly LABEL = nls.localize('debugExtensionHost', "Start Debugging Extension Host In New Window"); static readonly CSS_CLASS = 'debug-extension-host'; constructor( - @IDebugService private readonly _debugService: IDebugService, @INativeHostService private readonly _nativeHostService: INativeHostService, @IDialogService private readonly _dialogService: IDialogService, @IExtensionService private readonly _extensionService: IExtensionService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IHostService private readonly _hostService: IHostService, ) { super(DebugExtensionHostAction.ID, DebugExtensionHostAction.LABEL, DebugExtensionHostAction.CSS_CLASS); } - override async run(): Promise { - + override async run(_args: unknown): Promise { const inspectPorts = await this._extensionService.getInspectPorts(ExtensionHostKind.LocalProcess, false); if (inspectPorts.length === 0) { const res = await this._dialogService.confirm({ - message: nls.localize('restart1', "Profile Extensions"), - detail: nls.localize('restart2', "In order to profile extensions a restart is required. Do you want to restart '{0}' now?", this.productService.nameLong), + message: nls.localize('restart1', "Debug Extensions"), + detail: nls.localize('restart2', "In order to debug extensions a restart is required. Do you want to restart '{0}' now?", this.productService.nameLong), primaryButton: nls.localize({ key: 'restart3', comment: ['&& denotes a mnemonic'] }, "&&Restart") }); if (res.confirmed) { @@ -49,11 +55,52 @@ export class DebugExtensionHostAction extends Action { console.warn(`There are multiple extension hosts available for debugging. Picking the first one...`); } - return this._debugService.startDebugging(undefined, { - type: 'node', - name: nls.localize('debugExtensionHost.launch.name', "Attach Extension Host"), - request: 'attach', - port: inspectPorts[0].port, - }); + const s = this._instantiationService.createInstance(Storage); + s.storeDebugOnNewWindow(inspectPorts[0].port); + + this._hostService.openWindow(); + } +} + +class Storage { + constructor(@IStorageService private readonly _storageService: IStorageService,) { + } + + storeDebugOnNewWindow(targetPort: number) { + this._storageService.store('debugExtensionHost.debugPort', targetPort, StorageScope.APPLICATION, StorageTarget.MACHINE); + } + + getAndDeleteDebugPortIfSet(): number | undefined { + const port = this._storageService.getNumber('debugExtensionHost.debugPort', StorageScope.APPLICATION); + if (port !== undefined) { + this._storageService.remove('debugExtensionHost.debugPort', StorageScope.APPLICATION); + } + return port; + } +} + +export class DebugExtensionsContribution extends Disposable implements IWorkbenchContribution { + constructor( + @IDebugService private readonly _debugService: IDebugService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IProgressService _progressService: IProgressService, + ) { + super(); + + const storage = this._instantiationService.createInstance(Storage); + const port = storage.getAndDeleteDebugPortIfSet(); + if (port !== undefined) { + _progressService.withProgress({ + location: ProgressLocation.Notification, + title: nls.localize('debugExtensionHost.progress', "Attaching Debugger To Extension Host"), + }, async p => { + await this._debugService.startDebugging(undefined, { + type: 'node', + name: nls.localize('debugExtensionHost.launch.name', "Attach Extension Host"), + request: 'attach', + port, + }); + }); + } } } diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts index ff915c04865..024719af399 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts @@ -13,7 +13,7 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { RuntimeExtensionsEditor, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, SaveExtensionHostProfileAction, IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; -import { DebugExtensionHostAction } from 'vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction'; +import { DebugExtensionHostAction, DebugExtensionsContribution } from 'vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction'; import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -75,11 +75,12 @@ const workbenchRegistry = Registry.as(Workbench workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(RemoteExtensionsInitializerContribution, LifecyclePhase.Restored); +workbenchRegistry.registerWorkbenchContribution(DebugExtensionsContribution, LifecyclePhase.Restored); // Register Commands -CommandsRegistry.registerCommand(DebugExtensionHostAction.ID, (accessor: ServicesAccessor) => { +CommandsRegistry.registerCommand(DebugExtensionHostAction.ID, (accessor: ServicesAccessor, ...args) => { const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(DebugExtensionHostAction).run(); + return instantiationService.createInstance(DebugExtensionHostAction).run(args); }); CommandsRegistry.registerCommand(StartExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { @@ -109,6 +110,15 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { when: ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID) }); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: DebugExtensionHostAction.ID, + title: localize('debugExtensionHost', "Debug Extensions In New Window"), + category: localize('developer', "Developer"), + icon: Codicon.debugStart + }, +}); + MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: StartExtensionHostProfileAction.ID, diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index eee8b207a29..a2ce22d1979 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -725,10 +725,9 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionB = aLocalExtension('b'); const extensionC = aLocalExtension('c'); - instantiationService.stub(INotificationService, { - prompt(severity, message, choices, options) { - choices[0].run(); - return null!; + instantiationService.stub(IDialogService, { + prompt() { + return Promise.resolve({ result: true }); } }); return instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionA], EnablementState.EnabledGlobally) diff --git a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts index 3388ec7baea..535fbe2b0f6 100644 --- a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts @@ -10,12 +10,10 @@ import { MenuId, MenuRegistry, IMenuItem } from 'vs/platform/actions/common/acti import { ITerminalGroupService, ITerminalService as IIntegratedTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { IFileService } from 'vs/platform/files/common/files'; -import { IListService } from 'vs/platform/list/browser/listService'; import { getMultiSelectedResources, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { Schemas } from 'vs/base/common/network'; import { distinct } from 'vs/base/common/arrays'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; @@ -26,6 +24,8 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Registry } from 'vs/platform/registry/common/platform'; import { IExternalTerminalConfiguration, IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { IListService } from 'vs/platform/list/browser/listService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const OPEN_IN_TERMINAL_COMMAND_ID = 'openInTerminal'; @@ -37,7 +37,6 @@ function registerOpenTerminalCommand(id: string, explorerKind: 'integrated' | 'e handler: async (accessor, resource: URI) => { const configurationService = accessor.get(IConfigurationService); - const editorService = accessor.get(IEditorService); const fileService = accessor.get(IFileService); const integratedTerminalService = accessor.get(IIntegratedTerminalService); const remoteAgentService = accessor.get(IRemoteAgentService); @@ -45,10 +44,9 @@ function registerOpenTerminalCommand(id: string, explorerKind: 'integrated' | 'e let externalTerminalService: IExternalTerminalService | undefined = undefined; try { externalTerminalService = accessor.get(IExternalTerminalService); - } catch { - } + } catch { } - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); return fileService.resolveAll(resources.map(r => ({ resource: r }))).then(async stats => { // Always use integrated terminal when using a remote const config = configurationService.getValue(); diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 066e0372c6b..a598f8b7b2e 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -158,8 +158,8 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa // Save As primaryActions.push(this.instantiationService.createInstance(SaveModelAsAction, model)); - // Discard - primaryActions.push(this.instantiationService.createInstance(DiscardModelAction, model)); + // Revert + primaryActions.push(this.instantiationService.createInstance(RevertModelAction, model)); // Message if (isWriteLocked) { @@ -306,12 +306,12 @@ class RetrySaveModelAction extends Action { } } -class DiscardModelAction extends Action { +class RevertModelAction extends Action { constructor( private model: ITextFileEditorModel ) { - super('workbench.files.action.discardModel', localize('discard', "Discard")); + super('workbench.files.action.revertModel', localize('revert', "Revert")); } override async run(): Promise { diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index 1c67fc0f491..4bc6f717eb5 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -152,6 +152,7 @@ export class ExplorerService implements IExplorerService { return { sortOrder: this.config.sortOrder, lexicographicOptions: this.config.sortOrderLexicographicOptions, + reverse: this.config.sortOrderReverse, }; } @@ -521,6 +522,11 @@ export class ExplorerService implements IExplorerService { if (this.config.sortOrderLexicographicOptions !== configLexicographicOptions) { shouldRefresh = shouldRefresh || this.config.sortOrderLexicographicOptions !== undefined; } + const sortOrderReverse = configuration?.explorer?.sortOrderReverse || false; + + if (this.config.sortOrderReverse !== sortOrderReverse) { + shouldRefresh = shouldRefresh || this.config.sortOrderReverse !== undefined; + } this.config = configuration.explorer; diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index b910fb3f2ee..ca0942332ac 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -300,7 +300,12 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { id: REOPEN_WITH_COMMAND_ID, title: nls.localize('reopenWith', "Reopen Editor With...") }, - when: ActiveEditorAvailableEditorIdsContext + when: ContextKeyExpr.and( + // Editors with Available Choices to Open With + ActiveEditorAvailableEditorIdsContext, + // Not: editor groups + OpenEditorsGroupContext.toNegated() + ) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -417,7 +422,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { - group: '3_compare', + group: '1_compare', order: 30, command: compareSelectedCommand, when: ContextKeyExpr.and(ResourceContextKey.HasResource, TwoEditorsSelectedInGroupContext, SelectedEditorsInGroupFileOrUntitledResourceContextKey) diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 97fb6ec7c65..1311bd1839b 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -15,7 +15,6 @@ import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, ExplorerCo import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { IListService } from 'vs/platform/list/browser/listService'; import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IFileService } from 'vs/platform/files/common/files'; @@ -40,7 +39,7 @@ import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/em import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { isCancellationError } from 'vs/base/common/errors'; -import { toAction } from 'vs/base/common/actions'; +import { IAction, toAction } from 'vs/base/common/actions'; import { EditorOpenSource, EditorResolution } from 'vs/platform/editor/common/editor'; import { hash } from 'vs/base/common/hash'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -52,6 +51,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { RemoveRootFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import { OpenEditorsView } from 'vs/workbench/contrib/files/browser/views/openEditorsView'; import { ExplorerView } from 'vs/workbench/contrib/files/browser/views/explorerView'; +import { IListService } from 'vs/platform/list/browser/listService'; export const openWindowCommand = (accessor: ServicesAccessor, toOpen: IWindowOpenable[], options?: IOpenWindowOptions) => { if (Array.isArray(toOpen)) { @@ -89,11 +89,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }, id: OPEN_TO_SIDE_COMMAND_ID, handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); - const editorGroupService = accessor.get(IEditorGroupsService); - const listService = accessor.get(IListService); const fileService = accessor.get(IFileService); const explorerService = accessor.get(IExplorerService); - const resources = getMultiSelectedResources(resource, listService, editorService, editorGroupService, explorerService); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, accessor.get(IEditorGroupsService), explorerService); // Set side input if (resources.length) { @@ -150,6 +148,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const textModelService = accessor.get(ITextModelService); const editorService = accessor.get(IEditorService); const fileService = accessor.get(IFileService); + const listService = accessor.get(IListService); // Register provider at first as needed let registerEditorListener = false; @@ -162,7 +161,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } // Open editor (only resources that can be handled by file service are supported) - const uri = getResourceForCommand(resource, accessor.get(IListService), editorService); + const uri = getResourceForCommand(resource, editorService, listService); if (uri && fileService.hasProvider(uri)) { const name = basename(uri); const editorLabel = nls.localize('modifiedLabel', "{0} (in file) ↔ {1}", name, name); @@ -189,9 +188,7 @@ let resourceSelectedForCompareContext: IContextKey; CommandsRegistry.registerCommand({ id: SELECT_FOR_COMPARE_COMMAND_ID, handler: (accessor, resource: URI | object) => { - const listService = accessor.get(IListService); - - globalResourceToCompare = getResourceForCommand(resource, listService, accessor.get(IEditorService)); + globalResourceToCompare = getResourceForCommand(resource, accessor.get(IEditorService), accessor.get(IListService)); if (!resourceSelectedForCompareContext) { resourceSelectedForCompareContext = ResourceSelectedForCompareContext.bindTo(accessor.get(IContextKeyService)); } @@ -203,14 +200,12 @@ CommandsRegistry.registerCommand({ id: COMPARE_SELECTED_COMMAND_ID, handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); - const editorGroupService = accessor.get(IEditorGroupsService); - const explorerService = accessor.get(IExplorerService); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, editorGroupService, explorerService); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); if (resources.length === 2) { return editorService.openEditor({ - original: { resource: resources[0] }, - modified: { resource: resources[1] }, + original: { resource: resources[1] }, + modified: { resource: resources[0] }, options: { pinned: true } }); } @@ -223,9 +218,7 @@ CommandsRegistry.registerCommand({ id: COMPARE_RESOURCE_COMMAND_ID, handler: (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); - const listService = accessor.get(IListService); - - const rightResource = getResourceForCommand(resource, listService, editorService); + const rightResource = getResourceForCommand(resource, editorService, accessor.get(IListService)); if (globalResourceToCompare && rightResource) { editorService.openEditor({ original: { resource: globalResourceToCompare }, @@ -327,7 +320,9 @@ CommandsRegistry.registerCommand({ const viewService = accessor.get(IViewsService); const contextService = accessor.get(IWorkspaceContextService); const explorerService = accessor.get(IExplorerService); - const uri = getResourceForCommand(resource, accessor.get(IListService), accessor.get(IEditorService)); + const editorService = accessor.get(IEditorService); + const listService = accessor.get(IListService); + const uri = getResourceForCommand(resource, editorService, listService); if (uri && contextService.isInsideWorkspace(uri)) { const explorerView = await viewService.openView(VIEW_ID, false); @@ -355,8 +350,8 @@ CommandsRegistry.registerCommand({ id: OPEN_WITH_EXPLORER_COMMAND_ID, handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); - - const uri = getResourceForCommand(resource, accessor.get(IListService), accessor.get(IEditorService)); + const listService = accessor.get(IListService); + const uri = getResourceForCommand(resource, editorService, listService); if (uri) { return editorService.openEditor({ resource: uri, options: { override: EditorResolution.PICK, source: EditorOpenSource.USER } }); } @@ -368,13 +363,12 @@ CommandsRegistry.registerCommand({ // Save / Save As / Save All / Revert async function saveSelectedEditors(accessor: ServicesAccessor, options?: ISaveEditorsOptions): Promise { - const listService = accessor.get(IListService); const editorGroupService = accessor.get(IEditorGroupsService); const codeEditorService = accessor.get(ICodeEditorService); const textFileService = accessor.get(ITextFileService); // Retrieve selected or active editor - let editors = getOpenEditorsViewMultiSelection(listService, editorGroupService); + let editors = getOpenEditorsViewMultiSelection(accessor); if (!editors) { const activeGroup = editorGroupService.activeGroup; if (activeGroup.activeEditor) { @@ -450,16 +444,17 @@ async function doSaveEditors(accessor: ServicesAccessor, editors: IEditorIdentif await editorService.save(editors, options); } catch (error) { if (!isCancellationError(error)) { + const actions: IAction[] = [toAction({ id: 'workbench.action.files.saveEditors', label: nls.localize('retry', "Retry"), run: () => instantiationService.invokeFunction(accessor => doSaveEditors(accessor, editors, options)) })]; + const editorsToRevert = editors.filter(({ editor }) => !editor.hasCapability(EditorInputCapabilities.Untitled) /* all except untitled to prevent unexpected data-loss */); + if (editorsToRevert.length > 0) { + actions.push(toAction({ id: 'workbench.action.files.revertEditors', label: editorsToRevert.length > 1 ? nls.localize('revertAll', "Revert All") : nls.localize('revert', "Revert"), run: () => editorService.revert(editorsToRevert) })); + } + notificationService.notify({ id: editors.map(({ editor }) => hash(editor.resource?.toString())).join(), // ensure unique notification ID per set of editor severity: Severity.Error, message: nls.localize({ key: 'genericSaveError', comment: ['{0} is the resource that failed to save and {1} the error message'] }, "Failed to save '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false)), - actions: { - primary: [ - toAction({ id: 'workbench.action.files.saveEditors', label: nls.localize('retry', "Retry"), run: () => instantiationService.invokeFunction(accessor => doSaveEditors(accessor, editors, options)) }), - toAction({ id: 'workbench.action.files.revertEditors', label: nls.localize('discard', "Discard"), run: () => editorService.revert(editors) }) - ] - } + actions: { primary: actions } }); } } @@ -511,13 +506,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ CommandsRegistry.registerCommand({ id: SAVE_ALL_IN_GROUP_COMMAND_ID, handler: (accessor, _: URI | object, editorContext: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); - const resolvedContext = resolveCommandsContext(accessor, [editorContext]); + const resolvedContext = resolveCommandsContext([editorContext], accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); let groups: readonly IEditorGroup[] | undefined = undefined; if (!resolvedContext.groupedEditors.length) { - groups = editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); + groups = editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); } else { groups = resolvedContext.groupedEditors.map(({ group }) => group); } @@ -539,13 +534,11 @@ CommandsRegistry.registerCommand({ CommandsRegistry.registerCommand({ id: REVERT_FILE_COMMAND_ID, handler: async accessor => { - const notificationService = accessor.get(INotificationService); - const listService = accessor.get(IListService); const editorGroupService = accessor.get(IEditorGroupsService); const editorService = accessor.get(IEditorService); // Retrieve selected or active editor - let editors = getOpenEditorsViewMultiSelection(listService, editorGroupService); + let editors = getOpenEditorsViewMultiSelection(accessor); if (!editors) { const activeGroup = editorGroupService.activeGroup; if (activeGroup.activeEditor) { @@ -560,6 +553,7 @@ CommandsRegistry.registerCommand({ try { await editorService.revert(editors.filter(({ editor }) => !editor.hasCapability(EditorInputCapabilities.Untitled) /* all except untitled */), { force: true }); } catch (error) { + const notificationService = accessor.get(INotificationService); notificationService.error(nls.localize('genericRevertError', "Failed to revert '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false))); } } @@ -568,7 +562,6 @@ CommandsRegistry.registerCommand({ CommandsRegistry.registerCommand({ id: REMOVE_ROOT_FOLDER_COMMAND_ID, handler: (accessor, resource: URI | object) => { - const workspaceEditingService = accessor.get(IWorkspaceEditingService); const contextService = accessor.get(IWorkspaceContextService); const uriIdentityService = accessor.get(IUriIdentityService); const workspace = contextService.getWorkspace(); @@ -582,6 +575,7 @@ CommandsRegistry.registerCommand({ return commandService.executeCommand(RemoveRootFolderAction.ID); } + const workspaceEditingService = accessor.get(IWorkspaceEditingService); return workspaceEditingService.removeFolders(resources); } }); diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 86f4f228580..4f3a858af61 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -564,7 +564,8 @@ export class ExternalFileImport { }); // if we only add one file, just open it directly - if (resourceFileEdits.length === 1) { + const autoOpen = this.configurationService.getValue().explorer.autoOpenDroppedFile; + if (autoOpen && resourceFileEdits.length === 1) { const item = this.explorerService.findClosest(resourceFileEdits[0].newResource!); if (item && !item.isDirectory) { this.editorService.openEditor({ resource: item.resource, options: { pinned: true } }); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index d525ba5860c..097721653f6 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -533,6 +533,11 @@ configurationRegistry.registerConfiguration({ ], 'description': nls.localize('sortOrderLexicographicOptions', "Controls the lexicographic sorting of file and folder names in the Explorer.") }, + 'explorer.sortOrderReverse': { + 'type': 'boolean', + 'description': nls.localize('sortOrderReverse', "Controls whether the file and folder sort order, should be reversed."), + 'default': false, + }, 'explorer.decorations.colors': { type: 'boolean', description: nls.localize('explorer.decorations.colors', "Controls whether file decorations should use colors."), @@ -554,6 +559,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('explorer.incrementalNaming', "Controls which naming strategy to use when giving a new name to a duplicated Explorer item on paste."), default: 'simple' }, + 'explorer.autoOpenDroppedFile': { + 'type': 'boolean', + 'description': nls.localize('autoOpenDroppedFile', "Controls whether the Explorer should automatically open a file when it is dropped into the explorer"), + 'default': true + }, 'explorer.compactFolders': { 'type': 'boolean', 'description': nls.localize('compressSingleChildFolders', "Controls whether the Explorer should render folders in a compact form. In such a form, single child folders will be compressed in a combined tree element. Useful for Java package structures, for example."), diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index f1dbd239f4e..2d5683ecf3b 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -14,7 +14,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditableData } from 'vs/workbench/common/views'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { ProgressLocation } from 'vs/platform/progress/common/progress'; import { isActiveElement } from 'vs/base/browser/dom'; @@ -90,9 +90,9 @@ function getFocus(listService: IListService): unknown | undefined { // Commands can get executed from a command palette, from a context menu or from some list using a keybinding // To cover all these cases we need to properly compute the resource on which the command is being executed -export function getResourceForCommand(resource: URI | object | undefined, listService: IListService, editorService: IEditorService): URI | undefined { - if (URI.isUri(resource)) { - return resource; +export function getResourceForCommand(commandArg: unknown, editorService: IEditorService, listService: IListService): URI | undefined { + if (URI.isUri(commandArg)) { + return commandArg; } const focus = getFocus(listService); @@ -105,7 +105,7 @@ export function getResourceForCommand(resource: URI | object | undefined, listSe return EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); } -export function getMultiSelectedResources(resource: URI | object | undefined, listService: IListService, editorService: IEditorService, editorGroupService: IEditorGroupsService, explorerService: IExplorerService): Array { +export function getMultiSelectedResources(commandArg: unknown, listService: IListService, editorSerice: IEditorService, editorGroupService: IEditorGroupsService, explorerService: IExplorerService): Array { const list = listService.lastFocusedList; const element = list?.getHTMLElement(); if (element && isActiveElement(element)) { @@ -124,36 +124,45 @@ export function getMultiSelectedResources(resource: URI | object | undefined, li const focusedElements = list.getFocusedElements(); const focus = focusedElements.length ? focusedElements[0] : undefined; let mainUriStr: string | undefined = undefined; - if (URI.isUri(resource)) { - mainUriStr = resource.toString(); + if (URI.isUri(commandArg)) { + mainUriStr = commandArg.toString(); } else if (focus instanceof OpenEditor) { const focusedResource = focus.getResource(); mainUriStr = focusedResource ? focusedResource.toString() : undefined; } // We only respect the selection if it contains the main element. - if (selection.some(s => s.toString() === mainUriStr)) { + const mainIndex = selection.findIndex(s => s.toString() === mainUriStr); + if (mainIndex !== -1) { + // Move the main resource to the front of the selection. + const mainResource = selection[mainIndex]; + selection.splice(mainIndex, 1); + selection.unshift(mainResource); return selection; } } } - // Check for tabs multiselect. + // Check for tabs multiselect const activeGroup = editorGroupService.activeGroup; const selection = activeGroup.selectedEditors; - if (selection.length > 1 && URI.isUri(resource)) { + if (selection.length > 1 && URI.isUri(commandArg)) { // If the resource is part of the tabs selection, return all selected tabs/resources. // It's possible that multiple tabs are selected but the action was applied to a resource that is not part of the selection. - if (selection.some(e => e.matches({ resource }))) { + const mainEditorSelectionIndex = selection.findIndex(e => e.matches({ resource: commandArg })); + if (mainEditorSelectionIndex !== -1) { + const mainEditor = selection[mainEditorSelectionIndex]; + selection.splice(mainEditorSelectionIndex, 1); + selection.unshift(mainEditor); return selection.map(editor => EditorResourceAccessor.getOriginalUri(editor)).filter(uri => !!uri); } } - const result = getResourceForCommand(resource, listService, editorService); + const result = getResourceForCommand(commandArg, editorSerice, listService); return !!result ? [result] : []; } -export function getOpenEditorsViewMultiSelection(listService: IListService, editorGroupService: IEditorGroupsService): Array | undefined { - const list = listService.lastFocusedList; +export function getOpenEditorsViewMultiSelection(accessor: ServicesAccessor): Array | undefined { + const list = accessor.get(IListService).lastFocusedList; const element = list?.getHTMLElement(); if (element && isActiveElement(element)) { // Open editors view diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index bda9eb9b2aa..c568bbdba10 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -880,6 +880,10 @@ export class FileSorter implements ITreeSorter { const sortOrder = this.explorerService.sortOrderConfiguration.sortOrder; const lexicographicOptions = this.explorerService.sortOrderConfiguration.lexicographicOptions; + const reverse = this.explorerService.sortOrderConfiguration.reverse; + if (reverse) { + [statA, statB] = [statB, statA]; + } let compareFileNames; let compareFileExtensions; diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index edf32a0e189..bf28812f28e 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -97,6 +97,7 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb expandSingleFolderWorkspaces: boolean; sortOrder: SortOrder; sortOrderLexicographicOptions: LexicographicOptions; + sortOrderReverse: boolean; decorations: { colors: boolean; badges: boolean; @@ -108,6 +109,7 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb expand: boolean; patterns: { [parent: string]: string }; }; + autoOpenDroppedFile: boolean; }; editor: IEditorOptions; } @@ -142,6 +144,7 @@ export const enum LexicographicOptions { export interface ISortOrderConfiguration { sortOrder: SortOrder; lexicographicOptions: LexicographicOptions; + reverse: boolean; } export class TextFileContentProvider extends Disposable implements ITextModelContentProvider { diff --git a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts index 8cb362e422c..dc0ae482b0a 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts @@ -14,7 +14,6 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { getMultiSelectedResources, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; -import { IListService } from 'vs/platform/list/browser/listService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { revealResourcesInOS } from 'vs/workbench/contrib/files/electron-sandbox/fileCommands'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; @@ -22,6 +21,7 @@ import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { appendToCommandPalette, appendEditorTitleContextMenuItem } from 'vs/workbench/contrib/files/browser/fileActions.contribution'; import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IListService } from 'vs/platform/list/browser/listService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 7d3e4041271..de45082752f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -25,6 +25,7 @@ import { CONTEXT_CHAT_INPUT_HAS_TEXT } from 'vs/workbench/contrib/chat/common/ch import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { InlineChatAccessibilityHelp } from 'vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp'; // --- browser @@ -94,8 +95,8 @@ const workbenchContributionsRegistry = Registry.as renderMarkdownAsPlaintext(new MarkdownString(responseContent), true), + () => controller.focus(), + AccessibilityVerbositySettingId.InlineChat + ); } - dispose() { } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 2009001a7ea..918e5b0e4ff 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -29,6 +29,7 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -342,7 +343,7 @@ export class RerunAction extends AbstractInlineChatAction { const lastRequest = model?.getRequests().at(-1); if (lastRequest) { - await chatService.resendRequest(lastRequest, { noCommandDetection: false, attempt: lastRequest.attempt + 1, location: ctrl.chatWidget.location }); + await chatService.resendRequest(lastRequest, { noCommandDetection: false, attempt: lastRequest.attempt + 1, location: ctrl.chatWidget.location, locationData: ctrl.chatWidget.getLocationData() }); } } } @@ -504,6 +505,7 @@ export class ViewInChatAction extends AbstractInlineChatAction { keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.DownArrow, + when: CONTEXT_IN_CHAT_INPUT } }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts index 92ba84eb48e..be23211e5db 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -26,6 +26,7 @@ import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuItemAction } from 'vs/platform/actions/common/actions'; import { TextOnlyMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export class InlineChatContentWidget implements IContentWidget { @@ -53,7 +54,8 @@ export class InlineChatContentWidget implements IContentWidget { private readonly _editor: ICodeEditor, @IInstantiationService instaService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IQuickInputService quickInputService: IQuickInputService ) { this._defaultChatModel = this._store.add(instaService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); @@ -76,7 +78,7 @@ export class InlineChatContentWidget implements IContentWidget { renderStyle: 'minimal', renderInputOnTop: true, renderFollowups: true, - supportsFileReferences: false, + supportsFileReferences: configurationService.getValue(`chat.experimental.variables.${location.location}`) === true, menus: { telemetrySource: 'inlineChat-content', executeToolbar: MENU_INLINE_CHAT_EXECUTE, @@ -118,9 +120,19 @@ export class InlineChatContentWidget implements IContentWidget { this._domNode.classList.toggle('contents', toolbar.getItemsLength() > 1); })); + // note when the widget has been interaced with and disable "close on blur" if so + let widgetHasBeenInteractedWith = false; + this._store.add(this._widget.inputEditor.onDidChangeModelContent(() => { + widgetHasBeenInteractedWith ||= this._widget.inputEditor.getModel()?.getValueLength() !== 0; + })); + this._store.add(this._widget.onDidChangeContext(() => { + widgetHasBeenInteractedWith ||= true; + _editor.layoutContentWidget(this);// https://github.com/microsoft/vscode/issues/221385 + })); + const tracker = dom.trackFocus(this._domNode); this._store.add(tracker.onDidBlur(() => { - if (this._visible && this._widget.inputEditor.getModel()?.getValueLength() === 0) { + if (this._visible && !widgetHasBeenInteractedWith && !quickInputService.currentQuickInput) { this._onDidBlur.fire(); } })); @@ -154,10 +166,11 @@ export class InlineChatContentWidget implements IContentWidget { const maxHeight = this._widget.input.inputEditor.getOption(EditorOption.lineHeight) * 5; const inputEditorHeight = this._widget.contentHeight; - this._widget.layout(Math.min(maxHeight, inputEditorHeight), 390); + const height = Math.min(maxHeight, inputEditorHeight); + const width = 390; + this._widget.layout(height, width); - // const actualHeight = this._widget.inputPartHeight; - // return new dom.Dimension(width, actualHeight); + dom.size(this._domNode, width, null); return null; } @@ -193,6 +206,7 @@ export class InlineChatContentWidget implements IContentWidget { this._position = wordInfo ? new Position(position.lineNumber, wordInfo.startColumn) : position; this._editor.addContentWidget(this); + this._widget.setContext(true); this._widget.setVisible(true); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 782bc3b95bc..5f078bf49d7 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -31,12 +31,11 @@ import { ILogService } from 'vs/platform/log/common/log'; import { showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IInlineChatSavingService } from './inlineChatSavingService'; -import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionPrompt, StashedSession } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from './inlineChatSessionService'; import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { StashedSession } from './inlineChatSession'; import { IValidEditOperation } from 'vs/editor/common/model'; import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 2e2f692cc88..347943488b9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -26,10 +26,9 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { Progress } from 'vs/platform/progress/common/progress'; import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { HunkInformation, Session, HunkState } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { HunkState } from './inlineChatSession'; import { assertType } from 'vs/base/common/types'; import { IModelService } from 'vs/editor/common/services/model'; import { performAsyncTextEdit, asProgressiveEdit } from './utils'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index e920c34947b..35ad5dbf148 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -145,8 +145,17 @@ export class InlineChatWidget { renderStyle: 'minimal', renderInputOnTop: false, renderFollowups: true, - supportsFileReferences: false, - filter: item => !isWelcomeVM(item), + supportsFileReferences: _configurationService.getValue(`chat.experimental.variables.${location.location}`) === true, + filter: item => { + if (isWelcomeVM(item)) { + return false; + } + if (isResponseVM(item) && item.isComplete && item.response.value.every(item => item.kind === 'textEditGroup' && options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) { + // filter responses that are just text edits (prevents the "Made Edits") + return false; + } + return true; + }, ...options.chatWidgetViewOptions }, { @@ -490,6 +499,7 @@ export class InlineChatWidget { } reset() { + this._chatWidget.setContext(true); this._chatWidget.saveState(); this.updateChatMessage(undefined); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index cc4ddad6e7a..60d8a40d7ca 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -37,10 +37,6 @@ import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/ import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, InlineChatConfigKeys } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { TestViewsService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { IInlineChatSavingService } from '../../browser/inlineChatSavingService'; -import { IInlineChatSessionService } from '../../browser/inlineChatSessionService'; -import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl'; -import { TestWorkerService } from './testWorkerService'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; @@ -63,6 +59,12 @@ import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/se import { RerunAction } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { assertType } from 'vs/base/common/types'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; +import { NullWorkbenchAssignmentService } from 'vs/workbench/services/assignment/test/common/nullAssignmentService'; +import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; +import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; +import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; +import { TestWorkerService } from 'vs/workbench/contrib/inlineChat/test/browser/testWorkerService'; suite('InteractiveChatController', function () { @@ -196,7 +198,8 @@ suite('InteractiveChatController', function () { }], [INotebookEditorService, new class extends mock() { override listNotebookEditors() { return []; } - }] + }], + [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()] ); instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection)); @@ -765,6 +768,7 @@ suite('InteractiveChatController', function () { const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); ctrl.run({ message: 'Hello-', autoSend: true }); assert.strictEqual(await p, undefined); + await timeout(10); assert.deepStrictEqual(attempts, [0]); // RERUN (cancel, undo, redo) @@ -803,6 +807,7 @@ suite('InteractiveChatController', function () { // REQUEST 1 const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); ctrl.run({ message: 'Hello', autoSend: true }); + await timeout(10); assert.strictEqual(await p, undefined); assertType(progress); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 26e72175e2c..8545dad41ff 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -56,6 +56,8 @@ import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVari import { ICommandService } from 'vs/platform/commands/common/commands'; import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; +import { NullWorkbenchAssignmentService } from 'vs/workbench/services/assignment/test/common/nullAssignmentService'; suite('InlineChatSession', function () { @@ -115,7 +117,8 @@ suite('InlineChatSession', function () { [IConfigurationService, new TestConfigurationService()], [IViewDescriptorService, new class extends mock() { override onDidChangeLocation = Event.None; - }] + }], + [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()] ); diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 2109f7ef705..7a488364abc 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -23,11 +23,10 @@ import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/sug import { localize, localize2 } from 'vs/nls'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { EditorActivation, IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { EditorActivation, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -138,17 +137,25 @@ export class InteractiveDocumentContribution extends Disposable implements IWork { createEditorInput: ({ resource, options }) => { const data = CellUri.parse(resource); - let cellOptions: IResourceEditorInput | undefined; - let IwResource = resource; + let cellOptions: ITextResourceEditorInput | undefined; + let iwResource = resource; if (data) { cellOptions = { resource, options }; - IwResource = data.notebook; + iwResource = data.notebook; } - const notebookOptions = { ...options, cellOptions } as INotebookEditorOptions; + const notebookOptions: INotebookEditorOptions | undefined = { + ...options, + cellOptions, + cellRevealType: undefined, + cellSelections: undefined, + isReadOnly: undefined, + viewState: undefined, + indexedCellOptions: undefined + }; - const editorInput = createEditor(IwResource, this.instantiationService); + const editorInput = createEditor(iwResource, this.instantiationService); return { editor: editorInput, options: notebookOptions @@ -159,13 +166,21 @@ export class InteractiveDocumentContribution extends Disposable implements IWork throw new Error('Interactive window editors must have a resource name'); } const data = CellUri.parse(resource); - let cellOptions: IResourceEditorInput | undefined; + let cellOptions: ITextResourceEditorInput | undefined; if (data) { cellOptions = { resource, options }; } - const notebookOptions = { ...options, cellOptions } as INotebookEditorOptions; + const notebookOptions: INotebookEditorOptions = { + ...options, + cellOptions, + cellRevealType: undefined, + cellSelections: undefined, + isReadOnly: undefined, + viewState: undefined, + indexedCellOptions: undefined + }; const editorInput = createEditor(resource, this.instantiationService); return { @@ -432,25 +447,6 @@ registerAction2(class extends Action2 { } }); -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'interactive.configure', - title: localize2('interactive.configExecute', 'Configure input box behavior'), - category: interactiveWindowCategory, - f1: false, - icon: icons.configIcon, - menu: { - id: MenuId.InteractiveInputConfig - } - }); - } - - override run(accessor: ServicesAccessor, ...args: any[]): void { - accessor.get(ICommandService).executeCommand('workbench.action.openSettings', '@tag:replExecute'); - } -}); - registerAction2(class extends Action2 { constructor() { super({ @@ -526,7 +522,7 @@ registerAction2(class extends Action2 { } if (editorControl && isReplEditor) { - executeReplInput(accessor, editorControl); + executeReplInput(bulkEditService, historyService, notebookEditorService, editorControl); } if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index a6b48582111..cbd701d383f 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -60,7 +60,8 @@ import { INTERACTIVE_WINDOW_EDITOR_ID } from 'vs/workbench/contrib/notebook/comm import 'vs/css!./interactiveEditor'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { deepClone } from 'vs/base/common/objects'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; +import { MarginHoverController } from 'vs/editor/contrib/hover/browser/marginHoverController'; import { ReplInputHintContentWidget } from 'vs/workbench/contrib/interactive/browser/replInputHintContentWidget'; const DECORATION_KEY = 'interactiveInputDecoration'; @@ -88,7 +89,6 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro private _inputCellContainer!: HTMLElement; private _inputFocusIndicator!: HTMLElement; private _inputRunButtonContainer!: HTMLElement; - private _inputConfigContainer!: HTMLElement; private _inputEditorContainer!: HTMLElement; private _codeEditorWidget!: CodeEditorWidget; private _notebookWidgetService: INotebookEditorService; @@ -199,36 +199,9 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._inputRunButtonContainer = DOM.append(this._inputCellContainer, DOM.$('.run-button-container')); this._setupRunButtonToolbar(this._inputRunButtonContainer); this._inputEditorContainer = DOM.append(this._inputCellContainer, DOM.$('.input-editor-container')); - this._setupConfigButtonToolbar(); this._createLayoutStyles(); } - private _setupConfigButtonToolbar() { - this._inputConfigContainer = DOM.append(this._inputEditorContainer, DOM.$('.input-toolbar-container')); - this._inputConfigContainer.style.position = 'absolute'; - this._inputConfigContainer.style.right = '0'; - this._inputConfigContainer.style.marginTop = '6px'; - this._inputConfigContainer.style.marginRight = '12px'; - this._inputConfigContainer.style.zIndex = '1'; - this._inputConfigContainer.style.display = 'none'; - - const menu = this._register(this._menuService.createMenu(MenuId.InteractiveInputConfig, this._contextKeyService)); - const toolbar = this._register(new ToolBar(this._inputConfigContainer, this._contextMenuService, { - getKeyBinding: action => this._keybindingService.lookupKeybinding(action.id), - actionViewItemProvider: (action, options) => { - return createActionViewItem(this._instantiationService, action, options); - }, - renderDropdownAsChildElement: true - })); - - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary }; - - createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result); - toolbar.setActions([...primary, ...secondary]); - } - private _setupRunButtonToolbar(runButtonContainer: HTMLElement) { const menu = this._register(this._menuService.createMenu(MenuId.InteractiveInputExecute, this._contextKeyService)); this._runbuttonToolbar = this._register(new ToolBar(runButtonContainer, this._contextMenuService, { @@ -410,7 +383,8 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro cellEditorContributions: EditorExtensionsRegistry.getSomeEditorContributions([ SelectionClipboardContributionID, ContextMenuController.ID, - HoverController.ID, + ContentHoverController.ID, + MarginHoverController.ID, MarkerController.ID ]), options: this._notebookOptions, @@ -428,7 +402,8 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro ParameterHintsController.ID, SnippetController2.ID, TabCompletionController.ID, - HoverController.ID, + ContentHoverController.ID, + MarginHoverController.ID, MarkerController.ID ]) } @@ -683,11 +658,9 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro if (!this._hintElement && !shouldHide) { this._hintElement = this._instantiationService.createInstance(ReplInputHintContentWidget, this._codeEditorWidget); - this._inputConfigContainer.style.display = 'block'; } else if (this._hintElement && shouldHide) { this._hintElement.dispose(); this._hintElement = undefined; - this._inputConfigContainer.style.display = 'none'; } } diff --git a/src/vs/workbench/contrib/issue/browser/issue.ts b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts similarity index 83% rename from src/vs/workbench/contrib/issue/browser/issue.ts rename to src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts index 4f097ba4603..ec21ac3891f 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.ts +++ b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts @@ -2,24 +2,27 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { IProductConfiguration } from 'vs/base/common/product'; -import { $, createStyleSheet, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { $, createStyleSheet, isHTMLInputElement, isHTMLTextAreaElement, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; import { Button, unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { mainWindow } from 'vs/base/browser/window'; import { Delayer, RunOnceScheduler } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; +import { groupBy } from 'vs/base/common/collections'; import { debounce } from 'vs/base/common/decorators'; import { CancellationError } from 'vs/base/common/errors'; -import { isLinuxSnap } from 'vs/base/common/platform'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { isLinuxSnap, isMacintosh } from 'vs/base/common/platform'; +import { IProductConfiguration } from 'vs/base/common/product'; import { escape } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue'; -import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; +import { OldIssueReporterData } from 'vs/platform/issue/common/issue'; import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; -import { mainWindow } from 'vs/base/browser/window'; +import { IIssueFormService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueType } from 'vs/workbench/contrib/issue/common/issue'; +import { normalizeGitHubUrl } from 'vs/workbench/contrib/issue/common/issueReporterUtil'; +import { ThemeIcon } from 'vs/base/common/themables'; const MAX_URL_LENGTH = 7500; @@ -53,7 +56,7 @@ export class BaseIssueReporterService extends Disposable { constructor( public disableExtensions: boolean, - public data: IssueReporterData, + public data: IssueReporterData | OldIssueReporterData, public os: { type: string; arch: string; @@ -62,7 +65,7 @@ export class BaseIssueReporterService extends Disposable { public product: IProductConfiguration, public readonly window: Window, public readonly isWeb: boolean, - @IIssueMainService public readonly issueMainService: IIssueMainService + @IIssueFormService public readonly issueFormService: IIssueFormService ) { super(); const targetExtension = data.extensionId ? data.enabledExtensions.find(extension => extension.id.toLocaleLowerCase() === data.extensionId?.toLocaleLowerCase()) : undefined; @@ -127,6 +130,7 @@ export class BaseIssueReporterService extends Disposable { iconsStyleSheet.onDidChange(() => delayer.schedule()); delayer.schedule(); + this.handleExtensionData(data.enabledExtensions); this.setUpTypes(); this.applyStyles(data.styles); @@ -160,6 +164,11 @@ export class BaseIssueReporterService extends Disposable { content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { background-color: ${styles.inputBackground} !important; }`); } + if (styles.backgroundColor) { + content.push(`.monaco-workbench { background-color: ${styles.backgroundColor} !important; }`); + content.push(`.issue-reporter-body::-webkit-scrollbar-track { background-color: ${styles.backgroundColor}; }`); + } + if (styles.inputBorder) { content.push(`input[type="text"], textarea, select { border: 1px solid ${styles.inputBorder}; }`); } else { @@ -199,16 +208,13 @@ export class BaseIssueReporterService extends Disposable { content.push(`a:hover, .workbenchCommand:hover { color: ${styles.textLinkActiveForeground}; }`); } - if (styles.sliderBackgroundColor) { - content.push(`::-webkit-scrollbar-thumb { background-color: ${styles.sliderBackgroundColor}; }`); - } - if (styles.sliderActiveColor) { - content.push(`::-webkit-scrollbar-thumb:active { background-color: ${styles.sliderActiveColor}; }`); + content.push(`.issue-reporter-body::-webkit-scrollbar-thumb:active { background-color: ${styles.sliderActiveColor}; }`); } if (styles.sliderHoverColor) { - content.push(`::--webkit-scrollbar-thumb:hover { background-color: ${styles.sliderHoverColor}; }`); + content.push(`.issue-reporter-body::-webkit-scrollbar-thumb { background-color: ${styles.sliderHoverColor}; }`); + content.push(`.issue-reporter-body::--webkit-scrollbar-thumb:hover { background-color: ${styles.sliderHoverColor}; }`); } if (styles.buttonBackground) { @@ -239,6 +245,134 @@ export class BaseIssueReporterService extends Disposable { } } + private handleExtensionData(extensions: IssueReporterExtensionData[]) { + const installedExtensions = extensions.filter(x => !x.isBuiltin); + const { nonThemes, themes } = groupBy(installedExtensions, ext => { + return ext.isTheme ? 'themes' : 'nonThemes'; + }); + + const numberOfThemeExtesions = themes && themes.length; + this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions }); + this.updateExtensionTable(nonThemes, numberOfThemeExtesions); + if (this.disableExtensions || installedExtensions.length === 0) { + (this.getElementById('disableExtensions')).disabled = true; + } + + this.updateExtensionSelector(installedExtensions); + } + + private updateExtensionSelector(extensions: IssueReporterExtensionData[]): void { + interface IOption { + name: string; + id: string; + } + + const extensionOptions: IOption[] = extensions.map(extension => { + return { + name: extension.displayName || extension.name || '', + id: extension.id + }; + }); + + // Sort extensions by name + extensionOptions.sort((a, b) => { + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + if (aName > bName) { + return 1; + } + + if (aName < bName) { + return -1; + } + + return 0; + }); + + const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => { + const selected = selectedExtension && extension.id === selectedExtension.id; + return $('option', { + 'value': extension.id, + 'selected': selected || '' + }, extension.name); + }; + + const extensionsSelector = this.getElementById('extension-selector'); + if (extensionsSelector) { + const { selectedExtension } = this.issueReporterModel.getData(); + reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); + + if (!selectedExtension) { + extensionsSelector.selectedIndex = 0; + } + + this.addEventListener('extension-selector', 'change', async (e: Event) => { + this.clearExtensionData(); + const selectedExtensionId = (e.target).value; + this.selectedExtension = selectedExtensionId; + const extensions = this.issueReporterModel.getData().allExtensions; + const matches = extensions.filter(extension => extension.id === selectedExtensionId); + if (matches.length) { + this.issueReporterModel.update({ selectedExtension: matches[0] }); + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension) { + const iconElement = document.createElement('span'); + iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); + this.setLoading(iconElement); + const openReporterData = await this.sendReporterMenu(selectedExtension); + if (openReporterData) { + if (this.selectedExtension === selectedExtensionId) { + this.removeLoading(iconElement, true); + // this.configuration.data = openReporterData; + this.data = openReporterData; + } + // else if (this.selectedExtension !== selectedExtensionId) { + // } + } + else { + if (!this.loadingExtensionData) { + iconElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); + } + this.removeLoading(iconElement); + // if not using command, should have no configuration data in fields we care about and check later. + this.clearExtensionData(); + + // case when previous extension was opened from normal openIssueReporter command + selectedExtension.data = undefined; + selectedExtension.uri = undefined; + } + if (this.selectedExtension === selectedExtensionId) { + // repopulates the fields with the new data given the selected extension. + this.updateExtensionStatus(matches[0]); + this.openReporter = false; + } + } else { + this.issueReporterModel.update({ selectedExtension: undefined }); + this.clearSearchResults(); + this.clearExtensionData(); + this.validateSelectedExtension(); + this.updateExtensionStatus(matches[0]); + } + } + }); + } + + this.addEventListener('problem-source', 'change', (_) => { + this.clearExtensionData(); + this.validateSelectedExtension(); + }); + } + + private async sendReporterMenu(extension: IssueReporterExtensionData): Promise { + try { + const data = await this.issueFormService.sendReporterMenu(extension.id); + return data; + } catch (e) { + console.error(e); + return undefined; + } + } + public setEventHandlers(): void { (['includeSystemInfo', 'includeProcessInfo', 'includeWorkspaceInfo', 'includeExtensions', 'includeExperiments', 'includeExtensionData'] as const).forEach(elementId => { this.addEventListener(elementId, 'click', (event: Event) => { @@ -341,6 +475,65 @@ export class BaseIssueReporterService extends Disposable { const { fileOnExtension, fileOnMarketplace } = this.issueReporterModel.getData(); this.searchIssues(title, fileOnExtension, fileOnMarketplace); }); + + this.previewButton.onDidClick(async () => { + this.delayedSubmit.trigger(async () => { + this.createIssue(); + }); + }); + + this.addEventListener('disableExtensions', 'click', () => { + this.issueFormService.reloadWithExtensionsDisabled(); + }); + + this.addEventListener('extensionBugsLink', 'click', (e: Event) => { + const url = (e.target).innerText; + windowOpenNoOpener(url); + }); + + this.addEventListener('disableExtensions', 'keydown', (e: Event) => { + e.stopPropagation(); + if ((e as KeyboardEvent).key === 'Enter' || (e as KeyboardEvent).key === ' ') { + this.issueFormService.reloadWithExtensionsDisabled(); + } + }); + + this.window.document.onkeydown = async (e: KeyboardEvent) => { + const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; + // Cmd/Ctrl+Enter previews issue and closes window + if (cmdOrCtrlKey && e.key === 'Enter') { + this.delayedSubmit.trigger(async () => { + if (await this.createIssue()) { + this.close(); + } + }); + } + + // Cmd/Ctrl + w closes issue window + if (cmdOrCtrlKey && e.key === 'w') { + e.stopPropagation(); + e.preventDefault(); + + const issueTitle = (this.getElementById('issue-title'))!.value; + const { issueDescription } = this.issueReporterModel.getData(); + if (!this.hasBeenSubmitted && (issueTitle || issueDescription)) { + // fire and forget + this.issueFormService.showConfirmCloseDialog(); + } else { + this.close(); + } + } + + // With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac + // Manually perform the selection + if (isMacintosh) { + if (cmdOrCtrlKey && e.key === 'a' && e.target) { + if (isHTMLInputElement(e.target) || isHTMLTextAreaElement(e.target)) { + (e.target).select(); + } + } + } + }; } public updatePerformanceInfo(info: Partial) { @@ -414,6 +607,10 @@ export class BaseIssueReporterService extends Disposable { if (issueType === IssueType.PerformanceIssue && this.receivedSystemInfo && this.receivedPerformanceInfo) { return true; } + + if (issueType === IssueType.FeatureRequest) { + return true; + } } return false; @@ -480,7 +677,7 @@ export class BaseIssueReporterService extends Disposable { } public async close(): Promise { - await this.issueMainService.$closeReporter(); + await this.issueFormService.closeReporter(); } public clearSearchResults(): void { @@ -911,7 +1108,7 @@ export class BaseIssueReporterService extends Disposable { } public async writeToClipboard(baseUrl: string, issueBody: string): Promise { - const shouldWrite = await this.issueMainService.$showClipboardDialog(); + const shouldWrite = await this.issueFormService.showClipboardDialog(); if (!shouldWrite) { throw new CancellationError(); } @@ -974,7 +1171,7 @@ export class BaseIssueReporterService extends Disposable { public clearExtensionData(): void { this.nonGitHubIssueUrl = false; this.issueReporterModel.update({ extensionData: undefined }); - this.data.issueBody = undefined; + this.data.issueBody = this.data.issueBody || ''; this.data.data = undefined; this.data.uri = undefined; } diff --git a/src/vs/workbench/contrib/issue/browser/issue.contribution.ts b/src/vs/workbench/contrib/issue/browser/issue.contribution.ts index 668418a0d46..4dea77bb077 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/browser/issue.contribution.ts @@ -5,19 +5,18 @@ import * as nls from 'vs/nls'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { BrowserIssueService } from 'vs/workbench/contrib/issue/browser/issueService'; -import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; -import { BaseIssueContribution } from 'vs/workbench/contrib/issue/common/issue.contribution'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { IIssueMainService } from 'vs/platform/issue/common/issue'; import { IssueFormService } from 'vs/workbench/contrib/issue/browser/issueFormService'; +import { BrowserIssueService } from 'vs/workbench/contrib/issue/browser/issueService'; import 'vs/workbench/contrib/issue/browser/issueTroubleshoot'; +import { IIssueFormService, IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; +import { BaseIssueContribution } from 'vs/workbench/contrib/issue/common/issue.contribution'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; class WebIssueContribution extends BaseIssueContribution { @@ -38,7 +37,7 @@ class WebIssueContribution extends BaseIssueContribution { Registry.as(Extensions.Workbench).registerWorkbenchContribution(WebIssueContribution, LifecyclePhase.Restored); registerSingleton(IWorkbenchIssueService, BrowserIssueService, InstantiationType.Delayed); -registerSingleton(IIssueMainService, IssueFormService, InstantiationType.Delayed); +registerSingleton(IIssueFormService, IssueFormService, InstantiationType.Delayed); CommandsRegistry.registerCommand('_issues.getSystemStatus', (accessor) => { return nls.localize('statusUnsupported', "The --status argument is not yet supported in browsers."); diff --git a/src/vs/workbench/contrib/issue/browser/issueFormService.ts b/src/vs/workbench/contrib/issue/browser/issueFormService.ts index 1ac7d114454..3a7b2ef1fdc 100644 --- a/src/vs/workbench/contrib/issue/browser/issueFormService.ts +++ b/src/vs/workbench/contrib/issue/browser/issueFormService.ts @@ -3,102 +3,80 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { safeInnerHtml } from 'vs/base/browser/dom'; -import { mainWindow } from 'vs/base/browser/window'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import BaseHtml from 'vs/workbench/contrib/issue/browser/issueReporterPage'; +import Severity from 'vs/base/common/severity'; import 'vs/css!./media/issueReporter'; +import { localize } from 'vs/nls'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { PerformanceInfo, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ExtensionIdentifier, ExtensionIdentifierSet } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IIssueMainService, IssueReporterData, ProcessExplorerData } from 'vs/platform/issue/common/issue'; +import { ILogService } from 'vs/platform/log/common/log'; import product from 'vs/platform/product/common/product'; +import { IRectangle } from 'vs/platform/window/common/window'; +import BaseHtml from 'vs/workbench/contrib/issue/browser/issueReporterPage'; import { IssueWebReporter } from 'vs/workbench/contrib/issue/browser/issueReporterService'; +import { IIssueFormService, IssueReporterData } from 'vs/workbench/contrib/issue/common/issue'; import { AuxiliaryWindowMode, IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; export interface IssuePassData { issueTitle: string; issueBody: string; } -export class IssueFormService implements IIssueMainService { +export class IssueFormService implements IIssueFormService { readonly _serviceBrand: undefined; - private issueReporterWindow: Window | null = null; - private extensionIdentifierSet: ExtensionIdentifierSet = new ExtensionIdentifierSet(); + protected currentData: IssueReporterData | undefined; + + protected issueReporterWindow: Window | null = null; + protected extensionIdentifierSet: ExtensionIdentifierSet = new ExtensionIdentifierSet(); + + protected arch: string = ''; + protected release: string = ''; + protected type: string = ''; constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService, - @IMenuService private readonly menuService: IMenuService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - ) { - - // listen for messages from the main window - mainWindow.addEventListener('message', async (event) => { - if (event.data && event.data.sendChannel === 'vscode:triggerReporterMenu') { - // gets menu actions from contributed - const actions = this.menuService.getMenuActions(MenuId.IssueReporter, this.contextKeyService, { renderShortTitle: true }).flatMap(entry => entry[1]); - - // render menu - for (const action of actions) { - try { - if (action.item && 'source' in action.item && action.item.source?.id === event.data.extensionId) { - this.extensionIdentifierSet.add(event.data.extensionId); - await action.run(); - } - } catch (error) { - console.error(error); - } - } - - if (!this.extensionIdentifierSet.has(event.data.extensionId)) { - // send undefined to indicate no action was taken - const replyChannel = `vscode:triggerReporterMenuResponse`; - mainWindow.postMessage({ replyChannel }, '*'); - } - } - }); - - } + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IAuxiliaryWindowService protected readonly auxiliaryWindowService: IAuxiliaryWindowService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService, + @ILogService protected readonly logService: ILogService, + @IDialogService protected readonly dialogService: IDialogService, + @IHostService protected readonly hostService: IHostService + ) { } async openReporter(data: IssueReporterData): Promise { - if (data.extensionId && this.extensionIdentifierSet.has(data.extensionId)) { - const replyChannel = `vscode:triggerReporterMenuResponse`; - mainWindow.postMessage({ data, replyChannel }, '*'); - this.extensionIdentifierSet.delete(new ExtensionIdentifier(data.extensionId)); - } - - if (this.issueReporterWindow) { - const getModelData = await this.getIssueData(); - if (getModelData) { - const { issueTitle, issueBody } = getModelData; - if (issueTitle || issueBody) { - data.issueTitle = data.issueTitle ?? issueTitle; - data.issueBody = data.issueBody ?? issueBody; - - // close issue reporter and re-open with new data - this.issueReporterWindow.close(); - this.openAuxIssueReporter(data); - return; - } - } - this.issueReporterWindow.focus(); + if (this.hasToReload(data)) { return; } - this.openAuxIssueReporter(data); + + await this.openAuxIssueReporter(data); + + if (this.issueReporterWindow) { + const issueReporter = this.instantiationService.createInstance(IssueWebReporter, false, data, { type: this.type, arch: this.arch, release: this.release }, product, this.issueReporterWindow); + issueReporter.render(); + } } - async openAuxIssueReporter(data: IssueReporterData): Promise { + async openAuxIssueReporter(data: IssueReporterData, bounds?: IRectangle): Promise { + + let issueReporterBounds: Partial = { width: 700, height: 800 }; + + // Center Issue Reporter Window based on bounds from native host service + if (bounds && bounds.x && bounds.y) { + const centerX = bounds.x + bounds.width / 2; + const centerY = bounds.y + bounds.height / 2; + issueReporterBounds = { ...issueReporterBounds, x: centerX - 350, y: centerY - 400 }; + } + const disposables = new DisposableStore(); // Auxiliary Window - const auxiliaryWindow = disposables.add(await this.auxiliaryWindowService.open({ mode: AuxiliaryWindowMode.Normal })); - - this.issueReporterWindow = auxiliaryWindow.window; + const auxiliaryWindow = disposables.add(await this.auxiliaryWindowService.open({ mode: AuxiliaryWindowMode.Normal, bounds: issueReporterBounds, nativeTitlebar: true, disableFullscreen: true })); if (auxiliaryWindow) { await auxiliaryWindow.whenStylesHaveLoaded; @@ -114,9 +92,7 @@ export class IssueFormService implements IIssueMainService { auxiliaryWindow.window.document.body.appendChild(div); safeInnerHtml(div, BaseHtml()); - // create issue reporter and instantiate - const issueReporter = this.instantiationService.createInstance(IssueWebReporter, false, data, { type: '', arch: '', release: '' }, product, auxiliaryWindow.window); - issueReporter.render(); + this.issueReporterWindow = auxiliaryWindow.window; } else { console.error('Failed to open auxiliary window'); } @@ -128,94 +104,108 @@ export class IssueFormService implements IIssueMainService { }); } - async openProcessExplorer(data: ProcessExplorerData): Promise { - throw new Error('Method not implemented.'); - } + async sendReporterMenu(extensionId: string): Promise { + const menu = this.menuService.createMenu(MenuId.IssueReporter, this.contextKeyService); - stopTracing(): Promise { - throw new Error('Method not implemented.'); - } - getSystemStatus(): Promise { - throw new Error('Method not implemented.'); - } - $getSystemInfo(): Promise { - throw new Error('Method not implemented.'); - } - $getPerformanceInfo(): Promise { - throw new Error('Method not implemented.'); - } - $reloadWithExtensionsDisabled(): Promise { - throw new Error('Method not implemented.'); - } - $showConfirmCloseDialog(): Promise { - throw new Error('Method not implemented.'); - } - $showClipboardDialog(): Promise { - throw new Error('Method not implemented.'); - } - $getIssueReporterUri(extensionId: string): Promise { - throw new Error('Method not implemented.'); - } - $getIssueReporterData(extensionId: string): Promise { - throw new Error('Method not implemented.'); - } - $getIssueReporterTemplate(extensionId: string): Promise { - throw new Error('Method not implemented.'); - } - $getReporterStatus(extensionId: string, extensionName: string): Promise { - throw new Error('Method not implemented.'); - } - - async $sendReporterMenu(extensionId: string, extensionName: string): Promise { - const sendChannel = `vscode:triggerReporterMenu`; - mainWindow.postMessage({ sendChannel, extensionId, extensionName }, '*'); - - const result = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - mainWindow.removeEventListener('message', listener); - reject(new Error('Timeout exceeded')); - }, 5000); // Set the timeout value in milliseconds (e.g., 5000 for 5 seconds) - - const listener = (event: MessageEvent) => { - const replyChannel = `vscode:triggerReporterMenuResponse`; - if (event.data && event.data.replyChannel === replyChannel) { - clearTimeout(timeout); - mainWindow.removeEventListener('message', listener); - resolve(event.data.data); + // render menu and dispose + const actions = menu.getActions({ renderShortTitle: true }).flatMap(entry => entry[1]); + for (const action of actions) { + try { + if (action.item && 'source' in action.item && action.item.source?.id === extensionId) { + this.extensionIdentifierSet.add(extensionId); + await action.run(); } - }; - mainWindow.addEventListener('message', listener); - }); + } catch (error) { + console.error(error); + } + } - return result as IssueReporterData | undefined; + if (!this.extensionIdentifierSet.has(extensionId)) { + // send undefined to indicate no action was taken + return undefined; + } + + // we found the extension, now we clean up the menu and remove it from the set. This is to ensure that we do duplicate extension identifiers + this.extensionIdentifierSet.delete(new ExtensionIdentifier(extensionId)); + menu.dispose(); + + const result = this.currentData; + + // reset current data. + this.currentData = undefined; + + return result ?? undefined; } - // Listens to data from the issue reporter model, which is updated regularly - async getIssueData(): Promise { - const sendChannel = `vscode:triggerIssueData`; - mainWindow.postMessage({ sendChannel }, '*'); + //#region used by issue reporter - const result = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - mainWindow.removeEventListener('message', listener); - reject(new Error('Timeout exceeded')); - }, 5000); // Set the timeout value in milliseconds (e.g., 5000 for 5 seconds) - - const listener = (event: MessageEvent) => { - const replyChannel = `vscode:triggerIssueDataResponse`; - if (event.data && event.data.replyChannel === replyChannel) { - clearTimeout(timeout); - mainWindow.removeEventListener('message', listener); - resolve(event.data.data); - } - }; - mainWindow.addEventListener('message', listener); - }); - - return result as IssuePassData | undefined; - } - - async $closeReporter(): Promise { + async closeReporter(): Promise { this.issueReporterWindow?.close(); } + + async reloadWithExtensionsDisabled(): Promise { + if (this.issueReporterWindow) { + try { + await this.hostService.reload({ disableExtensions: true }); + } catch (error) { + this.logService.error(error); + } + } + } + + async showConfirmCloseDialog(): Promise { + await this.dialogService.prompt({ + type: Severity.Warning, + message: localize('confirmCloseIssueReporter', "Your input will not be saved. Are you sure you want to close this window?"), + buttons: [ + { + label: localize({ key: 'yes', comment: ['&& denotes a mnemonic'] }, "&&Yes"), + run: () => { + this.closeReporter(); + this.issueReporterWindow = null; + } + }, + { + label: localize('cancel', "Cancel"), + run: () => { } + } + ] + }); + } + + async showClipboardDialog(): Promise { + let result = false; + + await this.dialogService.prompt({ + type: Severity.Warning, + message: localize('issueReporterWriteToClipboard', "There is too much data to send to GitHub directly. The data will be copied to the clipboard, please paste it into the GitHub issue page that is opened."), + buttons: [ + { + label: localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"), + run: () => { result = true; } + }, + { + label: localize('cancel', "Cancel"), + run: () => { result = false; } + } + ] + }); + + return result; + } + + hasToReload(data: IssueReporterData): boolean { + if (data.extensionId && this.extensionIdentifierSet.has(data.extensionId)) { + this.currentData = data; + this.issueReporterWindow?.focus(); + return true; + } + + if (this.issueReporterWindow) { + this.issueReporterWindow.focus(); + return true; + } + + return false; + } } diff --git a/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts index a131f19d865..7e9169f0ea8 100644 --- a/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts +++ b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts @@ -14,7 +14,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ThemeIcon } from 'vs/base/common/themables'; import { Codicon } from 'vs/base/common/codicons'; -import { IssueSource } from 'vs/platform/issue/common/issue'; +import { IssueSource } from 'vs/workbench/contrib/issue/common/issue'; import { IProductService } from 'vs/platform/product/common/productService'; export class IssueQuickAccess extends PickerQuickAccessProvider { diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterModel.ts b/src/vs/workbench/contrib/issue/browser/issueReporterModel.ts index f8274d9e4c0..439487440d3 100644 --- a/src/vs/workbench/contrib/issue/browser/issueReporterModel.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterModel.ts @@ -5,10 +5,11 @@ import { mainWindow } from 'vs/base/browser/window'; import { isRemoteDiagnosticError, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { ISettingSearchResult, IssueReporterExtensionData, IssueType } from 'vs/platform/issue/common/issue'; +import { OldIssueType } from 'vs/platform/issue/common/issue'; +import { ISettingSearchResult, IssueReporterExtensionData, IssueType } from 'vs/workbench/contrib/issue/common/issue'; export interface IssueReporterData { - issueType: IssueType; + issueType: IssueType | OldIssueType; issueDescription?: string; issueTitle?: string; extensionData?: string; diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterService.ts b/src/vs/workbench/contrib/issue/browser/issueReporterService.ts index 1c7b878ce39..cef5f3d9cba 100644 --- a/src/vs/workbench/contrib/issue/browser/issueReporterService.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterService.ts @@ -2,15 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, isHTMLInputElement, isHTMLTextAreaElement, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; -import { Codicon } from 'vs/base/common/codicons'; -import { groupBy } from 'vs/base/common/collections'; -import { isMacintosh } from 'vs/base/common/platform'; import { IProductConfiguration } from 'vs/base/common/product'; -import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; -import { IIssueMainService, IssueReporterData, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; -import { BaseIssueReporterService } from 'vs/workbench/contrib/issue/browser/issue'; +import { BaseIssueReporterService } from 'vs/workbench/contrib/issue/browser/baseIssueReporterService'; +import { IIssueFormService, IssueReporterData } from 'vs/workbench/contrib/issue/common/issue'; // GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe. // ref https://github.com/microsoft/vscode/issues/159191 @@ -26,9 +21,9 @@ export class IssueWebReporter extends BaseIssueReporterService { }, product: IProductConfiguration, window: Window, - @IIssueMainService issueMainService: IIssueMainService + @IIssueFormService issueFormService: IIssueFormService ) { - super(disableExtensions, data, os, product, window, true, issueMainService); + super(disableExtensions, data, os, product, window, true, issueFormService); const target = this.window.document.querySelector('.block-system .block-info'); @@ -37,41 +32,14 @@ export class IssueWebReporter extends BaseIssueReporterService { target?.appendChild(this.window.document.createTextNode(webInfo)); this.receivedSystemInfo = true; this.issueReporterModel.update({ systemInfoWeb: webInfo }); - } this.setEventHandlers(); - this.handleExtensionData(data.enabledExtensions); - } - - private handleExtensionData(extensions: IssueReporterExtensionData[]) { - const installedExtensions = extensions.filter(x => !x.isBuiltin); - const { nonThemes, themes } = groupBy(installedExtensions, ext => { - return ext.isTheme ? 'themes' : 'nonThemes'; - }); - - const numberOfThemeExtesions = themes && themes.length; - this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions }); - this.updateExtensionTable(nonThemes, numberOfThemeExtesions); - if (this.disableExtensions || installedExtensions.length === 0) { - (this.getElementById('disableExtensions')).disabled = true; - } - - this.updateExtensionSelector(installedExtensions); - } - - private async sendReporterMenu(extension: IssueReporterExtensionData): Promise { - try { - const data = await this.issueMainService.$sendReporterMenu(extension.id, extension.name); - return data; - } catch (e) { - console.error(e); - return undefined; - } } public override setEventHandlers(): void { super.setEventHandlers(); + this.addEventListener('issue-type', 'change', (event: Event) => { const issueType = parseInt((event.target).value); this.issueReporterModel.update({ issueType: issueType }); @@ -86,159 +54,5 @@ export class IssueWebReporter extends BaseIssueReporterService { this.setSourceOptions(); this.render(); }); - this.previewButton.onDidClick(async () => { - this.delayedSubmit.trigger(async () => { - this.createIssue(); - }); - }); - - this.addEventListener('disableExtensions', 'click', () => { - this.issueMainService.$reloadWithExtensionsDisabled(); - }); - - this.addEventListener('extensionBugsLink', 'click', (e: Event) => { - const url = (e.target).innerText; - windowOpenNoOpener(url); - }); - - this.addEventListener('disableExtensions', 'keydown', (e: Event) => { - e.stopPropagation(); - if ((e as KeyboardEvent).key === 'Enter' || (e as KeyboardEvent).key === ' ') { - this.issueMainService.$reloadWithExtensionsDisabled(); - } - }); - - this.window.document.onkeydown = async (e: KeyboardEvent) => { - const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; - // Cmd/Ctrl+Enter previews issue and closes window - if (cmdOrCtrlKey && e.key === 'Enter') { - this.delayedSubmit.trigger(async () => { - if (await this.createIssue()) { - this.close(); - } - }); - } - - // Cmd/Ctrl + w closes issue window - if (cmdOrCtrlKey && e.key === 'w') { - e.stopPropagation(); - e.preventDefault(); - - const issueTitle = (this.getElementById('issue-title'))!.value; - const { issueDescription } = this.issueReporterModel.getData(); - if (!this.hasBeenSubmitted && (issueTitle || issueDescription)) { - // fire and forget - this.issueMainService.$showConfirmCloseDialog(); - } else { - this.close(); - } - } - - // With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac - // Manually perform the selection - if (isMacintosh) { - if (cmdOrCtrlKey && e.key === 'a' && e.target) { - if (isHTMLInputElement(e.target) || isHTMLTextAreaElement(e.target)) { - (e.target).select(); - } - } - } - }; - } - - private updateExtensionSelector(extensions: IssueReporterExtensionData[]): void { - interface IOption { - name: string; - id: string; - } - - const extensionOptions: IOption[] = extensions.map(extension => { - return { - name: extension.displayName || extension.name || '', - id: extension.id - }; - }); - - // Sort extensions by name - extensionOptions.sort((a, b) => { - const aName = a.name.toLowerCase(); - const bName = b.name.toLowerCase(); - if (aName > bName) { - return 1; - } - - if (aName < bName) { - return -1; - } - - return 0; - }); - - const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => { - const selected = selectedExtension && extension.id === selectedExtension.id; - return $('option', { - 'value': extension.id, - 'selected': selected || '' - }, extension.name); - }; - - const extensionsSelector = this.getElementById('extension-selector'); - if (extensionsSelector) { - const { selectedExtension } = this.issueReporterModel.getData(); - reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); - - if (!selectedExtension) { - extensionsSelector.selectedIndex = 0; - } - - this.addEventListener('extension-selector', 'change', async (e: Event) => { - this.clearExtensionData(); - const selectedExtensionId = (e.target).value; - this.selectedExtension = selectedExtensionId; - const extensions = this.issueReporterModel.getData().allExtensions; - const matches = extensions.filter(extension => extension.id === selectedExtensionId); - if (matches.length) { - this.issueReporterModel.update({ selectedExtension: matches[0] }); - const selectedExtension = this.issueReporterModel.getData().selectedExtension; - if (selectedExtension) { - const iconElement = document.createElement('span'); - iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); - this.setLoading(iconElement); - const openReporterData = await this.sendReporterMenu(selectedExtension); - if (openReporterData) { - if (this.selectedExtension === selectedExtensionId) { - this.removeLoading(iconElement, true); - this.data = openReporterData; - } else if (this.selectedExtension !== selectedExtensionId) { - } - } - else { - if (!this.loadingExtensionData) { - iconElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); - } - this.removeLoading(iconElement); - this.clearExtensionData(); - selectedExtension.data = undefined; - selectedExtension.uri = undefined; - } - if (this.selectedExtension === selectedExtensionId) { - // repopulates the fields with the new data given the selected extension. - this.updateExtensionStatus(matches[0]); - this.openReporter = false; - } - } else { - this.issueReporterModel.update({ selectedExtension: undefined }); - this.clearSearchResults(); - this.clearExtensionData(); - this.validateSelectedExtension(); - this.updateExtensionStatus(matches[0]); - } - } - }); - } - - this.addEventListener('problem-source', 'change', (_) => { - this.validateSelectedExtension(); - }); } } diff --git a/src/vs/workbench/contrib/issue/browser/issueService.ts b/src/vs/workbench/contrib/issue/browser/issueService.ts index 6030bbfd928..0089a78d7a9 100644 --- a/src/vs/workbench/contrib/issue/browser/issueService.ts +++ b/src/vs/workbench/contrib/issue/browser/issueService.ts @@ -3,27 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from 'vs/base/browser/dom'; -import { userAgent } from 'vs/base/common/platform'; -import { IExtensionDescription, ExtensionType } from 'vs/platform/extensions/common/extensions'; -import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; import { getZoomLevel } from 'vs/base/browser/browser'; +import * as dom from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { userAgent } from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles } from 'vs/platform/issue/common/issue'; +import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; import { IProductService } from 'vs/platform/product/common/productService'; import { buttonBackground, buttonForeground, buttonHoverBackground, foreground, inputActiveOptionBorder, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IIssueFormService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; -import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; export class BrowserIssueService implements IWorkbenchIssueService { @@ -32,7 +31,7 @@ export class BrowserIssueService implements IWorkbenchIssueService { constructor( @IExtensionService private readonly extensionService: IExtensionService, @IProductService private readonly productService: IProductService, - @IIssueMainService private readonly issueMainService: IIssueMainService, + @IIssueFormService private readonly issueFormService: IIssueFormService, @IThemeService private readonly themeService: IThemeService, @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @@ -43,11 +42,6 @@ export class BrowserIssueService implements IWorkbenchIssueService { @IConfigurationService private readonly configurationService: IConfigurationService ) { } - //TODO @TylerLeonhardt @Tyriar to implement a process explorer for the web - async openProcessExplorer(): Promise { - console.error('openProcessExplorer is not implemented in web'); - } - async openReporter(options: Partial): Promise { // If web reporter setting is false open the old GitHub issue reporter if (!this.configurationService.getValue('issueReporter.experimental.webReporter')) { @@ -141,7 +135,7 @@ export class BrowserIssueService implements IWorkbenchIssueService { githubAccessToken }, options); - return this.issueMainService.openReporter(issueReporterData); + return this.issueFormService.openReporter(issueReporterData); } throw new Error(`No issue reporting URL configured for ${this.productService.nameLong}.`); diff --git a/src/vs/workbench/contrib/issue/browser/test/testReporterModel.test.ts b/src/vs/workbench/contrib/issue/browser/test/testReporterModel.test.ts index f90b10bade8..0ca567fa059 100644 --- a/src/vs/workbench/contrib/issue/browser/test/testReporterModel.test.ts +++ b/src/vs/workbench/contrib/issue/browser/test/testReporterModel.test.ts @@ -8,8 +8,8 @@ import assert from 'assert'; // eslint-disable-next-line local/code-import-patterns import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IssueReporterModel } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; -import { IssueType } from 'vs/platform/issue/common/issue'; -import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; +import { IssueType } from 'vs/workbench/contrib/issue/common/issue'; +import { normalizeGitHubUrl } from 'vs/workbench/contrib/issue/common/issueReporterUtil'; suite('IssueReporter', () => { ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/workbench/contrib/issue/common/issue.contribution.ts b/src/vs/workbench/contrib/issue/common/issue.contribution.ts index 8e4d7795710..b3e9a06e7e5 100644 --- a/src/vs/workbench/contrib/issue/common/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/common/issue.contribution.ts @@ -8,10 +8,9 @@ import { ICommandAction } from 'vs/platform/action/common/action'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandMetadata } from 'vs/platform/commands/common/commands'; -import { IssueReporterData } from 'vs/platform/issue/common/issue'; import { IProductService } from 'vs/platform/product/common/productService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; +import { IWorkbenchIssueService, IssueReporterData } from 'vs/workbench/contrib/issue/common/issue'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/issue/common/issue.ts b/src/vs/workbench/contrib/issue/common/issue.ts index 5834ba051be..978b6fb1886 100644 --- a/src/vs/workbench/contrib/issue/common/issue.ts +++ b/src/vs/workbench/contrib/issue/common/issue.ts @@ -2,8 +2,140 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +import { UriComponents } from 'vs/base/common/uri'; +import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IssueReporterData } from 'vs/platform/issue/common/issue'; +import { OldIssueReporterData } from 'vs/platform/issue/common/issue'; + +// Since data sent through the service is serialized to JSON, functions will be lost, so Color objects +// should not be sent as their 'toString' method will be stripped. Instead convert to strings before sending. +export interface WindowStyles { + backgroundColor?: string; + color?: string; +} +export interface WindowData { + styles: WindowStyles; + zoomLevel: number; +} + +export const enum IssueType { + Bug, + PerformanceIssue, + FeatureRequest +} + +export enum IssueSource { + VSCode = 'vscode', + Extension = 'extension', + Marketplace = 'marketplace' +} + +export interface IssueReporterStyles extends WindowStyles { + textLinkColor?: string; + textLinkActiveForeground?: string; + inputBackground?: string; + inputForeground?: string; + inputBorder?: string; + inputErrorBorder?: string; + inputErrorBackground?: string; + inputErrorForeground?: string; + inputActiveBorder?: string; + buttonBackground?: string; + buttonForeground?: string; + buttonHoverBackground?: string; + sliderBackgroundColor?: string; + sliderHoverColor?: string; + sliderActiveColor?: string; +} + +export interface IssueReporterExtensionData { + name: string; + publisher: string | undefined; + version: string; + id: string; + isTheme: boolean; + isBuiltin: boolean; + displayName: string | undefined; + repositoryUrl: string | undefined; + bugsUrl: string | undefined; + extensionData?: string; + extensionTemplate?: string; + data?: string; + uri?: UriComponents; +} + +export interface IssueReporterData extends WindowData { + styles: IssueReporterStyles; + enabledExtensions: IssueReporterExtensionData[]; + issueType?: IssueType; + issueSource?: IssueSource; + extensionId?: string; + experiments?: string; + restrictedMode: boolean; + isUnsupported: boolean; + githubAccessToken: string; + issueTitle?: string; + issueBody?: string; + data?: string; + uri?: UriComponents; +} + +export interface ISettingSearchResult { + extensionId: string; + key: string; + score: number; +} + +export interface ProcessExplorerStyles extends WindowStyles { + listHoverBackground?: string; + listHoverForeground?: string; + listFocusBackground?: string; + listFocusForeground?: string; + listFocusOutline?: string; + listActiveSelectionBackground?: string; + listActiveSelectionForeground?: string; + listHoverOutline?: string; + scrollbarShadowColor?: string; + scrollbarSliderBackgroundColor?: string; + scrollbarSliderHoverBackgroundColor?: string; + scrollbarSliderActiveBackgroundColor?: string; +} + +export interface ProcessExplorerData extends WindowData { + pid: number; + styles: ProcessExplorerStyles; + platform: string; + applicationName: string; +} + +export interface IssueReporterWindowConfiguration extends ISandboxConfiguration { + disableExtensions: boolean; + data: IssueReporterData | OldIssueReporterData; + os: { + type: string; + arch: string; + release: string; + }; +} + +export interface ProcessExplorerWindowConfiguration extends ISandboxConfiguration { + data: ProcessExplorerData; +} + +export const IIssueFormService = createDecorator('issueFormService'); + +export interface IIssueFormService { + readonly _serviceBrand: undefined; + + // Used by the issue reporter + openReporter(data: IssueReporterData): Promise; + reloadWithExtensionsDisabled(): Promise; + showConfirmCloseDialog(): Promise; + showClipboardDialog(): Promise; + sendReporterMenu(extensionId: string): Promise; + closeReporter(): Promise; +} export const IWorkbenchIssueService = createDecorator('workbenchIssueService'); diff --git a/src/vs/workbench/contrib/issue/common/issueReporterUtil.ts b/src/vs/workbench/contrib/issue/common/issueReporterUtil.ts new file mode 100644 index 00000000000..da2b397ba94 --- /dev/null +++ b/src/vs/workbench/contrib/issue/common/issueReporterUtil.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import { rtrim } from 'vs/base/common/strings'; + +export function normalizeGitHubUrl(url: string): string { + // If the url has a .git suffix, remove it + if (url.endsWith('.git')) { + url = url.substr(0, url.length - 4); + } + + // Remove trailing slash + url = rtrim(url, '/'); + + if (url.endsWith('/new')) { + url = rtrim(url, '/new'); + } + + if (url.endsWith('/issues')) { + url = rtrim(url, '/issues'); + } + + return url; +} diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index 9eacc2f7574..0269acf647f 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts @@ -5,7 +5,7 @@ import { localize, localize2 } from 'vs/nls'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; -import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; +import { IWorkbenchIssueService, IssueType, IIssueFormService } from 'vs/workbench/contrib/issue/common/issue'; import { BaseIssueContribution } from 'vs/workbench/contrib/issue/common/issue.contribution'; import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -13,17 +13,23 @@ import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IssueType } from 'vs/platform/issue/common/issue'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { IssueQuickAccess } from 'vs/workbench/contrib/issue/browser/issueQuickAccess'; +import { registerSingleton, InstantiationType } from 'vs/platform/instantiation/common/extensions'; +import { NativeIssueService } from 'vs/workbench/contrib/issue/electron-sandbox/issueService'; import 'vs/workbench/contrib/issue/electron-sandbox/issueMainService'; -import 'vs/workbench/contrib/issue/electron-sandbox/issueService'; import 'vs/workbench/contrib/issue/browser/issueTroubleshoot'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { NativeIssueFormService } from 'vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService'; + //#region Issue Contribution +registerSingleton(IWorkbenchIssueService, NativeIssueService, InstantiationType.Delayed); +registerSingleton(IIssueFormService, NativeIssueFormService, InstantiationType.Delayed); + class NativeIssueContribution extends BaseIssueContribution { constructor( @@ -51,6 +57,16 @@ class NativeIssueContribution extends BaseIssueContribution { }); }; + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + properties: { + 'issueReporter.experimental.auxWindow': { + type: 'boolean', + default: productService.quality !== 'stable', + description: 'Enable the new experimental issue reporter in electron.', + }, + } + }); + this._register(configurationService.onDidChangeConfiguration(e => { if (!configurationService.getValue('extensions.experimental.issueQuickAccess') && disposable) { disposable.dispose(); diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts index cf16313519a..b347982dd09 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; -import { IIssueMainService, IProcessMainService } from 'vs/platform/issue/common/issue'; +import { IProcessMainService, IIssueMainService } from 'vs/platform/issue/common/issue'; registerMainProcessRemoteService(IIssueMainService, 'issue'); registerMainProcessRemoteService(IProcessMainService, 'process'); diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.html b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.html index 455e823692a..9d853e358a0 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.html +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.html @@ -39,8 +39,7 @@ - - + diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.html b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.html index c6290004d2d..8d0bcc6c87c 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.html +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.html @@ -39,5 +39,5 @@ - + diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js index aad5671f1f0..15a7f2a211d 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ //@ts-check +'use strict'; + (function () { - 'use strict'; /** * @import { ISandboxConfiguration } from '../../../../base/parts/sandbox/common/sandboxTypes' diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts index ca9253bb1e9..84e28306d66 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts @@ -5,8 +5,8 @@ import { safeInnerHtml } from 'vs/base/browser/dom'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded +import { mainWindow } from 'vs/base/browser/window'; import { isLinux, isWindows } from 'vs/base/common/platform'; -import BaseHtml from 'vs/workbench/contrib/issue/browser/issueReporterPage'; import 'vs/css!./media/issueReporter'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; @@ -15,14 +15,14 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; -import { IIssueMainService, IProcessMainService, IssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; import { INativeHostService } from 'vs/platform/native/common/native'; import { NativeHostService } from 'vs/platform/native/common/nativeHostService'; -import { IssueReporter2 } from 'vs/workbench/contrib/issue/electron-sandbox/issueReporterService2'; -import { mainWindow } from 'vs/base/browser/window'; +import BaseHtml from 'vs/workbench/contrib/issue/browser/issueReporterPage'; +import { IProcessMainService, IIssueMainService, OldIssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; +import { IssueReporter } from 'vs/workbench/contrib/issue/electron-sandbox/issueReporterService'; -export function startup(configuration: IssueReporterWindowConfiguration) { +export function startup(configuration: OldIssueReporterWindowConfiguration) { const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac'; mainWindow.document.body.classList.add(platformClass); // used by our fonts @@ -30,7 +30,7 @@ export function startup(configuration: IssueReporterWindowConfiguration) { const instantiationService = initServices(configuration.windowId); - const issueReporter = instantiationService.createInstance(IssueReporter2, configuration); + const issueReporter = instantiationService.createInstance(IssueReporter, configuration); issueReporter.render(); mainWindow.document.body.style.display = 'block'; issueReporter.setInitialFocus(); diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts index 4fdf80be67d..b923388b64f 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts @@ -16,14 +16,14 @@ import { isLinuxSnap, isMacintosh } from 'vs/base/common/platform'; import { escape } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; -import { IIssueMainService, IProcessMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; -import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; import { INativeHostService } from 'vs/platform/native/common/native'; import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/window/electron-sandbox/window'; +import { IssueReporterData, IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; +import { IProcessMainService, IIssueMainService, OldIssueReporterData, OldIssueReporterExtensionData, OldIssueReporterStyles, OldIssueReporterWindowConfiguration, OldIssueType } from 'vs/platform/issue/common/issue'; +import { normalizeGitHubUrl } from 'vs/workbench/contrib/issue/common/issueReporterUtil'; // GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe. // ref https://github.com/microsoft/vscode/issues/159191 @@ -57,7 +57,7 @@ export class IssueReporter extends Disposable { private nonGitHubIssueUrl = false; constructor( - private readonly configuration: IssueReporterWindowConfiguration, + private readonly configuration: OldIssueReporterWindowConfiguration, @INativeHostService private readonly nativeHostService: INativeHostService, @IIssueMainService private readonly issueMainService: IIssueMainService, @IProcessMainService private readonly processMainService: IProcessMainService @@ -66,7 +66,7 @@ export class IssueReporter extends Disposable { const targetExtension = configuration.data.extensionId ? configuration.data.enabledExtensions.find(extension => extension.id.toLocaleLowerCase() === configuration.data.extensionId?.toLocaleLowerCase()) : undefined; this.issueReporterModel = new IssueReporterModel({ ...configuration.data, - issueType: configuration.data.issueType || IssueType.Bug, + issueType: configuration.data.issueType || OldIssueType.Bug, versionInfo: { vscodeVersion: `${configuration.product.nameShort} ${!!configuration.product.darwinUniversalAssetId ? `${configuration.product.version} (Universal)` : configuration.product.version} (${configuration.product.commit || 'Commit unknown'}, ${configuration.product.date || 'Date unknown'})`, os: `${this.configuration.os.type} ${this.configuration.os.arch} ${this.configuration.os.release}${isLinuxSnap ? ' snap' : ''}` @@ -115,7 +115,7 @@ export class IssueReporter extends Disposable { this.updateSystemInfo(this.issueReporterModel.getData()); this.updatePreviewButtonState(); }); - if (configuration.data.issueType === IssueType.PerformanceIssue) { + if (configuration.data.issueType === OldIssueType.PerformanceIssue) { this.processMainService.$getPerformanceInfo().then(info => { this.updatePerformanceInfo(info as Partial); }); @@ -169,7 +169,7 @@ export class IssueReporter extends Disposable { } // TODO @justschen: After migration to Aux Window, switch to dedicated css. - private applyStyles(styles: IssueReporterStyles) { + private applyStyles(styles: OldIssueReporterStyles) { const styleTag = document.createElement('style'); const content: string[] = []; @@ -245,7 +245,7 @@ export class IssueReporter extends Disposable { mainWindow.document.body.style.color = styles.color || ''; } - private handleExtensionData(extensions: IssueReporterExtensionData[]) { + private handleExtensionData(extensions: OldIssueReporterExtensionData[]) { const installedExtensions = extensions.filter(x => !x.isBuiltin); const { nonThemes, themes } = groupBy(installedExtensions, ext => { return ext.isTheme ? 'themes' : 'nonThemes'; @@ -261,7 +261,7 @@ export class IssueReporter extends Disposable { this.updateExtensionSelector(installedExtensions); } - private async updateIssueReporterUri(extension: IssueReporterExtensionData): Promise { + private async updateIssueReporterUri(extension: OldIssueReporterExtensionData): Promise { try { if (extension.uri) { const uri = URI.revive(extension.uri); @@ -272,7 +272,7 @@ export class IssueReporter extends Disposable { } } - private async sendReporterMenu(extension: IssueReporterExtensionData): Promise { + private async sendReporterMenu(extension: OldIssueReporterExtensionData): Promise { try { const data = await this.issueMainService.$sendReporterMenu(extension.id, extension.name); return data; @@ -286,7 +286,7 @@ export class IssueReporter extends Disposable { this.addEventListener('issue-type', 'change', (event: Event) => { const issueType = parseInt((event.target).value); this.issueReporterModel.update({ issueType: issueType }); - if (issueType === IssueType.PerformanceIssue && !this.receivedPerformanceInfo) { + if (issueType === OldIssueType.PerformanceIssue && !this.receivedPerformanceInfo) { this.processMainService.$getPerformanceInfo().then(info => { this.updatePerformanceInfo(info as Partial); }); @@ -526,15 +526,15 @@ export class IssueReporter extends Disposable { return false; } - if (issueType === IssueType.Bug && this.receivedSystemInfo) { + if (issueType === OldIssueType.Bug && this.receivedSystemInfo) { return true; } - if (issueType === IssueType.PerformanceIssue && this.receivedSystemInfo && this.receivedPerformanceInfo) { + if (issueType === OldIssueType.PerformanceIssue && this.receivedSystemInfo && this.receivedPerformanceInfo) { return true; } - if (issueType === IssueType.FeatureRequest) { + if (issueType === OldIssueType.FeatureRequest) { return true; } @@ -725,14 +725,14 @@ export class IssueReporter extends Disposable { } private setUpTypes(): void { - const makeOption = (issueType: IssueType, description: string) => $('option', { 'value': issueType.valueOf() }, escape(description)); + const makeOption = (issueType: OldIssueType, description: string) => $('option', { 'value': issueType.valueOf() }, escape(description)); const typeSelect = this.getElementById('issue-type')! as HTMLSelectElement; const { issueType } = this.issueReporterModel.getData(); reset(typeSelect, - makeOption(IssueType.Bug, localize('bugReporter', "Bug Report")), - makeOption(IssueType.FeatureRequest, localize('featureRequest', "Feature Request")), - makeOption(IssueType.PerformanceIssue, localize('performanceIssue', "Performance Issue (freeze, slow, crash)")) + makeOption(OldIssueType.Bug, localize('bugReporter', "Bug Report")), + makeOption(OldIssueType.FeatureRequest, localize('featureRequest', "Feature Request")), + makeOption(OldIssueType.PerformanceIssue, localize('performanceIssue', "Performance Issue (freeze, slow, crash)")) ); typeSelect.value = issueType.toString(); @@ -773,7 +773,7 @@ export class IssueReporter extends Disposable { sourceSelect.append(this.makeOption(IssueSource.Marketplace, localize('marketplace', "Extensions Marketplace"), false)); } - if (issueType !== IssueType.FeatureRequest) { + if (issueType !== OldIssueType.FeatureRequest) { sourceSelect.append(this.makeOption(IssueSource.Unknown, localize('unknown', "Don't know"), false)); } @@ -852,7 +852,7 @@ export class IssueReporter extends Disposable { }, 100); } - if (issueType === IssueType.Bug) { + if (issueType === OldIssueType.Bug) { if (!fileOnMarketplace) { show(blockContainer); show(systemBlock); @@ -864,7 +864,7 @@ export class IssueReporter extends Disposable { reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*')); reset(descriptionSubtitle, localize('bugDescription', "Share the steps needed to reliably reproduce the problem. Please include actual and expected results. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); - } else if (issueType === IssueType.PerformanceIssue) { + } else if (issueType === OldIssueType.PerformanceIssue) { if (!fileOnMarketplace) { show(blockContainer); show(systemBlock); @@ -881,7 +881,7 @@ export class IssueReporter extends Disposable { reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*')); reset(descriptionSubtitle, localize('performanceIssueDesciption', "When did this performance issue happen? Does it occur on startup or after a specific series of actions? We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); - } else if (issueType === IssueType.FeatureRequest) { + } else if (issueType === OldIssueType.FeatureRequest) { reset(descriptionTitle, localize('description', "Description") + ' ', $('span.required-input', undefined, '*')); reset(descriptionSubtitle, localize('featureRequestDescription', "Please describe the feature you would like to see. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); } @@ -1172,7 +1172,7 @@ export class IssueReporter extends Disposable { } } - private updateExtensionSelector(extensions: IssueReporterExtensionData[]): void { + private updateExtensionSelector(extensions: OldIssueReporterExtensionData[]): void { interface IOption { name: string; id: string; @@ -1200,7 +1200,7 @@ export class IssueReporter extends Disposable { return 0; }); - const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => { + const makeOption = (extension: IOption, selectedExtension?: OldIssueReporterExtensionData): HTMLOptionElement => { const selected = selectedExtension && extension.id === selectedExtension.id; return $('option', { 'value': extension.id, @@ -1279,7 +1279,7 @@ export class IssueReporter extends Disposable { this.configuration.data.uri = undefined; } - private async updateExtensionStatus(extension: IssueReporterExtensionData) { + private async updateExtensionStatus(extension: OldIssueReporterExtensionData) { this.issueReporterModel.update({ selectedExtension: extension }); // uses this.configuuration.data to ensure that data is coming from `openReporter` command. @@ -1416,7 +1416,7 @@ export class IssueReporter extends Disposable { mainWindow.document.querySelector('.block-workspace .block-info code')!.textContent = '\n' + state.workspaceInfo; } - private updateExtensionTable(extensions: IssueReporterExtensionData[], numThemeExtensions: number): void { + private updateExtensionTable(extensions: OldIssueReporterExtensionData[], numThemeExtensions: number): void { const target = mainWindow.document.querySelector('.block-extensions .block-info'); if (target) { if (this.configuration.disableExtensions) { @@ -1452,7 +1452,7 @@ export class IssueReporter extends Disposable { } } - private getExtensionTableHtml(extensions: IssueReporterExtensionData[]): HTMLTableElement { + private getExtensionTableHtml(extensions: OldIssueReporterExtensionData[]): HTMLTableElement { return $('table', undefined, $('tr', undefined, $('th', undefined, 'Extension'), diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts index 02b3adb817d..b224dc238f4 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts @@ -2,21 +2,18 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, isHTMLInputElement, isHTMLTextAreaElement, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; -import { mainWindow } from 'vs/base/browser/window'; -import { Codicon } from 'vs/base/common/codicons'; -import { groupBy } from 'vs/base/common/collections'; +import { $, reset } from 'vs/base/browser/dom'; import { CancellationError } from 'vs/base/common/errors'; -import { isMacintosh } from 'vs/base/common/platform'; -import { ThemeIcon } from 'vs/base/common/themables'; +import { IProductConfiguration } from 'vs/base/common/product'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; -import { IIssueMainService, IProcessMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; +import { IProcessMainService } from 'vs/platform/issue/common/issue'; import { INativeHostService } from 'vs/platform/native/common/native'; -import { applyZoom, zoomIn, zoomOut } from 'vs/platform/window/electron-sandbox/window'; -import { BaseIssueReporterService, hide, show } from 'vs/workbench/contrib/issue/browser/issue'; +import { applyZoom } from 'vs/platform/window/electron-sandbox/window'; +import { BaseIssueReporterService } from 'vs/workbench/contrib/issue/browser/baseIssueReporterService'; import { IssueReporterData as IssueReporterModelData } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; +import { IIssueFormService, IssueReporterData, IssueType } from 'vs/workbench/contrib/issue/common/issue'; // GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe. // ref https://github.com/microsoft/vscode/issues/159191 @@ -26,12 +23,20 @@ const MAX_URL_LENGTH = 7500; export class IssueReporter2 extends BaseIssueReporterService { private readonly processMainService: IProcessMainService; constructor( - private readonly configuration: IssueReporterWindowConfiguration, + disableExtensions: boolean, + data: IssueReporterData, + os: { + type: string; + arch: string; + release: string; + }, + product: IProductConfiguration, + window: Window, @INativeHostService private readonly nativeHostService: INativeHostService, - @IIssueMainService issueMainService: IIssueMainService, + @IIssueFormService issueFormService: IIssueFormService, @IProcessMainService processMainService: IProcessMainService ) { - super(configuration.disableExtensions, configuration.data, configuration.os, configuration.product, mainWindow, false, issueMainService); + super(disableExtensions, data, os, product, window, false, issueFormService); this.processMainService = processMainService; this.processMainService.$getSystemInfo().then(info => { @@ -41,44 +46,17 @@ export class IssueReporter2 extends BaseIssueReporterService { this.updateSystemInfo(this.issueReporterModel.getData()); this.updatePreviewButtonState(); }); - if (configuration.data.issueType === IssueType.PerformanceIssue) { + if (this.data.issueType === IssueType.PerformanceIssue) { this.processMainService.$getPerformanceInfo().then(info => { this.updatePerformanceInfo(info as Partial); }); } this.setEventHandlers(); - applyZoom(configuration.data.zoomLevel, mainWindow); - this.handleExtensionData(configuration.data.enabledExtensions); - this.updateExperimentsInfo(configuration.data.experiments); - this.updateRestrictedMode(configuration.data.restrictedMode); - this.updateUnsupportedMode(configuration.data.isUnsupported); - } - - private handleExtensionData(extensions: IssueReporterExtensionData[]) { - const installedExtensions = extensions.filter(x => !x.isBuiltin); - const { nonThemes, themes } = groupBy(installedExtensions, ext => { - return ext.isTheme ? 'themes' : 'nonThemes'; - }); - - const numberOfThemeExtesions = themes && themes.length; - this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions }); - this.updateExtensionTable(nonThemes, numberOfThemeExtesions); - if (this.disableExtensions || installedExtensions.length === 0) { - (this.getElementById('disableExtensions')).disabled = true; - } - - this.updateExtensionSelector(installedExtensions); - } - - private async sendReporterMenu(extension: IssueReporterExtensionData): Promise { - try { - const data = await this.issueMainService.$sendReporterMenu(extension.id, extension.name); - return data; - } catch (e) { - console.error(e); - return undefined; - } + applyZoom(this.data.zoomLevel, this.window); + this.updateExperimentsInfo(this.data.experiments); + this.updateRestrictedMode(this.data.restrictedMode); + this.updateUnsupportedMode(this.data.isUnsupported); } public override setEventHandlers(): void { @@ -103,78 +81,6 @@ export class IssueReporter2 extends BaseIssueReporterService { this.setSourceOptions(); this.render(); }); - - // Keep all event listerns involving window and issue creation - this.previewButton.onDidClick(async () => { - this.delayedSubmit.trigger(async () => { - this.createIssue(); - }); - }); - - this.addEventListener('disableExtensions', 'click', () => { - this.issueMainService.$reloadWithExtensionsDisabled(); - }); - - this.addEventListener('extensionBugsLink', 'click', (e: Event) => { - const url = (e.target).innerText; - windowOpenNoOpener(url); - }); - - this.addEventListener('disableExtensions', 'keydown', (e: Event) => { - e.stopPropagation(); - if ((e as KeyboardEvent).keyCode === 13 || (e as KeyboardEvent).keyCode === 32) { - this.issueMainService.$reloadWithExtensionsDisabled(); - } - }); - - - // THIS IS THE MAIN IMPORTANT PART - mainWindow.document.onkeydown = async (e: KeyboardEvent) => { - const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; - // Cmd/Ctrl+Enter previews issue and closes window - if (cmdOrCtrlKey && e.key === 'Enter') { - this.delayedSubmit.trigger(async () => { - if (await this.createIssue()) { - this.close(); - } - }); - } - - // Cmd/Ctrl + w closes issue window - if (cmdOrCtrlKey && e.key === 'w') { - e.stopPropagation(); - e.preventDefault(); - - const issueTitle = (this.getElementById('issue-title'))!.value; - const { issueDescription } = this.issueReporterModel.getData(); - if (!this.hasBeenSubmitted && (issueTitle || issueDescription)) { - // fire and forget - this.issueMainService.$showConfirmCloseDialog(); - } else { - this.close(); - } - } - - // Cmd/Ctrl + zooms in - if (cmdOrCtrlKey && (e.key === '+' || e.key === '=')) { - zoomIn(mainWindow); - } - - // Cmd/Ctrl - zooms out - if (cmdOrCtrlKey && e.key === '-') { - zoomOut(mainWindow); - } - - // With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac - // Manually perform the selection - if (isMacintosh) { - if (cmdOrCtrlKey && e.key === 'a' && e.target) { - if (isHTMLInputElement(e.target) || isHTMLTextAreaElement(e.target)) { - (e.target).select(); - } - } - } - }; } public override async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise { @@ -218,7 +124,7 @@ export class IssueReporter2 extends BaseIssueReporterService { if (!this.validateInputs()) { // If inputs are invalid, set focus to the first one and add listeners on them // to detect further changes - const invalidInput = mainWindow.document.getElementsByClassName('invalid-input'); + const invalidInput = this.window.document.getElementsByClassName('invalid-input'); if (invalidInput.length) { (invalidInput[0]).focus(); } @@ -283,7 +189,7 @@ export class IssueReporter2 extends BaseIssueReporterService { } public override async writeToClipboard(baseUrl: string, issueBody: string): Promise { - const shouldWrite = await this.issueMainService.$showClipboardDialog(); + const shouldWrite = await this.issueFormService.showClipboardDialog(); if (!shouldWrite) { throw new CancellationError(); } @@ -294,7 +200,7 @@ export class IssueReporter2 extends BaseIssueReporterService { } private updateSystemInfo(state: IssueReporterModelData) { - const target = mainWindow.document.querySelector('.block-system .block-info'); + const target = this.window.document.querySelector('.block-system .block-info'); if (target) { const systemInfo = state.systemInfo!; @@ -373,147 +279,6 @@ export class IssueReporter2 extends BaseIssueReporterService { } } - public updateExtensionSelector(extensions: IssueReporterExtensionData[]): void { - interface IOption { - name: string; - id: string; - } - - const extensionOptions: IOption[] = extensions.map(extension => { - return { - name: extension.displayName || extension.name || '', - id: extension.id - }; - }); - - // Sort extensions by name - extensionOptions.sort((a, b) => { - const aName = a.name.toLowerCase(); - const bName = b.name.toLowerCase(); - if (aName > bName) { - return 1; - } - - if (aName < bName) { - return -1; - } - - return 0; - }); - - const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => { - const selected = selectedExtension && extension.id === selectedExtension.id; - return $('option', { - 'value': extension.id, - 'selected': selected || '' - }, extension.name); - }; - - const extensionsSelector = this.getElementById('extension-selector'); - if (extensionsSelector) { - const { selectedExtension } = this.issueReporterModel.getData(); - reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); - - if (!selectedExtension) { - extensionsSelector.selectedIndex = 0; - } - - this.addEventListener('extension-selector', 'change', async (e: Event) => { - this.clearExtensionData(); - const selectedExtensionId = (e.target).value; - this.selectedExtension = selectedExtensionId; - const extensions = this.issueReporterModel.getData().allExtensions; - const matches = extensions.filter(extension => extension.id === selectedExtensionId); - if (matches.length) { - this.issueReporterModel.update({ selectedExtension: matches[0] }); - const selectedExtension = this.issueReporterModel.getData().selectedExtension; - if (selectedExtension) { - const iconElement = document.createElement('span'); - iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); - this.setLoading(iconElement); - const openReporterData = await this.sendReporterMenu(selectedExtension); - if (openReporterData) { - if (this.selectedExtension === selectedExtensionId) { - this.removeLoading(iconElement, true); - this.configuration.data = openReporterData; - this.data = openReporterData; - } else if (this.selectedExtension !== selectedExtensionId) { - } - } - else { - if (!this.loadingExtensionData) { - iconElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); - } - this.removeLoading(iconElement); - // if not using command, should have no configuration data in fields we care about and check later. - this.clearExtensionData(); - - // case when previous extension was opened from normal openIssueReporter command - selectedExtension.data = undefined; - selectedExtension.uri = undefined; - } - if (this.selectedExtension === selectedExtensionId) { - // repopulates the fields with the new data given the selected extension. - this.updateExtensionStatus(matches[0]); - this.openReporter = false; - } - } else { - this.issueReporterModel.update({ selectedExtension: undefined }); - this.clearSearchResults(); - this.clearExtensionData(); - this.validateSelectedExtension(); - this.updateExtensionStatus(matches[0]); - } - } - }); - } - - this.addEventListener('problem-source', 'change', (_) => { - this.validateSelectedExtension(); - }); - } - - public override setLoading(element: HTMLElement) { - // Show loading - this.openReporter = true; - this.loadingExtensionData = true; - this.updatePreviewButtonState(); - - const extensionDataCaption = this.getElementById('extension-id')!; - hide(extensionDataCaption); - - const extensionDataCaption2 = Array.from(mainWindow.document.querySelectorAll('.ext-parens')); - extensionDataCaption2.forEach(extensionDataCaption2 => hide(extensionDataCaption2)); - - const showLoading = this.getElementById('ext-loading')!; - show(showLoading); - while (showLoading.firstChild) { - showLoading.firstChild.remove(); - } - showLoading.append(element); - - this.renderBlocks(); - } - - public override removeLoading(element: HTMLElement, fromReporter: boolean = false) { - this.openReporter = fromReporter; - this.loadingExtensionData = false; - this.updatePreviewButtonState(); - - const extensionDataCaption = this.getElementById('extension-id')!; - show(extensionDataCaption); - - const extensionDataCaption2 = Array.from(mainWindow.document.querySelectorAll('.ext-parens')); - extensionDataCaption2.forEach(extensionDataCaption2 => show(extensionDataCaption2)); - - const hideLoading = this.getElementById('ext-loading')!; - hide(hideLoading); - if (hideLoading.firstChild) { - element.remove(); - } - this.renderBlocks(); - } - private updateRestrictedMode(restrictedMode: boolean) { this.issueReporterModel.update({ restrictedMode }); } @@ -524,7 +289,7 @@ export class IssueReporter2 extends BaseIssueReporterService { private updateExperimentsInfo(experimentInfo: string | undefined) { this.issueReporterModel.update({ experimentInfo }); - const target = mainWindow.document.querySelector('.block-experiments .block-info'); + const target = this.window.document.querySelector('.block-experiments .block-info'); if (target) { target.textContent = experimentInfo ? experimentInfo : localize('noCurrentExperiments', "No current experiments."); } diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts index 651dfa6c509..b60b6f54543 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts @@ -4,23 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { getZoomLevel } from 'vs/base/browser/browser'; +import { mainWindow } from 'vs/base/browser/window'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionIdentifier, ExtensionType, ExtensionIdentifierSet } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, ExtensionIdentifierSet, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, OldIssueReporterData, OldIssueReporterExtensionData, OldIssueReporterStyles } from 'vs/platform/issue/common/issue'; import { buttonBackground, buttonForeground, buttonHoverBackground, foreground, inputActiveOptionBorder, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IIssueFormService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; -import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; -import { mainWindow } from 'vs/base/browser/window'; -import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export class NativeIssueService implements IWorkbenchIssueService { declare readonly _serviceBrand: undefined; @@ -28,6 +29,7 @@ export class NativeIssueService implements IWorkbenchIssueService { constructor( @IIssueMainService private readonly issueMainService: IIssueMainService, + @IIssueFormService private readonly issueFormService: IIssueFormService, @IThemeService private readonly themeService: IThemeService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @@ -36,7 +38,8 @@ export class NativeIssueService implements IWorkbenchIssueService { @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IIntegrityService private readonly integrityService: IIntegrityService, @IMenuService private readonly menuService: IMenuService, - @IContextKeyService private readonly contextKeyService: IContextKeyService + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { ipcRenderer.on('vscode:triggerReporterMenu', async (event, arg) => { const extensionId = arg.extensionId; @@ -65,6 +68,8 @@ export class NativeIssueService implements IWorkbenchIssueService { async openReporter(dataOverrides: Partial = {}): Promise { const extensionData: IssueReporterExtensionData[] = []; + const oldExtensionData: OldIssueReporterExtensionData[] = []; + const oldDataOverrides = dataOverrides as Partial; try { const extensions = await this.extensionManagementService.getInstalled(); const enabledExtensions = extensions.filter(extension => this.extensionEnablementService.isEnabled(extension) || (dataOverrides.extensionId && extension.identifier.id === dataOverrides.extensionId)); @@ -88,6 +93,26 @@ export class NativeIssueService implements IWorkbenchIssueService { extensionData: 'Extensions data loading', }; })); + oldExtensionData.push(...enabledExtensions.map((extension): OldIssueReporterExtensionData => { + const { manifest } = extension; + const manifestKeys = manifest.contributes ? Object.keys(manifest.contributes) : []; + const isTheme = !manifest.main && !manifest.browser && manifestKeys.length === 1 && manifestKeys[0] === 'themes'; + const isBuiltin = extension.type === ExtensionType.System; + return { + name: manifest.name, + publisher: manifest.publisher, + version: manifest.version, + repositoryUrl: manifest.repository && manifest.repository.url, + bugsUrl: manifest.bugs && manifest.bugs.url, + displayName: manifest.displayName, + id: extension.identifier.id, + data: dataOverrides.data, + uri: dataOverrides.uri, + isTheme, + isBuiltin, + extensionData: 'Extensions data loading', + }; + })); } catch (e) { extensionData.push({ name: 'Workbench Issue Service', @@ -101,6 +126,18 @@ export class NativeIssueService implements IWorkbenchIssueService { isTheme: false, isBuiltin: true }); + oldExtensionData.push({ + name: 'Workbench Issue Service', + publisher: 'Unknown', + version: '0.0.0', + repositoryUrl: undefined, + bugsUrl: undefined, + extensionData: 'Extensions data loading', + displayName: `Extensions not loaded: ${e}`, + id: 'workbench.issue', + isTheme: false, + isBuiltin: true + }); } const experiments = await this.experimentService.getCurrentExperiments(); @@ -132,6 +169,16 @@ export class NativeIssueService implements IWorkbenchIssueService { githubAccessToken }, dataOverrides); + const oldIssueReporterData: OldIssueReporterData = Object.assign({ + styles: oldGetIssueReporterStyles(theme), + zoomLevel: getZoomLevel(mainWindow), + enabledExtensions: oldExtensionData, + experiments: experiments?.join('\n'), + restrictedMode: !this.workspaceTrustManagementService.isWorkspaceTrusted(), + isUnsupported, + githubAccessToken + }, oldDataOverrides); + if (issueReporterData.extensionId) { const extensionExists = extensionData.some(extension => ExtensionIdentifier.equals(extension.id, issueReporterData.extensionId)); if (!extensionExists) { @@ -143,12 +190,40 @@ export class NativeIssueService implements IWorkbenchIssueService { ipcRenderer.send(`vscode:triggerReporterMenuResponse:${issueReporterData.extensionId}`, issueReporterData); this.extensionIdentifierSet.delete(new ExtensionIdentifier(issueReporterData.extensionId)); } - return this.issueMainService.openReporter(issueReporterData); + + + if (this.configurationService.getValue('issueReporter.experimental.auxWindow')) { + return this.issueFormService.openReporter(issueReporterData); + } + + return this.issueMainService.openReporter(oldIssueReporterData); } } export function getIssueReporterStyles(theme: IColorTheme): IssueReporterStyles { + return { + backgroundColor: getColor(theme, SIDE_BAR_BACKGROUND), + color: getColor(theme, foreground), + textLinkColor: getColor(theme, textLinkForeground), + textLinkActiveForeground: getColor(theme, textLinkActiveForeground), + inputBackground: getColor(theme, inputBackground), + inputForeground: getColor(theme, inputForeground), + inputBorder: getColor(theme, inputBorder), + inputActiveBorder: getColor(theme, inputActiveOptionBorder), + inputErrorBorder: getColor(theme, inputValidationErrorBorder), + inputErrorBackground: getColor(theme, inputValidationErrorBackground), + inputErrorForeground: getColor(theme, inputValidationErrorForeground), + buttonBackground: getColor(theme, buttonBackground), + buttonForeground: getColor(theme, buttonForeground), + buttonHoverBackground: getColor(theme, buttonHoverBackground), + sliderActiveColor: getColor(theme, scrollbarSliderActiveBackground), + sliderBackgroundColor: getColor(theme, SIDE_BAR_BACKGROUND), + sliderHoverColor: getColor(theme, scrollbarSliderHoverBackground), + }; +} + +export function oldGetIssueReporterStyles(theme: IColorTheme): OldIssueReporterStyles { return { backgroundColor: getColor(theme, SIDE_BAR_BACKGROUND), color: getColor(theme, foreground), diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css b/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css index 152d6c38bc8..fa2cf8a105a 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css +++ b/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css @@ -3,6 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* +* TODO: @justschen - remove this file once the new issue reporter is enabled by default. +* Split between this and a `newIssueReporter` because of specificy from new issue reporter monaco-workbench stylesheets. +*/ + /** * Table */ diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/media/newIssueReporter.css b/src/vs/workbench/contrib/issue/electron-sandbox/media/newIssueReporter.css new file mode 100644 index 00000000000..00b0be10773 --- /dev/null +++ b/src/vs/workbench/contrib/issue/electron-sandbox/media/newIssueReporter.css @@ -0,0 +1,470 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Table + */ + +.issue-reporter-body table { + width: 100%; + max-width: 100%; + background-color: transparent; + border-collapse: collapse; +} + +.issue-reporter-body th { + vertical-align: bottom; + border-bottom: 1px solid; + padding: 5px; + text-align: inherit; +} + +.issue-reporter-body td { + padding: 5px; + vertical-align: top; +} + +.issue-reporter-body tr td:first-child { + width: 30%; +} + +.issue-reporter-body label { + user-select: none; +} + +.issue-reporter-body .block-settingsSearchResults-details { + padding-bottom: .5rem; +} + +.issue-reporter-body .block-settingsSearchResults-details > div { + padding: .5rem .75rem; +} + +.issue-reporter-body .section { + margin-bottom: .5em; +} + +/** + * Forms + */ +.issue-reporter-body input[type="text"], +.issue-reporter-body textarea { + display: block; + width: 100%; + padding: .375rem .75rem; + font-size: 1rem; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; +} + +.issue-reporter-body textarea { + overflow: auto; + resize: vertical; +} + +/** + * Button + */ + +.issue-reporter-body .monaco-text-button { + display: block; + width: auto; + padding: 4px 10px; + align-self: flex-end; + margin-bottom: 1em; + font-size: 13px; +} + +.issue-reporter-body select { + height: calc(2.25rem + 2px); + display: inline-block; + padding: 3px 3px; + font-size: 14px; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: none; +} + +.issue-reporter-body * { + box-sizing: border-box; +} + +.issue-reporter-body textarea, +.issue-reporter-body input, +.issue-reporter-body select { + font-family: inherit; +} + +.issue-reporter-body html { + color: #CCCCCC; + height: 100%; +} + +.issue-reporter-body .extension-caption .codicon-modifier-spin { + padding-bottom: 3px; + margin-left: 2px; +} + +/* Font Families (with CJK support) */ + +.issue-reporter-body .mac { + font-family: -apple-system, BlinkMacSystemFont, sans-serif; +} + +.issue-reporter-body .mac:lang(zh-Hans) { + font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; +} + +.issue-reporter-body .mac:lang(zh-Hant) { + font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; +} + +.issue-reporter-body .mac:lang(ja) { + font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; +} + +.issue-reporter-body .mac:lang(ko) { + font-family: -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; +} + +.issue-reporter-body .windows { + font-family: "Segoe WPC", "Segoe UI", sans-serif; +} + +.issue-reporter-body .windows:lang(zh-Hans) { + font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; +} + +.issue-reporter-body .windows:lang(zh-Hant) { + font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; +} + +.issue-reporter-body .windows:lang(ja) { + font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; +} + +.issue-reporter-body .windows:lang(ko) { + font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; +} + +/* Linux: add `system-ui` as first font and not `Ubuntu` to allow other distribution pick their standard OS font */ +.issue-reporter-body .linux { + font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; +} + +.issue-reporter-body .linux:lang(zh-Hans) { + font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; +} + +.issue-reporter-body .linux:lang(zh-Hant) { + font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; +} + +.issue-reporter-body .linux:lang(ja) { + font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; +} + +.issue-reporter-body .linux:lang(ko) { + font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; +} + +body.issue-reporter-body { + margin: 0 !important; + overflow-y: scroll !important; + height: 100% !important; +} + +.issue-reporter-body .hidden { + display: none; +} + +.issue-reporter-body .block { + font-size: 12px; +} + +.issue-reporter-body .block .block-info { + width: 100%; + font-size: 12px; + overflow: auto; + overflow-wrap: break-word; + margin: 5px; + padding: 10px; +} + +.issue-reporter-body #issue-reporter { + max-width: 85vw; + margin-left: auto; + margin-right: auto; + padding-top: 2em; + padding-bottom: 2em; + display: flex; + flex-direction: column; + min-height: 100%; + overflow: visible; +} + +.issue-reporter-body .description-section { + flex-grow: 1; + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.issue-reporter-body textarea { + flex-grow: 1; + min-height: 150px; +} + +.issue-reporter-body .block-info-text { + display: flex; + flex-grow: 1; +} + +.issue-reporter-body #github-submit-btn { + flex-shrink: 0; + margin-left: auto; + margin-top: 10px; + margin-bottom: 10px; +} + +.issue-reporter-body .two-col { + display: inline-block; + width: 49%; +} + +.issue-reporter-body #vscode-version { + width: 90%; +} + +.issue-reporter-body .input-group { + margin-bottom: 1em; +} + +.issue-reporter-body #extension-selection { + margin-top: 1em; +} + +.issue-reporter-body .issue-reporter select, +.issue-reporter-body .issue-reporter input, +.issue-reporter-body .issue-reporter textarea { + border: 1px solid transparent; + margin-top: 10px; +} + + +.issue-reporter-body #issue-reporter .validation-error { + font-size: 12px; + padding: 10px; + border-top: 0px !important; +} + +.issue-reporter-body #issue-reporter .system-info { + margin-bottom: 10px; +} + + +.issue-reporter-body input[type="checkbox"] { + width: auto; + display: inline-block; + margin-top: 0; + vertical-align: middle; + cursor: pointer; +} + +.issue-reporter-body input:disabled { + opacity: 0.6; +} + +.issue-reporter-body .list-title { + margin-top: 1em; + margin-left: 1em; +} + +.issue-reporter-body .instructions { + font-size: 12px; + margin-top: .5em; +} + +.issue-reporter-body a, +.issue-reporter-body .workbenchCommand { + cursor: pointer; + border: 1px solid transparent; +} + +.issue-reporter-body .workbenchCommand:disabled { + color: #868e96; + cursor: default +} + +.issue-reporter-body .block-extensions .block-info { + margin-bottom: 1.5em; +} + +/* Default styles, overwritten if a theme is provided */ +.issue-reporter-body input, +.issue-reporter-body select, +.issue-reporter-body textarea { + background-color: #3c3c3c; + border: none; + color: #cccccc; +} + +.issue-reporter-body a { + color: #CCCCCC; + text-decoration: none; +} + +.issue-reporter-body .showInfo, +.issue-reporter-body .input-group a { + color: var(--vscode-textLink-foreground); +} + +.issue-reporter-body .section .input-group .validation-error { + margin-left: 100px; +} + +.issue-reporter-body .section .inline-form-control, +.issue-reporter-body .section .inline-label { + display: inline-block; + font-size: initial; +} + +.issue-reporter-body .section .inline-label { + width: 95px; +} + +.issue-reporter-body .issue-reporter .inline-label, +.issue-reporter-body .issue-reporter #issue-description-label { + font-size: initial; + cursor: default; +} + +.issue-reporter-body .monaco-workbench .issue-reporter label { + cursor: default; +} + +.issue-reporter-body .section .inline-form-control, +.issue-reporter-body .section .input-group .validation-error { + width: calc(100% - 100px); +} + +.issue-reporter-body #issue-type, +.issue-reporter-body #issue-source, +.issue-reporter-body #extension-selector { + cursor: pointer; + appearance: auto; + border: none; + border-right: 6px solid transparent; + padding-left: 10px; +} + +.issue-reporter-body #similar-issues { + margin-left: 15%; + display: block; +} + +.issue-reporter-body #problem-source-help-text { + margin-left: calc(15% + 1em); +} + +@media (max-width: 950px) { + .issue-reporter-body .section .inline-label { + width: 15%; + } + + .issue-reporter-body #problem-source-help-text { + margin-left: calc(15% + 1em); + } + + .issue-reporter-body .section .inline-form-control, + .issue-reporter-body .section .input-group .validation-error { + width: calc(85% - 5px); + } + + .issue-reporter-body .section .input-group .validation-error { + margin-left: calc(15% + 4px); + } +} + +@media (max-width: 620px) { + .issue-reporter-body .section .inline-label { + display: none !important; + } + + .issue-reporter-body #problem-source-help-text { + margin-left: 1em; + } + + .issue-reporter-body .section .inline-form-control, + .issue-reporter-body .section .input-group .validation-error { + width: 100%; + } + + .issue-reporter-body #similar-issues, + .issue-reporter-body .section .input-group .validation-error { + margin-left: 0; + } +} + +.issue-reporter-body::-webkit-scrollbar { + width: 14px; +} + +.issue-reporter-body::-webkit-scrollbar-thumb { + min-height: 20px; +} + +.issue-reporter-body::-webkit-scrollbar-corner { + display: none; +} + +.issue-reporter-body .issues-container { + margin-left: 1.5em; + margin-top: .5em; + max-height: 92px; + overflow-y: auto; +} + +.issue-reporter-body .issues-container > .issue { + padding: 4px 0; + display: flex; +} + +.issue-reporter-body .issues-container > .issue > .issue-link { + width: calc(100% - 82px); + overflow: hidden; + padding-top: 3px; + white-space: nowrap; + text-overflow: ellipsis; +} + +.issue-reporter-body .issues-container > .issue > .issue-state .codicon { + width: 16px; +} + +.issue-reporter-body .issues-container > .issue > .issue-state { + display: flex; + width: 77px; + padding: 3px 6px; + margin-right: 5px; + color: #CCCCCC; + background-color: #3c3c3c; + border-radius: .25rem; +} + +.issue-reporter-body .issues-container > .issue .label { + padding-top: 2px; + margin-left: 5px; + width: 44px; + text-overflow: ellipsis; + overflow: hidden; +} + +.issue-reporter-body .issues-container > .issue .issue-icon { + padding-top: 2px; +} diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts new file mode 100644 index 00000000000..ee2de28ed40 --- /dev/null +++ b/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/newIssueReporter'; +import { IMenuService } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INativeHostService } from 'vs/platform/native/common/native'; +import product from 'vs/platform/product/common/product'; +import { IssueFormService } from 'vs/workbench/contrib/issue/browser/issueFormService'; +import { IIssueFormService, IssueReporterData } from 'vs/workbench/contrib/issue/common/issue'; +import { IssueReporter2 } from 'vs/workbench/contrib/issue/electron-sandbox/issueReporterService2'; +import { IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; + +export class NativeIssueFormService extends IssueFormService implements IIssueFormService { + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @IAuxiliaryWindowService auxiliaryWindowService: IAuxiliaryWindowService, + @ILogService logService: ILogService, + @IDialogService dialogService: IDialogService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + @IHostService hostService: IHostService, + @INativeHostService private readonly nativeHostService: INativeHostService, + @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,) { + super(instantiationService, auxiliaryWindowService, menuService, contextKeyService, logService, dialogService, hostService); + } + + // override to grab platform info + override async openReporter(data: IssueReporterData): Promise { + if (this.hasToReload(data)) { + return; + } + + const bounds = await this.nativeHostService.getActiveWindowPosition(); + if (!bounds) { + return; + } + + await this.openAuxIssueReporter(data, bounds); + + // Get platform information + const { arch, release, type } = await this.nativeHostService.getOSProperties(); + this.arch = arch; + this.release = release; + this.type = type; + + // create issue reporter and instantiate + if (this.issueReporterWindow) { + const issueReporter = this.instantiationService.createInstance(IssueReporter2, !!this.environmentService.disableExtensions, data, { type: this.type, arch: this.arch, release: this.release }, product, this.issueReporterWindow); + issueReporter.render(); + } + } +} diff --git a/src/vs/workbench/contrib/issue/test/browser/testReporterModel.test.ts b/src/vs/workbench/contrib/issue/test/browser/testReporterModel.test.ts new file mode 100644 index 00000000000..ccc2ae55f96 --- /dev/null +++ b/src/vs/workbench/contrib/issue/test/browser/testReporterModel.test.ts @@ -0,0 +1,338 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IssueReporterModel } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; +import { IssueType } from 'vs/workbench/contrib/issue/common/issue'; +import { normalizeGitHubUrl } from 'vs/workbench/contrib/issue/common/issueReporterUtil'; + +suite('IssueReporter', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('sets defaults to include all data', () => { + const issueReporterModel = new IssueReporterModel(); + assert.deepStrictEqual(issueReporterModel.getData(), { + allExtensions: [], + includeSystemInfo: true, + includeExtensionData: true, + includeWorkspaceInfo: true, + includeProcessInfo: true, + includeExtensions: true, + includeExperiments: true, + issueType: 0 + }); + }); + + test('serializes model skeleton when no data is provided', () => { + const issueReporterModel = new IssueReporterModel({}); + assert.strictEqual(issueReporterModel.serialize(), + ` +Type: Bug + +undefined + +VS Code version: undefined +OS version: undefined +Modes: + +Extensions: none +`); + }); + + test('serializes GPU information when data is provided', () => { + const issueReporterModel = new IssueReporterModel({ + issueType: 0, + systemInfo: { + os: 'Darwin', + cpus: 'Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)', + memory: '16.00GB', + vmHint: '0%', + processArgs: '', + screenReader: 'no', + remoteData: [], + gpuStatus: { + '2d_canvas': 'enabled', + 'checker_imaging': 'disabled_off' + } + } + }); + assert.strictEqual(issueReporterModel.serialize(), + ` +Type: Bug + +undefined + +VS Code version: undefined +OS version: undefined +Modes: + +
+System Info + +|Item|Value| +|---|---| +|CPUs|Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)| +|GPU Status|2d_canvas: enabled
checker_imaging: disabled_off| +|Load (avg)|undefined| +|Memory (System)|16.00GB| +|Process Argv|| +|Screen Reader|no| +|VM|0%| +
Extensions: none +`); + }); + + test('serializes experiment info when data is provided', () => { + const issueReporterModel = new IssueReporterModel({ + issueType: 0, + systemInfo: { + os: 'Darwin', + cpus: 'Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)', + memory: '16.00GB', + vmHint: '0%', + processArgs: '', + screenReader: 'no', + remoteData: [], + gpuStatus: { + '2d_canvas': 'enabled', + 'checker_imaging': 'disabled_off' + } + }, + experimentInfo: 'vsliv695:30137379\nvsins829:30139715' + }); + assert.strictEqual(issueReporterModel.serialize(), + ` +Type: Bug + +undefined + +VS Code version: undefined +OS version: undefined +Modes: + +
+System Info + +|Item|Value| +|---|---| +|CPUs|Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)| +|GPU Status|2d_canvas: enabled
checker_imaging: disabled_off| +|Load (avg)|undefined| +|Memory (System)|16.00GB| +|Process Argv|| +|Screen Reader|no| +|VM|0%| +
Extensions: none
+A/B Experiments + +\`\`\` +vsliv695:30137379 +vsins829:30139715 +\`\`\` + +
+ +`); + }); + + test('serializes Linux environment information when data is provided', () => { + const issueReporterModel = new IssueReporterModel({ + issueType: 0, + systemInfo: { + os: 'Darwin', + cpus: 'Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)', + memory: '16.00GB', + vmHint: '0%', + processArgs: '', + screenReader: 'no', + remoteData: [], + gpuStatus: {}, + linuxEnv: { + desktopSession: 'ubuntu', + xdgCurrentDesktop: 'ubuntu', + xdgSessionDesktop: 'ubuntu:GNOME', + xdgSessionType: 'x11' + } + } + }); + assert.strictEqual(issueReporterModel.serialize(), + ` +Type: Bug + +undefined + +VS Code version: undefined +OS version: undefined +Modes: + +
+System Info + +|Item|Value| +|---|---| +|CPUs|Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)| +|GPU Status|| +|Load (avg)|undefined| +|Memory (System)|16.00GB| +|Process Argv|| +|Screen Reader|no| +|VM|0%| +|DESKTOP_SESSION|ubuntu| +|XDG_CURRENT_DESKTOP|ubuntu| +|XDG_SESSION_DESKTOP|ubuntu:GNOME| +|XDG_SESSION_TYPE|x11| +
Extensions: none +`); + }); + + test('serializes remote information when data is provided', () => { + const issueReporterModel = new IssueReporterModel({ + issueType: 0, + systemInfo: { + os: 'Darwin', + cpus: 'Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)', + memory: '16.00GB', + vmHint: '0%', + processArgs: '', + screenReader: 'no', + gpuStatus: { + '2d_canvas': 'enabled', + 'checker_imaging': 'disabled_off' + }, + remoteData: [ + { + hostName: 'SSH: Pineapple', + machineInfo: { + os: 'Linux x64 4.18.0', + cpus: 'Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz (2 x 2294)', + memory: '8GB', + vmHint: '100%' + } + } + ] + } + }); + assert.strictEqual(issueReporterModel.serialize(), + ` +Type: Bug + +undefined + +VS Code version: undefined +OS version: undefined +Modes: +Remote OS version: Linux x64 4.18.0 + +
+System Info + +|Item|Value| +|---|---| +|CPUs|Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)| +|GPU Status|2d_canvas: enabled
checker_imaging: disabled_off| +|Load (avg)|undefined| +|Memory (System)|16.00GB| +|Process Argv|| +|Screen Reader|no| +|VM|0%| + +|Item|Value| +|---|---| +|Remote|SSH: Pineapple| +|OS|Linux x64 4.18.0| +|CPUs|Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz (2 x 2294)| +|Memory (System)|8GB| +|VM|100%| +
Extensions: none +`); + }); + + test('escapes backslashes in processArgs', () => { + const issueReporterModel = new IssueReporterModel({ + issueType: 0, + systemInfo: { + os: 'Darwin', + cpus: 'Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)', + memory: '16.00GB', + vmHint: '0%', + processArgs: '\\\\HOST\\path', + screenReader: 'no', + remoteData: [], + gpuStatus: {} + } + }); + assert.strictEqual(issueReporterModel.serialize(), + ` +Type: Bug + +undefined + +VS Code version: undefined +OS version: undefined +Modes: + +
+System Info + +|Item|Value| +|---|---| +|CPUs|Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)| +|GPU Status|| +|Load (avg)|undefined| +|Memory (System)|16.00GB| +|Process Argv|\\\\\\\\HOST\\\\path| +|Screen Reader|no| +|VM|0%| +
Extensions: none +`); + }); + + test('should supply mode if applicable', () => { + const issueReporterModel = new IssueReporterModel({ + isUnsupported: true, + restrictedMode: true + }); + assert.strictEqual(issueReporterModel.serialize(), + ` +Type: Bug + +undefined + +VS Code version: undefined +OS version: undefined +Modes: Restricted, Unsupported + +Extensions: none +`); + }); + test('should normalize GitHub urls', () => { + [ + 'https://github.com/repo', + 'https://github.com/repo/', + 'https://github.com/repo.git', + 'https://github.com/repo/issues', + 'https://github.com/repo/issues/', + 'https://github.com/repo/issues/new', + 'https://github.com/repo/issues/new/' + ].forEach(url => { + assert.strictEqual('https://github.com/repo', normalizeGitHubUrl(url)); + }); + }); + + test('should have support for filing on extensions for bugs, performance issues, and feature requests', () => { + [ + IssueType.Bug, + IssueType.FeatureRequest, + IssueType.PerformanceIssue + ].forEach(type => { + const issueReporterModel = new IssueReporterModel({ + issueType: type, + fileOnExtension: true + }); + + assert.strictEqual(issueReporterModel.fileOnExtension(), true); + }); + }); +}); diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index daf83ca4cb0..c4a3d7c41ef 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -28,12 +28,12 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { equals } from 'vs/base/common/arrays'; import { URI } from 'vs/base/common/uri'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IEditorGroupsService, IEditorPart } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IHoverService, nativeHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { Event } from 'vs/base/common/event'; class LanguageStatusViewModel { @@ -65,22 +65,23 @@ class StoredCounter { class LanguageStatusContribution extends Disposable implements IWorkbenchContribution { constructor( - @IInstantiationService instantiationService: IInstantiationService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, ) { super(); - // --- main language status - const mainInstantiationService = this._register(instantiationService.createChild(new ServiceCollection( - [IEditorService, editorService.createScoped('main', this._store)] - ))); - this._register(mainInstantiationService.createInstance(LanguageStatus)); + for (const part of editorGroupService.parts) { + this.createLanguageStatus(part); + } - // --- auxiliary language status - this._register(editorGroupService.onDidCreateAuxiliaryEditorPart(({ instantiationService, disposables }) => { - disposables.add(instantiationService.createInstance(LanguageStatus)); - })); + this._register(editorGroupService.onDidCreateAuxiliaryEditorPart(part => this.createLanguageStatus(part))); + } + + private createLanguageStatus(part: IEditorPart): void { + const disposables = new DisposableStore(); + Event.once(part.onWillDispose)(() => disposables.dispose()); + + const scopedInstantiationService = this.editorGroupService.getScopedInstantiationService(part); + disposables.add(scopedInstantiationService.createInstance(LanguageStatus)); } } diff --git a/src/vs/workbench/contrib/list/browser/list.contribution.ts b/src/vs/workbench/contrib/list/browser/list.contribution.ts index dffda536058..e89365852d7 100644 --- a/src/vs/workbench/contrib/list/browser/list.contribution.ts +++ b/src/vs/workbench/contrib/list/browser/list.contribution.ts @@ -5,6 +5,8 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { ListResizeColumnAction } from 'vs/workbench/contrib/list/browser/listResizeColumnAction'; export class ListContext implements IWorkbenchContribution { @@ -21,3 +23,5 @@ export class ListContext implements IWorkbenchContribution { } registerWorkbenchContribution2(ListContext.ID, ListContext, WorkbenchPhase.BlockStartup); +registerAction2(ListResizeColumnAction); + diff --git a/src/vs/workbench/contrib/list/browser/listResizeColumnAction.ts b/src/vs/workbench/contrib/list/browser/listResizeColumnAction.ts new file mode 100644 index 00000000000..9c106b6c3c9 --- /dev/null +++ b/src/vs/workbench/contrib/list/browser/listResizeColumnAction.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TableColumnResizeQuickPick } from 'vs/workbench/contrib/list/browser/tableColumnResizeQuickPick'; +import { Table } from 'vs/base/browser/ui/table/tableWidget'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; + +export class ListResizeColumnAction extends Action2 { + constructor() { + super({ + id: 'list.resizeColumn', + title: { value: localize('list.resizeColumn', "Resize Column"), original: 'Resize Column' }, + category: { value: localize('list', "List"), original: 'List' }, + precondition: WorkbenchListFocusContextKey, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const listService = accessor.get(IListService); + const instantiationService = accessor.get(IInstantiationService); + + const list = listService.lastFocusedList; + if (list instanceof Table) { + await instantiationService.createInstance(TableColumnResizeQuickPick, list).show(); + } + } +} + diff --git a/src/vs/workbench/contrib/list/browser/tableColumnResizeQuickPick.ts b/src/vs/workbench/contrib/list/browser/tableColumnResizeQuickPick.ts new file mode 100644 index 00000000000..370baff0f86 --- /dev/null +++ b/src/vs/workbench/contrib/list/browser/tableColumnResizeQuickPick.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Table } from 'vs/base/browser/ui/table/tableWidget'; +import { Disposable } from 'vs/base/common/lifecycle'; +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; + +interface IColumnResizeQuickPickItem extends IQuickPickItem { + index: number; +} + +export class TableColumnResizeQuickPick extends Disposable { + constructor( + private readonly _table: Table, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + ) { + super(); + } + + async show(): Promise { + const items: IColumnResizeQuickPickItem[] = []; + this._table.getColumnLabels().forEach((label, index) => { + if (label) { + items.push({ label, index }); + } + }); + const column = await this._quickInputService.pick(items, { placeHolder: localize('table.column.selection', "Select the column to resize, type to filter.") }); + if (!column) { + return; + } + const value = await this._quickInputService.input({ + placeHolder: localize('table.column.resizeValue.placeHolder', "i.e. 20, 60, 100..."), + prompt: localize('table.column.resizeValue.prompt', "Please enter a width in percentage for the '{0}' column.", column.label), + validateInput: (input: string) => this._validateColumnResizeValue(input) + }); + const percentageValue = value ? Number.parseInt(value) : undefined; + if (!percentageValue) { + return; + } + this._table.resizeColumn(column.index, percentageValue); + } + + private async _validateColumnResizeValue(input: string): Promise { + const percentage = Number.parseInt(input); + if (input && !Number.isInteger(percentage)) { + return localize('table.column.resizeValue.invalidType', "Please enter an integer."); + } else if (percentage < 0 || percentage > 100) { + return localize('table.column.resizeValue.invalidRange', "Please enter a number greater than 0 and less than or equal to 100."); + } + return null; + } +} diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts index 098357445d0..16b0eb6dd6e 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts @@ -28,9 +28,13 @@ import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILabelService } from 'vs/platform/label/common/label'; -import { firstOrDefault } from 'vs/base/common/arrays'; +import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; import { getLocalHistoryDateFormatter, LOCAL_HISTORY_ICON_RESTORE, LOCAL_HISTORY_MENU_CONTEXT_KEY } from 'vs/workbench/contrib/localHistory/browser/localHistory'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { ResourceSet } from 'vs/base/common/map'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; const LOCAL_HISTORY_CATEGORY = localize2('localHistory.category', 'Local History'); const CTX_LOCAL_HISTORY_ENABLED = ContextKeyExpr.has('config.workbench.localHistory.enabled'); @@ -330,10 +334,14 @@ registerAction2(class extends Action2 { const editorService = accessor.get(IEditorService); const fileService = accessor.get(IFileService); const commandService = accessor.get(ICommandService); + const historyService = accessor.get(IHistoryService); // Show all resources with associated history entries in picker // with progress because this operation will take longer the more // files have been saved overall. + // + // Sort the resources by history to put more relevant entries + // to the top. const resourcePicker = quickInputService.createQuickPick(); @@ -343,18 +351,28 @@ registerAction2(class extends Action2 { resourcePicker.busy = true; resourcePicker.show(); - const resources = await workingCopyHistoryService.getAll(cts.token); + const resources = new ResourceSet(await workingCopyHistoryService.getAll(cts.token)); + const recentEditorResources = new ResourceSet(coalesce(historyService.getHistory().map(({ resource }) => resource))); + + const resourcesSortedByRecency: URI[] = []; + for (const resource of recentEditorResources) { + if (resources.has(resource)) { + resourcesSortedByRecency.push(resource); + resources.delete(resource); + } + } + resourcesSortedByRecency.push(...[...resources].sort((r1, r2) => r1.fsPath < r2.fsPath ? -1 : 1)); resourcePicker.busy = false; resourcePicker.placeholder = localize('restoreViaPicker.filePlaceholder', "Select the file to show local history for"); resourcePicker.matchOnLabel = true; resourcePicker.matchOnDescription = true; - resourcePicker.items = resources.map(resource => ({ + resourcePicker.items = [...resourcesSortedByRecency].map(resource => ({ resource, label: basenameOrAuthority(resource), description: labelService.getUriLabel(dirname(resource), { relative: true }), iconClasses: getIconClasses(modelService, languageService, resource) - })).sort((r1, r2) => r1.resource.fsPath < r2.resource.fsPath ? -1 : 1); + })); await Event.toPromise(resourcePicker.onDidAccept); resourcePicker.dispose(); @@ -367,10 +385,11 @@ registerAction2(class extends Action2 { // Show all entries for the picked resource in another picker // and open the entry in the end that was selected by the user - const entryPicker = quickInputService.createQuickPick(); + const disposables = new DisposableStore(); + const entryPicker = disposables.add(quickInputService.createQuickPick()); cts = new CancellationTokenSource(); - entryPicker.onDidHide(() => cts.dispose(true)); + disposables.add(entryPicker.onDidHide(() => cts.dispose(true))); entryPicker.busy = true; entryPicker.show(); @@ -378,6 +397,7 @@ registerAction2(class extends Action2 { const entries = await workingCopyHistoryService.getEntries(resource, cts.token); entryPicker.busy = false; + entryPicker.canAcceptInBackground = true; entryPicker.placeholder = localize('restoreViaPicker.entryPlaceholder', "Select the local history entry to open"); entryPicker.matchOnLabel = true; entryPicker.matchOnDescription = true; @@ -387,20 +407,23 @@ registerAction2(class extends Action2 { description: toLocalHistoryEntryDateLabel(entry.timestamp) })); - await Event.toPromise(entryPicker.onDidAccept); - entryPicker.dispose(); + disposables.add(entryPicker.onDidAccept(async e => { + if (!e.inBackground) { + disposables.dispose(); + } - const selectedItem = firstOrDefault(entryPicker.selectedItems); - if (!selectedItem) { - return; - } + const selectedItem = firstOrDefault(entryPicker.selectedItems); + if (!selectedItem) { + return; + } - const resourceExists = await fileService.exists(selectedItem.entry.workingCopy.resource); - if (resourceExists) { - return commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(selectedItem.entry, selectedItem.entry.workingCopy.resource)); - } + const resourceExists = await fileService.exists(selectedItem.entry.workingCopy.resource); + if (resourceExists) { + return commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(selectedItem.entry, selectedItem.entry.workingCopy.resource, { preserveFocus: e.inBackground })); + } - return openEntry(selectedItem.entry, editorService); + return openEntry(selectedItem.entry, editorService, { preserveFocus: e.inBackground }); + })); } }); @@ -572,12 +595,13 @@ registerAction2(class extends Action2 { //#region Helpers -async function openEntry(entry: IWorkingCopyHistoryEntry, editorService: IEditorService): Promise { +async function openEntry(entry: IWorkingCopyHistoryEntry, editorService: IEditorService, options?: IEditorOptions): Promise { const resource = LocalHistoryFileSystemProvider.toLocalHistoryFileSystem({ location: entry.location, associatedResource: entry.workingCopy.resource }); await editorService.openEditor({ resource, - label: localize('localHistoryEditorLabel', "{0} ({1} • {2})", entry.workingCopy.name, SaveSourceRegistry.getSourceLabel(entry.source), toLocalHistoryEntryDateLabel(entry.timestamp)) + label: localize('localHistoryEditorLabel', "{0} ({1} • {2})", entry.workingCopy.name, SaveSourceRegistry.getSourceLabel(entry.source), toLocalHistoryEntryDateLabel(entry.timestamp)), + options }); } @@ -588,9 +612,9 @@ async function closeEntry(entry: IWorkingCopyHistoryEntry, editorService: IEdito await editorService.closeEditors(editors, { preserveFocus: true }); } -export function toDiffEditorArguments(entry: IWorkingCopyHistoryEntry, resource: URI): unknown[]; -export function toDiffEditorArguments(previousEntry: IWorkingCopyHistoryEntry, entry: IWorkingCopyHistoryEntry): unknown[]; -export function toDiffEditorArguments(arg1: IWorkingCopyHistoryEntry, arg2: IWorkingCopyHistoryEntry | URI): unknown[] { +export function toDiffEditorArguments(entry: IWorkingCopyHistoryEntry, resource: URI, options?: IEditorOptions): unknown[]; +export function toDiffEditorArguments(previousEntry: IWorkingCopyHistoryEntry, entry: IWorkingCopyHistoryEntry, options?: IEditorOptions): unknown[]; +export function toDiffEditorArguments(arg1: IWorkingCopyHistoryEntry, arg2: IWorkingCopyHistoryEntry | URI, options?: IEditorOptions): unknown[] { // Left hand side is always a working copy history entry const originalResource = LocalHistoryFileSystemProvider.toLocalHistoryFileSystem({ location: arg1.location, associatedResource: arg1.workingCopy.resource }); @@ -623,7 +647,7 @@ export function toDiffEditorArguments(arg1: IWorkingCopyHistoryEntry, arg2: IWor originalResource, modifiedResource, label, - undefined // important to keep order of arguments in command proper + options ? [undefined, options] : undefined ]; } diff --git a/src/vs/workbench/contrib/localization/common/localizationsActions.ts b/src/vs/workbench/contrib/localization/common/localizationsActions.ts index d9ad34f1c1b..c094aaaeedf 100644 --- a/src/vs/workbench/contrib/localization/common/localizationsActions.ts +++ b/src/vs/workbench/contrib/localization/common/localizationsActions.ts @@ -37,7 +37,7 @@ export class ConfigureDisplayLanguageAction extends Action2 { const installedLanguages = await languagePackService.getInstalledLanguages(); - const qp = quickInputService.createQuickPick(); + const qp = quickInputService.createQuickPick({ useSeparators: true }); qp.matchOnDescription = true; qp.placeholder = localize('chooseLocale', "Select Display Language"); diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index 86c59a48995..3a4b1764048 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -75,7 +75,7 @@ export class SetLogLevelAction extends Action { return new Promise((resolve, reject) => { const disposables = new DisposableStore(); - const quickPick = this.quickInputService.createQuickPick(); + const quickPick = this.quickInputService.createQuickPick({ useSeparators: true }); quickPick.placeholder = nls.localize('selectlog', "Set Log Level"); quickPick.items = entries; let selectedItem: IQuickPickItem | undefined; diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index b1c6b962fab..4e417a65291 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { hookDomPurifyHrefAndSrcSanitizer, basicMarkupHtmlTags } from 'vs/base/browser/dom'; +import { basicMarkupHtmlTags, hookDomPurifyHrefAndSrcSanitizer } from 'vs/base/browser/dom'; import * as dompurify from 'vs/base/browser/dompurify/dompurify'; import { allowedMarkdownAttr } from 'vs/base/browser/markdownRenderer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { marked } from 'vs/base/common/marked/marked'; +import * as marked from 'vs/base/common/marked/marked'; import { Schemas } from 'vs/base/common/network'; +import { escape } from 'vs/base/common/strings'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { tokenizeToString } from 'vs/editor/common/languages/textToHtmlTokenizer'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { escape } from 'vs/base/common/strings'; export const DEFAULT_MARKDOWN_STYLES = ` body { @@ -190,7 +190,7 @@ interface IRenderMarkdownDocumentOptions { readonly token?: CancellationToken; } -/** +/*marked.* * Renders a string of markdown as a document. * * Uses VS Code's syntax highlighting code blocks. @@ -201,37 +201,115 @@ export async function renderMarkdownDocument( languageService: ILanguageService, options?: IRenderMarkdownDocumentOptions ): Promise { + const m = new marked.Marked( + MarkedHighlight.markedHighlight({ + async: true, + async highlight(code: string, lang: string): Promise { + if (typeof lang !== 'string') { + return escape(code); + } - const highlight = (code: string, lang: string | undefined, callback: ((error: any, code: string) => void) | undefined): any => { - if (!callback) { - return code; - } + await extensionService.whenInstalledExtensionsRegistered(); + if (options?.token?.isCancellationRequested) { + return ''; + } - if (typeof lang !== 'string') { - callback(null, escape(code)); - return ''; - } - - extensionService.whenInstalledExtensionsRegistered().then(async () => { - if (options?.token?.isCancellationRequested) { - callback(null, ''); - return; + const languageId = languageService.getLanguageIdByLanguageName(lang) ?? languageService.getLanguageIdByLanguageName(lang.split(/\s+|:|,|(?!^)\{|\?]/, 1)[0]); + return tokenizeToString(languageService, code, languageId); } + }) + ); - const languageId = languageService.getLanguageIdByLanguageName(lang) ?? languageService.getLanguageIdByLanguageName(lang.split(/\s+|:|,|(?!^)\{|\?]/, 1)[0]); - const html = await tokenizeToString(languageService, code, languageId); - callback(null, html); - }); - return ''; - }; - - return new Promise((resolve, reject) => { - marked(text, { highlight, renderer: options?.renderer }, (err, value) => err ? reject(err) : resolve(value)); - }).then(raw => { - if (options?.shouldSanitize ?? true) { - return sanitize(raw, options?.allowUnknownProtocols ?? false); - } else { - return raw; - } - }); + const raw = await m.parse(text, { renderer: options?.renderer, async: true }); + if (options?.shouldSanitize ?? true) { + return sanitize(raw, options?.allowUnknownProtocols ?? false); + } else { + return raw; + } +} + +namespace MarkedHighlight { + // Copied from https://github.com/markedjs/marked-highlight/blob/main/src/index.js + + export function markedHighlight(options: marked.MarkedOptions & { highlight: (code: string, lang: string, info: string) => string | Promise }) { + if (typeof options === 'function') { + options = { + highlight: options, + }; + } + + if (!options || typeof options.highlight !== 'function') { + throw new Error('Must provide highlight function'); + } + + return { + async: !!options.async, + walkTokens(token: marked.Token): Promise | void { + if (token.type !== 'code') { + return; + } + + const lang = getLang(token.lang); + + if (options.async) { + return Promise.resolve(options.highlight(token.text, lang, token.lang || '')).then(updateToken(token)); + } + + const code = options.highlight(token.text, lang, token.lang || ''); + if (code instanceof Promise) { + throw new Error('markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.'); + } + updateToken(token)(code); + }, + renderer: { + code({ text, lang, escaped }: marked.Tokens.Code) { + const classAttr = lang + ? ` class="language-${escape(lang)}"` + : ''; + text = text.replace(/\n$/, ''); + return `
${escaped ? text : escape(text, true)}\n
`; + }, + } as any, + }; + } + + function getLang(lang: string) { + return (lang || '').match(/\S*/)![0]; + } + + function updateToken(token: any) { + return (code: string) => { + if (typeof code === 'string' && code !== token.text) { + token.escaped = true; + token.text = code; + } + }; + } + + // copied from marked helpers + const escapeTest = /[&<>"']/; + const escapeReplace = new RegExp(escapeTest.source, 'g'); + const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/; + const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g'); + const escapeReplacement: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + [`'`]: ''', + }; + const getEscapeReplacement = (ch: string) => escapeReplacement[ch]; + function escape(html: string, encode?: boolean) { + if (encode) { + if (escapeTest.test(html)) { + return html.replace(escapeReplace, getEscapeReplacement); + } + } else { + if (escapeTestNoEncode.test(html)) { + return html.replace(escapeReplaceNoEncode, getEscapeReplacement); + } + } + + return html; + } } diff --git a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts index fe1c3c04e26..00dd93a6357 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts @@ -14,6 +14,7 @@ import { IAction } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { Schemas } from 'vs/base/common/network'; +import { Tokens } from 'vs/base/common/marked/marked'; export class SimpleSettingRenderer { private readonly codeSettingRegex: RegExp; @@ -40,17 +41,17 @@ export class SimpleSettingRenderer { return result; } - getHtmlRenderer(): (html: string) => string { - return (html): string => { - const match = this.codeSettingRegex.exec(html); + getHtmlRenderer(): (token: Tokens.HTML) => string { + return ({ raw }: Tokens.HTML): string => { + const match = this.codeSettingRegex.exec(raw); if (match && match.length === 4) { const settingId = match[2]; const rendered = this.render(settingId, match[3]); if (rendered) { - html = html.replace(this.codeSettingRegex, rendered); + raw = raw.replace(this.codeSettingRegex, rendered); } } - return html; + return raw; }; } @@ -149,7 +150,7 @@ export class SimpleSettingRenderer { return ` ${setting.key} - `; + `; } private getSettingMessage(setting: ISetting, newValue: boolean | string | number): string | undefined { diff --git a/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts b/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts index 1889507c974..2da36560758 100644 --- a/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts +++ b/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts @@ -79,12 +79,12 @@ suite('Markdown Setting Renderer Test', () => { test('render code setting button with value', () => { const htmlRenderer = settingRenderer.getHtmlRenderer(); const htmlNoValue = ''; - const renderedHtmlNoValue = htmlRenderer(htmlNoValue); + const renderedHtmlNoValue = htmlRenderer({ block: false, raw: htmlNoValue, pre: false, text: '', type: 'html' }); assert.strictEqual(renderedHtmlNoValue, ` example.booleanSetting - `); + `); }); test('actions with no value', () => { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index 1c0f093dc27..2094bfec019 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -193,6 +193,7 @@ export class MergeEditorViewModel extends Disposable { ): void { this.manuallySetActiveModifiedBaseRange.set({ range: baseRange, counter: this.counter++ }, tx); this.model.setState(baseRange, state, inputNumber, tx); + this.lastFocusedEditor.clearCache(tx); } private goToConflict(getModifiedBaseRange: (editor: CodeEditorView, curLineNumber: number) => ModifiedBaseRange | undefined): void { diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts index 30e6c69a641..e8abecf6c00 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts @@ -12,9 +12,11 @@ import { Action2, MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ITextEditorOptions, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IListService } from 'vs/platform/list/browser/listService'; import { resolveCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommandsContext'; import { MultiDiffEditor } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor'; import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class GoToFileAction extends Action2 { @@ -81,7 +83,7 @@ export class CollapseAllAction extends Action2 { } async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); const groupContext = resolvedContext.groupedEditors[0]; if (!groupContext) { @@ -114,7 +116,7 @@ export class ExpandAllAction extends Action2 { } async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { - const resolvedContext = resolveCommandsContext(accessor, args); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); const groupContext = resolvedContext.groupedEditors[0]; if (!groupContext) { diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts index 30684e69201..5aafc4bb2f2 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts @@ -7,7 +7,7 @@ import { LazyStatefulPromise, raceTimeout } from 'vs/base/common/async'; import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; import { Event, ValueWithChangeEvent } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { parse } from 'vs/base/common/marshalling'; import { Schemas } from 'vs/base/common/network'; import { deepClone } from 'vs/base/common/objects'; @@ -16,7 +16,8 @@ import { ValueWithChangeEventFromObservable, constObservable, mapObservableArray import { ThemeIcon } from 'vs/base/common/themables'; import { isDefined, isObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { ConstLazyPromise, IDocumentDiffItem, IMultiDiffEditorModel, LazyPromise } from 'vs/editor/browser/widget/multiDiffEditor/model'; +import { RefCounted } from 'vs/editor/browser/widget/diffEditor/utils'; +import { IDocumentDiffItem, IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditor/model'; import { MultiDiffEditorViewModel } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -123,7 +124,7 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor public setLanguageId(languageId: string, source?: string | undefined): void { const activeDiffItem = this._viewModel.requireValue().activeDiffItem.get(); - const value = activeDiffItem?.entry?.value; + const value = activeDiffItem?.documentDiffItem; if (!value) { return; } const target = value.modified ?? value.original; if (!target) { return; } @@ -147,26 +148,20 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor const source = await this._resolvedSource.getPromise(); const textResourceConfigurationService = this._textResourceConfigurationService; - // Enables delayed disposing - const garbage = new DisposableStore(); - const documentsWithPromises = mapObservableArrayCached(this, source.resources, async (r, store) => { /** @description documentsWithPromises */ let original: IReference | undefined; let modified: IReference | undefined; - const store2 = new DisposableStore(); - store.add(toDisposable(() => { - // Mark the text model references as garbage when they get stale (don't dispose them yet) - garbage.add(store2); - })); + + const multiDiffItemStore = new DisposableStore(); try { [original, modified] = await Promise.all([ r.originalUri ? this._textModelService.createModelReference(r.originalUri) : undefined, r.modifiedUri ? this._textModelService.createModelReference(r.modifiedUri) : undefined, ]); - if (original) { store2.add(original); } - if (modified) { store2.add(modified); } + if (original) { multiDiffItemStore.add(original); } + if (modified) { multiDiffItemStore.add(modified); } } catch (e) { // e.g. "File seems to be binary and cannot be opened as text" console.error(e); @@ -175,7 +170,7 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor } const uri = (r.modifiedUri ?? r.originalUri)!; - return new ConstLazyPromise({ + const result: IDocumentDiffItemWithMultiDiffEditorItem = { multiDiffEditorItem: r, original: original?.object.textEditorModel, modified: modified?.object.textEditorModel, @@ -190,10 +185,11 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor h(); } }), - }); + }; + return store.add(RefCounted.createOfNonDisposable(result, multiDiffItemStore, this)); }, i => JSON.stringify([i.modifiedUri?.toString(), i.originalUri?.toString()])); - const documents = observableValue[]>('documents', []); + const documents = observableValue[]>('documents', []); const updateDocuments = derived(async reader => { /** @description Update documents */ @@ -201,18 +197,13 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor const docs = await Promise.all(docsPromises); const newDocuments = docs.filter(isDefined); documents.set(newDocuments, undefined); - - garbage.clear(); // Only dispose text models after the documents have been updated }); const a = recomputeInitiallyAndOnChange(updateDocuments); await updateDocuments.get(); const result: IMultiDiffEditorModel & IDisposable = { - dispose: () => { - a.dispose(); - garbage.dispose(); - }, + dispose: () => a.dispose(), documents: new ValueWithChangeEventFromObservable(documents), contextKeys: source.source?.contextKeys, }; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index 197230a61cc..ea1766022d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -20,8 +20,8 @@ import { NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/no import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IEditorGroupsService, IEditorPart } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { Event } from 'vs/base/common/event'; class ImplictKernelSelector implements IDisposable { @@ -340,28 +340,26 @@ export class NotebookEditorStatusContribution extends Disposable implements IWor static readonly ID = 'notebook.contrib.editorStatus'; constructor( - @IInstantiationService instantiationService: IInstantiationService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService ) { super(); - // Main Editor Status - const mainInstantiationService = instantiationService.createChild(new ServiceCollection( - [IEditorService, editorService.createScoped('main', this._store)] - )); - this._register(mainInstantiationService.createInstance(KernelStatus)); - this._register(mainInstantiationService.createInstance(ActiveCellStatus)); - this._register(mainInstantiationService.createInstance(NotebookIndentationStatus)); + for (const part of editorGroupService.parts) { + this.createNotebookStatus(part); + } - // Auxiliary Editor Status - this._register(editorGroupService.onDidCreateAuxiliaryEditorPart(({ part, instantiationService, disposables }) => { - disposables.add(instantiationService.createInstance(KernelStatus)); - disposables.add(instantiationService.createInstance(ActiveCellStatus)); - disposables.add(instantiationService.createInstance(NotebookIndentationStatus)); - })); + this._register(editorGroupService.onDidCreateAuxiliaryEditorPart(part => this.createNotebookStatus(part))); + } + + private createNotebookStatus(part: IEditorPart): void { + const disposables = new DisposableStore(); + Event.once(part.onWillDispose)(() => disposables.dispose()); + + const scopedInstantiationService = this.editorGroupService.getScopedInstantiationService(part); + disposables.add(scopedInstantiationService.createInstance(KernelStatus)); + disposables.add(scopedInstantiationService.createInstance(ActiveCellStatus)); + disposables.add(scopedInstantiationService.createInstance(NotebookIndentationStatus)); } } - registerWorkbenchContribution2(NotebookEditorStatusContribution.ID, NotebookEditorStatusContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts index 64c90739dea..e9a7569377b 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts @@ -184,7 +184,7 @@ export function runDeleteAction(editor: IActiveNotebookEditor, cell: ICellViewMo } } -export async function moveCellRange(context: INotebookCellActionContext, direction: 'up' | 'down'): Promise { +export async function moveCellRange(context: INotebookActionContext, direction: 'up' | 'down'): Promise { if (!context.notebookEditor.hasModel()) { return; } @@ -195,9 +195,17 @@ export async function moveCellRange(context: INotebookCellActionContext, directi return; } - const selections = editor.getSelections(); - const modelRanges = expandCellRangesWithHiddenCells(editor, selections); - const range = modelRanges[0]; + let range: ICellRange | undefined = undefined; + + if (context.cell) { + const idx = editor.getCellIndex(context.cell); + range = { start: idx, end: idx + 1 }; + } else { + const selections = editor.getSelections(); + const modelRanges = expandCellRangesWithHiddenCells(editor, selections); + range = modelRanges[0]; + } + if (!range || range.start === range.end) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index 8ef65e7a121..4a8192b5e24 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts @@ -250,12 +250,14 @@ export abstract class NotebookMultiCellAction extends Action2 { // no parsed args, try handle active editor const editor = getEditorFromArgsOrActivePane(accessor); if (editor) { + const selectedCellRange: ICellRange[] = editor.getSelections().length === 0 ? [editor.getFocus()] : editor.getSelections(); + telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: from }); return this.runWithContext(accessor, { ui: false, notebookEditor: editor, - selectedCells: cellRangeToViewCells(editor, editor.getSelections()) + selectedCells: cellRangeToViewCells(editor, selectedCellRange) }); } } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 82b3f2b8eaf..defeb5124b8 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -7,10 +7,15 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Mimes } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Selection } from 'vs/editor/common/core/selection'; +import { CommandExecutor } from 'vs/editor/common/cursor/cursor'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IModelService } from 'vs/editor/common/services/model'; +import { LineCommentCommand, Type } from 'vs/editor/contrib/comment/browser/lineCommentCommand'; import { localize, localize2 } from 'vs/nls'; import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -24,23 +29,24 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/ import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { changeCellToKind, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; -import { CELL_TITLE_CELL_GROUP_ID, CELL_TITLE_OUTPUT_GROUP_ID, CellToolbarOrder, INotebookActionContext, INotebookCellActionContext, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, NotebookAction, NotebookCellAction, executeNotebookCondition, findTargetCellEditor } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { CELL_TITLE_CELL_GROUP_ID, CELL_TITLE_OUTPUT_GROUP_ID, CellToolbarOrder, INotebookActionContext, INotebookCellActionContext, INotebookCommandContext, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, NotebookAction, NotebookCellAction, NotebookMultiCellAction, executeNotebookCondition, findTargetCellEditor } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { NotebookChangeTabDisplaySize, NotebookIndentUsingSpaces, NotebookIndentUsingTabs, NotebookIndentationToSpacesAction, NotebookIndentationToTabsAction } from 'vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions'; import { CHANGE_CELL_LANGUAGE, CellEditState, DETECT_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellEditType, CellKind, ICellEditOperation, NotebookCellExecutionState, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_INPUT_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_CELL_IS_FIRST_OUTPUT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_IS_FIRST_OUTPUT, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_OUTPUT_INPUT_FOCUSED, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; -import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs'; const EDIT_CELL_COMMAND_ID = 'notebook.cell.edit'; const DELETE_CELL_COMMAND_ID = 'notebook.cell.delete'; export const CLEAR_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.clearOutputs'; export const SELECT_NOTEBOOK_INDENTATION_ID = 'notebook.selectIndentation'; +export const COMMENT_SELECTED_CELLS_ID = 'notebook.commentSelectedCells'; registerAction2(class EditCellAction extends NotebookCellAction { constructor() { @@ -271,7 +277,6 @@ registerAction2(class ClearCellOutputsAction extends NotebookCellAction { } }); - registerAction2(class ClearAllCellOutputsAction extends NotebookAction { constructor() { super({ @@ -337,7 +342,6 @@ registerAction2(class ClearAllCellOutputsAction extends NotebookAction { } }); - interface ILanguagePickInput extends IQuickPickItem { languageId: string; description: string; @@ -621,3 +625,56 @@ registerAction2(class SelectNotebookIndentation extends NotebookAction { return; } }); + +registerAction2(class CommentSelectedCellsAction extends NotebookMultiCellAction { + constructor() { + super({ + id: COMMENT_SELECTED_CELLS_ID, + title: localize('commentSelectedCells', "Comment Selected Cells"), + keybinding: { + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_FOCUSED, + NOTEBOOK_EDITOR_EDITABLE, + ContextKeyExpr.not(InputFocusedContextKey), + ), + primary: KeyMod.CtrlCmd | KeyCode.Slash, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCommandContext): Promise { + const languageConfigurationService = accessor.get(ILanguageConfigurationService); + + context.selectedCells.forEach(async cellViewModel => { + const textModel = await cellViewModel.resolveTextModel(); + + const commentsOptions = cellViewModel.commentOptions; + const cellCommentCommand = new LineCommentCommand( + languageConfigurationService, + new Selection(1, 1, textModel.getLineCount(), textModel.getLineMaxColumn(textModel.getLineCount())), // comment the entire cell + textModel.getOptions().tabSize, + Type.Toggle, + commentsOptions.insertSpace ?? true, + commentsOptions.ignoreEmptyLines ?? true, + false + ); + + // store any selections that are in the cell, allows them to be shifted by comments and preserved + const cellEditorSelections = cellViewModel.getSelections(); + const initialTrackedRangesIDs: string[] = cellEditorSelections.map(selection => { + return textModel._setTrackedRange(null, selection, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + }); + + CommandExecutor.executeCommands(textModel, cellEditorSelections, [cellCommentCommand]); + + const newTrackedSelections = initialTrackedRangesIDs.map(i => { + return textModel._getTrackedRange(i); + }).filter(r => !!r).map((range,) => { + return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + }); + cellViewModel.setSelections(newTrackedSelections ?? []); + }); // end of cells forEach + } + +}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts index c2b1d9cf252..cc1137f6a7e 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts @@ -29,8 +29,7 @@ export const fixedEditorOptions: IEditorOptions = { selectOnLineNumbers: false, wordWrap: 'off', lineNumbers: 'off', - lineDecorationsWidth: 0, - glyphMargin: false, + glyphMargin: true, fixedOverflowWidgets: true, minimap: { enabled: false }, renderValidationDecorations: 'on', diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 263d08b0c14..c5668620255 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { DiffElementViewModelBase, getFormattedMetadataJSON, getFormattedOutputJSON, OutputComparison, outputEqual, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; -import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DiffSide, DIFF_CELL_MARGIN, INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_INPUT, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { DiffElementCellViewModelBase, getFormattedMetadataJSON, getFormattedOutputJSON, OutputComparison, outputEqual, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel, DiffElementPlaceholderViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DiffSide, DIFF_CELL_MARGIN, INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_INPUT, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED, CellDiffPlaceholderRenderTemplate, IDiffCellMarginOverlay } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -31,7 +31,7 @@ import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestCont import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion'; -import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -43,6 +43,9 @@ import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibil import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { localize } from 'vs/nls'; +import { Emitter } from 'vs/base/common/event'; export function getOptimizedNestedCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { return { @@ -58,6 +61,29 @@ export function getOptimizedNestedCodeEditorWidgetOptions(): ICodeEditorWidgetOp }; } +export class CellDiffPlaceholderElement extends Disposable { + constructor( + placeholder: DiffElementPlaceholderViewModel, + templateData: CellDiffPlaceholderRenderTemplate, + ) { + super(); + templateData.body.classList.remove('left', 'right', 'full'); + const text = (placeholder.hiddenCells.length === 1) ? + localize('hiddenCell', '{0} hidden cell', placeholder.hiddenCells.length) : + localize('hiddenCells', '{0} hidden cells', placeholder.hiddenCells.length); + templateData.placeholder.innerText = text; + + this._register(DOM.addDisposableListener(templateData.placeholder, 'dblclick', (e: MouseEvent) => { + if (e.button !== 0) { + return; + } + e.preventDefault(); + placeholder.showHiddenCells(); + })); + this._register(templateData.marginOverlay.onAction(() => placeholder.showHiddenCells())); + templateData.marginOverlay.show(); + } +} class PropertyHeader extends Disposable { protected _foldingIndicator!: HTMLElement; @@ -68,14 +94,14 @@ class PropertyHeader extends Disposable { protected _propertyExpanded?: IContextKey; constructor( - readonly cell: DiffElementViewModelBase, + readonly cell: DiffElementCellViewModelBase, readonly propertyHeaderContainer: HTMLElement, readonly notebookEditor: INotebookTextDiffEditor, readonly accessor: { updateInfoRendering: (renderOutput: boolean) => void; - checkIfModified: (cell: DiffElementViewModelBase) => false | { reason: string | undefined }; - getFoldingState: (cell: DiffElementViewModelBase) => PropertyFoldingState; - updateFoldingState: (cell: DiffElementViewModelBase, newState: PropertyFoldingState) => void; + checkIfModified: (cell: DiffElementCellViewModelBase) => false | { reason: string | undefined }; + getFoldingState: (cell: DiffElementCellViewModelBase) => PropertyFoldingState; + updateFoldingState: (cell: DiffElementCellViewModelBase, newState: PropertyFoldingState) => void; unChangedLabel: string; changedLabel: string; prefix: string; @@ -239,6 +265,9 @@ abstract class AbstractElementRenderer extends Disposable { protected readonly _outputLocalDisposable = this._register(new DisposableStore()); protected _ignoreMetadata: boolean = false; protected _ignoreOutputs: boolean = false; + protected _cellHeaderContainer!: HTMLElement; + protected _editorContainer!: HTMLElement; + protected _cellHeader!: PropertyHeader; protected _metadataHeaderContainer!: HTMLElement; protected _metadataHeader!: PropertyHeader; protected _metadataInfoContainer!: HTMLElement; @@ -267,7 +296,7 @@ abstract class AbstractElementRenderer extends Disposable { constructor( readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: DiffElementViewModelBase, + readonly cell: DiffElementCellViewModelBase, readonly templateData: CellDiffSingleSideRenderTemplate | CellDiffSideBySideRenderTemplate, readonly style: 'left' | 'right' | 'full', protected readonly instantiationService: IInstantiationService, @@ -286,7 +315,9 @@ abstract class AbstractElementRenderer extends Disposable { this._isDisposed = false; this._metadataEditorDisposeStore = this._register(new DisposableStore()); this._outputEditorDisposeStore = this._register(new DisposableStore()); - this._register(cell.onDidLayoutChange(e => this.layout(e))); + this._register(cell.onDidLayoutChange(e => { + this.layout(e); + })); this._register(cell.onDidLayoutChange(e => this.updateBorders())); this.init(); this.buildBody(); @@ -321,6 +352,9 @@ abstract class AbstractElementRenderer extends Disposable { this.styleContainer(this._diffEditorContainer); this.updateSourceEditor(); + if (this.cell.modified) { + this._register(this.cell.modified.textModel.onDidChangeContent(() => this._cellHeader.refresh())); + } this._ignoreMetadata = this.configurationService.getValue('notebook.diff.ignoreMetadata'); if (this._ignoreMetadata) { @@ -510,6 +544,8 @@ abstract class AbstractElementRenderer extends Disposable { originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() }); + + this._metadataEditorDisposeStore.add(this.updateEditorOptions(this._metadataEditor!, ['renderSideBySide', 'useInlineViewWhenSpaceIsLimited'])); this.layout({ metadataHeight: true }); this._metadataEditorDisposeStore.add(this._metadataEditor); @@ -623,6 +659,7 @@ abstract class AbstractElementRenderer extends Disposable { originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() }); + this._outputEditorDisposeStore.add(this.updateEditorOptions(this._outputEditor!, ['renderSideBySide', 'useInlineViewWhenSpaceIsLimited'])); this._outputEditorDisposeStore.add(this._outputEditor); this._outputEditorContainer?.classList.add('diff'); @@ -713,13 +750,55 @@ abstract class AbstractElementRenderer extends Disposable { abstract updateSourceEditor(): void; abstract layout(state: IDiffElementLayoutState): void; + + protected updateEditorOptions(editor: DiffEditorWidget, optionsToUpdate: ('hideUnchangedRegions' | 'renderSideBySide' | 'useInlineViewWhenSpaceIsLimited')[]): IDisposable { + return Disposable.None; + // if (!optionsToUpdate.length) { + // return Disposable.None; + // } + + // const options: { + // renderSideBySide?: boolean; + // useInlineViewWhenSpaceIsLimited?: boolean; + // hideUnchangedRegions?: { enabled: boolean }; + // } = {}; + + // if (optionsToUpdate.includes('renderSideBySide')) { + // options.renderSideBySide = this.configurationService.getValue('diffEditor.renderSideBySide'); + // } + // if (optionsToUpdate.includes('hideUnchangedRegions')) { + // const enabled = this.configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'); + // options.hideUnchangedRegions = { enabled }; + // } + // if (optionsToUpdate.includes('useInlineViewWhenSpaceIsLimited')) { + // options.useInlineViewWhenSpaceIsLimited = this.configurationService.getValue('diffEditor.useInlineViewWhenSpaceIsLimited'); + // } + + // editor.updateOptions(options); + + // return this.configurationService.onDidChangeConfiguration(e => { + // if (e.affectsConfiguration('diffEditor.hideUnchangedRegions.enabled')) { + // const enabled = this.configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'); + // editor.updateOptions({ hideUnchangedRegions: { enabled } }); + // } + // if (e.affectsConfiguration('diffEditor.renderSideBySide')) { + // const renderSideBySide = this.configurationService.getValue('diffEditor.renderSideBySide'); + // editor.updateOptions({ renderSideBySide }); + // } + // if (e.affectsConfiguration('diffEditor.useInlineViewWhenSpaceIsLimited')) { + // const useInlineViewWhenSpaceIsLimited = this.configurationService.getValue('diffEditor.useInlineViewWhenSpaceIsLimited'); + // editor.updateOptions({ useInlineViewWhenSpaceIsLimited }); + // } + // }); + } } abstract class SingleSideDiffElement extends AbstractElementRenderer { - + protected _editor!: CodeEditorWidget; override readonly cell: SingleSideDiffElementViewModel; override readonly templateData: CellDiffSingleSideRenderTemplate; - + abstract get nestedCellViewModel(): DiffNestedCellViewModel; + abstract get readonly(): boolean; constructor( notebookEditor: INotebookTextDiffEditor, cell: SingleSideDiffElementViewModel, @@ -750,7 +829,7 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { notificationService, menuService, contextKeyService, - configurationService + configurationService, ); this.cell = cell; this.templateData = templateData; @@ -825,9 +904,107 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { })); } + override updateSourceEditor(): void { + this._cellHeaderContainer = this.templateData.cellHeaderContainer; + this._cellHeaderContainer.style.display = 'flex'; + this._cellHeaderContainer.innerText = ''; + this._editorContainer = this.templateData.editorContainer; + this._editorContainer.classList.add('diff'); + + const renderSourceEditor = () => { + if (this.cell.cellFoldingState === PropertyFoldingState.Collapsed) { + this._editorContainer.style.display = 'none'; + this.cell.editorHeight = 0; + return; + } + + const lineCount = this.nestedCellViewModel.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; + + this._editorContainer.style.height = `${editorHeight}px`; + this._editorContainer.style.display = 'block'; + + if (this._editor) { + const contentHeight = this._editor.getContentHeight(); + if (contentHeight >= 0) { + this.cell.editorHeight = contentHeight; + } + return; + } + + this._editor = this.templateData.sourceEditor; + this._editor.layout( + { + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + } + ); + this._editor.updateOptions({ readOnly: this.readonly }); + this.cell.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (this.cell.cellFoldingState === PropertyFoldingState.Expanded && e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + this._initializeSourceDiffEditor(this.nestedCellViewModel); + }; + + this._cellHeader = this._register(this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._cellHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: () => renderSourceEditor(), + checkIfModified: (_) => ({ reason: undefined }), + getFoldingState: (cell) => cell.cellFoldingState, + updateFoldingState: (cell, state) => cell.cellFoldingState = state, + unChangedLabel: 'Input', + changedLabel: 'Input', + prefix: 'input', + menuId: MenuId.NotebookDiffCellInputTitle + } + )); + this._cellHeader.buildHeader(); + renderSourceEditor(); + + this._initializeSourceDiffEditor(this.nestedCellViewModel); + } + protected calculateDiagonalFillHeight() { + return this.cell.layoutInfo.cellStatusHeight + this.cell.layoutInfo.editorHeight + this.cell.layoutInfo.editorMargin + this.cell.layoutInfo.metadataStatusHeight + this.cell.layoutInfo.metadataHeight + this.cell.layoutInfo.outputTotalHeight + this.cell.layoutInfo.outputStatusHeight; + } + + private async _initializeSourceDiffEditor(modifiedCell: DiffNestedCellViewModel) { + const modifiedRef = await this.textModelService.createModelReference(modifiedCell.uri); + + if (this._isDisposed) { + return; + } + + const modifiedTextModel = modifiedRef.object.textEditorModel; + this._register(modifiedRef); + + this._editor!.setModel(modifiedTextModel); + + const editorViewState = this.cell.getSourceEditorViewState() as editorCommon.IDiffEditorViewState | null; + if (editorViewState) { + this._editor!.restoreViewState(editorViewState); + } + + const contentHeight = this._editor!.getContentHeight(); + this.cell.editorHeight = contentHeight; + const height = `${this.calculateDiagonalFillHeight()}px`; + if (this._diagonalFill!.style.height !== height) { + this._diagonalFill!.style.height = height; + } + } + _disposeMetadata() { this.cell.metadataStatusHeight = 0; this.cell.metadataHeight = 0; + this.templateData.cellHeaderContainer.style.display = 'none'; this.templateData.metadataHeaderContainer.style.display = 'none'; this.templateData.metadataInfoContainer.style.display = 'none'; this._metadataEditor = undefined; @@ -916,7 +1093,6 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { } } export class DeletedElement extends SingleSideDiffElement { - private _editor!: CodeEditorWidget; constructor( notebookEditor: INotebookTextDiffEditor, cell: SingleSideDiffElementViewModel, @@ -936,53 +1112,33 @@ export class DeletedElement extends SingleSideDiffElement { super(notebookEditor, cell, templateData, 'left', instantiationService, languageService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService, configurationService); } + get nestedCellViewModel() { + return this.cell.original!; + } + get readonly() { + return true; + } + styleContainer(container: HTMLElement) { container.classList.remove('inserted'); container.classList.add('removed'); } - updateSourceEditor(): void { - const originalCell = this.cell.original!; - const lineCount = originalCell.textModel.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; - - this._editor = this.templateData.sourceEditor; - this._editor.layout({ - width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, - height: editorHeight - }); - - this.cell.editorHeight = editorHeight; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { - this.cell.editorHeight = e.contentHeight; - } - })); - - this.textModelService.createModelReference(originalCell.uri).then(ref => { - if (this._isDisposed) { - return; - } - - this._register(ref); - - const textModel = ref.object.textEditorModel; - this._editor.setModel(textModel); - this.cell.editorHeight = this._editor.getContentHeight(); - }); - } - layout(state: IDiffElementLayoutState) { DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => { if (state.editorHeight || state.outerWidth) { + this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; this._editor.layout({ width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), height: this.cell.layoutInfo.editorHeight }); } + if (state.outerWidth && this._editor) { + this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; + this._editor.layout(); + } + if (state.metadataHeight || state.outerWidth) { this._metadataEditor?.layout({ width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), @@ -998,13 +1154,14 @@ export class DeletedElement extends SingleSideDiffElement { } if (this._diagonalFill) { - this._diagonalFill.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`; + this._diagonalFill.style.height = `${this.calculateDiagonalFillHeight()}px`; } this.layoutNotebookCell(); }); } + _buildOutputRendererContainer() { if (!this._outputViewContainer) { this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); @@ -1068,7 +1225,6 @@ export class DeletedElement extends SingleSideDiffElement { } export class InsertElement extends SingleSideDiffElement { - private _editor!: CodeEditorWidget; constructor( notebookEditor: INotebookTextDiffEditor, cell: SingleSideDiffElementViewModel, @@ -1086,48 +1242,18 @@ export class InsertElement extends SingleSideDiffElement { ) { super(notebookEditor, cell, templateData, 'right', instantiationService, languageService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService, configurationService); } + get nestedCellViewModel() { + return this.cell.modified!; + } + get readonly() { + return false; + } styleContainer(container: HTMLElement): void { container.classList.remove('removed'); container.classList.add('inserted'); } - updateSourceEditor(): void { - const modifiedCell = this.cell.modified!; - const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; - - this._editor = this.templateData.sourceEditor; - this._editor.layout( - { - width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, - height: editorHeight - } - ); - this._editor.updateOptions({ readOnly: false }); - this.cell.editorHeight = editorHeight; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { - this.cell.editorHeight = e.contentHeight; - } - })); - - this.textModelService.createModelReference(modifiedCell.uri).then(ref => { - if (this._isDisposed) { - return; - } - - this._register(ref); - - const textModel = ref.object.textEditorModel; - this._editor.setModel(textModel); - this._editor.restoreViewState(this.cell.getSourceEditorViewState() as editorCommon.ICodeEditorViewState); - this.cell.editorHeight = this._editor.getContentHeight(); - }); - } - _buildOutputRendererContainer() { if (!this._outputViewContainer) { this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); @@ -1180,12 +1306,18 @@ export class InsertElement extends SingleSideDiffElement { layout(state: IDiffElementLayoutState) { DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => { if (state.editorHeight || state.outerWidth) { + this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; this._editor.layout({ width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), height: this.cell.layoutInfo.editorHeight }); } + if (state.outerWidth && this._editor) { + this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; + this._editor.layout(); + } + if (state.metadataHeight || state.outerWidth) { this._metadataEditor?.layout({ width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), @@ -1203,7 +1335,7 @@ export class InsertElement extends SingleSideDiffElement { this.layoutNotebookCell(); if (this._diagonalFill) { - this._diagonalFill.style.height = `${this.cell.layoutInfo.editorHeight + this.cell.layoutInfo.editorMargin + this.cell.layoutInfo.metadataStatusHeight + this.cell.layoutInfo.metadataHeight + this.cell.layoutInfo.outputTotalHeight + this.cell.layoutInfo.outputStatusHeight}px`; + this._diagonalFill.style.height = `${this.calculateDiagonalFillHeight()}px`; } }); } @@ -1220,8 +1352,6 @@ export class InsertElement extends SingleSideDiffElement { export class ModifiedElement extends AbstractElementRenderer { private _editor?: DiffEditorWidget; private _editorViewStateChanged: boolean; - private _editorContainer!: HTMLElement; - private _inputToolbarContainer!: HTMLElement; protected _toolbar!: ToolBar; protected _menu!: IMenu; @@ -1256,6 +1386,15 @@ export class ModifiedElement extends AbstractElementRenderer { container.classList.remove('inserted', 'removed'); } + override buildBody(): void { + super.buildBody(); + if (this.cell.displayIconToHideUnmodifiedCells) { + this._register(this.templateData.marginOverlay.onAction(() => this.cell.hideUnchangedCells())); + this.templateData.marginOverlay.show(); + } else { + this.templateData.marginOverlay.hide(); + } + } _disposeMetadata() { this.cell.metadataStatusHeight = 0; this.cell.metadataHeight = 0; @@ -1436,6 +1575,8 @@ export class ModifiedElement extends AbstractElementRenderer { originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() }); + + this._register(this.updateEditorOptions(this._outputMetadataEditor, ['renderSideBySide', 'useInlineViewWhenSpaceIsLimited'])); this._register(this._outputMetadataEditor); const originalOutputMetadataSource = JSON.stringify(this.cell.original.outputs[0].metadata ?? {}, undefined, '\t'); const modifiedOutputMetadataSource = JSON.stringify(this.cell.modified.outputs[0].metadata ?? {}, undefined, '\t'); @@ -1491,63 +1632,100 @@ export class ModifiedElement extends AbstractElementRenderer { } updateSourceEditor(): void { + this._cellHeaderContainer = this.templateData.cellHeaderContainer; + this._cellHeaderContainer.style.display = 'flex'; + this._cellHeaderContainer.innerText = ''; const modifiedCell = this.cell.modified; - const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - - const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; this._editorContainer = this.templateData.editorContainer; - this._editor = this.templateData.sourceEditor; - this._editorContainer.classList.add('diff'); - this._editor.layout({ - width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, - height: editorHeight - }); - - this._editorContainer.style.height = `${editorHeight}px`; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { - this.cell.editorHeight = e.contentHeight; + const renderSourceEditor = () => { + if (this.cell.cellFoldingState === PropertyFoldingState.Collapsed) { + this._editorContainer.style.display = 'none'; + this.cell.editorHeight = 0; + return; } - })); - this._initializeSourceDiffEditor(); - const scopedContextKeyService = this.contextKeyService.createScoped(this.templateData.inputToolbarContainer); + const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : (lineCount * lineHeight) + fixedEditorPadding.top + fixedEditorPadding.bottom; + + this._editorContainer.style.height = `${editorHeight}px`; + this._editorContainer.style.display = 'block'; + + if (this._editor) { + const contentHeight = this._editor.getContentHeight(); + if (contentHeight >= 0) { + this.cell.editorHeight = contentHeight; + } + return; + } + + this._editor = this.templateData.sourceEditor; + this._editor.layout({ + width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, + height: editorHeight + }); + this._register(this._editor.onDidContentSizeChange((e) => { + if (this.cell.cellFoldingState === PropertyFoldingState.Expanded && e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + this._initializeSourceDiffEditor(); + }; + + this._cellHeader = this._register(this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._cellHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: () => renderSourceEditor(), + checkIfModified: (cell) => { + return cell.modified?.textModel.getValue() !== cell.original?.textModel.getValue() ? { reason: undefined } : false; + }, + getFoldingState: (cell) => cell.cellFoldingState, + updateFoldingState: (cell, state) => cell.cellFoldingState = state, + unChangedLabel: 'Input', + changedLabel: 'Input changed', + prefix: 'input', + menuId: MenuId.NotebookDiffCellInputTitle + } + )); + this._cellHeader.buildHeader(); + renderSourceEditor(); + + const scopedContextKeyService = this.contextKeyService.createScoped(this._cellHeaderContainer); this._register(scopedContextKeyService); const inputChanged = NOTEBOOK_DIFF_CELL_INPUT.bindTo(scopedContextKeyService); + inputChanged.set(this.cell.modified.textModel.getValue() !== this.cell.original.textModel.getValue()); - this._inputToolbarContainer = this.templateData.inputToolbarContainer; this._toolbar = this.templateData.toolbar; this._toolbar.context = { cell: this.cell }; - if (this.cell.modified.textModel.getValue() !== this.cell.original.textModel.getValue()) { - this._inputToolbarContainer.style.display = 'block'; - inputChanged.set(true); - } else { - this._inputToolbarContainer.style.display = 'none'; - inputChanged.set(false); - } - - this._register(this.cell.modified.textModel.onDidChangeContent(() => { - if (this.cell.modified.textModel.getValue() !== this.cell.original.textModel.getValue()) { - this._inputToolbarContainer.style.display = 'block'; - inputChanged.set(true); - } else { - this._inputToolbarContainer.style.display = 'none'; - inputChanged.set(false); - } - })); - - const menu = this.menuService.getMenuActions(MenuId.NotebookDiffCellInputTitle, scopedContextKeyService, { shouldForwardArgs: true }); const actions: IAction[] = []; - createAndFillInActionBarActions(menu, actions); - this._toolbar.setActions(actions); + + const refreshToolbar = () => { + const hasChanges = this.cell.modified.textModel.getValue() !== this.cell.original.textModel.getValue(); + inputChanged.set(hasChanges); + + if (!actions.length) { + const menu = this.menuService.getMenuActions(MenuId.NotebookDiffCellInputTitle, scopedContextKeyService, { shouldForwardArgs: true }); + createAndFillInActionBarActions(menu, actions); + } + + if (hasChanges) { + this._toolbar.setActions(actions); + } else { + this._toolbar.setActions([]); + } + }; + + this._register(this.cell.modified.textModel.onDidChangeContent(() => refreshToolbar())); + refreshToolbar(); } private async _initializeSourceDiffEditor() { @@ -1581,6 +1759,7 @@ export class ModifiedElement extends AbstractElementRenderer { } }; + this._register(this.updateEditorOptions(this._editor!, ['hideUnchangedRegions', 'renderSideBySide', 'useInlineViewWhenSpaceIsLimited'])); this._register(this._editor!.getOriginalEditor().onDidChangeCursorSelection(handleViewStateChange)); this._register(this._editor!.getOriginalEditor().onDidScrollChange(handleScrollChange)); this._register(this._editor!.getModifiedEditor().onDidChangeCursorSelection(handleViewStateChange)); @@ -1597,23 +1776,26 @@ export class ModifiedElement extends AbstractElementRenderer { layout(state: IDiffElementLayoutState) { DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => { - if (state.editorHeight) { + if (state.editorHeight && this._editor) { this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; - this._editor!.layout({ + this._editor.layout({ width: this._editor!.getViewWidth(), height: this.cell.layoutInfo.editorHeight }); } - if (state.outerWidth) { + if (state.outerWidth && this._editor) { this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; - this._editor!.layout(); + this._editor.layout(); } if (state.metadataHeight || state.outerWidth) { if (this._metadataEditorContainer) { this._metadataEditorContainer.style.height = `${this.cell.layoutInfo.metadataHeight}px`; - this._metadataEditor?.layout(); + this._metadataEditor?.layout({ + width: this._editor?.getViewWidth() || this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this.cell.layoutInfo.metadataHeight + }); } } @@ -1642,3 +1824,83 @@ export class ModifiedElement extends AbstractElementRenderer { super.dispose(); } } + + +export class CollapsedCellOverlayWidget extends Disposable implements IDiffCellMarginOverlay { + private readonly _nodes = DOM.h('div.diff-hidden-cells', [ + DOM.h('div.center@content', { style: { display: 'flex' } }, [ + DOM.$('a', { + title: localize('showUnchangedCells', 'Show Unchanged Cells'), + role: 'button', + onclick: () => { this._action.fire(); } + }, + ...renderLabelWithIcons('$(unfold)'))] + ), + ]); + + private readonly _action = this._register(new Emitter()); + public readonly onAction = this._action.event; + constructor( + private readonly container: HTMLElement + ) { + super(); + + this._nodes.root.style.display = 'none'; + container.appendChild(this._nodes.root); + } + + public show() { + this._nodes.root.style.display = 'block'; + } + + public hide() { + this._nodes.root.style.display = 'none'; + } + + public override dispose() { + this.hide(); + this.container.removeChild(this._nodes.root); + DOM.reset(this._nodes.root); + super.dispose(); + } +} + +export class UnchangedCellOverlayWidget extends Disposable implements IDiffCellMarginOverlay { + private readonly _nodes = DOM.h('div.diff-hidden-cells', [ + DOM.h('div.center@content', { style: { display: 'flex' } }, [ + DOM.$('a', { + title: localize('hideUnchangedCells', 'Hide Unchanged Cells'), + role: 'button', + onclick: () => { this._action.fire(); } + }, + ...renderLabelWithIcons('$(fold)') + ), + ] + ), + ]); + + private readonly _action = this._register(new Emitter()); + public readonly onAction = this._action.event; + constructor( + private readonly container: HTMLElement + ) { + super(); + + this._nodes.root.style.display = 'none'; + container.appendChild(this._nodes.root); + } + + public show() { + this._nodes.root.style.display = 'block'; + } + + public hide() { + this._nodes.root.style.display = 'none'; + } + public override dispose() { + this.hide(); + this.container.removeChild(this._nodes.root); + DOM.reset(this._nodes.root); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts index ea94a04d2ad..4787cc9bcda 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import * as nls from 'vs/nls'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { DiffElementViewModelBase, SideBySideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { DiffElementCellViewModelBase, SideBySideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { DiffSide, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { ICellOutputViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -33,7 +33,7 @@ export class OutputElement extends Disposable { private _notebookTextModel: NotebookTextModel, private _notebookService: INotebookService, private _quickInputService: IQuickInputService, - private _diffElementViewModel: DiffElementViewModelBase, + private _diffElementViewModel: DiffElementCellViewModelBase, private _diffSide: DiffSide, private _nestedCell: DiffNestedCellViewModel, private _outputContainer: HTMLElement, @@ -228,7 +228,7 @@ export class OutputContainer extends Disposable { constructor( private _editor: INotebookTextDiffEditor, private _notebookTextModel: NotebookTextModel, - private _diffElementViewModel: DiffElementViewModelBase, + private _diffElementViewModel: DiffElementCellViewModelBase, private _nestedCellViewModel: DiffNestedCellViewModel, private _diffSide: DiffSide, private _outputContainer: HTMLElement, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index fd3336fc32b..793c7f89fa0 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -33,15 +33,77 @@ interface ILayoutInfoDelta extends ILayoutInfoDelta0 { recomputeOutput?: boolean; } +export type IDiffElementViewModelBase = DiffElementCellViewModelBase | DiffElementPlaceholderViewModel; + export abstract class DiffElementViewModelBase extends Disposable { - public metadataFoldingState: PropertyFoldingState; - public outputFoldingState: PropertyFoldingState; protected _layoutInfoEmitter = this._register(new Emitter()); onDidLayoutChange = this._layoutInfoEmitter.event; + constructor( + public readonly mainDocumentTextModel: INotebookTextModel, + public readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher, + public readonly initData: { + metadataStatusHeight: number; + outputStatusHeight: number; + fontInfo: FontInfo | undefined; + } + ) { + super(); + + this._register(this.editorEventDispatcher.onDidChangeLayout(e => this._layoutInfoEmitter.fire({ outerWidth: true }))); + } + + abstract layoutChange(): void; + abstract getHeight(lineHeight: number): number; + abstract get totalHeight(): number; +} + +export class DiffElementPlaceholderViewModel extends DiffElementViewModelBase { + readonly type: 'placeholder' = 'placeholder'; + public hiddenCells: DiffElementCellViewModelBase[] = []; + protected _unfoldHiddenCells = this._register(new Emitter()); + onUnfoldHiddenCells = this._unfoldHiddenCells.event; + + constructor( + mainDocumentTextModel: INotebookTextModel, + editorEventDispatcher: NotebookDiffEditorEventDispatcher, + initData: { + metadataStatusHeight: number; + outputStatusHeight: number; + fontInfo: FontInfo | undefined; + } + ) { + super(mainDocumentTextModel, editorEventDispatcher, initData); + + } + get totalHeight() { + return 24 + (2 * DIFF_CELL_MARGIN); + } + getHeight(_: number): number { + return this.totalHeight; + } + override layoutChange(): void { + // + } + showHiddenCells() { + this._unfoldHiddenCells.fire(); + } +} + +export abstract class DiffElementCellViewModelBase extends DiffElementViewModelBase { + public cellFoldingState: PropertyFoldingState; + public metadataFoldingState: PropertyFoldingState; + public outputFoldingState: PropertyFoldingState; protected _stateChangeEmitter = this._register(new Emitter<{ renderOutput: boolean }>()); onDidStateChange = this._stateChangeEmitter.event; protected _layoutInfo!: IDiffElementLayoutInfo; + public displayIconToHideUnmodifiedCells?: boolean; + private _hideUnchangedCells = this._register(new Emitter()); + public onHideUnchangedCells = this._hideUnchangedCells.event; + + hideUnchangedCells() { + this._hideUnchangedCells.fire(); + } set rawOutputHeight(height: number) { this._layout({ rawOutputHeight: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, height) }); } @@ -114,45 +176,50 @@ export abstract class DiffElementViewModelBase extends Disposable { return this._layoutInfo; } + get totalHeight() { + return this.layoutInfo.totalHeight; + } + private _sourceEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; private _outputEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; private _metadataEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; constructor( - readonly mainDocumentTextModel: INotebookTextModel, + mainDocumentTextModel: INotebookTextModel, readonly original: DiffNestedCellViewModel | undefined, readonly modified: DiffNestedCellViewModel | undefined, readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', - readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher, - readonly initData: { + editorEventDispatcher: NotebookDiffEditorEventDispatcher, + initData: { metadataStatusHeight: number; outputStatusHeight: number; fontInfo: FontInfo | undefined; } ) { - super(); + super(mainDocumentTextModel, editorEventDispatcher, initData); const editorHeight = this._estimateEditorHeight(initData.fontInfo); + const cellStatusHeight = 25; this._layoutInfo = { width: 0, editorHeight: editorHeight, editorMargin: 0, metadataHeight: 0, + cellStatusHeight, metadataStatusHeight: 25, rawOutputHeight: 0, outputTotalHeight: 0, outputStatusHeight: 25, outputMetadataHeight: 0, bodyMargin: 32, - totalHeight: 82 + editorHeight, + totalHeight: 82 + cellStatusHeight + editorHeight, layoutState: CellLayoutState.Uninitialized }; + this.cellFoldingState = modified?.textModel?.getValue() !== original?.textModel?.getValue() ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed; this.metadataFoldingState = PropertyFoldingState.Collapsed; this.outputFoldingState = PropertyFoldingState.Collapsed; - this._register(this.editorEventDispatcher.onDidChangeLayout(e => { - this._layoutInfoEmitter.fire({ outerWidth: true }); - })); + this._register(this.editorEventDispatcher.onDidChangeLayout(e => this._layoutInfoEmitter.fire({ outerWidth: true }))); } layoutChange() { @@ -185,6 +252,7 @@ export abstract class DiffElementViewModelBase extends Disposable { const editorHeight = delta.editorHeight !== undefined ? delta.editorHeight : this._layoutInfo.editorHeight; const editorMargin = delta.editorMargin !== undefined ? delta.editorMargin : this._layoutInfo.editorMargin; const metadataHeight = delta.metadataHeight !== undefined ? delta.metadataHeight : this._layoutInfo.metadataHeight; + const cellStatusHeight = delta.cellStatusHeight !== undefined ? delta.cellStatusHeight : this._layoutInfo.cellStatusHeight; const metadataStatusHeight = delta.metadataStatusHeight !== undefined ? delta.metadataStatusHeight : this._layoutInfo.metadataStatusHeight; const rawOutputHeight = delta.rawOutputHeight !== undefined ? delta.rawOutputHeight : this._layoutInfo.rawOutputHeight; const outputStatusHeight = delta.outputStatusHeight !== undefined ? delta.outputStatusHeight : this._layoutInfo.outputStatusHeight; @@ -194,6 +262,7 @@ export abstract class DiffElementViewModelBase extends Disposable { const totalHeight = editorHeight + editorMargin + + cellStatusHeight + metadataHeight + metadataStatusHeight + outputHeight @@ -205,6 +274,7 @@ export abstract class DiffElementViewModelBase extends Disposable { editorHeight: editorHeight, editorMargin: editorMargin, metadataHeight: metadataHeight, + cellStatusHeight, metadataStatusHeight: metadataStatusHeight, outputTotalHeight: outputHeight, outputStatusHeight: outputStatusHeight, @@ -239,6 +309,11 @@ export abstract class DiffElementViewModelBase extends Disposable { somethingChanged = true; } + if (newLayout.cellStatusHeight !== this._layoutInfo.cellStatusHeight) { + changeEvent.cellStatusHeight = true; + somethingChanged = true; + } + if (newLayout.metadataStatusHeight !== this._layoutInfo.metadataStatusHeight) { changeEvent.metadataStatusHeight = true; somethingChanged = true; @@ -288,6 +363,7 @@ export abstract class DiffElementViewModelBase extends Disposable { const totalHeight = editorHeight + this._layoutInfo.editorMargin + this._layoutInfo.metadataHeight + + this._layoutInfo.cellStatusHeight + this._layoutInfo.metadataStatusHeight + this._layoutInfo.outputTotalHeight + this._layoutInfo.outputStatusHeight @@ -372,7 +448,7 @@ export abstract class DiffElementViewModelBase extends Disposable { } } -export class SideBySideDiffElementViewModel extends DiffElementViewModelBase { +export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase { get originalDocument() { return this.otherDocumentTextModel; } @@ -410,6 +486,7 @@ export class SideBySideDiffElementViewModel extends DiffElementViewModelBase { this.modified = modified; this.type = type; + this.cellFoldingState = modified.textModel.getValue() !== original.textModel.getValue() ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed; this.metadataFoldingState = PropertyFoldingState.Collapsed; this.outputFoldingState = PropertyFoldingState.Collapsed; @@ -435,7 +512,9 @@ export class SideBySideDiffElementViewModel extends DiffElementViewModelBase { const modifiedMedataRaw = Object.assign({}, this.modified.metadata); const originalCellMetadata = this.original.metadata; for (const key of cellMetadataKeys) { - modifiedMedataRaw[key] = originalCellMetadata[key]; + if (key in originalCellMetadata) { + modifiedMedataRaw[key] = originalCellMetadata[key]; + } } this.modified.textModel.metadata = modifiedMedataRaw; @@ -491,6 +570,7 @@ export class SideBySideDiffElementViewModel extends DiffElementViewModelBase { return this._layoutInfo.editorHeight + this._layoutInfo.editorMargin + this._layoutInfo.metadataHeight + + this._layoutInfo.cellStatusHeight + this._layoutInfo.metadataStatusHeight + this._layoutInfo.outputStatusHeight + this._layoutInfo.bodyMargin / 2 @@ -528,7 +608,7 @@ export class SideBySideDiffElementViewModel extends DiffElementViewModelBase { } } -export class SingleSideDiffElementViewModel extends DiffElementViewModelBase { +export class SingleSideDiffElementViewModel extends DiffElementCellViewModelBase { get cellViewModel() { return this.type === 'insert' ? this.modified! : this.original!; } @@ -599,6 +679,7 @@ export class SingleSideDiffElementViewModel extends DiffElementViewModelBase { return this._layoutInfo.editorHeight + this._layoutInfo.editorMargin + this._layoutInfo.metadataHeight + + this._layoutInfo.cellStatusHeight + this._layoutInfo.metadataStatusHeight + this._layoutInfo.outputStatusHeight + this._layoutInfo.bodyMargin / 2 diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css index a0a89a75d84..9004d5f6e12 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css @@ -25,6 +25,11 @@ flex-direction: row; } +.notebook-text-diff-editor .cell-placeholder-body { + display: flex; + flex-direction: row; +} + .notebook-text-diff-editor .webview-cover { user-select: initial; -webkit-user-select: initial; @@ -99,6 +104,7 @@ width: 50%; } +.notebook-text-diff-editor .cell-diff-editor-container .input-header-container, .notebook-text-diff-editor .cell-diff-editor-container .output-header-container, .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { display: flex; @@ -107,13 +113,15 @@ cursor: default; } +.notebook-text-diff-editor .cell-diff-editor-container .input-header-container .property-folding-indicator .codicon, .notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-folding-indicator .codicon, .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-folding-indicator .codicon { visibility: visible; - padding: 4px 0 0 10px; + padding: 4px 0 0 6px; cursor: pointer; } +.notebook-text-diff-editor .cell-diff-editor-container .input-header-container, .notebook-text-diff-editor .cell-diff-editor-container .output-header-container, .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { display: flex; @@ -121,22 +129,26 @@ align-items: center; } +.notebook-text-diff-editor .cell-diff-editor-container .input-header-container .property-toolbar, .notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-toolbar, .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-toolbar { margin-left: auto; } +.notebook-text-diff-editor .cell-diff-editor-container .input-header-container .property-status, .notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status, .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status { font-size: 12px; } +.notebook-text-diff-editor .cell-diff-editor-container .input-header-container .property-status span, .notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status span, .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status span { - margin: 0 0 0 8px; + margin: 0 0 0 5px; line-height: 21px; } +.notebook-text-diff-editor .cell-diff-editor-container .input-header-container .property-status span.property-description, .notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status span.property-description, .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status span.property-description { font-style: italic; @@ -292,7 +304,7 @@ width: 15px !important; } -.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { z-index: var(--z-index-notebook-scrollbar); cursor: default; } @@ -357,6 +369,7 @@ .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container, .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .margin, .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .monaco-editor-background, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .input-header-container, .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container, .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container .monaco-editor .margin, .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container .monaco-editor .monaco-editor-background, @@ -374,6 +387,7 @@ .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container, .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .margin, .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .monaco-editor-background, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .input-header-container, .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container, .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container .monaco-editor .margin, .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container .monaco-editor .monaco-editor-background, @@ -390,3 +404,60 @@ .notebook-text-diff-editor .cell-body .cell-diff-editor-container .source-container .monaco-editor .monaco-editor-background { background: var(--vscode-notebook-cellEditorBackground, var(--vscode-editor-background)); } + +/** Overlay to hide the unchanged cells */ +.notebook-text-diff-editor .cell-body.full div.diff-hidden-cells { + position: absolute; + left: 0; + + font-size: 13px; + line-height: 14px; +} + +.notebook-text-diff-editor .cell-body.full div.diff-hidden-cells .center { + color: var(--vscode-diffEditor-unchangedRegionForeground); + overflow: hidden; + display: block; + white-space: nowrap; + + height: 24px; +} + +.notebook-text-diff-editor .cell-body.full div.diff-hidden-cells .center span.codicon { + vertical-align: middle; +} + +.notebook-text-diff-editor .cell-body.full div.diff-hidden-cells .center a:hover .codicon { + cursor: pointer; +} + +/** Overlay to unhide the unchanged cells */ +.notebook-text-diff-editor .cell-placeholder-body { + background: var(--vscode-diffEditor-unchangedRegionBackground); + color: var(--vscode-diffEditor-unchangedRegionForeground); + min-height: 24px; +} + +.notebook-text-diff-editor .cell-placeholder-body div.diff-hidden-cells .center { + overflow: hidden; + display: block; + text-overflow: ellipsis; + white-space: nowrap; + + height: 24px; +} + +.notebook-text-diff-editor .cell-placeholder-body .text { + /** Add a gap between text and the unfold icon */ + padding-left: 2px; +} + +.notebook-text-diff-editor .cell-placeholder-body div.diff-hidden-cells .center span.codicon, +.notebook-text-diff-editor .cell-placeholder-body .text { + vertical-align: middle; +} + +.notebook-text-diff-editor .cell-placeholder-body div.diff-hidden-cells .center a:hover .codicon { + cursor: pointer; + color: var(--vscode-editorLink-activeForeground) !important; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts index f7cfcf23fec..11b4d887ba9 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -10,7 +10,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; -import { DiffElementViewModelBase, SideBySideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { DiffElementCellViewModelBase, SideBySideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_INPUT, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor'; import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/common/notebookDiffEditorInput'; @@ -62,6 +62,37 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.toggle.diff.renderSideBySide', + title: localize('inlineView', "Inline View"), + toggled: ContextKeyExpr.equals('config.diffEditor.renderSideBySide', false), + precondition: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), + menu: [{ + id: MenuId.EditorTitle, + order: 0, + group: '1_diff', + when: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID) + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const activeEditor = editorService.activeEditorPane; + if (activeEditor && activeEditor instanceof NotebookTextDiffEditor) { + const diffEditorInput = activeEditor.input as NotebookDiffEditorInput; + if (diffEditorInput.resource) { + const configurationService = accessor.get(IConfigurationService); + + const oldValue = configurationService.getValue('diffEditor.renderSideBySide'); + configurationService.updateValue('diffEditor.renderSideBySide', !oldValue); + } + } + } +}); + registerAction2(class extends Action2 { constructor() { super( @@ -78,7 +109,7 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { if (!context) { return; } @@ -133,7 +164,7 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { if (!context) { return; } @@ -158,7 +189,7 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { if (!context) { return; } @@ -199,7 +230,7 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { if (!context) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index dd98ad9d3a4..5fe866a59fd 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -14,9 +14,9 @@ import { getDefaultNotebookCreationOptions } from 'vs/workbench/contrib/notebook import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookDiffEditorInput } from '../../common/notebookDiffEditorInput'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { DiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { DiffElementCellViewModelBase, IDiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CellDiffSideBySideRenderer, CellDiffSingleSideRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffList'; +import { CellDiffPlaceholderRenderer, CellDiffSideBySideRenderer, CellDiffSingleSideRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffList'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { diffDiagonalFill, editorBackground, focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; @@ -25,9 +25,9 @@ import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/ed import { BareFontInfo, FontInfo } from 'vs/editor/common/config/fontInfo'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; import { CellEditState, ICellOutputViewModel, IDisplayOutputLayoutUpdateRequest, IGenericCellViewModel, IInsetRenderOutput, INotebookEditorCreationOptions, INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { DiffSide, DIFF_CELL_MARGIN, IDiffCellInfo, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { DiffSide, DIFF_CELL_MARGIN, IDiffCellInfo, INotebookTextDiffEditor, INotebookDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { CellUri, INotebookDiffEditorModel, INotebookDiffResult, NOTEBOOK_DIFF_EDITOR_ID, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { URI } from 'vs/base/common/uri'; @@ -46,6 +46,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { NotebookDiffOverviewRuler } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffOverviewRuler'; import { registerZIndex, ZIndex } from 'vs/platform/layout/browser/zIndexRegistry'; +import { NotebookDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel'; const $ = DOM.$; @@ -94,14 +95,14 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _overviewRulerContainer!: HTMLElement; private _overviewRuler!: NotebookDiffOverviewRuler; private _dimension: DOM.Dimension | null = null; - private _diffElementViewModels: DiffElementViewModelBase[] = []; + private notebookDiffViewModel: INotebookDiffViewModel = this._register(new NotebookDiffViewModel()); private _list!: NotebookTextDiffList; private _modifiedWebview: BackLayerWebView | null = null; private _originalWebview: BackLayerWebView | null = null; private _webviewTransparentCover: HTMLElement | null = null; private _fontInfo: FontInfo | undefined; - private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase }>()); + private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: IDiffElementViewModelBase }>()); public readonly onMouseUp = this._onMouseUp.event; private readonly _onDidScroll = this._register(new Emitter()); readonly onDidScroll: Event = this._onDidScroll.event; @@ -277,6 +278,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const renderers = [ this.instantiationService.createInstance(CellDiffSingleSideRenderer, this), this.instantiationService.createInstance(CellDiffSideBySideRenderer, this), + this.instantiationService.createInstance(CellDiffPlaceholderRenderer, this), ]; this._listViewContainer = DOM.append(this._rootElement, DOM.$('.notebook-diff-list-view')); @@ -331,9 +333,18 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD ); this._register(this._list); + this._register(this.notebookDiffViewModel.onDidChangeItems(e => { + this._list.splice(e.start, e.deleteCount, e.elements); + if (this.isOverviewRulerEnabled()) { + this._overviewRuler.updateViewModels(this.notebookDiffViewModel.items, this._eventDispatcher); + } + })); this._register(this._list.onMouseUp(e => { if (e.element) { + if (typeof e.index === 'number') { + this._list.setFocus([e.index]); + } this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); } })); @@ -375,7 +386,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._overviewRuler = this._register(this.instantiationService.createInstance(NotebookDiffOverviewRuler, this, NotebookTextDiffEditor.ENTIRE_DIFF_OVERVIEW_WIDTH, this._overviewRulerContainer)); } - private _updateOutputsOffsetsInWebview(scrollTop: number, scrollHeight: number, activeWebview: BackLayerWebView, getActiveNestedCell: (diffElement: DiffElementViewModelBase) => DiffNestedCellViewModel | undefined, diffSide: DiffSide) { + private _updateOutputsOffsetsInWebview(scrollTop: number, scrollHeight: number, activeWebview: BackLayerWebView, getActiveNestedCell: (diffElement: DiffElementCellViewModelBase) => DiffNestedCellViewModel | undefined, diffSide: DiffSide) { activeWebview.element.style.height = `${scrollHeight}px`; if (activeWebview.insetMapping) { @@ -482,13 +493,13 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } if (this._modifiedWebview) { - this._updateOutputsOffsetsInWebview(this._list.scrollTop, this._list.scrollHeight, this._modifiedWebview, (diffElement: DiffElementViewModelBase) => { + this._updateOutputsOffsetsInWebview(this._list.scrollTop, this._list.scrollHeight, this._modifiedWebview, (diffElement: DiffElementCellViewModelBase) => { return diffElement.modified; }, DiffSide.Modified); } if (this._originalWebview) { - this._updateOutputsOffsetsInWebview(this._list.scrollTop, this._list.scrollHeight, this._originalWebview, (diffElement: DiffElementViewModelBase) => { + this._updateOutputsOffsetsInWebview(this._list.scrollTop, this._list.scrollHeight, this._originalWebview, (diffElement: DiffElementCellViewModelBase) => { return diffElement.original; }, DiffSide.Original); } @@ -556,17 +567,15 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD NotebookTextDiffEditor.prettyChanges(this._model, diffResult.cellsDiff); const { viewModels, firstChangeIndex } = NotebookTextDiffEditor.computeDiff(this.instantiationService, this.configurationService, this._model, this._eventDispatcher!, diffResult, this.fontInfo); - const isSame = this._isViewModelTheSame(viewModels); - if (!isSame) { + if (this.notebookDiffViewModel.isEqual(viewModels)) { + dispose(viewModels); + } else { this._originalWebview?.removeInsets([...this._originalWebview?.insetMapping.keys()]); this._modifiedWebview?.removeInsets([...this._modifiedWebview?.insetMapping.keys()]); - this._setViewModel(viewModels); + this.notebookDiffViewModel.setViewModel(viewModels); } - // this._diffElementViewModels = viewModels; - // this._list.splice(0, this._list.length, this._diffElementViewModels); - if (this._revealFirst && firstChangeIndex !== -1 && firstChangeIndex < this._list.length) { this._revealFirst = false; this._list.setFocus([firstChangeIndex]); @@ -578,34 +587,6 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } } - private _isViewModelTheSame(viewModels: DiffElementViewModelBase[]) { - let isSame = true; - if (this._diffElementViewModels.length === viewModels.length) { - for (let i = 0; i < viewModels.length; i++) { - const a = this._diffElementViewModels[i]; - const b = viewModels[i]; - - if (a.original?.textModel.getHashValue() !== b.original?.textModel.getHashValue() - || a.modified?.textModel.getHashValue() !== b.modified?.textModel.getHashValue()) { - isSame = false; - break; - } - } - } else { - isSame = false; - } - - return isSame; - } - - private _setViewModel(viewModels: DiffElementViewModelBase[]) { - this._diffElementViewModels = viewModels; - this._list.splice(0, this._list.length, this._diffElementViewModels); - if (this.isOverviewRulerEnabled()) { - this._overviewRuler.updateViewModels(this._diffElementViewModels, this._eventDispatcher); - } - } - /** * making sure that swapping cells are always translated to `insert+delete`. */ @@ -646,7 +627,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD static computeDiff(instantiationService: IInstantiationService, configurationService: IConfigurationService, model: INotebookDiffEditorModel, eventDispatcher: NotebookDiffEditorEventDispatcher, diffResult: INotebookDiffResult, fontInfo: FontInfo | undefined) { const cellChanges = diffResult.cellsDiff.changes; - const diffElementViewModels: DiffElementViewModelBase[] = []; + const diffElementViewModels: DiffElementCellViewModelBase[] = []; const originalModel = model.original.notebook; const modifiedModel = model.modified.notebook; let originalCellIndex = 0; @@ -726,7 +707,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD outputStatusHeight: number; fontInfo: FontInfo | undefined; }) { - const result: DiffElementViewModelBase[] = []; + const result: DiffElementCellViewModelBase[] = []; // modified cells const modifiedLen = Math.min(change.originalLength, change.modifiedLength); @@ -795,11 +776,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, 10); } - private pendingLayouts = new WeakMap(); + private pendingLayouts = new WeakMap(); - layoutNotebookCell(cell: DiffElementViewModelBase, height: number) { - const relayout = (cell: DiffElementViewModelBase, height: number) => { + layoutNotebookCell(cell: DiffElementCellViewModelBase, height: number) { + const relayout = (cell: DiffElementCellViewModelBase, height: number) => { this._list.updateElementHeight2(cell, height); }; @@ -840,8 +821,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD // find the index of previous change let prevChangeIndex = currFocus - 1; + const currentViewModels = this.notebookDiffViewModel.items; while (prevChangeIndex >= 0) { - const vm = this._diffElementViewModels[prevChangeIndex]; + const vm = currentViewModels[prevChangeIndex]; if (vm.type !== 'unchanged') { break; } @@ -854,7 +836,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._list.reveal(prevChangeIndex); } else { // go to the last one - const index = findLastIdx(this._diffElementViewModels, vm => vm.type !== 'unchanged'); + const index = findLastIdx(currentViewModels, vm => vm.type !== 'unchanged'); if (index >= 0) { this._list.setFocus([index]); this._list.reveal(index); @@ -871,8 +853,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD // find the index of next change let nextChangeIndex = currFocus + 1; - while (nextChangeIndex < this._diffElementViewModels.length) { - const vm = this._diffElementViewModels[nextChangeIndex]; + const currentViewModels = this.notebookDiffViewModel.items; + while (nextChangeIndex < currentViewModels.length) { + const vm = currentViewModels[nextChangeIndex]; if (vm.type !== 'unchanged') { break; } @@ -880,12 +863,12 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD nextChangeIndex++; } - if (nextChangeIndex < this._diffElementViewModels.length) { + if (nextChangeIndex < currentViewModels.length) { this._list.setFocus([nextChangeIndex]); this._list.reveal(nextChangeIndex); } else { // go to the first one - const index = this._diffElementViewModels.findIndex(vm => vm.type !== 'unchanged'); + const index = currentViewModels.findIndex(vm => vm.type !== 'unchanged'); if (index >= 0) { this._list.setFocus([index]); this._list.reveal(index); @@ -893,7 +876,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } } - createOutput(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void { + createOutput(cellDiffViewModel: DiffElementCellViewModelBase, cellViewModel: DiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void { this._insetModifyQueueByOutputId.queue(output.source.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; if (!activeWebview) { @@ -930,7 +913,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD throw new Error('Not implemented'); } - removeInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: ICellOutputViewModel, diffSide: DiffSide) { + removeInset(cellDiffViewModel: DiffElementCellViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: ICellOutputViewModel, diffSide: DiffSide) { this._insetModifyQueueByOutputId.queue(displayOutput.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; if (!activeWebview) { @@ -945,7 +928,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }); } - showInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: ICellOutputViewModel, diffSide: DiffSide) { + showInset(cellDiffViewModel: DiffElementCellViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: ICellOutputViewModel, diffSide: DiffSide) { this._insetModifyQueueByOutputId.queue(displayOutput.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; if (!activeWebview) { @@ -969,7 +952,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }); } - hideInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, output: ICellOutputViewModel) { + hideInset(cellDiffViewModel: DiffElementCellViewModelBase, cellViewModel: DiffNestedCellViewModel, output: ICellOutputViewModel) { this._modifiedWebview?.hideInset(output); this._originalWebview?.hideInset(output); } @@ -998,8 +981,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._modifiedResourceDisposableStore.clear(); this._list?.splice(0, this._list?.length || 0); this._model = null; - this._diffElementViewModels.forEach(vm => vm.dispose()); - this._diffElementViewModels = []; + this.notebookDiffViewModel.clear(); } deltaCellOutputContainerClassNames(diffSide: DiffSide, cellId: string, added: string[], removed: string[]) { @@ -1024,56 +1006,6 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }; } - getCellOutputLayoutInfo(nestedCell: DiffNestedCellViewModel) { - if (!this._model) { - throw new Error('Editor is not attached to model yet'); - } - const documentModel = CellUri.parse(nestedCell.uri); - if (!documentModel) { - throw new Error('Nested cell in the diff editor has wrong Uri'); - } - - const belongToOriginalDocument = this._model.original.notebook.uri.toString() === documentModel.notebook.toString(); - const viewModel = this._diffElementViewModels.find(element => { - const textModel = belongToOriginalDocument ? element.original : element.modified; - if (!textModel) { - return false; - } - - if (textModel.uri.toString() === nestedCell.uri.toString()) { - return true; - } - - return false; - }); - - if (!viewModel) { - throw new Error('Nested cell in the diff editor does not match any diff element'); - } - - if (viewModel.type === 'unchanged') { - return this.getLayoutInfo(); - } - - if (viewModel.type === 'insert' || viewModel.type === 'delete') { - return { - width: this._dimension!.width / 2, - height: this._dimension!.height / 2, - fontInfo: this.fontInfo - }; - } - - if (viewModel.checkIfOutputsModified()) { - return { - width: this._dimension!.width / 2, - height: this._dimension!.height / 2, - fontInfo: this.fontInfo - }; - } else { - return this.getLayoutInfo(); - } - } - layout(dimension: DOM.Dimension, _position: DOM.IDomPosition): void { this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); this._rootElement.classList.toggle('narrow-width', dimension.width < 600); @@ -1133,4 +1065,6 @@ registerThemingParticipant((theme, collector) => { `); collector.addRule(`.notebook-text-diff-editor .cell-body { margin: ${DIFF_CELL_MARGIN}px; }`); + // We do not want a left margin, as we add an overlay for expanind the collapsed/hidden cells. + collector.addRule(`.notebook-text-diff-editor .cell-placeholder-body { margin: ${DIFF_CELL_MARGIN}px 0; }`); }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts index 031c2afdc7f..105ce9b0677 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { CellLayoutState, ICellOutputViewModel, ICommonCellInfo, IGenericCellViewModel, IInsetRenderOutput } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { DiffElementViewModelBase } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { DiffElementCellViewModelBase, IDiffElementViewModelBase } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { Event } from 'vs/base/common/event'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; @@ -23,24 +23,24 @@ export enum DiffSide { } export interface IDiffCellInfo extends ICommonCellInfo { - diffElement: DiffElementViewModelBase; + diffElement: DiffElementCellViewModelBase; } export interface INotebookTextDiffEditor { notebookOptions: NotebookOptions; readonly textModel?: NotebookTextModel; - onMouseUp: Event<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase }>; + onMouseUp: Event<{ readonly event: MouseEvent; readonly target: IDiffElementViewModelBase }>; onDidScroll: Event; onDidDynamicOutputRendered: Event<{ cell: IGenericCellViewModel; output: ICellOutputViewModel }>; getOverflowContainerDomNode(): HTMLElement; getLayoutInfo(): NotebookLayoutInfo; getScrollTop(): number; getScrollHeight(): number; - layoutNotebookCell(cell: DiffElementViewModelBase, height: number): void; - createOutput(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void; - showInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, displayOutput: ICellOutputViewModel, diffSide: DiffSide): void; - removeInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: ICellOutputViewModel, diffSide: DiffSide): void; - hideInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: ICellOutputViewModel): void; + layoutNotebookCell(cell: DiffElementCellViewModelBase, height: number): void; + createOutput(cellDiffViewModel: DiffElementCellViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void; + showInset(cellDiffViewModel: DiffElementCellViewModelBase, cellViewModel: IDiffNestedCellViewModel, displayOutput: ICellOutputViewModel, diffSide: DiffSide): void; + removeInset(cellDiffViewModel: DiffElementCellViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: ICellOutputViewModel, diffSide: DiffSide): void; + hideInset(cellDiffViewModel: DiffElementCellViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: ICellOutputViewModel): void; /** * Trigger the editor to scroll from scroll event programmatically */ @@ -66,12 +66,22 @@ export interface CellDiffCommonRenderTemplate { readonly bottomBorder: HTMLElement; } +export interface CellDiffPlaceholderRenderTemplate { + readonly container: HTMLElement; + readonly placeholder: HTMLElement; + readonly body: HTMLElement; + readonly marginOverlay: IDiffCellMarginOverlay; + readonly elementDisposables: DisposableStore; +} + export interface CellDiffSingleSideRenderTemplate extends CellDiffCommonRenderTemplate { readonly container: HTMLElement; readonly body: HTMLElement; readonly diffEditorContainer: HTMLElement; readonly diagonalFill: HTMLElement; readonly elementDisposables: DisposableStore; + readonly cellHeaderContainer: HTMLElement; + readonly editorContainer: HTMLElement; readonly sourceEditor: CodeEditorWidget; readonly metadataHeaderContainer: HTMLElement; readonly metadataInfoContainer: HTMLElement; @@ -79,12 +89,18 @@ export interface CellDiffSingleSideRenderTemplate extends CellDiffCommonRenderTe readonly outputInfoContainer: HTMLElement; } +export interface IDiffCellMarginOverlay extends IDisposable { + onAction: Event; + show(): void; + hide(): void; +} export interface CellDiffSideBySideRenderTemplate extends CellDiffCommonRenderTemplate { readonly container: HTMLElement; readonly body: HTMLElement; readonly diffEditorContainer: HTMLElement; readonly elementDisposables: DisposableStore; + readonly cellHeaderContainer: HTMLElement; readonly sourceEditor: DiffEditorWidget; readonly editorContainer: HTMLElement; readonly inputToolbarContainer: HTMLElement; @@ -93,6 +109,7 @@ export interface CellDiffSideBySideRenderTemplate extends CellDiffCommonRenderTe readonly metadataInfoContainer: HTMLElement; readonly outputHeaderContainer: HTMLElement; readonly outputInfoContainer: HTMLElement; + readonly marginOverlay: IDiffCellMarginOverlay; } export interface IDiffElementLayoutInfo { @@ -101,6 +118,7 @@ export interface IDiffElementLayoutInfo { editorHeight: number; editorMargin: number; metadataHeight: number; + cellStatusHeight: number; metadataStatusHeight: number; rawOutputHeight: number; outputMetadataHeight: number; @@ -124,3 +142,17 @@ export const DIFF_CELL_MARGIN = 16; export const NOTEBOOK_DIFF_CELL_INPUT = new RawContextKey('notebookDiffCellInputChanged', false); export const NOTEBOOK_DIFF_CELL_PROPERTY = new RawContextKey('notebookDiffCellPropertyChanged', false); export const NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED = new RawContextKey('notebookDiffCellPropertyExpanded', false); + +export interface INotebookDiffViewModelUpdateEvent { + readonly start: number; + readonly deleteCount: number; + readonly elements: readonly IDiffElementViewModelBase[]; +} + +export interface INotebookDiffViewModel { + readonly items: readonly IDiffElementViewModelBase[]; + onDidChangeItems: Event; + isEqual(viewModels: DiffElementCellViewModelBase[]): boolean; + setViewModel(cellViewModels: DiffElementCellViewModelBase[]): void; + clear(): void; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts index a3cfecd2a6f..c0f4fcd898a 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts @@ -14,9 +14,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { DiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; -import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; -import { DeletedElement, getOptimizedNestedCodeEditorWidgetOptions, InsertElement, ModifiedElement } from 'vs/workbench/contrib/notebook/browser/diff/diffComponents'; +import { DiffElementPlaceholderViewModel, IDiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { CellDiffPlaceholderRenderTemplate, CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { CellDiffPlaceholderElement, CollapsedCellOverlayWidget, DeletedElement, getOptimizedNestedCodeEditorWidgetOptions, InsertElement, ModifiedElement, UnchangedCellOverlayWidget } from 'vs/workbench/contrib/notebook/browser/diff/diffComponents'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; @@ -30,8 +30,9 @@ import { PixelRatio } from 'vs/base/browser/pixelRatio'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { fixedDiffEditorOptions, fixedEditorOptions } from 'vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { localize } from 'vs/nls'; -export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { +export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { private readonly lineHeight: number; constructor( @@ -42,15 +43,15 @@ export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { + static readonly TEMPLATE_ID = 'cell_diff_placeholder'; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + @IInstantiationService protected readonly instantiationService: IInstantiationService + ) { } + + get templateId() { + return CellDiffPlaceholderRenderer.TEMPLATE_ID; + } + + renderTemplate(container: HTMLElement): CellDiffPlaceholderRenderTemplate { + const body = DOM.$('.cell-placeholder-body'); + DOM.append(container, body); + + const elementDisposables = new DisposableStore(); + const marginOverlay = new CollapsedCellOverlayWidget(body); + const contents = DOM.append(body, DOM.$('.contents')); + const placeholder = DOM.append(contents, DOM.$('span.text', { title: localize('notebook.diff.hiddenCells.expandAll', 'Double click to show') })); + + return { + body, + container, + placeholder, + marginOverlay, + elementDisposables + }; + } + + renderElement(element: DiffElementPlaceholderViewModel, index: number, templateData: CellDiffPlaceholderRenderTemplate, height: number | undefined): void { + templateData.body.classList.remove('left', 'right', 'full'); + templateData.elementDisposables.add(this.instantiationService.createInstance(CellDiffPlaceholderElement, element, templateData)); + } + + disposeTemplate(templateData: CellDiffPlaceholderRenderTemplate): void { + templateData.container.innerText = ''; + } + + disposeElement(element: DiffElementPlaceholderViewModel, index: number, templateData: CellDiffPlaceholderRenderTemplate): void { + templateData.elementDisposables.clear(); + } +} + export class CellDiffSingleSideRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'cell_diff_single'; @@ -82,8 +129,9 @@ export class CellDiffSingleSideRenderer implements IListRenderer extends MouseController { } } -export class NotebookTextDiffList extends WorkbenchList implements IDisposable, IStyleController { +export class NotebookTextDiffList extends WorkbenchList implements IDisposable, IStyleController { private styleElement?: HTMLStyleElement; get rowsContainer(): HTMLElement { @@ -313,21 +364,21 @@ export class NotebookTextDiffList extends WorkbenchList, - renderers: IListRenderer[], + delegate: IListVirtualDelegate, + renderers: IListRenderer[], contextKeyService: IContextKeyService, - options: IWorkbenchListOptions, + options: IWorkbenchListOptions, @IListService listService: IListService, @IConfigurationService configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService) { super(listUser, container, delegate, renderers, options, contextKeyService, listService, configurationService, instantiationService); } - protected override createMouseController(options: IListOptions): MouseController { + protected override createMouseController(options: IListOptions): MouseController { return new NotebookMouseController(this); } - getCellViewScrollTop(element: DiffElementViewModelBase): number { + getCellViewScrollTop(element: IDiffElementViewModelBase): number { const index = this.indexOf(element); // if (index === undefined || index < 0 || index >= this.length) { // this._getViewIndexUpperBound(element); @@ -354,7 +405,7 @@ export class NotebookTextDiffList extends WorkbenchList; private readonly _overviewViewportDomElement: FastDomNode; - private _diffElementViewModels: DiffElementViewModelBase[] = []; + private _diffElementViewModels: readonly IDiffElementViewModelBase[] = []; private _lanes = 2; private _insertColor: Color | null; @@ -94,7 +94,7 @@ export class NotebookDiffOverviewRuler extends Themable { this._layoutNow(); } - updateViewModels(elements: DiffElementViewModelBase[], eventDispatcher: NotebookDiffEditorEventDispatcher | undefined) { + updateViewModels(elements: readonly IDiffElementViewModelBase[], eventDispatcher: NotebookDiffEditorEventDispatcher | undefined) { this._disposables.clear(); this._diffElementViewModels = elements; @@ -126,7 +126,7 @@ export class NotebookDiffOverviewRuler extends Themable { private _layoutNow() { const layoutInfo = this.notebookEditor.getLayoutInfo(); const height = layoutInfo.height; - const contentHeight = this._diffElementViewModels.map(view => view.layoutInfo.totalHeight).reduce((a, b) => a + b, 0); + const contentHeight = this._diffElementViewModels.map(view => view.totalHeight).reduce((a, b) => a + b, 0); const ratio = PixelRatio.getInstance(DOM.getWindow(this._domNode.domNode)).value; this._domNode.setWidth(this.width); this._domNode.setHeight(height); @@ -182,8 +182,7 @@ export class NotebookDiffOverviewRuler extends Themable { for (let i = 0; i < this._diffElementViewModels.length; i++) { const element = this._diffElementViewModels[i]; - const cellHeight = Math.round((element.layoutInfo.totalHeight / scrollHeight) * ratio * height); - + const cellHeight = Math.round((element.totalHeight / scrollHeight) * ratio * height); switch (element.type) { case 'insert': ctx.fillStyle = this._insertColorHex; @@ -203,6 +202,7 @@ export class NotebookDiffOverviewRuler extends Themable { break; } + currentFrom += cellHeight; } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts new file mode 100644 index 00000000000..8e1f6d95c20 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { DiffElementCellViewModelBase, DiffElementPlaceholderViewModel, IDiffElementViewModelBase } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { INotebookDiffViewModel, INotebookDiffViewModelUpdateEvent } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; + +export class NotebookDiffViewModel extends Disposable implements INotebookDiffViewModel { + private readonly placeholderAndRelatedCells = new Map(); + private readonly _items: IDiffElementViewModelBase[] = []; + get items(): readonly IDiffElementViewModelBase[] { + return this._items; + } + private readonly _onDidChangeItems = this._register(new Emitter()); + public readonly onDidChangeItems = this._onDidChangeItems.event; + private readonly disposables = this._register(new DisposableStore()); + + private originalCellViewModels: DiffElementCellViewModelBase[] = []; + override dispose() { + this.clear(); + super.dispose(); + } + clear() { + this.disposables.clear(); + dispose(Array.from(this.placeholderAndRelatedCells.keys())); + this.placeholderAndRelatedCells.clear(); + dispose(this.originalCellViewModels); + this.originalCellViewModels = []; + dispose(this._items); + this._items.splice(0, this._items.length); + } + + setViewModel(cellViewModels: DiffElementCellViewModelBase[]) { + const oldLength = this._items.length; + this.clear(); + this._items.splice(0, oldLength); + + let placeholder: DiffElementPlaceholderViewModel | undefined = undefined; + this.originalCellViewModels = cellViewModels; + cellViewModels.forEach((vm, index) => { + if (vm.type === 'unchanged') { + if (!placeholder) { + vm.displayIconToHideUnmodifiedCells = true; + placeholder = new DiffElementPlaceholderViewModel(vm.mainDocumentTextModel, vm.editorEventDispatcher, vm.initData); + this._items.push(placeholder); + const placeholderItem = placeholder; + + this.disposables.add(placeholderItem.onUnfoldHiddenCells(() => { + const hiddenCellViewModels = this.placeholderAndRelatedCells.get(placeholderItem); + if (!Array.isArray(hiddenCellViewModels)) { + return; + } + const start = this._items.indexOf(placeholderItem); + this._items.splice(start, 1, ...hiddenCellViewModels); + this._onDidChangeItems.fire({ start, deleteCount: 1, elements: hiddenCellViewModels }); + })); + this.disposables.add(vm.onHideUnchangedCells(() => { + const hiddenCellViewModels = this.placeholderAndRelatedCells.get(placeholderItem); + if (!Array.isArray(hiddenCellViewModels)) { + return; + } + const start = this._items.indexOf(vm); + this._items.splice(start, hiddenCellViewModels.length, placeholderItem); + this._onDidChangeItems.fire({ start, deleteCount: hiddenCellViewModels.length, elements: [placeholderItem] }); + })); + } + const hiddenCellViewModels = this.placeholderAndRelatedCells.get(placeholder) || []; + hiddenCellViewModels.push(vm); + this.placeholderAndRelatedCells.set(placeholder, hiddenCellViewModels); + placeholder.hiddenCells.push(vm); + } else { + placeholder = undefined; + this._items.push(vm); + } + }); + + this._onDidChangeItems.fire({ start: 0, deleteCount: oldLength, elements: this._items }); + } + public isEqual(viewModels: DiffElementCellViewModelBase[]) { + if (this.originalCellViewModels.length !== viewModels.length) { + return false; + } + for (let i = 0; i < viewModels.length; i++) { + const a = this.originalCellViewModels[i]; + const b = viewModels[i]; + if (a.original?.textModel.getHashValue() !== b.original?.textModel.getHashValue() + || a.modified?.textModel.getHashValue() !== b.modified?.textModel.getHashValue()) { + return false; + } + } + + return true; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 4e174318c11..e1206b813bd 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -1004,18 +1004,6 @@ configurationRegistry.registerConfiguration({ tags: ['notebookLayout'], default: false }, - [NotebookSetting.codeActionsOnSave]: { - markdownDescription: nls.localize('notebook.codeActionsOnSave', 'Run a series of Code Actions for a notebook on save. Code Actions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `"notebook.source.organizeImports": "explicit"`'), - type: 'object', - additionalProperties: { - type: ['string', 'boolean'], - enum: ['explicit', 'never', true, false], - // enum: ['explicit', 'always', 'never'], -- autosave support needs to be built first - // nls.localize('always', 'Always triggers Code Actions on save, including autosave, focus, and window change events.'), - enumDescriptions: [nls.localize('explicit', 'Triggers Code Actions only when explicitly saved.'), nls.localize('never', 'Never triggers Code Actions on save.'), nls.localize('explicitBoolean', 'Triggers Code Actions only when explicitly saved. This value will be deprecated in favor of "explicit".'), nls.localize('neverBoolean', 'Triggers Code Actions only when explicitly saved. This value will be deprecated in favor of "never".')], - }, - default: {} - }, [NotebookSetting.formatOnCellExecution]: { markdownDescription: nls.localize('notebook.formatOnCellExecution', "Format a notebook cell upon execution. A formatter must be available."), type: 'boolean', diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts index b146d0f71d2..1068c933d39 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; -import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { localize } from 'vs/nls'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IVisibleEditorPane } from 'vs/workbench/common/editor'; @@ -16,47 +16,42 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService export class NotebookAccessibilityHelp implements IAccessibleViewImplentation { readonly priority = 105; readonly name = 'notebook'; - readonly when = NOTEBOOK_IS_ACTIVE_EDITOR; + readonly when = NOTEBOOK_EDITOR_FOCUSED; readonly type: AccessibleViewType = AccessibleViewType.Help; getProvider(accessor: ServicesAccessor) { const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor() || accessor.get(IEditorService).activeEditorPane; - if (activeEditor) { - return runAccessibilityHelpAction(accessor, activeEditor); + if (!activeEditor) { + return; } - return; + return getAccessibilityHelpProvider(accessor, activeEditor); } - dispose() { } } - - export function getAccessibilityHelpText(): string { return [ localize('notebook.overview', 'The notebook view is a collection of code and markdown cells. Code cells can be executed and will produce output directly below the cell.'), - localize('notebook.cell.edit', 'The Edit Cell command will focus on the cell input.'), - localize('notebook.cell.quitEdit', 'The Quit Edit command will set focus on the cell container. The default (Escape) key may need to be pressed twice first exit the virtual cursor if active.'), - localize('notebook.cell.focusInOutput', 'The Focus Output command will set focus in the cell\'s output.'), - localize('notebook.focusNextEditor', 'The Focus Next Cell Editor command will set focus in the next cell\'s editor.'), - localize('notebook.focusPreviousEditor', 'The Focus Previous Cell Editor command will set focus in the previous cell\'s editor.'), + localize('notebook.cell.edit', 'The Edit Cell command{0} will focus on the cell input.', ''), + localize('notebook.cell.quitEdit', 'The Quit Edit command{0} will set focus on the cell container. The default (Escape) key may need to be pressed twice first exit the virtual cursor if active.', ''), + localize('notebook.cell.focusInOutput', 'The Focus Output command{0} will set focus in the cell\'s output.', ''), + localize('notebook.focusNextEditor', 'The Focus Next Cell Editor command{0} will set focus in the next cell\'s editor.', ''), + localize('notebook.focusPreviousEditor', 'The Focus Previous Cell Editor command{0} will set focus in the previous cell\'s editor.', ''), localize('notebook.cellNavigation', 'The up and down arrows will also move focus between cells while focused on the outer cell container.'), - localize('notebook.cell.executeAndFocusContainer', 'The Execute Cell command executes the cell that currently has focus.',), - localize('notebook.cell.insertCodeCellBelowAndFocusContainer', 'The Insert Cell Above/Below commands will create new empty code cells'), + localize('notebook.cell.executeAndFocusContainer', 'The Execute Cell command{0} executes the cell that currently has focus.', ''), + localize('notebook.cell.insertCodeCellBelowAndFocusContainer', 'The Insert Cell Above{0} and Below{1} commands will create new empty code cells.', '', ''), localize('notebook.changeCellType', 'The Change Cell to Code/Markdown commands are used to switch between cell types.') - ].join('\n\n'); + ].join('\n'); } -export function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor | IVisibleEditorPane) { +export function getAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | IVisibleEditorPane) { const helpText = getAccessibilityHelpText(); - return { - id: AccessibleViewProviderId.Notebook, - verbositySettingKey: AccessibilityVerbositySettingId.Notebook, - provideContent: () => helpText, - onClose: () => { - editor.focus(); - }, - options: { type: AccessibleViewType.Help } - }; + return new AccessibleContentProvider( + AccessibleViewProviderId.Notebook, + { type: AccessibleViewType.Help }, + () => helpText, + () => editor.focus(), + AccessibilityVerbositySettingId.Notebook, + ); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts index daa9bae804d..8771a492e2f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -18,13 +18,12 @@ export class NotebookAccessibleView implements IAccessibleViewImplentation { readonly when = ContextKeyExpr.and(NOTEBOOK_OUTPUT_FOCUSED, ContextKeyExpr.equals('resourceExtname', '.ipynb')); getProvider(accessor: ServicesAccessor) { const editorService = accessor.get(IEditorService); - return showAccessibleOutput(editorService); + return getAccessibleOutputProvider(editorService); } - dispose() { } } -export function showAccessibleOutput(editorService: IEditorService) { +export function getAccessibleOutputProvider(editorService: IEditorService) { const activePane = editorService.activeEditorPane; const notebookEditor = getNotebookEditorFromEditorPane(activePane); const notebookViewModel = notebookEditor?.getViewModel(); @@ -73,15 +72,15 @@ export function showAccessibleOutput(editorService: IEditorService) { return; } - return { - id: AccessibleViewProviderId.Notebook, - verbositySettingKey: AccessibilityVerbositySettingId.Notebook, - provideContent(): string { return outputContent; }, - onClose() { + return new AccessibleContentProvider( + AccessibleViewProviderId.Notebook, + { type: AccessibleViewType.View }, + () => { return outputContent; }, + () => { notebookEditor?.setFocus(selections[0]); activePane?.focus(); }, - options: { type: AccessibleViewType.View } - }; + AccessibilityVerbositySettingId.Notebook, + ); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index b0abb66c5fb..27672061b88 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -28,7 +28,7 @@ import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKe import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { cellRangesToIndexes, ICellRange, reduceCellRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IWebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IEditorCommentsOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IObservable } from 'vs/base/common/observable'; @@ -256,6 +256,7 @@ export interface ICellViewModel extends IGenericCellViewModel { readonly mime: string; cellKind: CellKind; lineNumbers: 'on' | 'off' | 'inherit'; + commentOptions: IEditorCommentsOptions; chatHeight: number; commentHeight: number; focusMode: CellFocusMode; @@ -271,6 +272,7 @@ export interface ICellViewModel extends IGenericCellViewModel { hasModel(): this is IEditableCellViewModel; resolveTextModel(): Promise; getSelections(): Selection[]; + setSelections(selections: Selection[]): void; getSelectionsStartPosition(): IPosition[] | undefined; getCellDecorations(): INotebookCellDecorationOptions[]; getCellStatusBarItems(): INotebookCellStatusBarItem[]; @@ -483,6 +485,7 @@ export interface INotebookEditor { readonly onDidFocusWidget: Event; readonly onDidBlurWidget: Event; readonly onDidScroll: Event; + readonly onDidChangeLayout: Event; readonly onDidChangeActiveCell: Event; readonly onDidChangeActiveKernel: Event; readonly onMouseUp: Event; @@ -499,6 +502,7 @@ export interface INotebookEditor { readonly isDisposed: boolean; readonly activeKernel: INotebookKernel | undefined; readonly scrollTop: number; + readonly scrollBottom: number; readonly scopedContextKeyService: IContextKeyService; readonly activeCodeEditor: ICodeEditor | undefined; readonly codeEditors: [ICellViewModel, ICodeEditor][]; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 072a2a70593..3d63f27aae6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -101,6 +101,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; import { PreventDefaultContextMenuItemsContextKeyName } from 'vs/workbench/contrib/webview/browser/webview.contribution'; import { NotebookAccessibilityProvider } from 'vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider'; +import { NotebookHorizontalTracker } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookHorizontalTracker'; const $ = DOM.$; @@ -150,6 +151,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD readonly onDidChangeDecorations: Event = this._onDidChangeDecorations.event; private readonly _onDidScroll = this._register(new Emitter()); readonly onDidScroll: Event = this._onDidScroll.event; + private readonly _onDidChangeLayout = this._register(new Emitter()); + readonly onDidChangeLayout: Event = this._onDidChangeLayout.event; private readonly _onDidChangeActiveCell = this._register(new Emitter()); readonly onDidChangeActiveCell: Event = this._onDidChangeActiveCell.event; private readonly _onDidChangeFocus = this._register(new Emitter()); @@ -323,6 +326,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._notebookOptions, eventDispatcher, language => this.getBaseCellEditorOptions(language)); + this._register(this._viewContext.eventDispatcher.onDidChangeLayout(() => { + this._onDidChangeLayout.fire(); + })); this._register(this._viewContext.eventDispatcher.onDidChangeCellState(e => { this._onDidChangeCellState.fire(e); })); @@ -610,6 +616,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._list.scrollableElement.appendChild(this._notebookOverviewRulerContainer); this._registerNotebookOverviewRuler(); + this._register(this.instantiationService.createInstance(NotebookHorizontalTracker, this, this._list.scrollableElement)); + this._overflowContainer = document.createElement('div'); this._overflowContainer.classList.add('notebook-overflow-widget-container', 'monaco-editor'); DOM.append(parent, this._overflowContainer); @@ -1065,27 +1073,22 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } private _registerNotebookStickyScroll() { - this._notebookStickyScroll = this._register(this.instantiationService.createInstance(NotebookStickyScroll, this._notebookStickyScrollContainer, this, this._list)); + this._notebookStickyScroll = this._register(this.instantiationService.createInstance(NotebookStickyScroll, this._notebookStickyScrollContainer, this, this._list, (sizeDelta) => { + if (this.isDisposed) { + return; + } - const localDisposableStore = this._register(new DisposableStore()); - - this._register(this._notebookStickyScroll.onDidChangeNotebookStickyScroll((sizeDelta) => { - const d = localDisposableStore.add(DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this.getDomNode()), () => { - if (this.isDisposed) { - return; + if (this._dimension && this._isVisible) { + if (sizeDelta > 0) { // delta > 0 ==> sticky is growing, cell list shrinking + this.layout(this._dimension); + this.setScrollTop(this.scrollTop + sizeDelta); + } else if (sizeDelta < 0) { // delta < 0 ==> sticky is shrinking, cell list growing + this.setScrollTop(this.scrollTop + sizeDelta); + this.layout(this._dimension); } + } - if (this._dimension && this._isVisible) { - if (sizeDelta > 0) { // delta > 0 ==> sticky is growing, cell list shrinking - this.layout(this._dimension); - this.setScrollTop(this.scrollTop + sizeDelta); - } else if (sizeDelta < 0) { // delta < 0 ==> sticky is shrinking, cell list growing - this.setScrollTop(this.scrollTop + sizeDelta); - this.layout(this._dimension); - } - } - localDisposableStore.delete(d); - })); + this._onDidScroll.fire(); })); } @@ -2100,6 +2103,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return this._list.scrollTop; } + get scrollBottom() { + return this._list.scrollTop + this._list.getRenderHeight(); + } + getAbsoluteTopOfElement(cell: ICellViewModel) { return this._list.getCellViewScrollTop(cell); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index 63c68389080..20ffe3867e8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -9,7 +9,6 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; export const selectKernelIcon = registerIcon('notebook-kernel-select', Codicon.serverEnvironment, localize('selectKernelIcon', 'Configure icon to select a kernel in notebook editors.')); export const executeIcon = registerIcon('notebook-execute', Codicon.play, localize('executeIcon', 'Icon to execute in notebook editors.')); -export const configIcon = registerIcon('notebook-config', Codicon.gear, localize('configIcon', 'Icon to configure in notebook editors.')); export const executeAboveIcon = registerIcon('notebook-execute-above', Codicon.runAbove, localize('executeAboveIcon', 'Icon to execute above cells in notebook editors.')); export const executeBelowIcon = registerIcon('notebook-execute-below', Codicon.runBelow, localize('executeBelowIcon', 'Icon to execute below cells in notebook editors.')); export const stopIcon = registerIcon('notebook-stop', Codicon.primitiveSquare, localize('stopIcon', 'Icon to stop an execution in notebook editors.')); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts index 9ca31a78982..c2085d617e7 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts @@ -20,7 +20,7 @@ export class NotebookLoggingService extends Disposable implements INotebookLoggi @ILoggerService loggerService: ILoggerService, ) { super(); - this._logger = this._register(loggerService.createLogger(logChannelId, { name: nls.localize('renderChannelName', "Notebook rendering") })); + this._logger = this._register(loggerService.createLogger(logChannelId, { name: nls.localize('renderChannelName', "Notebook") })); } debug(category: string, output: string): void { @@ -30,5 +30,13 @@ export class NotebookLoggingService extends Disposable implements INotebookLoggi info(category: string, output: string): void { this._logger.info(`[${category}] ${output}`); } + + warn(category: string, output: string): void { + this._logger.warn(`[${category}] ${output}`); + } + + error(category: string, output: string): void { + this._logger.error(`[${category}] ${output}`); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts index dcdd1a08860..6363dc3a6cc 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -110,10 +110,6 @@ export class CellComments extends CellContentPart { this.currentElement.commentHeight = 0; return; } - if (this._commentThreadWidget.value.commentThread === info.thread) { - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value.getDimensions().height); - return; - } await this._commentThreadWidget.value.updateCommentThread(info.thread); this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value.getDimensions().height); @@ -136,8 +132,11 @@ export class CellComments extends CellContentPart { private async _getCommentThreadForCell(element: ICellViewModel): Promise<{ thread: languages.CommentThread; owner: string } | null> { if (this.notebookEditor.hasModel()) { const commentInfos = coalesce(await this.commentService.getNotebookComments(element.uri)); - if (commentInfos.length && commentInfos[0].threads.length) { - return { owner: commentInfos[0].uniqueOwner, thread: commentInfos[0].threads[0] }; + for (const commentInfo of commentInfos) { + for (const thread of commentInfo.threads) { + // For now, only one thread per cell is supported. + return { owner: commentInfo.uniqueOwner, thread }; + } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts index d5c27d01001..c69d62f34f8 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -6,9 +6,11 @@ import * as DOM from 'vs/base/browser/dom'; import { disposableTimeout } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { clamp } from 'vs/base/common/numbers'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -39,6 +41,10 @@ export class CellExecutionPart extends CellContentPart { this.updateExecutionOrder(this.currentCell.internalMetadata); } })); + + this._register(this._notebookEditor.onDidScroll(() => { + this._updatePosition(); + })); } override didRenderCell(element: ICellViewModel): void { @@ -74,12 +80,38 @@ export class CellExecutionPart extends CellContentPart { } override updateInternalLayoutNow(element: ICellViewModel): void { - if (element.isInputCollapsed) { - DOM.hide(this._executionOrderLabel); - } else { - DOM.show(this._executionOrderLabel); - const top = element.layoutInfo.editorHeight - 22 + element.layoutInfo.statusBarHeight; - this._executionOrderLabel.style.top = `${top}px`; + this._updatePosition(); + } + + private _updatePosition() { + if (this.currentCell) { + if (this.currentCell.isInputCollapsed) { + DOM.hide(this._executionOrderLabel); + } else { + DOM.show(this._executionOrderLabel); + let top = this.currentCell.layoutInfo.editorHeight - 22 + this.currentCell.layoutInfo.statusBarHeight; + + if (this.currentCell instanceof CodeCellViewModel) { + const elementTop = this._notebookEditor.getAbsoluteTopOfElement(this.currentCell); + const editorBottom = elementTop + this.currentCell.layoutInfo.outputContainerOffset; + // another approach to avoid the flicker caused by sticky scroll is manually calculate the scrollBottom: + // const scrollBottom = this._notebookEditor.scrollTop + this._notebookEditor.getLayoutInfo().height - 26 - this._notebookEditor.getLayoutInfo().stickyHeight; + const scrollBottom = this._notebookEditor.scrollBottom; + + const lineHeight = 22; + if (scrollBottom <= editorBottom) { + const offset = editorBottom - scrollBottom; + top -= offset; + top = clamp( + top, + lineHeight + 12, // line height + padding for single line + this.currentCell.layoutInfo.editorHeight - lineHeight + this.currentCell.layoutInfo.statusBarHeight + ); + } + } + + this._executionOrderLabel.style.top = `${top}px`; + } } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index 5300567754b..7c750ff5a35 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -381,7 +381,7 @@ class CellOutputElement extends Disposable { }); } - const picker = this.quickInputService.createQuickPick(); + const picker = this.quickInputService.createQuickPick({ useSeparators: true }); picker.items = [ ...items, { type: 'separator' }, diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index 4370f036558..e6b7447fa91 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -10,6 +10,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; import { Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { clamp } from 'vs/base/common/numbers'; import * as strings from 'vs/base/common/strings'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; @@ -66,6 +67,7 @@ export class CodeCell extends Disposable { this.initializeEditor(editorHeight); this._renderedInputCollapseState = false; // editor is always expanded initially + this.registerNotebookEditorListeners(); this.registerViewCellLayoutChange(); this.registerCellEditorEventListeners(); this.registerDecorations(); @@ -264,12 +266,52 @@ export class CodeCell extends Disposable { } } + private registerNotebookEditorListeners() { + this._register(this.notebookEditor.onDidScroll(() => { + this.adjustEditorPosition(); + })); + + this._register(this.notebookEditor.onDidChangeLayout(() => { + this.adjustEditorPosition(); + this.onCellWidthChange(); + })); + } + + private adjustEditorPosition() { + const extraOffset = - 6 /** distance to the top of the cell editor, which is 6px under the focus indicator */ - 1 /** border */; + const min = 0; + + const scrollTop = this.notebookEditor.scrollTop; + const elementTop = this.notebookEditor.getAbsoluteTopOfElement(this.viewCell); + const diff = scrollTop - elementTop + extraOffset; + + const notebookEditorLayout = this.notebookEditor.getLayoutInfo(); + + // we should stop adjusting the top when users are viewing the bottom of the cell editor + const editorMaxHeight = notebookEditorLayout.height + - notebookEditorLayout.stickyHeight + - 26 /** notebook toolbar */; + + const maxTop = + this.viewCell.layoutInfo.editorHeight + // + this.viewCell.layoutInfo.statusBarHeight + - editorMaxHeight + ; + const top = maxTop > 20 ? + clamp(min, diff, maxTop) : + min; + this.templateData.editorPart.style.top = `${top}px`; + // scroll the editor with top + this.templateData.editor?.setScrollTop(top); + } + private registerViewCellLayoutChange() { this._register(this.viewCell.onDidChangeLayout((e) => { if (e.outerWidth !== undefined) { const layoutInfo = this.templateData.editor.getLayoutInfo(); if (layoutInfo.width !== this.viewCell.layoutInfo.editorWidth) { this.onCellWidthChange(); + this.adjustEditorPosition(); } } })); @@ -280,6 +322,7 @@ export class CodeCell extends Disposable { if (e.contentHeightChanged) { if (this.viewCell.layoutInfo.editorHeight !== e.contentHeight) { this.onCellEditorHeightChange(e.contentHeight); + this.adjustEditorPosition(); } } })); @@ -532,7 +575,17 @@ export class CodeCell extends Disposable { } private layoutEditor(dimension: IDimension): void { - this.templateData.editor?.layout(dimension, true); + const editorLayout = this.notebookEditor.getLayoutInfo(); + const maxHeight = Math.min( + editorLayout.height + - editorLayout.stickyHeight + - 26 /** notebook toolbar */, + dimension.height + ); + this.templateData.editor?.layout({ + width: dimension.width, + height: maxHeight + }, true); } private onCellWidthChange(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index a7cd34ca453..cd401a3b4c8 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -1152,9 +1152,6 @@ export class NotebookCellList extends WorkbenchList implements ID // Scrolled into view from above this.view.setScrollTop(positionTop - 30); } - - - element.revealRangeInCenter(range); } //#endregion diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 564ddd06414..52fdd57b680 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -288,6 +288,12 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende width: 0, height: 0 }, + scrollbar: { + vertical: 'hidden', + horizontal: 'auto', + handleMouseWheel: false, + useShadows: false, + }, }, { contributions: this.notebookEditor.creationOptions.cellEditorContributions }); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 9ed29128b2c..f75bce458f8 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -8,6 +8,7 @@ import { Disposable, IDisposable, IReference, MutableDisposable, dispose } from import { Mimes } from 'vs/base/common/mime'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IEditorCommentsOptions } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -85,6 +86,15 @@ export abstract class BaseCellViewModel extends Disposable { this._onDidChangeState.fire({ cellLineNumberChanged: true }); } + private _commentOptions: IEditorCommentsOptions; + public get commentOptions(): IEditorCommentsOptions { + return this._commentOptions; + } + + public set commentOptions(newOptions: IEditorCommentsOptions) { + this._commentOptions = newOptions; + } + private _focusMode: CellFocusMode = CellFocusMode.Container; get focusMode() { return this._focusMode; @@ -208,6 +218,13 @@ export abstract class BaseCellViewModel extends Disposable { if (this.model.collapseState?.outputCollapsed) { this._outputCollapsed = true; } + + this._commentOptions = this._configurationService.getValue('editor.comments', { overrideIdentifier: this.language }); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor.comments')) { + this._commentOptions = this._configurationService.getValue('editor.comments', { overrideIdentifier: this.language }); + } + })); } @@ -510,12 +527,24 @@ export abstract class BaseCellViewModel extends Disposable { setSelections(selections: Selection[]) { if (selections.length) { - this._textEditor?.setSelections(selections); + if (this._textEditor) { + this._textEditor?.setSelections(selections); + } else if (this._editorViewStates) { + this._editorViewStates.cursorState = selections.map(selection => { + return { + inSelectionMode: !selection.isEmpty(), + selectionStart: selection.getStartPosition(), + position: selection.getEndPosition(), + }; + }); + } } } getSelections() { - return this._textEditor?.getSelections() || []; + return this._textEditor?.getSelections() + ?? this._editorViewStates?.cursorState.map(state => new Selection(state.selectionStart.lineNumber, state.selectionStart.column, state.position.lineNumber, state.position.column)) + ?? []; } getSelectionsStartPosition(): IPosition[] | undefined { diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index 50a6758941b..e82cdf45abf 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -106,6 +106,8 @@ export class NotebookStickyScroll extends Disposable { readonly onDidChangeNotebookStickyScroll: Event = this._onDidChangeNotebookStickyScroll.event; private notebookCellOutlineReference?: IReference; + private readonly _layoutDisposableStore = this._register(new DisposableStore()); + getDomNode(): HTMLElement { return this.domNode; } @@ -143,6 +145,7 @@ export class NotebookStickyScroll extends Disposable { private readonly domNode: HTMLElement, private readonly notebookEditor: INotebookEditor, private readonly notebookCellList: INotebookCellList, + private readonly layoutFn: (delta: number) => void, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { @@ -269,8 +272,16 @@ export class NotebookStickyScroll extends Disposable { const sizeDelta = this.getCurrentStickyHeight() - oldStickyHeight; if (sizeDelta !== 0) { this._onDidChangeNotebookStickyScroll.fire(sizeDelta); + + const d = this._layoutDisposableStore.add(DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this.getDomNode()), () => { + this.layoutFn(sizeDelta); + this.updateDisplay(); + + this._layoutDisposableStore.delete(d); + })); + } else { + this.updateDisplay(); } - this.updateDisplay(); } private updateDisplay() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookHorizontalTracker.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookHorizontalTracker.ts new file mode 100644 index 00000000000..22b678d7813 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookHorizontalTracker.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { addDisposableListener, EventType, getWindow } from 'vs/base/browser/dom'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { isChrome } from 'vs/base/common/platform'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +export class NotebookHorizontalTracker extends Disposable { + constructor( + private readonly _notebookEditor: INotebookEditorDelegate, + private readonly _listViewScrollablement: HTMLElement, + ) { + super(); + + this._register(addDisposableListener(this._listViewScrollablement, EventType.MOUSE_WHEEL, (event: IMouseWheelEvent) => { + if (event.deltaX === 0) { + return; + } + + const hoveringOnEditor = this._notebookEditor.codeEditors.find(editor => { + const editorLayout = editor[1].getLayoutInfo(); + if (editorLayout.contentWidth === editorLayout.width) { + // no overflow + return false; + } + + const editorDOM = editor[1].getDomNode(); + if (editorDOM && editorDOM.contains(event.target as HTMLElement)) { + return true; + } + + return false; + }); + + if (!hoveringOnEditor) { + return; + } + + const targetWindow = getWindow(event); + const evt = { + deltaMode: event.deltaMode, + deltaX: event.deltaX, + deltaY: 0, + deltaZ: 0, + wheelDelta: event.wheelDelta && isChrome ? (event.wheelDelta / targetWindow.devicePixelRatio) : event.wheelDelta, + wheelDeltaX: event.wheelDeltaX && isChrome ? (event.wheelDeltaX / targetWindow.devicePixelRatio) : event.wheelDeltaX, + wheelDeltaY: 0, + detail: event.detail, + shiftKey: event.shiftKey, + type: event.type, + defaultPrevented: false, + preventDefault: () => { }, + stopPropagation: () => { } + }; + + (hoveringOnEditor[1] as CodeEditorWidget).delegateScrollFromMouseWheelEvent(evt as any); + })); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 8874848aabb..8038b0b2226 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -136,7 +136,7 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { return true; } - const quickPick = this._quickInputService.createQuickPick(); + const quickPick = this._quickInputService.createQuickPick({ useSeparators: true }); const quickPickItems = this._getKernelPickerQuickPickItems(notebook, matchResult, this._notebookKernelService, scopedContextKeyService); if (quickPickItems.length === 1 && supportAutoRun(quickPickItems[0]) && !skipAutoRun) { @@ -339,7 +339,7 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { private async _showInstallKernelExtensionRecommendation( notebookTextModel: NotebookTextModel, - quickPick: IQuickPick, + quickPick: IQuickPick, extensionWorkbenchService: IExtensionsWorkbenchService, token: CancellationToken ) { @@ -519,7 +519,7 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { private async displaySelectAnotherQuickPick(editor: IActiveNotebookEditor, kernelListEmpty: boolean): Promise { const notebook: NotebookTextModel = editor.textModel; const disposables = new DisposableStore(); - const quickPick = this._quickInputService.createQuickPick(); + const quickPick = this._quickInputService.createQuickPick({ useSeparators: true }); const quickPickItem = await new Promise(resolve => { // select from kernel sources quickPick.title = kernelListEmpty ? localize('select', "Select Kernel") : localize('selectAnotherKernel', "Select Another Kernel"); @@ -699,7 +699,7 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { private async _selectOneKernel(notebook: NotebookTextModel, source: string, kernels: INotebookKernel[]) { const quickPickItems: QuickPickInput[] = kernels.map(kernel => toKernelQuickPick(kernel, undefined)); - const quickPick = this._quickInputService.createQuickPick(); + const quickPick = this._quickInputService.createQuickPick({ useSeparators: true }); quickPick.items = quickPickItems; quickPick.canSelectMany = false; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 5edcb35f62c..7a851b42f9c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -90,8 +90,15 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return; } + const reason = e.auto + ? localize('vetoAutoExtHostRestart', "One of the opened editors is a notebook editor.") + : localize('vetoExtHostRestart', "Notebook '{0}' could not be saved.", this.resource.path); + e.veto((async () => { const editors = editorService.findEditors(this); + if (e.auto) { + return true; + } if (editors.length > 0) { const result = await editorService.save(editors[0]); if (result.success) { @@ -99,7 +106,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } } return true; // Veto - })(), localize('vetoExtHostRestart', "Notebook '{0}' could not be saved.", this.resource.path)); + })(), reason); })); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index a6a06d63186..90904bb4571 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -15,12 +15,12 @@ import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWriteFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; -import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { ICellDto2, INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookData, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService'; import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IFileWorkingCopyModelConfiguration, SnapshotContext } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; @@ -199,7 +199,7 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF private readonly _notebookService: INotebookService, private readonly _configurationService: IConfigurationService, private readonly _telemetryService: ITelemetryService, - private readonly _logService: ILogService + private readonly _notebookLogService: INotebookLoggingService, ) { super(); @@ -247,7 +247,7 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF let serializer = this._notebookService.tryGetDataProviderSync(this.notebookModel.viewType)?.serializer; if (!serializer) { - this._logService.warn('No serializer found for notebook model, checking if provider still needs to be resolved'); + this._notebookLogService.info('WorkingCopyModel', 'No serializer found for notebook model, checking if provider still needs to be resolved'); serializer = await this.getNotebookSerializer(); } @@ -342,6 +342,8 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF if (token.isCancellationRequested) { throw new CancellationError(); } + + this._notebookLogService.info('WorkingCopyModel', 'Notebook content updated from file system - ' + this._notebookModel.uri.toString()); this._notebookModel.reset(data.cells, data.metadata, serializer.options); } @@ -370,7 +372,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo @INotebookService private readonly _notebookService: INotebookService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @ILogService private readonly _logService: ILogService + @INotebookLoggingService private readonly _notebookLogService: INotebookLoggingService ) { } async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise { @@ -388,7 +390,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo } const notebookModel = this._notebookService.createNotebookTextModel(info.viewType, resource, data, info.serializer.options); - return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService, this._logService); + return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService, this._notebookLogService); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index d43273d4fa6..1ca105ea97b 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -9,7 +9,6 @@ import { CellUri, IResolvedNotebookEditorModel, NotebookEditorModelCreationOptio import { NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { ILogService } from 'vs/platform/log/common/log'; import { AsyncEmitter, Emitter, Event } from 'vs/base/common/event'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -23,6 +22,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileReadLimits } from 'vs/platform/files/common/files'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService'; class NotebookModelReferenceCollection extends ReferenceCollection> { @@ -42,9 +42,9 @@ class NotebookModelReferenceCollection extends ReferenceCollection>this._instantiationService.createInstance( FileWorkingCopyManager, workingCopyTypeId, @@ -138,7 +138,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection( export interface INotebookLoggingService { readonly _serviceBrand: undefined; info(category: string, output: string): void; + warn(category: string, output: string): void; + error(category: string, output: string): void; debug(category: string, output: string): void; } diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts index 65ea08b7f27..55dec43a8ee 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts @@ -48,7 +48,7 @@ suite('CellOperations', () => { ], async (editor, viewModel) => { viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 1, end: 2 }, selections: [{ start: 0, end: 2 }] }); - await moveCellRange({ notebookEditor: editor, cell: viewModel.cellAt(1)! }, 'down'); + await moveCellRange({ notebookEditor: editor }, 'down'); assert.strictEqual(viewModel.cellAt(0)?.getText(), '# header b'); assert.strictEqual(viewModel.cellAt(1)?.getText(), '# header a'); assert.strictEqual(viewModel.cellAt(2)?.getText(), 'var b = 1;'); @@ -74,7 +74,7 @@ suite('CellOperations', () => { editor.setHiddenAreas(viewModel.getHiddenRanges()); viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }); - await moveCellRange({ notebookEditor: editor, cell: viewModel.cellAt(1)! }, 'down'); + await moveCellRange({ notebookEditor: editor }, 'down'); assert.strictEqual(viewModel.cellAt(0)?.getText(), '# header b'); assert.strictEqual(viewModel.cellAt(1)?.getText(), '# header a'); assert.strictEqual(viewModel.cellAt(2)?.getText(), 'var b = 1;'); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookDiffViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookDiffViewModel.test.ts new file mode 100644 index 00000000000..378bd40cb79 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookDiffViewModel.test.ts @@ -0,0 +1,404 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import * as sinon from 'sinon'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DiffElementPlaceholderViewModel, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { mock } from 'vs/base/test/common/mock'; +import { Event } from 'vs/base/common/event'; +import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; +import { NotebookDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel'; +import { INotebookDiffViewModelUpdateEvent } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; + +suite('Notebook Diff ViewModel', () => { + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + const initData = { fontInfo: undefined, metadataStatusHeight: 0, outputStatusHeight: 0 }; + teardown(() => disposables.dispose()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + disposables = new DisposableStore(); + disposables.add({ dispose: () => sinon.restore() }); + instantiationService = setupInstantiationService(disposables); + instantiationService.stub(INotebookService, new class extends mock() { + override onDidAddNotebookDocument = Event.None; + override onWillRemoveNotebookDocument = Event.None; + override getNotebookTextModels() { return []; } + override getOutputMimeTypeInfo() { return []; } + }); + + }); + + + test('Cells are returned as they are', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ], + (editor, viewModel, ds) => { + const cell1 = ds.add(new SingleSideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + undefined, + 'delete', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const cell2 = ds.add(new SingleSideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + undefined, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'insert', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const diffViewModel = ds.add(new NotebookDiffViewModel()); + diffViewModel.setViewModel([cell1, cell2]); + + assert.deepStrictEqual(diffViewModel.items, [cell1, cell2]); + } + ); + }); + + test('Trigger change event when viewmodel is updated', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ], + (editor, viewModel, ds) => { + const cell1 = ds.add(new SingleSideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + undefined, + 'delete', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const cell2 = ds.add(new SingleSideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + undefined, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'insert', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const diffViewModel = ds.add(new NotebookDiffViewModel()); + diffViewModel.setViewModel([cell1]); + + assert.deepStrictEqual(diffViewModel.items, [cell1]); + + let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; + Event.once(diffViewModel.onDidChangeItems)(e => eventArgs = e); + + diffViewModel.setViewModel([cell2]); + + assert.deepStrictEqual(diffViewModel.items, [cell2]); + assert.deepStrictEqual(eventArgs, { start: 0, deleteCount: 1, elements: [cell2] }); + + eventArgs = undefined; + Event.once(diffViewModel.onDidChangeItems)(e => eventArgs = e); + + diffViewModel.setViewModel([]); + + assert.deepStrictEqual(diffViewModel.items, []); + assert.deepStrictEqual(eventArgs, { start: 0, deleteCount: 1, elements: [] }); + } + ); + }); + + test('Unmodified cells should be replace with placeholders', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ], + (editor, viewModel, ds) => { + const deleted = ds.add(new SingleSideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + undefined, + 'delete', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const inserted = ds.add(new SingleSideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + undefined, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'insert', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const unmodified1 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const unmodified2 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const unmodified3 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const unmodified4 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const diffViewModel = ds.add(new NotebookDiffViewModel()); + diffViewModel.setViewModel([deleted, unmodified1, inserted, unmodified2, unmodified3, unmodified4]); + + // Default state + assert.strictEqual(diffViewModel.items.length, 4); + assert.deepStrictEqual(diffViewModel.items[0], deleted); + assert.deepStrictEqual(diffViewModel.items[2], inserted); + assert.ok(diffViewModel.items[1] instanceof DiffElementPlaceholderViewModel); + assert.ok(diffViewModel.items[3] instanceof DiffElementPlaceholderViewModel); + + // Expand first collapsed section. + let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; + Event.once(diffViewModel.onDidChangeItems)(e => eventArgs = e); + diffViewModel.items[1].showHiddenCells(); + + assert.deepStrictEqual(diffViewModel.items[0], deleted); + assert.deepStrictEqual(diffViewModel.items[1], unmodified1); + assert.deepStrictEqual(diffViewModel.items[2], inserted); + assert.ok(diffViewModel.items[3] instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(eventArgs, { start: 1, deleteCount: 1, elements: [unmodified1] }); + + // Expand last collapsed section. + Event.once(diffViewModel.onDidChangeItems)(e => eventArgs = e); + diffViewModel.items[3].showHiddenCells(); + + assert.deepStrictEqual(diffViewModel.items, [deleted, unmodified1, inserted, unmodified2, unmodified3, unmodified4]); + + // Collapse the second cell + (diffViewModel.items[1] as SideBySideDiffElementViewModel).hideUnchangedCells(); + + // Verify we collpased second cell + assert.deepStrictEqual(diffViewModel.items[0], deleted); + assert.deepStrictEqual(diffViewModel.items[2], inserted); + assert.ok((diffViewModel.items[1] as any) instanceof DiffElementPlaceholderViewModel); + + // Collapse the 4th cell + (diffViewModel.items[3] as SideBySideDiffElementViewModel).hideUnchangedCells(); + + // Verify we collpased second cell + assert.strictEqual(diffViewModel.items.length, 4); + assert.deepStrictEqual(diffViewModel.items[0], deleted); + assert.deepStrictEqual(diffViewModel.items[2], inserted); + assert.ok((diffViewModel.items[1] as any) instanceof DiffElementPlaceholderViewModel); + assert.ok((diffViewModel.items[3] as any) instanceof DiffElementPlaceholderViewModel); + } + ); + }); + + + test('Ensure placeholder positions are as expected', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['# header 2', 'markdown', CellKind.Markup, [], {}], + ], + (editor, viewModel, ds) => { + const unmodified1 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const unmodified2 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const modified1 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(1)!.model)), + 'modified', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const modified2 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(1)!.model)), + 'modified', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const unmodified3 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const unmodified4 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const unmodified5 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const unmodified6 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const modified3 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(1)!.model)), + 'modified', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const unmodified7 = ds.add(new SideBySideDiffElementViewModel( + viewModel.notebookDocument, + viewModel.notebookDocument, + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + ds.add(createDiffNestedViewModel(viewModel.cellAt(0)!.model)), + 'unchanged', + ds.add(new NotebookDiffEditorEventDispatcher()), + initData)); + + const diffViewModel = ds.add(new NotebookDiffViewModel()); + diffViewModel.setViewModel([unmodified1, unmodified2, modified1, modified2, unmodified3, unmodified4, unmodified5, unmodified6, modified3, unmodified7]); + + // Default state + assert.strictEqual(diffViewModel.items.length, 6); + assert.ok(diffViewModel.items[0] instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(diffViewModel.items[1], modified1); + assert.deepStrictEqual(diffViewModel.items[2], modified2); + assert.ok(diffViewModel.items[3] instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(diffViewModel.items[4], modified3); + assert.ok(diffViewModel.items[5] instanceof DiffElementPlaceholderViewModel); + + // Expand first collapsed section. + let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; + ds.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); + diffViewModel.items[0].showHiddenCells(); + + assert.strictEqual(diffViewModel.items.length, 7); + assert.deepStrictEqual(diffViewModel.items[0], unmodified1); + assert.deepStrictEqual(diffViewModel.items[1], unmodified2); + assert.deepStrictEqual(diffViewModel.items[2], modified1); + assert.deepStrictEqual(diffViewModel.items[3], modified2); + assert.ok(diffViewModel.items[4] instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(diffViewModel.items[5], modified3); + assert.ok(diffViewModel.items[6] instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(eventArgs, { start: 0, deleteCount: 1, elements: [unmodified1, unmodified2] }); + + // Collapse the 1st two cells + (diffViewModel.items[0] as SideBySideDiffElementViewModel).hideUnchangedCells(); + + assert.strictEqual(diffViewModel.items.length, 6); + assert.ok((diffViewModel.items[0] as any) instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(diffViewModel.items[1], modified1); + assert.deepStrictEqual(diffViewModel.items[2], modified2); + assert.ok((diffViewModel.items[3] as any) instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(diffViewModel.items[4], modified3); + assert.ok((diffViewModel.items[5] as any) instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(eventArgs, { start: 0, deleteCount: 2, elements: [diffViewModel.items[0]] }); + + + // Expand second collapsed section. + (diffViewModel.items[3] as any).showHiddenCells(); + + assert.strictEqual(diffViewModel.items.length, 9); + assert.ok((diffViewModel.items[0] as any) instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(diffViewModel.items[1], modified1); + assert.deepStrictEqual(diffViewModel.items[2], modified2); + assert.deepStrictEqual(diffViewModel.items[3], unmodified3); + assert.deepStrictEqual(diffViewModel.items[4], unmodified4); + assert.deepStrictEqual(diffViewModel.items[5], unmodified5); + assert.deepStrictEqual(diffViewModel.items[6], unmodified6); + assert.deepStrictEqual(diffViewModel.items[7], modified3); + assert.ok((diffViewModel.items[8] as any) instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(eventArgs, { start: 3, deleteCount: 1, elements: [unmodified3, unmodified4, unmodified5, unmodified6] }); + + // Collapse the 2nd section + (diffViewModel.items[3] as SideBySideDiffElementViewModel).hideUnchangedCells(); + + assert.strictEqual(diffViewModel.items.length, 6); + assert.ok((diffViewModel.items[0] as any) instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(diffViewModel.items[1], modified1); + assert.deepStrictEqual(diffViewModel.items[2], modified2); + assert.ok((diffViewModel.items[3] as any) instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(diffViewModel.items[4], modified3); + assert.ok((diffViewModel.items[5] as any) instanceof DiffElementPlaceholderViewModel); + assert.deepStrictEqual(eventArgs, { start: 3, deleteCount: 4, elements: [diffViewModel.items[3]] }); + } + ); + }); + + function createDiffNestedViewModel(cellTextModel: NotebookCellTextModel) { + return new DiffNestedCellViewModel(cellTextModel, instantiationService.get(INotebookService)); + } +}); diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 6f3c5086186..0a710773a28 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -62,6 +62,7 @@ import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateF import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const $ = DOM.$; @@ -122,7 +123,8 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @IStorageService storageService: IStorageService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(KeybindingsEditor.ID, group, telemetryService, themeService, storageService); this.delayedFiltering = new Delayer(300); @@ -590,7 +592,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP if (this.keybindingsEditorModel) { const filter = this.searchWidget.getValue(); const keybindingsEntries: IKeybindingItemEntry[] = this.keybindingsEditorModel.fetch(filter, this.sortByPrecedenceAction.checked); - + this.accessibilityService.alert(localize('foundResults', "{0} results", keybindingsEntries.length)); this.ariaLabelElement.setAttribute('aria-label', this.getAriaLabel(keybindingsEntries)); if (keybindingsEntries.length === 0) { diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index b42d6a194a7..26034cdf7de 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -162,10 +162,16 @@ } .settings-editor > .settings-body .settings-tree-container .setting-item-extension-toggle .setting-item-extension-toggle-button { - display: block; + display: inline-block; width: fit-content; } +.settings-editor > .settings-body .settings-tree-container .setting-item-extension-toggle .setting-item-extension-dismiss-button { + display: inline-block; + width: fit-content; + margin-left: 8px; +} + .settings-editor.no-results > .settings-body .settings-toc-container, .settings-editor.no-results > .settings-body .settings-tree-container { display: none; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 4e68e88dc97..cac09b8c128 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -261,6 +261,7 @@ export class SettingsTargetsWidget extends Widget { orientation: ActionsOrientation.HORIZONTAL, focusOnlyEnabledItems: true, ariaLabel: localize('settingsSwitcherBarAriaLabel', "Settings Switcher"), + ariaRole: 'tablist', actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => action.id === 'folderSettings' ? this.folderSettings : undefined })); @@ -436,8 +437,7 @@ export class SearchWidget extends Widget { protected createInputBox(parent: HTMLElement): HistoryInputBox { const showHistoryHint = () => showHistoryKeybindingHint(this.keybindingService); - const box = this._register(new ContextScopedHistoryInputBox(parent, this.contextViewService, { ...this.options, showHistoryHint }, this.contextKeyService)); - return box; + return new ContextScopedHistoryInputBox(parent, this.contextViewService, { ...this.options, showHistoryHint }, this.contextKeyService); } showMessage(message: string): void { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 779b9928b57..6150fb35e10 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -67,6 +67,7 @@ import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetN import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { CodeWindow } from 'vs/base/browser/window'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export const enum SettingsFocusContext { @@ -218,6 +219,10 @@ export class SettingsEditor2 extends EditorPane { private dimension!: DOM.Dimension; private installedExtensionIds: string[] = []; + private dismissedExtensionSettings: string[] = []; + + private readonly DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY = 'settingsEditor2.dismissedExtensionSettings'; + private readonly DISMISSED_EXTENSION_SETTINGS_DELIMITER = '\t'; private readonly inputChangeListener: MutableDisposable; @@ -243,6 +248,7 @@ export class SettingsEditor2 extends EditorPane { @IProductService private readonly productService: IProductService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, + @IUserDataProfileService userDataProfileService: IUserDataProfileService, ) { super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer(1000); @@ -265,12 +271,20 @@ export class SettingsEditor2 extends EditorPane { this.editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, SETTINGS_EDITOR_STATE_KEY); + this.dismissedExtensionSettings = this.storageService + .get(this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, StorageScope.PROFILE, '') + .split(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER); + this._register(configurationService.onDidChangeConfiguration(e => { if (e.source !== ConfigurationTarget.DEFAULT) { this.onConfigUpdate(e.affectedKeys); } })); + this._register(userDataProfileService.onDidChangeCurrentProfile(e => { + e.join(this.whenCurrentProfileChanged()); + })); + this._register(workspaceTrustManagementService.onDidChangeTrust(() => { this.searchResultModel?.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted()); @@ -286,6 +300,13 @@ export class SettingsEditor2 extends EditorPane { } })); + this._register(extensionManagementService.onDidInstallExtensions(() => { + this.refreshInstalledExtensionsList(); + })); + this._register(extensionManagementService.onDidUninstallExtension(() => { + this.refreshInstalledExtensionsList(); + })); + this.modelDisposables = this._register(new DisposableStore()); if (ENABLE_LANGUAGE_FILTER && !SettingsEditor2.SUGGESTIONS.includes(`@${LANGUAGE_SETTING_TAG}`)) { @@ -294,6 +315,15 @@ export class SettingsEditor2 extends EditorPane { this.inputChangeListener = this._register(new MutableDisposable()); } + private async whenCurrentProfileChanged(): Promise { + this.updatedConfigSchemaDelayer.trigger(() => { + this.dismissedExtensionSettings = this.storageService + .get(this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, StorageScope.PROFILE, '') + .split(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER); + this.onConfigUpdate(undefined, true); + }); + } + override get minimumWidth(): number { return SettingsEditor2.EDITOR_MIN_WIDTH; } override get maximumWidth(): number { return Number.POSITIVE_INFINITY; } override get minimumHeight() { return 180; } @@ -399,7 +429,7 @@ export class SettingsEditor2 extends EditorPane { private async refreshInstalledExtensionsList(): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); this.installedExtensionIds = installedExtensions - .filter(ext => ext.manifest && ext.manifest.contributes && ext.manifest.contributes.configuration) + .filter(ext => ext.manifest.contributes?.configuration) .map(ext => ext.identifier.id); } @@ -679,6 +709,19 @@ export class SettingsEditor2 extends EditorPane { this.onConfigUpdate(undefined, true); } + private onDidDismissExtensionSetting(extensionId: string): void { + if (!this.dismissedExtensionSettings.includes(extensionId)) { + this.dismissedExtensionSettings.push(extensionId); + } + this.storageService.store( + this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, + this.dismissedExtensionSettings.join(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER), + StorageScope.PROFILE, + StorageTarget.USER + ); + this.onConfigUpdate(undefined, true); + } + private onDidClickSetting(evt: ISettingLinkClickEvent, recursed?: boolean): void { const targetElement = this.currentSettingsModel.getElementsByName(evt.targetKey)?.[0]; let revealFailed = false; @@ -916,6 +959,7 @@ export class SettingsEditor2 extends EditorPane { private createSettingsTree(container: HTMLElement): void { this.settingRenderers = this._register(this.instantiationService.createInstance(SettingTreeRenderers)); this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset, e.scope))); + this._register(this.settingRenderers.onDidDismissExtensionSetting((e) => this.onDidDismissExtensionSetting(e))); this._register(this.settingRenderers.onDidOpenSettings(settingKey => { this.openSettingsFile({ revealSetting: { key: settingKey, edit: true } }); })); @@ -1210,43 +1254,6 @@ export class SettingsEditor2 extends EditorPane { }); } - private addOrRemoveManageExtensionSetting(setting: ISetting, extension: IGalleryExtension, groups: ISettingsGroup[]): ISettingsGroup | undefined { - const matchingGroups = groups.filter(g => { - const lowerCaseId = g.extensionInfo?.id.toLowerCase(); - return (lowerCaseId === setting.stableExtensionId!.toLowerCase() || - lowerCaseId === setting.prereleaseExtensionId!.toLowerCase()); - }); - - const extensionId = setting.displayExtensionId!; - const extensionInstalled = this.installedExtensionIds.includes(extensionId); - if (!matchingGroups.length && !extensionInstalled) { - // Only show the recommendation when the extension hasn't been installed. - const newGroup: ISettingsGroup = { - sections: [{ - settings: [setting], - }], - id: extensionId, - title: setting.extensionGroupTitle!, - titleRange: nullRange, - range: nullRange, - extensionInfo: { - id: extensionId, - displayName: extension?.displayName, - } - }; - groups.push(newGroup); - return newGroup; - } else if (matchingGroups.length >= 2 || extensionInstalled) { - // Remove the group with the manage extension setting. - const matchingGroupIndex = matchingGroups.findIndex(group => - group.sections.length === 1 && group.sections[0].settings.length === 1 && group.sections[0].settings[0].displayExtensionId); - if (matchingGroupIndex !== -1) { - groups.splice(matchingGroupIndex, 1); - } - } - return undefined; - } - private createSettingsOrderByTocIndex(resolvedSettingsRoot: ITOCEntry): Map { const index = new Map(); function indexSettings(resolvedSettingsRoot: ITOCEntry, counter = 0): number { @@ -1301,10 +1308,37 @@ export class SettingsEditor2 extends EditorPane { } const additionalGroups: ISettingsGroup[] = []; + let setAdditionalGroups = false; const toggleData = await getExperimentalExtensionToggleData(this.extensionGalleryService, this.productService); if (toggleData && groups.filter(g => g.extensionInfo).length) { for (const key in toggleData.settingsEditorRecommendedExtensions) { - const extension = toggleData.recommendedExtensionsGalleryInfo[key]; + const extension: IGalleryExtension = toggleData.recommendedExtensionsGalleryInfo[key]; + if (!extension) { + continue; + } + + const extensionId = extension.identifier.id; + const extensionInstalled = this.installedExtensionIds.includes(extensionId); + + // Drill down to see whether the group and setting already exist + // and need to be removed. + const matchingGroupIndex = groups.findIndex(g => + g.extensionInfo && g.extensionInfo!.id.toLowerCase() === extensionId.toLowerCase() && + g.sections.length === 1 && g.sections[0].settings.length === 1 && g.sections[0].settings[0].displayExtensionId + ); + if (extensionInstalled || this.dismissedExtensionSettings.includes(extensionId)) { + if (matchingGroupIndex !== -1) { + groups.splice(matchingGroupIndex, 1); + setAdditionalGroups = true; + } + continue; + } + + if (matchingGroupIndex !== -1) { + continue; + } + + // Create the entry. extensionInstalled is false in this case. let manifest: IExtensionManifest | null = null; try { manifest = await this.extensionGalleryService.getManifest(extension, CancellationToken.None); @@ -1322,7 +1356,8 @@ export class SettingsEditor2 extends EditorPane { groupTitle = contributesConfiguration[0].title; } - const extensionName = extension?.displayName ?? extension?.name ?? extension.identifier.id; + const recommendationInfo = toggleData.settingsEditorRecommendedExtensions[key]; + const extensionName = extension.displayName ?? extension.name ?? extensionId; const settingKey = `${key}.manageExtension`; const setting: ISetting = { range: nullRange, @@ -1330,21 +1365,32 @@ export class SettingsEditor2 extends EditorPane { keyRange: nullRange, value: null, valueRange: nullRange, - description: [extension?.description || ''], + description: [recommendationInfo.onSettingsEditorOpen?.descriptionOverride ?? extension.description], descriptionIsMarkdown: false, descriptionRanges: [], - title: extensionName, scope: ConfigurationScope.WINDOW, type: 'null', - displayExtensionId: extension.identifier.id, - prereleaseExtensionId: key, - stableExtensionId: key, - extensionGroupTitle: groupTitle ?? extensionName + displayExtensionId: extensionId, + extensionGroupTitle: groupTitle ?? extensionName, + categoryLabel: 'Extensions', + title: extensionName }; - const additionalGroup = this.addOrRemoveManageExtensionSetting(setting, extension, groups); - if (additionalGroup) { - additionalGroups.push(additionalGroup); - } + const additionalGroup: ISettingsGroup = { + sections: [{ + settings: [setting], + }], + id: extensionId, + title: setting.extensionGroupTitle!, + titleRange: nullRange, + range: nullRange, + extensionInfo: { + id: extensionId, + displayName: extension.displayName, + } + }; + groups.push(additionalGroup); + additionalGroups.push(additionalGroup); + setAdditionalGroups = true; } } @@ -1354,7 +1400,7 @@ export class SettingsEditor2 extends EditorPane { const commonlyUsed = resolveSettingsTree(commonlyUsedDataToUse, groups, this.logService); resolvedSettingsRoot.children!.unshift(commonlyUsed.tree); - if (toggleData) { + if (toggleData && setAdditionalGroups) { // Add the additional groups to the model to help with searching. this.defaultSettingsEditorModel.setAdditionalGroups(additionalGroups); } @@ -1408,9 +1454,7 @@ export class SettingsEditor2 extends EditorPane { keys.forEach(key => this.settingsTreeModel.updateElementsByName(key)); } - // Attempt to render the tree once rather than - // once for each key to avoid redundant calls to this.refreshTree() - this.renderTree(); + keys.forEach(key => this.renderTree(key)); } else { this.renderTree(); } @@ -1450,7 +1494,6 @@ export class SettingsEditor2 extends EditorPane { // update `list`s live, as they have a separate "submit edit" step built in before this (focusedSetting.parentElement && !focusedSetting.parentElement.classList.contains('setting-item-list')) ) { - this.updateModifiedLabelForKey(key); this.scheduleRefresh(focusedSetting, key); return; @@ -1466,8 +1509,10 @@ export class SettingsEditor2 extends EditorPane { if (key) { const elements = this.currentSettingsModel.getElementsByName(key); if (elements && elements.length) { - // TODO https://github.com/microsoft/vscode/issues/57360 - this.refreshTree(); + if (elements.length >= 2) { + console.warn('More than one setting with key ' + key + ' found'); + } + this.refreshSingleElement(elements[0]); } else { // Refresh requested for a key that we don't know about return; @@ -1483,6 +1528,12 @@ export class SettingsEditor2 extends EditorPane { return !!DOM.findParentWithClass(this.rootElement.ownerDocument.activeElement, 'context-view'); } + private refreshSingleElement(element: SettingsTreeSettingElement): void { + if (this.isVisible()) { + this.settingsTree.rerender(element); + } + } + private refreshTree(): void { if (this.isVisible()) { this.settingsTree.setChildren(null, createGroupIterator(this.currentSettingsModel.root)); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index a47b4ff6e15..0073edc8a10 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -235,6 +235,11 @@ export const tocData: ITOCEntry = { id: 'features/chat', label: localize('chat', 'Chat'), settings: ['chat.*', 'inlineChat.*'] + }, + { + id: 'features/issueReporter', + label: localize('issueReporter', 'Issue Reporter'), + settings: ['issueReporter.*'] } ] }, diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index b404461da93..73d7c2e286a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -667,6 +667,7 @@ interface ISettingBoolItemTemplate extends ISettingItemTemplate { interface ISettingExtensionToggleItemTemplate extends ISettingItemTemplate { actionButton: Button; + dismissButton: Button; } interface ISettingTextItemTemplate extends ISettingItemTemplate { @@ -1055,7 +1056,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre } } -export class SettingGroupRenderer implements ITreeRenderer { +class SettingGroupRenderer implements ITreeRenderer { templateId = SETTINGS_ELEMENT_TEMPLATE_ID; renderTemplate(container: HTMLElement): IGroupTitleTemplate { @@ -1622,7 +1623,7 @@ abstract class SettingIncludeExcludeRenderer extends AbstractSettingRenderer imp } } -export class SettingExcludeRenderer extends SettingIncludeExcludeRenderer { +class SettingExcludeRenderer extends SettingIncludeExcludeRenderer { templateId = SETTINGS_EXCLUDE_TEMPLATE_ID; protected override isExclude(): boolean { @@ -1630,7 +1631,7 @@ export class SettingExcludeRenderer extends SettingIncludeExcludeRenderer { } } -export class SettingIncludeRenderer extends SettingIncludeExcludeRenderer { +class SettingIncludeRenderer extends SettingIncludeExcludeRenderer { templateId = SETTINGS_INCLUDE_TEMPLATE_ID; protected override isExclude(): boolean { @@ -1745,7 +1746,7 @@ class SettingMultilineTextRenderer extends AbstractSettingTextRenderer implement } } -export class SettingEnumRenderer extends AbstractSettingRenderer implements ITreeRenderer { +class SettingEnumRenderer extends AbstractSettingRenderer implements ITreeRenderer { templateId = SETTINGS_ENUM_TEMPLATE_ID; renderTemplate(container: HTMLElement): ISettingEnumItemTemplate { @@ -1862,7 +1863,7 @@ const settingsNumberInputBoxStyles = getInputBoxStyle({ inputBorder: settingsNumberInputBorder }); -export class SettingNumberRenderer extends AbstractSettingRenderer implements ITreeRenderer { +class SettingNumberRenderer extends AbstractSettingRenderer implements ITreeRenderer { templateId = SETTINGS_NUMBER_TEMPLATE_ID; renderTemplate(_container: HTMLElement): ISettingNumberItemTemplate { @@ -1916,7 +1917,7 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT } } -export class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRenderer { +class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRenderer { templateId = SETTINGS_BOOL_TEMPLATE_ID; renderTemplate(_container: HTMLElement): ISettingBoolItemTemplate { @@ -2008,12 +2009,15 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre type ManageExtensionClickTelemetryClassification = { extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension the user went to manage.' }; owner: 'rzhao271'; - comment: 'Event used to gain insights into when users are using an experimental extension management setting'; + comment: 'Event used to gain insights into when users interact with an extension management setting'; }; -export class SettingsExtensionToggleRenderer extends AbstractSettingRenderer implements ITreeRenderer { +class SettingsExtensionToggleRenderer extends AbstractSettingRenderer implements ITreeRenderer { templateId = SETTINGS_EXTENSION_TOGGLE_TEMPLATE_ID; + private readonly _onDidDismissExtensionSetting = this._register(new Emitter()); + readonly onDidDismissExtensionSetting = this._onDidDismissExtensionSetting.event; + renderTemplate(_container: HTMLElement): ISettingExtensionToggleItemTemplate { const common = super.renderCommonTemplate(null, _container, 'extension-toggle'); @@ -2024,9 +2028,18 @@ export class SettingsExtensionToggleRenderer extends AbstractSettingRenderer imp actionButton.element.classList.add('setting-item-extension-toggle-button'); actionButton.label = localize('showExtension', "Show Extension"); + const dismissButton = new Button(common.containerElement, { + title: false, + secondary: true, + ...defaultButtonStyles + }); + dismissButton.element.classList.add('setting-item-extension-dismiss-button'); + dismissButton.label = localize('dismiss', "Dismiss"); + const template: ISettingExtensionToggleItemTemplate = { ...common, - actionButton + actionButton, + dismissButton }; this.addSettingElementFocusHandler(template); @@ -2046,6 +2059,11 @@ export class SettingsExtensionToggleRenderer extends AbstractSettingRenderer imp this._telemetryService.publicLog2<{ extensionId: String }, ManageExtensionClickTelemetryClassification>('ManageExtensionClick', { extensionId }); this._commandService.executeCommand('extension.open', extensionId); })); + + template.elementDisposables.add(template.dismissButton.onDidClick(async () => { + this._telemetryService.publicLog2<{ extensionId: String }, ManageExtensionClickTelemetryClassification>('DismissExtensionClick', { extensionId }); + this._onDidDismissExtensionSetting.fire(extensionId); + })); } } @@ -2055,6 +2073,8 @@ export class SettingTreeRenderers extends Disposable { private readonly _onDidChangeSetting = this._register(new Emitter()); readonly onDidChangeSetting: Event; + readonly onDidDismissExtensionSetting: Event; + readonly onDidOpenSettings: Event; readonly onDidClickSettingLink: Event; @@ -2098,6 +2118,7 @@ export class SettingTreeRenderers extends Disposable { const actionFactory = (setting: ISetting, settingTarget: SettingsTarget) => this.getActionsForSetting(setting, settingTarget); const emptyActionFactory = (_: ISetting) => []; + const extensionRenderer = this._instantiationService.createInstance(SettingsExtensionToggleRenderer, [], emptyActionFactory); const settingRenderers = [ this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions, actionFactory), this._instantiationService.createInstance(SettingNumberRenderer, this.settingActions, actionFactory), @@ -2110,7 +2131,7 @@ export class SettingTreeRenderers extends Disposable { this._instantiationService.createInstance(SettingEnumRenderer, this.settingActions, actionFactory), this._instantiationService.createInstance(SettingObjectRenderer, this.settingActions, actionFactory), this._instantiationService.createInstance(SettingBoolObjectRenderer, this.settingActions, actionFactory), - this._instantiationService.createInstance(SettingsExtensionToggleRenderer, [], emptyActionFactory) + extensionRenderer ]; this.onDidClickOverrideElement = Event.any(...settingRenderers.map(r => r.onDidClickOverrideElement)); @@ -2118,6 +2139,7 @@ export class SettingTreeRenderers extends Disposable { ...settingRenderers.map(r => r.onDidChangeSetting), this._onDidChangeSetting.event ); + this.onDidDismissExtensionSetting = extensionRenderer.onDidDismissExtensionSetting; this.onDidOpenSettings = Event.any(...settingRenderers.map(r => r.onDidOpenSettings)); this.onDidClickSettingLink = Event.any(...settingRenderers.map(r => r.onDidClickSettingLink)); this.onDidFocusSetting = Event.any(...settingRenderers.map(r => r.onDidFocusSetting)); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index c900ed2c45c..eccc736095a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -203,7 +203,7 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { private initLabels(): void { if (this.setting.title) { this._displayLabel = this.setting.title; - this._displayCategory = ''; + this._displayCategory = this.setting.categoryLabel ?? null; return; } const displayKeyFormat = settingKeyToDisplayFormat(this.setting.key, this.parent!.id, this.setting.isLanguageTagSetting); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 6d2472b494b..d8feba0eecd 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -248,6 +248,7 @@ export abstract class AbstractListSettingWidget extend this.rowElements = this.model.items.map((item, i) => this.renderDataOrEditItem(item, i, focused)); this.rowElements.forEach(rowElement => this.listElement.appendChild(rowElement)); + } protected createBasicSelectBox(value: IObjectEnumData): SelectBox { @@ -685,7 +686,7 @@ export class ListSettingWidget extends Abst valueInput.element.classList.add('no-sibling'); } - const okButton = this._register(new Button(rowElement, defaultButtonStyles)); + const okButton = this.listDisposables.add(new Button(rowElement, defaultButtonStyles)); okButton.label = localize('okButton', "OK"); okButton.element.classList.add('setting-list-ok-button'); @@ -697,7 +698,7 @@ export class ListSettingWidget extends Abst } })); - const cancelButton = this._register(new Button(rowElement, { secondary: true, ...defaultButtonStyles })); + const cancelButton = this.listDisposables.add(new Button(rowElement, { secondary: true, ...defaultButtonStyles })); cancelButton.label = localize('cancelButton', "Cancel"); cancelButton.element.classList.add('setting-list-cancel-button'); @@ -1087,14 +1088,14 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget this.handleItemChange(item, changedItem, idx))); - const cancelButton = this._register(new Button(rowElement, { secondary: true, ...defaultButtonStyles })); + const cancelButton = this.listDisposables.add(new Button(rowElement, { secondary: true, ...defaultButtonStyles })); cancelButton.label = localize('cancelButton', "Cancel"); cancelButton.element.classList.add('setting-list-cancel-button'); diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index 958b8443d70..587d76eb1bc 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -3,17 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; -import { URI } from 'vs/base/common/uri'; -import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/model'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; @@ -24,38 +18,36 @@ import { RegisteredEditorPriority, IEditorResolverService } from 'vs/workbench/s import { ITextEditorService } from 'vs/workbench/services/textfile/common/textEditorService'; import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IPreferencesService, USE_SPLIT_JSON_SETTING } from 'vs/workbench/services/preferences/common/preferences'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { getCompressedContent, IJSONSchema } from 'vs/base/common/jsonSchema'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { IFileService } from 'vs/platform/files/common/files'; +import { SettingsFileSystemProvider } from 'vs/workbench/contrib/preferences/common/settingsFilesystemProvider'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -const schemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); - -export class PreferencesContribution implements IWorkbenchContribution { +export class PreferencesContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.preferences'; private editorOpeningListener: IDisposable | undefined; - private settingsListener: IDisposable; constructor( - @IModelService private readonly modelService: IModelService, - @ITextModelService private readonly textModelResolverService: ITextModelService, + @IFileService fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IPreferencesService private readonly preferencesService: IPreferencesService, - @ILanguageService private readonly languageService: ILanguageService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @ITextEditorService private readonly textEditorService: ITextEditorService, - @ILogService private readonly logService: ILogService, ) { - this.settingsListener = this.configurationService.onDidChangeConfiguration(e => { + super(); + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(USE_SPLIT_JSON_SETTING) || e.affectsConfiguration(DEFAULT_SETTINGS_EDITOR_SETTING)) { this.handleSettingsEditorRegistration(); } - }); + })); this.handleSettingsEditorRegistration(); - this.start(); + const fileSystemProvider = this._register(this.instantiationService.createInstance(SettingsFileSystemProvider)); + this._register(fileService.registerProvider(SettingsFileSystemProvider.SCHEMA, fileSystemProvider)); } private handleSettingsEditorRegistration(): void { @@ -105,52 +97,9 @@ export class PreferencesContribution implements IWorkbenchContribution { ); } } - - private start(): void { - - this.textModelResolverService.registerTextModelContentProvider('vscode', { - provideTextContent: async (uri: URI): Promise => { - if (uri.scheme !== 'vscode') { - return null; - } - if (uri.authority === 'schemas') { - return this.getSchemaModel(uri); - } - return this.preferencesService.resolveModel(uri); - } - }); - } - - private getSchemaModel(uri: URI): ITextModel { - let schema = schemaRegistry.getSchemaContributions().schemas[uri.toString()] ?? {} /* Use empty schema if not yet registered */; - const modelContent = this.getSchemaContent(uri, schema); - const languageSelection = this.languageService.createById('jsonc'); - const model = this.modelService.createModel(modelContent, languageSelection, uri); - const disposables = new DisposableStore(); - disposables.add(schemaRegistry.onDidChangeSchema(schemaUri => { - if (schemaUri === uri.toString()) { - schema = schemaRegistry.getSchemaContributions().schemas[uri.toString()]; - model.setValue(this.getSchemaContent(uri, schema)); - } - })); - disposables.add(model.onWillDispose(() => disposables.dispose())); - return model; - } - - private getSchemaContent(uri: URI, schema: IJSONSchema): string { - const startTime = Date.now(); - const content = getCompressedContent(schema); - if (this.logService.getLevel() === LogLevel.Debug) { - const endTime = Date.now(); - const uncompressed = JSON.stringify(schema); - this.logService.debug(`${uri.path}: ${uncompressed.length} -> ${content.length} (${Math.round((uncompressed.length - content.length) / uncompressed.length * 100)}%) Took ${endTime - startTime}ms`); - } - return content; - } - - dispose(): void { + override dispose(): void { dispose(this.editorOpeningListener); - dispose(this.settingsListener); + super.dispose(); } } diff --git a/src/vs/workbench/contrib/preferences/common/settingsFilesystemProvider.ts b/src/vs/workbench/contrib/preferences/common/settingsFilesystemProvider.ts new file mode 100644 index 00000000000..f0a80db832c --- /dev/null +++ b/src/vs/workbench/contrib/preferences/common/settingsFilesystemProvider.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotSupportedError } from 'vs/base/common/errors'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { FileChangeType, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Registry } from 'vs/platform/registry/common/platform'; +import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; + +const schemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); + + +export class SettingsFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { + + static readonly SCHEMA = Schemas.vscode; + + protected readonly _onDidChangeFile = this._register(new Emitter()); + readonly onDidChangeFile = this._onDidChangeFile.event; + + constructor( + @IPreferencesService private readonly preferencesService: IPreferencesService, + @ILogService private readonly logService: ILogService + ) { + super(); + this._register(schemaRegistry.onDidChangeSchema(schemaUri => { + this._onDidChangeFile.fire([{ resource: URI.parse(schemaUri), type: FileChangeType.UPDATED }]); + })); + this._register(preferencesService.onDidDefaultSettingsContentChanged(uri => { + this._onDidChangeFile.fire([{ resource: uri, type: FileChangeType.UPDATED }]); + })); + } + + readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly + FileSystemProviderCapabilities.FileReadWrite; + + async readFile(uri: URI): Promise { + if (uri.scheme !== SettingsFileSystemProvider.SCHEMA) { + throw new NotSupportedError(); + } + let content: string | undefined; + if (uri.authority === 'schemas') { + content = this.getSchemaContent(uri); + } else if (uri.authority === 'defaultsettings') { + content = this.preferencesService.getDefaultSettingsContent(uri); + } + if (content) { + return VSBuffer.fromString(content).buffer; + } + throw FileSystemProviderErrorCode.FileNotFound; + } + + async stat(uri: URI): Promise { + if (schemaRegistry.hasSchemaContent(uri.toString()) || this.preferencesService.hasDefaultSettingsContent(uri)) { + const currentTime = Date.now(); + return { + type: FileType.File, + permissions: FilePermission.Readonly, + mtime: currentTime, + ctime: currentTime, + size: 0 + }; + } + throw FileSystemProviderErrorCode.FileNotFound; + } + + readonly onDidChangeCapabilities = Event.None; + + watch(resource: URI, opts: IWatchOptions): IDisposable { return Disposable.None; } + + async mkdir(resource: URI): Promise { } + async readdir(resource: URI): Promise<[string, FileType][]> { return []; } + + async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { } + async delete(resource: URI, opts: IFileDeleteOptions): Promise { } + + async writeFile() { + throw new NotSupportedError(); + } + + private getSchemaContent(uri: URI): string { + const startTime = Date.now(); + const content = schemaRegistry.getSchemaContent(uri.toString()) ?? '{}' /* Use empty schema if not yet registered */; + const logLevel = this.logService.getLevel(); + if (logLevel === LogLevel.Debug || logLevel === LogLevel.Trace) { + const endTime = Date.now(); + const uncompressed = JSON.stringify(schemaRegistry.getSchemaContributions().schemas[uri.toString()]); + this.logService.debug(`${uri.toString()}: ${uncompressed.length} -> ${content.length} (${Math.round((uncompressed.length - content.length) / uncompressed.length * 100)}%) Took ${endTime - startTime}ms`); + } + return content; + } +} diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index a75a1b4b09b..8b83d9b4a92 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -29,6 +29,7 @@ interface IConfiguration extends IWindowsConfiguration { window: IWindowSettings; workbench?: { enableExperiments?: boolean }; _extensionsGallery?: { enablePPE?: boolean }; + accessibility?: { verbosity?: { debug?: boolean } }; } export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution { @@ -38,24 +39,28 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo 'window.nativeTabs', 'window.nativeFullScreen', 'window.clickThroughInactive', + 'window.experimentalControlOverlay', 'update.mode', 'editor.accessibilitySupport', 'security.workspace.trust.enabled', 'workbench.enableExperiments', '_extensionsGallery.enablePPE', - 'security.restrictUNCAccess' + 'security.restrictUNCAccess', + 'accessibility.verbosity.debug' ]; private readonly titleBarStyle = new ChangeObserver('string'); private readonly nativeTabs = new ChangeObserver('boolean'); private readonly nativeFullScreen = new ChangeObserver('boolean'); private readonly clickThroughInactive = new ChangeObserver('boolean'); + private readonly linuxWindowControlOverlay = new ChangeObserver('boolean'); private readonly updateMode = new ChangeObserver('string'); private accessibilitySupport: 'on' | 'off' | 'auto' | undefined; private readonly workspaceTrustEnabled = new ChangeObserver('boolean'); private readonly experimentsEnabled = new ChangeObserver('boolean'); private readonly enablePPEExtensionsGallery = new ChangeObserver('boolean'); private readonly restrictUNCAccess = new ChangeObserver('boolean'); + private readonly accessibilityVerbosityDebug = new ChangeObserver('boolean'); constructor( @IHostService private readonly hostService: IHostService, @@ -96,6 +101,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // macOS: Click through (accept first mouse) processChanged(isMacintosh && this.clickThroughInactive.handleChange(config.window?.clickThroughInactive)); + // Linux: WCO + processChanged(isLinux && this.linuxWindowControlOverlay.handleChange(config.window?.experimentalControlOverlay)); + // Update mode processChanged(this.updateMode.handleChange(config.update?.mode)); @@ -112,6 +120,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // UNC host access restrictions processChanged(this.restrictUNCAccess.handleChange(config?.security?.restrictUNCAccess)); + + // Debug accessibility verbosity + processChanged(this.accessibilityVerbosityDebug.handleChange(config?.accessibility?.verbosity?.debug)); } // Experiments diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 08dd88b0fae..cfda45055fd 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -778,7 +778,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr return items; }; - const quickPick = this.quickInputService.createQuickPick(); + const quickPick = this.quickInputService.createQuickPick({ useSeparators: true }); quickPick.placeholder = nls.localize('remoteActions', "Select an option to open a Remote Window"); quickPick.items = computeItems(); quickPick.sortByLabel = false; diff --git a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts index 0f750170457..f450a28771b 100644 --- a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -363,7 +363,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo private async getAuthenticationSession(): Promise { const sessions = await this.getAllSessions(); - const quickpick = this.quickInputService.createQuickPick(); + const quickpick = this.quickInputService.createQuickPick({ useSeparators: true }); quickpick.ok = false; quickpick.placeholder = localize('accountPreference.placeholder', "Sign in to an account to enable remote access"); quickpick.ignoreFocusOut = true; @@ -750,7 +750,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo return new Promise((c, e) => { const disposables = new DisposableStore(); - const quickPick = this.quickInputService.createQuickPick(); + const quickPick = this.quickInputService.createQuickPick({ useSeparators: true }); quickPick.placeholder = localize('manage.placeholder', 'Select a command to invoke'); disposables.add(quickPick); const items: Array = []; diff --git a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts index 1db3282ad9e..618c875f813 100644 --- a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts +++ b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts @@ -7,7 +7,6 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, IUntypedEditorInput } from 'vs/workbench/common/editor'; -// is one contrib allowed to import from another? import { parse } from 'vs/base/common/marshalling'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -24,7 +23,6 @@ import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common import { IWorkingCopyEditorHandler, IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { extname, isEqual } from 'vs/base/common/resources'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -// eslint-disable-next-line local/code-translation-remind import { localize2 } from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; @@ -40,6 +38,12 @@ import { IInteractiveHistoryService } from 'vs/workbench/contrib/interactive/bro import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { getReplView } from 'vs/workbench/contrib/debug/browser/repl'; +import { REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; type SerializedNotebookEditorData = { resource: URI; preferredResource: URI; viewType: string; options?: NotebookEditorInputOptions }; class ReplEditorSerializer implements IEditorSerializer { @@ -196,10 +200,11 @@ registerAction2(class extends Action2 { } }); -export async function executeReplInput(accessor: ServicesAccessor, editorControl: { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget }) { - const bulkEditService = accessor.get(IBulkEditService); - const historyService = accessor.get(IInteractiveHistoryService); - const notebookEditorService = accessor.get(INotebookEditorService); +export async function executeReplInput( + bulkEditService: IBulkEditService, + historyService: IInteractiveHistoryService, + notebookEditorService: INotebookEditorService, + editorControl: { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget }) { if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { const notebookDocument = editorControl.notebookEditor.textModel; @@ -259,3 +264,14 @@ export async function executeReplInput(accessor: ServicesAccessor, editorControl } } } + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.find.replInputFocus', + weight: KeybindingWeight.WorkbenchContrib + 1, + when: ContextKeyExpr.equals('view', REPL_VIEW_ID), + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyF, + secondary: [KeyCode.F3], + handler: (accessor) => { + getReplView(accessor.get(IViewsService))?.openFind(); + } +}); diff --git a/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts b/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts index 53a0b25667d..6696a03f28d 100644 --- a/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts +++ b/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/interactive'; -// eslint-disable-next-line local/code-translation-remind import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -61,7 +60,8 @@ import { EXECUTE_REPL_COMMAND_ID, REPL_EDITOR_ID } from 'vs/workbench/contrib/no import 'vs/css!./interactiveEditor'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { deepClone } from 'vs/base/common/objects'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { MarginHoverController } from 'vs/editor/contrib/hover/browser/marginHoverController'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; import { ReplEditorInput } from 'vs/workbench/contrib/replNotebook/browser/replEditorInput'; const DECORATION_KEY = 'interactiveInputDecoration'; @@ -378,7 +378,8 @@ export class ReplEditor extends EditorPane implements IEditorPaneWithScrolling { cellEditorContributions: EditorExtensionsRegistry.getSomeEditorContributions([ SelectionClipboardContributionID, ContextMenuController.ID, - HoverController.ID, + ContentHoverController.ID, + MarginHoverController.ID, MarkerController.ID ]), options: this._notebookOptions, @@ -396,7 +397,8 @@ export class ReplEditor extends EditorPane implements IEditorPaneWithScrolling { ParameterHintsController.ID, SnippetController2.ID, TabCompletionController.ID, - HoverController.ID, + ContentHoverController.ID, + MarginHoverController.ID, MarkerController.ID ]) } diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 36a6aeffdb3..332ac067fda 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -25,7 +25,6 @@ import { autorun, autorunWithStore, derived, IObservable, observableFromEvent } import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { derivedObservableWithCache, latestChangedValue, observableFromEventOpts } from 'vs/base/common/observableInternal/utils'; import { Command } from 'vs/editor/common/languages'; -import { ISCMHistoryItemGroup } from 'vs/workbench/contrib/scm/common/history'; const ActiveRepositoryContextKeys = { ActiveRepositoryName: new RawContextKey('scmActiveRepositoryName', ''), @@ -73,11 +72,11 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe switch (this._countBadgeConfig.read(reader)) { case 'all': { const repositories = this._repositories.read(reader); - return [...Iterable.map(repositories, r => ({ ...r.provider, resourceCount: this._getRepositoryResourceCount(r) }))]; + return [...Iterable.map(repositories, r => ({ provider: r.provider, resourceCount: this._getRepositoryResourceCount(r) }))]; } case 'focused': { const repository = this._activeRepository.read(reader); - return repository ? [{ ...repository.provider, resourceCount: this._getRepositoryResourceCount(repository) }] : []; + return repository ? [{ provider: repository.provider, resourceCount: this._getRepositoryResourceCount(repository) }] : []; } case 'off': return []; @@ -90,7 +89,7 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe let total = 0; for (const repository of this._countBadgeRepositories.read(reader)) { - const count = repository.count?.read(reader); + const count = repository.provider.count?.read(reader); const resourceCount = repository.resourceCount.read(reader); total = total + (count ?? resourceCount); @@ -135,9 +134,10 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe this._register(autorun(reader => { const repository = this._activeRepository.read(reader); - const currentHistoryItemGroup = repository?.provider.historyProviderObs.read(reader)?.currentHistoryItemGroupObs.read(reader); + const historyProvider = repository?.provider.historyProvider.read(reader); + const branchName = historyProvider?.currentHistoryItemGroupName.read(reader); - this._updateActiveRepositoryContextKeys(repository, currentHistoryItemGroup); + this._updateActiveRepositoryContextKeys(repository?.provider.name, branchName); })); } @@ -196,9 +196,9 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe } } - private _updateActiveRepositoryContextKeys(repository: ISCMRepository | undefined, currentHistoryItemGroup: ISCMHistoryItemGroup | undefined): void { - this._activeRepositoryNameContextKey.set(repository?.provider.name ?? ''); - this._activeRepositoryBranchNameContextKey.set(currentHistoryItemGroup?.name ?? ''); + private _updateActiveRepositoryContextKeys(repositoryName: string | undefined, branchName: string | undefined): void { + this._activeRepositoryNameContextKey.set(repositoryName ?? ''); + this._activeRepositoryBranchNameContextKey.set(branchName ?? ''); } } diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 173cdb04fd0..c01be2b8ce6 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -787,7 +787,7 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu if (this.editor.hasModel() && (typeof lineNumber === 'number' || !this.widget.provider)) { index = this.model.findNextClosestChange(typeof lineNumber === 'number' ? lineNumber : this.editor.getPosition().lineNumber, true, this.widget.provider); } else { - const providerChanges: number[] = this.model.mapChanges.get(this.widget.provider) ?? this.model.mapChanges.values().next().value; + const providerChanges: number[] = this.model.mapChanges.get(this.widget.provider) ?? this.model.mapChanges.values().next().value!; const mapIndex = providerChanges.findIndex(value => value === this.widget!.index); index = providerChanges[rot(mapIndex + 1, providerChanges.length)]; } @@ -807,7 +807,7 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu if (this.editor.hasModel() && (typeof lineNumber === 'number')) { index = this.model.findPreviousClosestChange(typeof lineNumber === 'number' ? lineNumber : this.editor.getPosition().lineNumber, true, this.widget.provider); } else { - const providerChanges: number[] = this.model.mapChanges.get(this.widget.provider) ?? this.model.mapChanges.values().next().value; + const providerChanges: number[] = this.model.mapChanges.get(this.widget.provider) ?? this.model.mapChanges.values().next().value!; const mapIndex = providerChanges.findIndex(value => value === this.widget!.index); index = providerChanges[rot(mapIndex - 1, providerChanges.length)]; } diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 288452b11c0..343c068d912 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -110,6 +110,10 @@ line-height: 22px; } +.scm-view .monaco-list-row .monaco-icon-label-container { + height: 22px; +} + .scm-view .monaco-list-row .history, .scm-view .monaco-list-row .history-item-group, .scm-view .monaco-list-row .resource-group { @@ -219,7 +223,9 @@ } .scm-view .monaco-list-row.focused .separator-container .label-name, -.scm-view .monaco-list-row.selected .separator-container .label-name { +.scm-view .monaco-list-row.selected .separator-container .label-name, +.scm-view .monaco-list-row.focused .separator-container .action-label::before, +.scm-view .monaco-list-row.selected .separator-container .action-label::before { color: var(--vscode-foreground); } @@ -520,3 +526,26 @@ .scm-repositories-view .scm-provider > .label > .name { font-weight: normal; } + +/* History item hover */ + +.monaco-hover.history-item-hover p:first-child { + margin-top: 4px; +} + +.monaco-hover.history-item-hover p:last-child { + margin-bottom: 4px; +} + +.monaco-hover.history-item-hover hr { + margin-top: 4px; + margin-bottom: 4px; +} + +.monaco-hover.history-item-hover hr + p { + margin: 4px 0; +} + +.monaco-hover.history-item-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) span:not(.codicon) { + margin-bottom: 0 !important; +} diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index a5719651927..b2516c76ab4 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -14,7 +14,7 @@ import { IMenu, IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/c import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ISCMHistoryProviderMenus, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryProviderMenus, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMHistoryItemViewModelTreeElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMMenus, ISCMProvider, ISCMRepository, ISCMRepositoryMenus, ISCMResource, ISCMResourceGroup, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; function actionEquals(a: IAction, b: IAction): boolean { @@ -176,7 +176,7 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { private _historyProviderMenu: SCMHistoryProviderMenus | undefined; get historyProviderMenu(): SCMHistoryProviderMenus | undefined { - if (this.provider.historyProvider && !this._historyProviderMenu) { + if (this.provider.historyProvider.get() && !this._historyProviderMenu) { this._historyProviderMenu = new SCMHistoryProviderMenus(this.contextKeyService, this.menuService); this.disposables.add(this._historyProviderMenu); } @@ -256,6 +256,7 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDisposable { private readonly historyItemMenus = new Map(); + private readonly historyItemMenus2 = new Map(); private readonly disposables = new DisposableStore(); constructor( @@ -266,6 +267,10 @@ export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDispo return this.getOrCreateHistoryItemMenu(historyItem); } + getHistoryItemMenu2(historyItem: SCMHistoryItemViewModelTreeElement): IMenu { + return this.getOrCreateHistoryItemMenu2(historyItem); + } + getHistoryItemGroupMenu(historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu { return historyItemGroup.direction === 'incoming' ? this.menuService.createMenu(MenuId.SCMIncomingChanges, this.contextKeyService) : @@ -304,9 +309,20 @@ export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDispo return result; } + private getOrCreateHistoryItemMenu2(historyItem: SCMHistoryItemViewModelTreeElement): IMenu { + let result = this.historyItemMenus2.get(historyItem); + + if (!result) { + result = this.menuService.createMenu(MenuId.SCMChangesContext, this.contextKeyService); + this.historyItemMenus2.set(historyItem, result); + } + + return result; + } + private getOutgoingHistoryItemGroupMenu(menuId: MenuId, historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu { const contextKeyService = this.contextKeyService.createOverlay([ - ['scmHistoryItemGroupHasRemote', !!historyItemGroup.repository.provider.historyProvider?.currentHistoryItemGroup?.remote], + ['scmHistoryItemGroupHasRemote', !!historyItemGroup.repository.provider.historyProvider.get()?.currentHistoryItemGroup.get()?.remote], ]); return this.menuService.createMenu(menuId, contextKeyService); diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 91129c1d6ca..8b1bd4fd458 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -32,9 +32,11 @@ import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/sug import { MANAGE_TRUST_COMMAND_ID, WorkspaceTrustContext } from 'vs/workbench/contrib/workspace/common/workspace'; import { IQuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiff'; import { QuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiffService'; -import { getActiveElement } from 'vs/base/browser/dom'; +import { getActiveElement, isActiveElement } from 'vs/base/browser/dom'; import { SCMWorkingSetController } from 'vs/workbench/contrib/scm/browser/workingSet'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { IListService, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { isSCMRepository } from 'vs/workbench/contrib/scm/browser/util'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -350,10 +352,15 @@ Registry.as(ConfigurationExtensions.Configuration).regis description: localize('scm.workingSets.default', "Controls the default working set to use when switching to a source control history item group that does not have a working set."), default: 'current' }, - 'scm.experimental.showHistoryGraph': { + 'scm.showHistoryGraph': { type: 'boolean', - description: localize('scm.experimental.showHistoryGraph', "Controls whether to show the history graph instead of incoming/outgoing changes in the Source Control view."), - default: false + description: localize('scm.showHistoryGraph', "Controls whether to render incoming/outgoing changes using a graph in the Source Control view."), + default: true + }, + 'scm.compactFolders': { + type: 'boolean', + description: localize('scm.compactFolders', "Controls whether the Source Control view should render folders in a compact form. In such a form, single child folders will be compressed in a combined tree element."), + default: true } } }); @@ -458,12 +465,35 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ primary: KeyMod.Alt | KeyCode.UpArrow }); -CommandsRegistry.registerCommand('scm.openInIntegratedTerminal', async (accessor, provider: ISCMProvider) => { - if (!provider || !provider.rootUri) { +CommandsRegistry.registerCommand('scm.openInIntegratedTerminal', async (accessor, ...providers: ISCMProvider[]) => { + if (!providers || providers.length === 0) { return; } const commandService = accessor.get(ICommandService); + const listService = accessor.get(IListService); + + let provider = providers.length === 1 ? providers[0] : undefined; + + if (!provider) { + const list = listService.lastFocusedList; + const element = list?.getHTMLElement(); + + if (list instanceof WorkbenchList && element && isActiveElement(element)) { + const [index] = list.getFocus(); + const focusedElement = list.element(index); + + // Source Control Repositories + if (isSCMRepository(focusedElement)) { + provider = focusedElement.provider; + } + } + } + + if (!provider?.rootUri) { + return; + } + await commandService.executeCommand('openInIntegratedTerminal', provider.rootUri); }); diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts index f6505a2eec4..b84292c733c 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -3,23 +3,43 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { lastOrDefault } from 'vs/base/common/arrays'; import { deepClone } from 'vs/base/common/objects'; import { ThemeIcon } from 'vs/base/common/themables'; +import { buttonForeground } from 'vs/platform/theme/common/colorRegistry'; +import { chartsBlue, chartsGreen, chartsOrange, chartsPurple, chartsRed, chartsYellow } from 'vs/platform/theme/common/colors/chartsColors'; +import { asCssVariable, ColorIdentifier, registerColor } from 'vs/platform/theme/common/colorUtils'; import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemViewModel } from 'vs/workbench/contrib/scm/common/history'; +import { rot } from 'vs/base/common/numbers'; const SWIMLANE_HEIGHT = 22; const SWIMLANE_WIDTH = 11; const CIRCLE_RADIUS = 4; const SWIMLANE_CURVE_RADIUS = 5; -const graphColors = ['#007ACC', '#BC3FBC', '#BF8803', '#CC6633', '#F14C4C', '#16825D']; +/** + * History graph colors (local, remote, base) + */ +export const historyItemGroupLocal = registerColor('scm.historyGraph.historyItemGroupLocal', chartsBlue, localize('scm.historyGraph.historyItemGroupLocal', "Local history item group color.")); +export const historyItemGroupRemote = registerColor('scm.historyGraph.historyItemGroupRemote', chartsPurple, localize('scm.historyItemGroupRemote', "Remote history item group color.")); +export const historyItemGroupBase = registerColor('scm.historyGraph.historyItemGroupBase', chartsOrange, localize('scm.historyItemGroupBase', "Base history item group color.")); -function getNextColorIndex(colorIndex: number): number { - return colorIndex < graphColors.length - 1 ? colorIndex + 1 : 1; -} +/** + * History item hover color + */ +export const historyItemGroupHoverLabelForeground = registerColor('scm.historyGraph.historyItemGroupHoverLabelForeground', buttonForeground, localize('scm.historyItemGroupHoverLabelForeground', "History item group hover label foreground color.")); -function getLabelColorIndex(historyItem: ISCMHistoryItem, colorMap: Map): number | undefined { +/** + * History graph color registry + */ +export const colorRegistry: ColorIdentifier[] = [ + registerColor('scm.historyGraph.green', chartsGreen, localize('scm.historyGraph.green', "The green color used in history graph.")), + registerColor('scm.historyGraph.red', chartsRed, localize('scm.historyGraph.red', "The red color used in history graph.")), + registerColor('scm.historyGraph.yellow', chartsYellow, localize('scm.historyGraph.yellow', "The yellow color used in history graph.")), +]; + +function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map): ColorIdentifier | undefined { for (const label of historyItem.labels ?? []) { const colorIndex = colorMap.get(label.title); if (colorIndex !== undefined) { @@ -30,22 +50,22 @@ function getLabelColorIndex(historyItem: ISCMHistoryItem, colorMap: Map 0) { - const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), SWIMLANE_HEIGHT / 2, SWIMLANE_HEIGHT, graphColors[circleColorIndex]); + const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), SWIMLANE_HEIGHT / 2, SWIMLANE_HEIGHT, circleColor); svg.append(path); } // Draw * if (historyItem.parentIds.length > 1) { // Multi-parent node - const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, graphColors[circleColorIndex]); + const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, circleColor); svg.append(circleOuter); - const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, graphColors[circleColorIndex]); + const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, circleColor); svg.append(circleInner); } else { // HEAD // TODO@lszomoru - implement a better way to determine if the commit is HEAD if (historyItem.labels?.some(l => ThemeIcon.isThemeIcon(l.icon) && l.icon.id === 'target')) { - const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 2, graphColors[circleColorIndex]); + const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 2, circleColor); svg.append(outerCircle); } // Node - const circle = drawCircle(circleIndex, CIRCLE_RADIUS, graphColors[circleColorIndex]); + const circle = drawCircle(circleIndex, CIRCLE_RADIUS, circleColor); svg.append(circle); } @@ -207,7 +228,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV return svg; } -export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map()): ISCMHistoryItemViewModel[] { +export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map()): ISCMHistoryItemViewModel[] { let colorIndex = -1; const viewModels: ISCMHistoryItemViewModel[] = []; @@ -218,16 +239,16 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], const inputSwimlanes = outputSwimlanesFromPreviousItem.map(i => deepClone(i)); const outputSwimlanes: ISCMHistoryItemGraphNode[] = []; - if (historyItem.parentIds.length > 0) { - let firstParentAdded = false; + let firstParentAdded = false; - // Add first parent to the output + // Add first parent to the output + if (historyItem.parentIds.length > 0) { for (const node of inputSwimlanes) { if (node.id === historyItem.id) { if (!firstParentAdded) { outputSwimlanes.push({ id: historyItem.parentIds[0], - color: getLabelColorIndex(historyItem, colorMap) ?? node.color + color: getLabelColorIdentifier(historyItem, colorMap) ?? node.color }); firstParentAdded = true; } @@ -237,17 +258,30 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], outputSwimlanes.push(deepClone(node)); } + } - // Add unprocessed parent(s) to the output - for (let i = firstParentAdded ? 1 : 0; i < historyItem.parentIds.length; i++) { - // Color index (label -> next color) - colorIndex = getLabelColorIndex(historyItem, colorMap) ?? getNextColorIndex(colorIndex); + // Add unprocessed parent(s) to the output + for (let i = firstParentAdded ? 1 : 0; i < historyItem.parentIds.length; i++) { + // Color index (label -> next color) + let colorIdentifier: string | undefined; - outputSwimlanes.push({ - id: historyItem.parentIds[i], - color: colorIndex - }); + if (!firstParentAdded) { + colorIdentifier = getLabelColorIdentifier(historyItem, colorMap); + } else { + const historyItemParent = historyItems + .find(h => h.id === historyItem.parentIds[i]); + colorIdentifier = historyItemParent ? getLabelColorIdentifier(historyItemParent, colorMap) : undefined; } + + if (!colorIdentifier) { + colorIndex = rot(colorIndex + 1, colorRegistry.length); + colorIdentifier = colorRegistry[colorIndex]; + } + + outputSwimlanes.push({ + id: historyItem.parentIds[i], + color: colorIdentifier + }); } viewModels.push({ diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 67564cd4aab..0c09b0212e1 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -21,9 +21,9 @@ import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from ' import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2, IMenu } from 'vs/platform/actions/common/actions'; -import { IAction, ActionRunner, Action, Separator, IActionRunner } from 'vs/base/common/actions'; +import { IAction, ActionRunner, Action, Separator, IActionRunner, toAction } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IThemeService, IFileIconTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu, isSCMHistoryItemViewModelTreeElement } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; @@ -43,7 +43,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; -import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IModelService } from 'vs/editor/common/services/model'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; @@ -95,25 +95,28 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { EditOperation } from 'vs/editor/common/core/editOperation'; import { stripIcons } from 'vs/base/common/iconLabels'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { editorSelectionBackground, foreground, inputBackground, inputForeground, listActiveSelectionForeground, registerColor, selectionBackground, transparent } from 'vs/platform/theme/common/colorRegistry'; -import { IMenuWorkbenchToolBarOptions, MenuWorkbenchToolBar, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { ColorIdentifier, foreground, listActiveSelectionForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; import { clamp, rot } from 'vs/base/common/numbers'; -import { ILogService } from 'vs/platform/log/common/log'; import { MarkdownString } from 'vs/base/common/htmlContent'; import type { IHoverOptions, IManagedHover, IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController2'; +import { MarginHoverController } from 'vs/editor/contrib/hover/browser/marginHoverController'; import { ITextModel } from 'vs/editor/common/model'; import { autorun } from 'vs/base/common/observable'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory'; +import { historyItemGroupBase, historyItemGroupHoverLabelForeground, historyItemGroupLocal, historyItemGroupRemote, renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory'; import { PlaceholderTextContribution } from 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { fromNow } from 'vs/base/common/date'; +import { equals } from 'vs/base/common/arrays'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; // type SCMResourceTreeNode = IResourceNode; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; @@ -829,6 +832,7 @@ class HistoryItemActionRunner extends ActionRunner { const args: (ISCMProvider | ISCMHistoryItem)[] = []; args.push(context.historyItemGroup.repository.provider); + args.push({ id: context.id, parentIds: context.parentIds, @@ -843,6 +847,48 @@ class HistoryItemActionRunner extends ActionRunner { } } +class HistoryItemActionRunner2 extends ActionRunner { + constructor(private readonly getSelectedHistoryItems: () => SCMHistoryItemViewModelTreeElement[]) { + super(); + } + + protected override async runAction(action: IAction, context: SCMHistoryItemViewModelTreeElement): Promise { + if (!(action instanceof MenuItemAction)) { + return super.runAction(action, context); + } + + const args: (ISCMProvider | ISCMHistoryItem)[] = []; + args.push(context.repository.provider); + + const selection = this.getSelectedHistoryItems(); + const contextIsSelected = selection.some(s => s === context); + if (contextIsSelected && selection.length > 1) { + args.push(...selection.map(h => ( + { + id: h.historyItemViewModel.historyItem.id, + parentIds: h.historyItemViewModel.historyItem.parentIds, + message: h.historyItemViewModel.historyItem.message, + author: h.historyItemViewModel.historyItem.author, + icon: h.historyItemViewModel.historyItem.icon, + timestamp: h.historyItemViewModel.historyItem.timestamp, + statistics: h.historyItemViewModel.historyItem.statistics, + } satisfies ISCMHistoryItem))); + } else { + args.push({ + id: context.historyItemViewModel.historyItem.id, + parentIds: context.historyItemViewModel.historyItem.parentIds, + message: context.historyItemViewModel.historyItem.message, + author: context.historyItemViewModel.historyItem.author, + icon: context.historyItemViewModel.historyItem.icon, + timestamp: context.historyItemViewModel.historyItem.timestamp, + statistics: context.historyItemViewModel.historyItem.statistics, + } satisfies ISCMHistoryItem); + } + + await action.run(...args); + } +} + class HistoryItemHoverDelegate extends WorkbenchHoverDelegate { constructor( private readonly viewContainerLocation: ViewContainerLocation | null, @@ -864,7 +910,7 @@ class HistoryItemHoverDelegate extends WorkbenchHoverDelegate { hoverPosition = HoverPosition.RIGHT; } - return { position: { hoverPosition, forcePosition: true } }; + return { additionalClasses: ['history-item-hover'], position: { hoverPosition, forcePosition: true } }; } } @@ -1051,7 +1097,7 @@ class HistoryItem2Renderer implements ICompressibleTreeRenderer${historyItem.statistics.files === 1 ? + localize('fileChanged', "{0} file changed", historyItem.statistics.files) : + localize('filesChanged', "{0} files changed", historyItem.statistics.files)}`); + + if (historyItem.statistics.insertions) { + const historyItemAdditionsForegroundColor = colorTheme.getColor(historyItemAdditionsForeground); + markdown.appendMarkdown(`, ${historyItem.statistics.insertions === 1 ? + localize('insertion', "{0} insertion{1}", historyItem.statistics.insertions, '(+)') : + localize('insertions', "{0} insertions{1}", historyItem.statistics.insertions, '(+)')}`); + } + + if (historyItem.statistics.deletions) { + const historyItemDeletionsForegroundColor = colorTheme.getColor(historyItemDeletionsForeground); + markdown.appendMarkdown(`, ${historyItem.statistics.deletions === 1 ? + localize('deletion', "{0} deletion{1}", historyItem.statistics.deletions, '(-)') : + localize('deletions', "{0} deletions{1}", historyItem.statistics.deletions, '(-)')}`); + } } - if (historyItem.statistics?.files) { - const colorTheme = this.themeService.getColorTheme(); - const historyItemAdditionsForegroundColor = colorTheme.getColor(historyItemAdditionsForeground); - const historyItemDeletionsForegroundColor = colorTheme.getColor(historyItemDeletionsForeground); + if (historyItem.labels) { + const historyItemGroupLocalColor = colorTheme.getColor(historyItemGroupLocal); + const historyItemGroupRemoteColor = colorTheme.getColor(historyItemGroupRemote); + const historyItemGroupBaseColor = colorTheme.getColor(historyItemGroupBase); - markdown.appendMarkdown(` | `); - markdown.appendMarkdown(`${historyItem.statistics.files}`); - markdown.appendMarkdown(historyItem.statistics.insertions ? `, +${historyItem.statistics.insertions}` : ''); - markdown.appendMarkdown(historyItem.statistics.deletions ? `, -${historyItem.statistics.deletions}` : ''); + const historyItemGroupHoverLabelForegroundColor = colorTheme.getColor(historyItemGroupHoverLabelForeground); + + markdown.appendMarkdown(`\n\n---\n\n`); + markdown.appendMarkdown(historyItem.labels.map(label => { + const historyItemGroupHoverLabelBackgroundColor = + label.title === currentHistoryItemGroup?.name ? historyItemGroupLocalColor : + label.title === currentHistoryItemGroup?.remote?.name ? historyItemGroupRemoteColor : + label.title === currentHistoryItemGroup?.base?.name ? historyItemGroupBaseColor : + undefined; + + const historyItemGroupHoverLabelIconId = ThemeIcon.isThemeIcon(label.icon) ? label.icon.id : ''; + + return ` $(${historyItemGroupHoverLabelIconId}) ${label.title} `; + }).join('  ')); } return { markdown, markdownNotSupportedFallback: historyItem.message }; @@ -1201,7 +1282,9 @@ class HistoryItemChangeRenderer implements ICompressibleTreeRenderer { @@ -1210,10 +1293,10 @@ class SeparatorRenderer implements ICompressibleTreeRenderer IAction[], @IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IKeybindingService private readonly keybindingService: IKeybindingService, - @IConfigurationService private readonly configurationService: IConfigurationService, @ICommandService private readonly commandService: ICommandService, @IMenuService private readonly menuService: IMenuService, @ITelemetryService private readonly telemetryService: ITelemetryService @@ -1226,31 +1309,49 @@ class SeparatorRenderer implements ICompressibleTreeRenderer('scm.experimental.showHistoryGraph') !== true) { - const toolBar = new MenuWorkbenchToolBar(append(element, $('.actions')), MenuId.SCMChangesSeparator, { moreIcon: Codicon.gear }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService); - disposables.add(toolBar); - } + const toolBar = new WorkbenchToolBar(append(element, $('.actions')), undefined, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService); + templateDisposables.add(toolBar); - return { label, disposables }; + return { label, toolBar, elementDisposables: new DisposableStore(), templateDisposables }; } renderElement(element: ITreeNode, index: number, templateData: SeparatorTemplate, height: number | undefined): void { + const provider = element.element.repository.provider; + const historyProvider = provider.historyProvider.get(); + const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.get(); + + // Label templateData.label.setLabel(element.element.label, undefined, { title: element.element.ariaLabel }); + + // Toolbar + const contextKeyService = this.contextKeyService.createOverlay([ + ['scmHistoryItemGroupHasRemote', !!currentHistoryItemGroup?.remote], + ]); + const menu = this.menuService.createMenu(MenuId.SCMChangesSeparator, contextKeyService); + templateData.elementDisposables.add(connectPrimaryMenu(menu, (primary, secondary) => { + secondary.splice(0, 0, ...this.getFilterActions(element.element.repository), new Separator()); + templateData.toolBar.setActions(primary, secondary, [MenuId.SCMChangesSeparator]); + })); + templateData.toolBar.context = provider; } renderCompressedElements(node: ITreeNode, void>, index: number, templateData: SeparatorTemplate, height: number | undefined): void { throw new Error('Should never happen since node is incompressible'); } - disposeTemplate(templateData: SeparatorTemplate): void { - templateData.disposables.dispose(); + disposeElement(node: ITreeNode, index: number, templateData: SeparatorTemplate, height: number | undefined): void { + templateData.elementDisposables.clear(); } + disposeTemplate(templateData: SeparatorTemplate): void { + templateData.elementDisposables.dispose(); + templateData.templateDisposables.dispose(); + } } class ListDelegate implements IListVirtualDelegate { @@ -1638,7 +1739,7 @@ MenuRegistry.appendMenuItem(MenuId.SCMTitle, { MenuRegistry.appendMenuItem(MenuId.SCMTitle, { title: localize('scmChanges', "Incoming & Outgoing"), submenu: Menus.ChangesSettings, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeyExpr.equals('config.scm.experimental.showHistoryGraph', true).negate()), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeyExpr.equals('config.scm.showHistoryGraph', true).negate()), group: '0_view&sort', order: 2 }); @@ -1672,14 +1773,16 @@ MenuRegistry.appendMenuItem(MenuId.SCMChangesSeparator, { title: localize('incomingChanges', "Show Incoming Changes"), submenu: MenuId.SCMIncomingChangesSetting, group: '1_incoming&outgoing', - order: 1 + order: 1, + when: ContextKeyExpr.equals('config.scm.showHistoryGraph', false) }); MenuRegistry.appendMenuItem(Menus.ChangesSettings, { title: localize('incomingChanges', "Show Incoming Changes"), submenu: MenuId.SCMIncomingChangesSetting, group: '1_incoming&outgoing', - order: 1 + order: 1, + when: ContextKeyExpr.equals('config.scm.showHistoryGraph', false) }); registerAction2(class extends SCMChangesSettingAction { @@ -1723,14 +1826,16 @@ MenuRegistry.appendMenuItem(MenuId.SCMChangesSeparator, { title: localize('outgoingChanges', "Show Outgoing Changes"), submenu: MenuId.SCMOutgoingChangesSetting, group: '1_incoming&outgoing', - order: 2 + order: 2, + when: ContextKeyExpr.equals('config.scm.showHistoryGraph', false) }); MenuRegistry.appendMenuItem(Menus.ChangesSettings, { title: localize('outgoingChanges', "Show Outgoing Changes"), submenu: MenuId.SCMOutgoingChangesSetting, group: '1_incoming&outgoing', - order: 2 + order: 2, + when: ContextKeyExpr.equals('config.scm.showHistoryGraph', false) }); registerAction2(class extends SCMChangesSettingAction { @@ -1781,8 +1886,16 @@ registerAction2(class extends Action2 { f1: false, toggled: ContextKeyExpr.equals('config.scm.showChangesSummary', true), menu: [ - { id: MenuId.SCMChangesSeparator, order: 3 }, - { id: Menus.ChangesSettings, order: 3 }, + { + id: MenuId.SCMChangesSeparator, + order: 3, + when: ContextKeyExpr.equals('config.scm.showHistoryGraph', false) + }, + { + id: Menus.ChangesSettings, + order: 3, + when: ContextKeyExpr.equals('config.scm.showHistoryGraph', false) + }, ] }); } @@ -1794,6 +1907,59 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.scm.action.scm.viewChanges', + title: localize('viewChanges', "View Changes"), + f1: false, + menu: [ + { + id: MenuId.SCMChangesContext, + group: '0_view', + when: ContextKeyExpr.equals('config.multiDiffEditor.experimental.enabled', true) + } + ] + }); + } + + override async run(accessor: ServicesAccessor, provider: ISCMProvider, ...historyItems: ISCMHistoryItem[]) { + const commandService = accessor.get(ICommandService); + + if (!provider || historyItems.length === 0) { + return; + } + + const historyItem = historyItems[0]; + const historyItemLast = historyItems[historyItems.length - 1]; + const historyProvider = provider.historyProvider.get(); + + if (historyItems.length > 1) { + const ancestor = await historyProvider?.resolveHistoryItemGroupCommonAncestor2([historyItem.id, historyItemLast.id]); + if (!ancestor || (ancestor !== historyItem.id && ancestor !== historyItemLast.id)) { + return; + } + } + + const historyItemParentId = historyItemLast.parentIds.length > 0 ? historyItemLast.parentIds[0] : undefined; + const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItem.id, historyItemParentId); + + if (!historyItemChanges?.length) { + return; + } + + const title = historyItems.length === 1 ? + `${historyItems[0].id.substring(0, 8)} - ${historyItems[0].message}` : + localize('historyItemChangesEditorTitle', "All Changes ({0} ↔ {1})", historyItemLast.id.substring(0, 8), historyItem.id.substring(0, 8)); + + const rootUri = provider.rootUri; + const path = rootUri ? rootUri.path : provider.label; + const multiDiffSourceUri = URI.from({ scheme: 'scm-history-item', path: `${path}/${historyItemParentId}..${historyItem.id}` }, true); + + commandService.executeCommand('_workbench.openMultiDiffEditor', { title, multiDiffSourceUri, resources: historyItemChanges }); + } +}); + class RepositoryVisibilityAction extends Action2 { private repository: ISCMRepository; @@ -2108,6 +2274,28 @@ class ExpandAllRepositoriesAction extends ViewAction { registerAction2(CollapseAllRepositoriesAction); registerAction2(ExpandAllRepositoriesAction); +class ShowHistoryGraphAction extends Action2 { + constructor() { + super({ + id: 'workbench.scm.action.showHistoryGraph', + title: localize('showHistoryGraph', "Show Incoming/Outgoing Changes"), + f1: false, + toggled: ContextKeyExpr.equals('config.scm.showHistoryGraph', true), + menu: [ + { id: MenuId.SCMChangesSeparator }, + { id: Menus.ViewSort, group: '3_other' }] + }); + } + + async run(accessor: ServicesAccessor) { + const configurationService = accessor.get(IConfigurationService); + const configValue = configurationService.getValue('scm.showHistoryGraph') === true; + configurationService.updateValue('scm.showHistoryGraph', !configValue); + } +} + +registerAction2(ShowHistoryGraphAction); + const enum SCMInputWidgetCommandId { CancelAction = 'scm.input.cancelAction' } @@ -2589,7 +2777,8 @@ class SCMInputWidget { DropIntoEditorController.ID, EditorDictation.ID, FormatOnType.ID, - HoverController.ID, + ContentHoverController.ID, + MarginHoverController.ID, InlineCompletionsController.ID, LinkDetector.ID, MenuPreventer.ID, @@ -2891,6 +3080,8 @@ export class SCMViewPane extends ViewPane { private readonly revealResourceThrottler = new Throttler(); private readonly updateChildrenThrottler = new Throttler(); + private historyProviderDataSource!: SCMTreeHistoryProviderDataSource; + private viewModeContextKey: IContextKey; private viewSortKeyContextKey: IContextKey; private areAllRepositoriesCollapsedContextKey: IContextKey; @@ -2906,7 +3097,6 @@ export class SCMViewPane extends ViewPane { options: IViewPaneOptions, @ICommandService private readonly commandService: ICommandService, @IEditorService private readonly editorService: IEditorService, - @ILogService private readonly logService: ILogService, @IMenuService private readonly menuService: IMenuService, @ISCMService private readonly scmService: ISCMService, @ISCMViewService private readonly scmViewService: ISCMViewService, @@ -3027,13 +3217,19 @@ export class SCMViewPane extends ViewPane { e.affectsConfiguration('scm.inputMinLineCount') || e.affectsConfiguration('scm.inputMaxLineCount') || e.affectsConfiguration('scm.showActionButton') || - e.affectsConfiguration('scm.showChangesSummary') || e.affectsConfiguration('scm.showIncomingChanges') || e.affectsConfiguration('scm.showOutgoingChanges') || - e.affectsConfiguration('scm.experimental.showHistoryGraph'), + e.affectsConfiguration('scm.showHistoryGraph'), this.visibilityDisposables) (() => this.updateChildren(), this, this.visibilityDisposables); + Event.filter(this.configurationService.onDidChangeConfiguration, + e => e.affectsConfiguration('scm.showChangesSummary'), this.visibilityDisposables) + (() => { + this.historyProviderDataSource.clearCache(); + this.updateChildren(); + }, this, this.visibilityDisposables); + // Add visible repositories this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); @@ -3094,9 +3290,14 @@ export class SCMViewPane extends ViewPane { const historyItemHoverDelegate = this.instantiationService.createInstance(HistoryItemHoverDelegate, this.viewDescriptorService.getViewLocationById(this.id), this.layoutService.getSideBarPosition()); this.disposables.add(historyItemHoverDelegate); - const treeDataSource = this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode); + this.historyProviderDataSource = this.instantiationService.createInstance(SCMTreeHistoryProviderDataSource, () => this.viewMode); + this.disposables.add(this.historyProviderDataSource); + + const treeDataSource = this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, this.historyProviderDataSource); this.disposables.add(treeDataSource); + const compressionEnabled = observableConfigValue('scm.compactFolders', true, this.configurationService); + this.tree = this.instantiationService.createInstance( WorkbenchCompressibleAsyncDataTree, 'SCM Tree Repo', @@ -3113,7 +3314,7 @@ export class SCMViewPane extends ViewPane { this.instantiationService.createInstance(HistoryItemRenderer, historyItemActionRunner, getActionViewItemProvider(this.instantiationService)), this.instantiationService.createInstance(HistoryItem2Renderer, historyItemHoverDelegate), this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewMode, this.listLabels), - this.instantiationService.createInstance(SeparatorRenderer) + this.instantiationService.createInstance(SeparatorRenderer, (repository) => this.getHistoryItemGroupFilterActions(repository)), ], treeDataSource, { @@ -3126,6 +3327,7 @@ export class SCMViewPane extends ViewPane { sorter: new SCMTreeSorter(() => this.viewMode, () => this.viewSortKey), keyboardNavigationLabelProvider: this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider, () => this.viewMode), overrideStyles: this.getLocationBasedColors().listOverrideStyles, + compressionEnabled: compressionEnabled.get(), collapseByDefault: (e: unknown) => { // Repository, Resource Group, Resource Folder (Tree), History Item Change Folder (Tree) if (isSCMRepository(e) || isSCMResourceGroup(e) || isSCMResourceNode(e) || isSCMHistoryItemChangeNode(e)) { @@ -3145,6 +3347,12 @@ export class SCMViewPane extends ViewPane { this.tree.onDidScroll(this.inputRenderer.clearValidation, this.inputRenderer, this.disposables); Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element?.element), this.disposables)(this.updateRepositoryCollapseAllContextKeys, this, this.disposables); + this.disposables.add(autorun(reader => { + this.tree.updateOptions({ + compressionEnabled: compressionEnabled.read(reader) + }); + })); + append(container, overflowWidgetsDomNode); } @@ -3239,15 +3447,14 @@ export class SCMViewPane extends ViewPane { const historyItem = e.element.historyItemViewModel.historyItem; const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined; - const historyProvider = e.element.repository.provider.historyProvider; + const historyProvider = e.element.repository.provider.historyProvider.get(); const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItem.id, historyItemParentId); if (historyItemChanges) { const title = `${historyItem.id.substring(0, 8)} - ${historyItem.message}`; const rootUri = e.element.repository.provider.rootUri; - const multiDiffSourceUri = rootUri ? - rootUri.with({ scheme: 'scm-history-item', path: `${rootUri.path}/${historyItem.id}` }) : - { scheme: 'scm-history-item', path: `${e.element.repository.provider.label}/${historyItem.id}` }; + const path = rootUri ? rootUri.path : e.element.repository.provider.label; + const multiDiffSourceUri = URI.from({ scheme: 'scm-history-item', path: `${path}/${historyItemParentId}..${historyItem.id}` }, true); await this.commandService.executeCommand('_workbench.openMultiDiffEditor', { title, multiDiffSourceUri, resources: historyItemChanges }); } @@ -3323,18 +3530,13 @@ export class SCMViewPane extends ViewPane { repositoryDisposables.add(repository.input.onDidChangeVisibility(() => this.updateChildren(repository))); repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => this.updateChildren(repository))); - repositoryDisposables.add(Event.runAndSubscribe(repository.provider.onDidChangeHistoryProvider, () => { - if (!repository.provider.historyProvider) { - this.logService.debug('SCMViewPane:onDidChangeVisibleRepositories - no history provider present'); - return; - } + repositoryDisposables.add(autorun(reader => { + repository.provider.historyProvider.read(reader)?.currentHistoryItemGroup.read(reader); - repositoryDisposables.add(repository.provider.historyProvider.onDidChangeCurrentHistoryItemGroup(() => { - this.updateChildren(repository); - this.logService.debug('SCMViewPane:onDidChangeCurrentHistoryItemGroup - update children'); - })); + this.historyProviderDataSource.deleteCacheEntry(repository); + this.historyProviderDataSource.deleteHistoryItemGroupFilter(repository); - this.logService.debug('SCMViewPane:onDidChangeVisibleRepositories - onDidChangeCurrentHistoryItemGroup listener added'); + this.updateChildren(repository); })); const resourceGroupDisposables = repositoryDisposables.add(new DisposableMap()); @@ -3365,6 +3567,7 @@ export class SCMViewPane extends ViewPane { // Removed repositories for (const repository of removed) { + this.historyProviderDataSource.deleteCacheEntry(repository); this.items.deleteAndDispose(repository); } @@ -3431,6 +3634,13 @@ export class SCMViewPane extends ViewPane { actionRunner = new HistoryItemActionRunner(); actions = collectContextMenuActions(menu); } + } else if (isSCMHistoryItemViewModelTreeElement(element)) { + const menus = this.scmViewService.menus.getRepositoryMenus(element.repository.provider); + const menu = menus.historyProviderMenu?.getHistoryItemMenu2(element); + if (menu) { + actionRunner = new HistoryItemActionRunner2(() => this.getSelectedHistoryItems()); + actions = collectContextMenuActions(menu); + } } actionRunner.onWillRun(() => this.tree.domFocus()); @@ -3455,6 +3665,11 @@ export class SCMViewPane extends ViewPane { .filter(r => !!r && !isSCMResourceGroup(r))! as any; } + private getSelectedHistoryItems(): SCMHistoryItemViewModelTreeElement[] { + return this.tree.getSelection() + .filter(r => !!r && isSCMHistoryItemViewModelTreeElement(r))!; + } + private getViewMode(): ViewMode { let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewMode.List : ViewMode.Tree; const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewMode; @@ -3672,6 +3887,40 @@ export class SCMViewPane extends ViewPane { } } + private getHistoryItemGroupFilterActions(repository: ISCMRepository): IAction[] { + const currentHistoryItemGroup = repository.provider.historyProvider.get()?.currentHistoryItemGroup?.get(); + if (!currentHistoryItemGroup) { + return []; + } + + const toHistoryItemGroupFilterAction = ( + historyItemGroupId: string, + historyItemGroupName: string): IAction => { + return toAction({ + id: `workbench.scm.action.toggleHistoryItemGroupVisibility.${repository.id}.${historyItemGroupId}`, + label: historyItemGroupName, + checked: !this.historyProviderDataSource.getHistoryItemGroupFilter(repository).has(historyItemGroupId), + run: () => { + this.historyProviderDataSource.toggleHistoryItemGroupFilter(repository, historyItemGroupId); + this.historyProviderDataSource.deleteCacheEntry(repository); + + this.updateChildren(repository); + } + }); + }; + + const actions: IAction[] = []; + if (currentHistoryItemGroup.remote) { + actions.push(toHistoryItemGroupFilterAction(currentHistoryItemGroup.remote.id, currentHistoryItemGroup.remote.name)); + } + + if (currentHistoryItemGroup.base) { + actions.push(toHistoryItemGroupFilterAction(currentHistoryItemGroup.base.id, currentHistoryItemGroup.base.name)); + } + + return actions; + } + override shouldShowWelcome(): boolean { return this.scmService.repositoryCount === 0; } @@ -3713,173 +3962,39 @@ export class SCMViewPane extends ViewPane { } } -class SCMTreeDataSource implements IAsyncDataSource { - - private readonly historyProviderCache = new Map(); - private readonly repositoryDisposables = new DisposableMap(); - private readonly disposables = new DisposableStore(); +class SCMTreeHistoryProviderDataSource extends Disposable { + private readonly _cache = new Map(); + private readonly _historyItemGroupFilter = new Map>(); constructor( private readonly viewMode: () => ViewMode, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILogService private readonly logService: ILogService, - @ISCMViewService private readonly scmViewService: ISCMViewService, @IUriIdentityService private uriIdentityService: IUriIdentityService, ) { - const onDidChangeConfiguration = Event.filter( - this.configurationService.onDidChangeConfiguration, - e => e.affectsConfiguration('scm.showChangesSummary'), - this.disposables); - this.disposables.add(onDidChangeConfiguration(() => this.historyProviderCache.clear())); - - this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.disposables); - this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); + super(); } - hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { - if (isSCMViewService(inputOrElement)) { - return this.scmViewService.visibleRepositories.length !== 0; - } else if (isSCMRepository(inputOrElement)) { - return true; - } else if (isSCMInput(inputOrElement)) { - return false; - } else if (isSCMActionButton(inputOrElement)) { - return false; - } else if (isSCMResourceGroup(inputOrElement)) { - return true; - } else if (isSCMResource(inputOrElement)) { - return false; - } else if (ResourceTree.isResourceNode(inputOrElement)) { - return inputOrElement.childrenCount > 0; - } else if (isSCMHistoryItemGroupTreeElement(inputOrElement)) { - return true; - } else if (isSCMHistoryItemTreeElement(inputOrElement)) { - return true; - } else if (isSCMHistoryItemViewModelTreeElement(inputOrElement)) { - return false; - } else if (isSCMHistoryItemChangeTreeElement(inputOrElement)) { - return false; - } else if (isSCMViewSeparator(inputOrElement)) { - return false; - } else { - throw new Error('hasChildren not implemented.'); - } + clearCache(): void { + this._cache.clear(); } - async getChildren(inputOrElement: ISCMViewService | TreeElement): Promise> { - const { alwaysShowRepositories, showActionButton } = this.getConfiguration(); - const repositoryCount = this.scmViewService.visibleRepositories.length; - - if (isSCMViewService(inputOrElement) && (repositoryCount > 1 || alwaysShowRepositories)) { - return this.scmViewService.visibleRepositories; - } else if ((isSCMViewService(inputOrElement) && repositoryCount === 1 && !alwaysShowRepositories) || isSCMRepository(inputOrElement)) { - const children: TreeElement[] = []; - - inputOrElement = isSCMRepository(inputOrElement) ? inputOrElement : this.scmViewService.visibleRepositories[0]; - const actionButton = inputOrElement.provider.actionButton; - const resourceGroups = inputOrElement.provider.groups; - - // SCM Input - if (inputOrElement.input.visible) { - children.push(inputOrElement.input); - } - - // Action Button - if (showActionButton && actionButton) { - children.push({ - type: 'actionButton', - repository: inputOrElement, - button: actionButton - } satisfies ISCMActionButton); - } - - // ResourceGroups - const hasSomeChanges = resourceGroups.some(group => group.resources.length > 0); - if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !actionButton))) { - children.push(...resourceGroups); - } - - // History item groups - const historyItemGroups = await this.getHistoryItemGroups(inputOrElement); - - // Incoming/Outgoing Separator - if (historyItemGroups.length > 0) { - let label = localize('syncSeparatorHeader', "Incoming/Outgoing"); - let ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); - - const incomingHistoryItems = historyItemGroups.find(g => g.direction === 'incoming'); - const outgoingHistoryItems = historyItemGroups.find(g => g.direction === 'outgoing'); - - if (incomingHistoryItems && !outgoingHistoryItems) { - label = localize('syncIncomingSeparatorHeader', "Incoming"); - ariaLabel = localize('syncIncomingSeparatorHeaderAriaLabel', "Incoming changes"); - } else if (!incomingHistoryItems && outgoingHistoryItems) { - label = localize('syncOutgoingSeparatorHeader', "Outgoing"); - ariaLabel = localize('syncOutgoingSeparatorHeaderAriaLabel', "Outgoing changes"); - } - - children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); - } - - children.push(...historyItemGroups); - - // History items - const historyItems = await this.getHistoryItems2(inputOrElement); - if (historyItems.length > 0) { - const label = localize('syncSeparatorHeader', "Incoming/Outgoing"); - const ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); - - children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); - } - - children.push(...historyItems); - - return children; - } else if (isSCMResourceGroup(inputOrElement)) { - if (this.viewMode() === ViewMode.List) { - // Resources (List) - return inputOrElement.resources; - } else if (this.viewMode() === ViewMode.Tree) { - // Resources (Tree) - const children: TreeElement[] = []; - for (const node of inputOrElement.resourceTree.root.children) { - children.push(node.element && node.childrenCount === 0 ? node.element : node); - } - - return children; - } - } else if (isSCMResourceNode(inputOrElement) || isSCMHistoryItemChangeNode(inputOrElement)) { - // Resources (Tree), History item changes (Tree) - const children: TreeElement[] = []; - for (const node of inputOrElement.children) { - children.push(node.element && node.childrenCount === 0 ? node.element : node); - } - - return children; - } else if (isSCMHistoryItemGroupTreeElement(inputOrElement)) { - // History item group - return this.getHistoryItems(inputOrElement); - } else if (isSCMHistoryItemTreeElement(inputOrElement)) { - // History item changes (List/Tree) - return this.getHistoryItemChanges(inputOrElement); - } - - return []; + deleteCacheEntry(repository: ISCMRepository): void { + this._cache.delete(repository); } - private async getHistoryItemGroups(element: ISCMRepository): Promise { - const { showIncomingChanges, showOutgoingChanges, showHistoryGraph } = this.getConfiguration(); + async getHistoryItemGroups(element: ISCMRepository): Promise { + const { showIncomingChanges, showOutgoingChanges, showHistoryGraph } = this._getConfiguration(); const scmProvider = element.provider; - const historyProvider = scmProvider.historyProvider; - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; + const historyProvider = scmProvider.historyProvider.get(); + const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.get(); if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never') || showHistoryGraph) { return []; } const children: SCMHistoryItemGroupTreeElement[] = []; - const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(element); + const historyProviderCacheEntry = this._getCacheEntry(element); let incomingHistoryItemGroup = historyProviderCacheEntry?.incomingHistoryItemGroup; let outgoingHistoryItemGroup = historyProviderCacheEntry?.outgoingHistoryItemGroup; @@ -3916,8 +4031,7 @@ class SCMTreeDataSource implements IAsyncDataSource { + getHistoryItemGroupFilter(element: ISCMRepository): Set { + return this._historyItemGroupFilter.get(element) ?? new Set(); + } + + deleteHistoryItemGroupFilter(repository: ISCMRepository): void { + this._historyItemGroupFilter.delete(repository); + } + + toggleHistoryItemGroupFilter(element: ISCMRepository, historyItemGroupId: string): void { + const filters = this.getHistoryItemGroupFilter(element); + if (!filters.delete(historyItemGroupId)) { + filters.add(historyItemGroupId); + } + + this._historyItemGroupFilter.set(element, filters); + } + + async getHistoryItems(element: SCMHistoryItemGroupTreeElement): Promise { const repository = element.repository; - const historyProvider = repository.provider.historyProvider; + const historyProvider = repository.provider.historyProvider.get(); if (!historyProvider) { return []; } - const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(repository); + const historyProviderCacheEntry = this._getCacheEntry(repository); const historyItemsMap = historyProviderCacheEntry.historyItems; let historyItemsElement = historyProviderCacheEntry.historyItems.get(element.id); @@ -3956,14 +4087,12 @@ class SCMTreeDataSource implements IAsyncDataSource= 2 ? await historyProvider.provideHistoryItemSummary(historyItems[0].id, element.ancestor) : undefined; historyItemsElement = [allChanges, historyItems]; - - this.historyProviderCache.set(repository, { - ...historyProviderCacheEntry, + this._updateCacheEntry(repository, { historyItems: historyItemsMap.set(element.id, historyItemsElement) }); } @@ -3989,17 +4118,17 @@ class SCMTreeDataSource implements IAsyncDataSource { - const { showHistoryGraph } = this.getConfiguration(); + async getHistoryItems2(element: ISCMRepository): Promise { + const { showHistoryGraph } = this._getConfiguration(); - const historyProvider = element.provider.historyProvider; - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; + const historyProvider = element.provider.historyProvider.get(); + const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.get(); - if (!currentHistoryItemGroup || !showHistoryGraph) { + if (!historyProvider || !currentHistoryItemGroup || !showHistoryGraph) { return []; } - const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(element); + const historyProviderCacheEntry = this._getCacheEntry(element); let historyItemsElement = historyProviderCacheEntry.historyItems2.get(element.id); const historyItemsMap = historyProviderCacheEntry.historyItems2; @@ -4010,31 +4139,52 @@ class SCMTreeDataSource implements IAsyncDataSource !filters.has(id)), + limit: { id: ancestor } + }) ?? []; + + this._updateCacheEntry(element, { historyItems2: historyItemsMap.set(element.id, historyItemsElement) }); } - // If we only have one history item that matches - // the current history item group, don't show it + // If we only have one history item that contains all the labels (current, remote, base), + // we don't need to show it, unless it is the root commit (does not have any parents) and + // the repository has not been published yet. if (historyItemsElement.length === 1 && - historyItemsElement[0].labels?.find(l => l.title === currentHistoryItemGroup.name)) { - return []; + (historyItemsElement[0].parentIds.length > 0 || currentHistoryItemGroup.remote)) { + const currentHistoryItemGroupLabels = [ + currentHistoryItemGroup.name, + ...currentHistoryItemGroup.remote ? [currentHistoryItemGroup.remote.name] : [], + ...currentHistoryItemGroup.base ? [currentHistoryItemGroup.base.name] : [], + ]; + + const labels = (historyItemsElement[0].labels ?? []) + .map(l => l.title); + + if (equals(currentHistoryItemGroupLabels.sort(), labels.sort())) { + return []; + } } // Create the color map - // TODO@lszomoru - use theme colors - const colorMap = new Map([ - [currentHistoryItemGroup.name, 0] + const colorMap = new Map([ + [currentHistoryItemGroup.name, historyItemGroupLocal] ]); if (currentHistoryItemGroup.remote) { - colorMap.set(currentHistoryItemGroup.remote.name, 1); + colorMap.set(currentHistoryItemGroup.remote.name, historyItemGroupRemote); } if (currentHistoryItemGroup.base) { - colorMap.set(currentHistoryItemGroup.base.name, 2); + colorMap.set(currentHistoryItemGroup.base.name, historyItemGroupBase); } return toISCMHistoryItemViewModelArray(historyItemsElement, colorMap) @@ -4045,15 +4195,15 @@ class SCMTreeDataSource implements IAsyncDataSource)[]> { + async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { const repository = element.historyItemGroup.repository; - const historyProvider = repository.provider.historyProvider; + const historyProvider = repository.provider.historyProvider.get(); if (!historyProvider) { return []; } - const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(repository); + const historyProviderCacheEntry = this._getCacheEntry(repository); const historyItemChangesMap = historyProviderCacheEntry.historyItemChanges; const historyItemParentId = element.parentIds.length > 0 ? element.parentIds[0] : undefined; @@ -4062,9 +4212,7 @@ class SCMTreeDataSource implements IAsyncDataSource 0 ? element.parentIds[0] : undefined; historyItemChanges = await historyProvider.provideHistoryItemChanges(element.id, historyItemParentId) ?? []; - - this.historyProviderCache.set(repository, { - ...historyProviderCacheEntry, + this._updateCacheEntry(repository, { historyItemChanges: historyItemChangesMap.set(`${element.id}/${historyItemParentId}`, historyItemChanges) }); } @@ -4096,6 +4244,159 @@ class SCMTreeDataSource implements IAsyncDataSource(), + historyItems2: new Map(), + historyItemChanges: new Map() + } satisfies ISCMHistoryProviderCacheEntry; + + this._cache.set(repository, entry); + } + + return entry; + } + + private _updateCacheEntry(repository: ISCMRepository, entry: Partial): void { + this._cache.set(repository, { + ...this._getCacheEntry(repository), + ...entry + }); + } + + private _getConfiguration(): { + showChangesSummary: boolean; + showIncomingChanges: ShowChangesSetting; + showOutgoingChanges: ShowChangesSetting; + showHistoryGraph: boolean; + } { + return { + showChangesSummary: this.configurationService.getValue('scm.showChangesSummary'), + showIncomingChanges: this.configurationService.getValue('scm.showIncomingChanges'), + showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges'), + showHistoryGraph: this.configurationService.getValue('scm.showHistoryGraph') + }; + } +} + +class SCMTreeDataSource extends Disposable implements IAsyncDataSource { + constructor( + private readonly viewMode: () => ViewMode, + private readonly historyProviderDataSource: SCMTreeHistoryProviderDataSource, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ISCMViewService private readonly scmViewService: ISCMViewService + ) { + super(); + } + + async getChildren(inputOrElement: ISCMViewService | TreeElement): Promise> { + const repositoryCount = this.scmViewService.visibleRepositories.length; + + const showActionButton = this.configurationService.getValue('scm.showActionButton') === true; + const alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories') === true; + + if (isSCMViewService(inputOrElement) && (repositoryCount > 1 || alwaysShowRepositories)) { + return this.scmViewService.visibleRepositories; + } else if ((isSCMViewService(inputOrElement) && repositoryCount === 1 && !alwaysShowRepositories) || isSCMRepository(inputOrElement)) { + const children: TreeElement[] = []; + + inputOrElement = isSCMRepository(inputOrElement) ? inputOrElement : this.scmViewService.visibleRepositories[0]; + const actionButton = inputOrElement.provider.actionButton; + const resourceGroups = inputOrElement.provider.groups; + + // SCM Input + if (inputOrElement.input.visible) { + children.push(inputOrElement.input); + } + + // Action Button + if (showActionButton && actionButton) { + children.push({ + type: 'actionButton', + repository: inputOrElement, + button: actionButton + } satisfies ISCMActionButton); + } + + // ResourceGroups + const hasSomeChanges = resourceGroups.some(group => group.resources.length > 0); + if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !actionButton))) { + children.push(...resourceGroups); + } + + // History item groups + const historyItemGroups = await this.historyProviderDataSource.getHistoryItemGroups(inputOrElement); + + // Incoming/Outgoing Separator + if (historyItemGroups.length > 0) { + let label = localize('syncSeparatorHeader', "Incoming/Outgoing"); + let ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); + + const incomingHistoryItems = historyItemGroups.find(g => g.direction === 'incoming'); + const outgoingHistoryItems = historyItemGroups.find(g => g.direction === 'outgoing'); + + if (incomingHistoryItems && !outgoingHistoryItems) { + label = localize('syncIncomingSeparatorHeader', "Incoming"); + ariaLabel = localize('syncIncomingSeparatorHeaderAriaLabel', "Incoming changes"); + } else if (!incomingHistoryItems && outgoingHistoryItems) { + label = localize('syncOutgoingSeparatorHeader', "Outgoing"); + ariaLabel = localize('syncOutgoingSeparatorHeaderAriaLabel', "Outgoing changes"); + } + + children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); + } + + children.push(...historyItemGroups); + + // History items + const historyItems = await this.historyProviderDataSource.getHistoryItems2(inputOrElement); + if (historyItems.length > 0) { + const label = localize('syncSeparatorHeader', "Incoming/Outgoing"); + const ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); + + children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); + } + + children.push(...historyItems); + + return children; + } else if (isSCMResourceGroup(inputOrElement)) { + if (this.viewMode() === ViewMode.List) { + // Resources (List) + return inputOrElement.resources; + } else if (this.viewMode() === ViewMode.Tree) { + // Resources (Tree) + const children: TreeElement[] = []; + for (const node of inputOrElement.resourceTree.root.children) { + children.push(node.element && node.childrenCount === 0 ? node.element : node); + } + + return children; + } + } else if (isSCMResourceNode(inputOrElement) || isSCMHistoryItemChangeNode(inputOrElement)) { + // Resources (Tree), History item changes (Tree) + const children: TreeElement[] = []; + for (const node of inputOrElement.children) { + children.push(node.element && node.childrenCount === 0 ? node.element : node); + } + + return children; + } else if (isSCMHistoryItemGroupTreeElement(inputOrElement)) { + // History item group + return this.historyProviderDataSource.getHistoryItems(inputOrElement); + } else if (isSCMHistoryItemTreeElement(inputOrElement)) { + // History item changes (List/Tree) + return this.historyProviderDataSource.getHistoryItemChanges(inputOrElement); + } + + return []; + } + getParent(element: TreeElement): ISCMViewService | TreeElement { if (isSCMResourceNode(element)) { if (element.parent === element.context.resourceTree.root) { @@ -4136,66 +4437,34 @@ class SCMTreeDataSource implements IAsyncDataSource('scm.alwaysShowRepositories'), - showActionButton: this.configurationService.getValue('scm.showActionButton'), - showChangesSummary: this.configurationService.getValue('scm.showChangesSummary'), - showIncomingChanges: this.configurationService.getValue('scm.showIncomingChanges'), - showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges'), - showHistoryGraph: this.configurationService.getValue('scm.experimental.showHistoryGraph') - }; - } - - private onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { - // Added repositories - for (const repository of added) { - const repositoryDisposables = new DisposableStore(); - - repositoryDisposables.add(Event.runAndSubscribe(repository.provider.onDidChangeHistoryProvider, () => { - if (!repository.provider.historyProvider) { - this.logService.debug('SCMTreeDataSource:onDidChangeVisibleRepositories - no history provider present'); - return; - } - - repositoryDisposables.add(repository.provider.historyProvider.onDidChangeCurrentHistoryItemGroup(() => { - this.historyProviderCache.delete(repository); - this.logService.debug('SCMTreeDataSource:onDidChangeCurrentHistoryItemGroup - cache cleared'); - })); - - this.logService.debug('SCMTreeDataSource:onDidChangeVisibleRepositories - onDidChangeCurrentHistoryItemGroup listener added'); - })); - - this.repositoryDisposables.set(repository, repositoryDisposables); + hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { + if (isSCMViewService(inputOrElement)) { + return this.scmViewService.visibleRepositories.length !== 0; + } else if (isSCMRepository(inputOrElement)) { + return true; + } else if (isSCMInput(inputOrElement)) { + return false; + } else if (isSCMActionButton(inputOrElement)) { + return false; + } else if (isSCMResourceGroup(inputOrElement)) { + return true; + } else if (isSCMResource(inputOrElement)) { + return false; + } else if (ResourceTree.isResourceNode(inputOrElement)) { + return inputOrElement.childrenCount > 0; + } else if (isSCMHistoryItemGroupTreeElement(inputOrElement)) { + return true; + } else if (isSCMHistoryItemTreeElement(inputOrElement)) { + return true; + } else if (isSCMHistoryItemViewModelTreeElement(inputOrElement)) { + return false; + } else if (isSCMHistoryItemChangeTreeElement(inputOrElement)) { + return false; + } else if (isSCMViewSeparator(inputOrElement)) { + return false; + } else { + throw new Error('hasChildren not implemented.'); } - - // Removed repositories - for (const repository of removed) { - this.repositoryDisposables.deleteAndDispose(repository); - this.historyProviderCache.delete(repository); - } - } - - private getHistoryProviderCacheEntry(repository: ISCMRepository): ISCMHistoryProviderCacheEntry { - return this.historyProviderCache.get(repository) ?? { - incomingHistoryItemGroup: undefined, - outgoingHistoryItemGroup: undefined, - historyItems: new Map(), - historyItems2: new Map(), - historyItemChanges: new Map() - }; - } - - dispose(): void { - this.repositoryDisposables.dispose(); - this.disposables.dispose(); } } @@ -4279,27 +4548,4 @@ export class SCMActionButton implements IDisposable { } } -// Override styles in selections.ts -registerThemingParticipant((theme, collector) => { - const selectionBackgroundColor = theme.getColor(selectionBackground); - - if (selectionBackgroundColor) { - // Override inactive selection bg - const inputBackgroundColor = theme.getColor(inputBackground); - if (inputBackgroundColor) { - collector.addRule(`.scm-view .scm-editor-container .monaco-editor-background { background-color: ${inputBackgroundColor}; } `); - collector.addRule(`.scm-view .scm-editor-container .monaco-editor .selected-text { background-color: ${inputBackgroundColor.transparent(0.4)}; }`); - } - - // Override selected fg - const inputForegroundColor = theme.getColor(inputForeground); - if (inputForegroundColor) { - collector.addRule(`.scm-view .scm-editor-container .monaco-editor .view-line span.inline-selected-text { color: ${inputForegroundColor}; }`); - } - - collector.addRule(`.scm-view .scm-editor-container .monaco-editor .focused .selected-text { background-color: ${selectionBackgroundColor}; }`); - } else { - // Use editor selection color if theme has not set a selection background color - collector.addRule(`.scm-view .scm-editor-container .monaco-editor .focused .selected-text { background-color: ${theme.getColor(editorSelectionBackground)}; }`); - } -}); +setupSimpleEditorSelectionStyling('.scm-view .scm-editor-container'); diff --git a/src/vs/workbench/contrib/scm/browser/workingSet.ts b/src/vs/workbench/contrib/scm/browser/workingSet.ts index 274bd713910..3befe2a6713 100644 --- a/src/vs/workbench/contrib/scm/browser/workingSet.ts +++ b/src/vs/workbench/contrib/scm/browser/workingSet.ts @@ -64,8 +64,8 @@ export class SCMWorkingSetController extends Disposable implements IWorkbenchCon const disposables = new DisposableStore(); disposables.add(autorun(async reader => { - const historyProvider = repository.provider.historyProviderObs.read(reader); - const currentHistoryItemGroupId = historyProvider?.currentHistoryItemGroupObs.read(reader)?.id; + const historyProvider = repository.provider.historyProvider.read(reader); + const currentHistoryItemGroupId = historyProvider?.currentHistoryItemGroupId.read(reader); if (!currentHistoryItemGroupId) { return; diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 83d983824f3..7e6bc762d0a 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; import { IObservable } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IMenu } from 'vs/platform/actions/common/actions'; +import { ColorIdentifier } from 'vs/platform/theme/common/colorUtils'; import { ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; export interface ISCMHistoryProviderMenus { @@ -15,21 +15,20 @@ export interface ISCMHistoryProviderMenus { getHistoryItemGroupContextMenu(historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu; getHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu; + getHistoryItemMenu2(historyItem: SCMHistoryItemViewModelTreeElement): IMenu; } export interface ISCMHistoryProvider { - - readonly onDidChangeCurrentHistoryItemGroup: Event; - - get currentHistoryItemGroup(): ISCMHistoryItemGroup | undefined; - set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined); - readonly currentHistoryItemGroupObs: IObservable; + readonly currentHistoryItemGroupId: IObservable; + readonly currentHistoryItemGroupName: IObservable; + readonly currentHistoryItemGroup: IObservable; provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise; provideHistoryItems2(options: ISCMHistoryOptions): Promise; provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined>; + resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds: string[]): Promise; } export interface ISCMHistoryProviderCacheEntry { @@ -49,6 +48,7 @@ export interface ISCMHistoryOptions { export interface ISCMHistoryItemGroup { readonly id: string; readonly name: string; + readonly revision?: string; readonly base?: Omit, 'remote'>; readonly remote?: Omit, 'remote'>; } @@ -90,7 +90,7 @@ export interface ISCMHistoryItem { export interface ISCMHistoryItemGraphNode { readonly id: string; - readonly color: number; + readonly color: ColorIdentifier; } export interface ISCMHistoryItemViewModel { diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 5bcd1c6fbe7..d4f812e822c 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -75,9 +75,7 @@ export interface ISCMProvider extends IDisposable { readonly inputBoxTextModel: ITextModel; readonly count: IObservable; readonly commitTemplate: IObservable; - readonly historyProvider?: ISCMHistoryProvider; - readonly historyProviderObs: IObservable; - readonly onDidChangeHistoryProvider: Event; + readonly historyProvider: IObservable; readonly acceptInputCommand?: Command; readonly actionButton?: ISCMActionButtonDescriptor; readonly statusBarCommands: IObservable; diff --git a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts index 5c64ccda402..bb326f513fd 100644 --- a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts +++ b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts @@ -5,7 +5,8 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory'; +import { ColorIdentifier } from 'vs/platform/theme/common/colorUtils'; +import { colorRegistry, historyItemGroupBase, historyItemGroupLocal, historyItemGroupRemote, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory'; import { ISCMHistoryItem } from 'vs/workbench/contrib/scm/common/history'; suite('toISCMHistoryItemViewModelArray', () => { @@ -61,39 +62,39 @@ suite('toISCMHistoryItemViewModelArray', () => { assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, colorRegistry[0]); // node b assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[1].outputSwimlanes.length, 1); assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); - assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, colorRegistry[0]); // node c assert.strictEqual(viewModels[2].inputSwimlanes.length, 1); assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); - assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[2].outputSwimlanes.length, 1); assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, colorRegistry[0]); // node d assert.strictEqual(viewModels[3].inputSwimlanes.length, 1); assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, colorRegistry[0]); // node e assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[4].outputSwimlanes.length, 0); }); @@ -125,50 +126,50 @@ suite('toISCMHistoryItemViewModelArray', () => { assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, colorRegistry[0]); // node b assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); - assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, colorRegistry[1]); // node d assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); - assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'c'); - assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'c'); - assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, colorRegistry[1]); // node c assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'c'); - assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'c'); - assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, colorRegistry[0]); // node e assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[4].outputSwimlanes.length, 1); assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'f'); - assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, colorRegistry[0]); }); /** @@ -200,72 +201,72 @@ suite('toISCMHistoryItemViewModelArray', () => { assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); - assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, colorRegistry[1]); // node c assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); - assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, colorRegistry[1]); // node b assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, colorRegistry[1]); // node e assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'f'); - assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, colorRegistry[1]); // node f assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'f'); - assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, colorRegistry[1]); // node d assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[5].outputSwimlanes.length, 1); assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g'); - assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, colorRegistry[0]); }); /** @@ -298,74 +299,74 @@ suite('toISCMHistoryItemViewModelArray', () => { assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); - assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, colorRegistry[1]); // node c assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); - assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'b'); - assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, colorRegistry[1]); // node b assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'b'); - assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'e'); - assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, colorRegistry[2]); // node e assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'e'); - assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, colorRegistry[2]); assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'f'); - assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, colorRegistry[2]); // node f assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'f'); - assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, colorRegistry[2]); assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'g'); - assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, colorRegistry[2]); // node d assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'g'); - assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, colorRegistry[2]); assert.strictEqual(viewModels[5].outputSwimlanes.length, 2); assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'h'); - assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'g'); - assert.strictEqual(viewModels[5].outputSwimlanes[1].color, 2); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, colorRegistry[2]); }); @@ -404,100 +405,188 @@ suite('toISCMHistoryItemViewModelArray', () => { assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); - assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, colorRegistry[1]); // node c assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); - assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, colorRegistry[1]); // node b assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[2].outputSwimlanes.length, 3); assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[2].outputSwimlanes[2].id, 'f'); - assert.strictEqual(viewModels[2].outputSwimlanes[2].color, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[2].color, colorRegistry[2]); // node f assert.strictEqual(viewModels[3].inputSwimlanes.length, 3); assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[3].inputSwimlanes[2].id, 'f'); - assert.strictEqual(viewModels[3].inputSwimlanes[2].color, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[2].color, colorRegistry[2]); assert.strictEqual(viewModels[3].outputSwimlanes.length, 3); assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[3].outputSwimlanes[2].id, 'g'); - assert.strictEqual(viewModels[3].outputSwimlanes[2].color, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[2].color, colorRegistry[2]); // node e assert.strictEqual(viewModels[4].inputSwimlanes.length, 3); assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[4].inputSwimlanes[2].id, 'g'); - assert.strictEqual(viewModels[4].inputSwimlanes[2].color, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[2].color, colorRegistry[2]); assert.strictEqual(viewModels[4].outputSwimlanes.length, 3); assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'g'); - assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[4].outputSwimlanes[2].id, 'g'); - assert.strictEqual(viewModels[4].outputSwimlanes[2].color, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[2].color, colorRegistry[2]); // node d assert.strictEqual(viewModels[5].inputSwimlanes.length, 3); assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'g'); - assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'd'); - assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[5].inputSwimlanes[2].id, 'g'); - assert.strictEqual(viewModels[5].inputSwimlanes[2].color, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[2].color, colorRegistry[2]); assert.strictEqual(viewModels[5].outputSwimlanes.length, 3); assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g'); - assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'g'); - assert.strictEqual(viewModels[5].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[5].outputSwimlanes[2].id, 'g'); - assert.strictEqual(viewModels[5].outputSwimlanes[2].color, 2); + assert.strictEqual(viewModels[5].outputSwimlanes[2].color, colorRegistry[2]); // node g assert.strictEqual(viewModels[6].inputSwimlanes.length, 3); assert.strictEqual(viewModels[6].inputSwimlanes[0].id, 'g'); - assert.strictEqual(viewModels[6].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[6].inputSwimlanes[0].color, colorRegistry[0]); assert.strictEqual(viewModels[6].inputSwimlanes[1].id, 'g'); - assert.strictEqual(viewModels[6].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[6].inputSwimlanes[1].color, colorRegistry[1]); assert.strictEqual(viewModels[6].inputSwimlanes[2].id, 'g'); - assert.strictEqual(viewModels[6].inputSwimlanes[2].color, 2); + assert.strictEqual(viewModels[6].inputSwimlanes[2].color, colorRegistry[2]); assert.strictEqual(viewModels[6].outputSwimlanes.length, 1); assert.strictEqual(viewModels[6].outputSwimlanes[0].id, 'h'); - assert.strictEqual(viewModels[6].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[6].outputSwimlanes[0].color, colorRegistry[0]); + }); + + /** + * * a(b) [topic] + * * b(c) + * * c(d) [origin/topic] + * * d(e) + * * e(f,g) + * |\ + * | * g(h) [origin/main] + */ + test('graph with color map', () => { + const models = [ + { id: 'a', parentIds: ['b'], labels: [{ title: 'topic' }] }, + { id: 'b', parentIds: ['c'] }, + { id: 'c', parentIds: ['d'], labels: [{ title: 'origin/topic' }] }, + { id: 'd', parentIds: ['e'] }, + { id: 'e', parentIds: ['f', 'g'] }, + { id: 'g', parentIds: ['h'], labels: [{ title: 'origin/main' }] } + ] as ISCMHistoryItem[]; + + const colorMap = new Map([ + ['topic', historyItemGroupLocal], + ['origin/topic', historyItemGroupRemote], + ['origin/main', historyItemGroupBase], + ]); + + const viewModels = toISCMHistoryItemViewModelArray(models, colorMap); + + assert.strictEqual(viewModels.length, 6); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, historyItemGroupLocal); + + // node b + assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, historyItemGroupLocal); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, historyItemGroupLocal); + + // node c + assert.strictEqual(viewModels[2].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, historyItemGroupLocal); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, historyItemGroupRemote); + + // node d + assert.strictEqual(viewModels[3].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, historyItemGroupRemote); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, historyItemGroupRemote); + + // node e + assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, historyItemGroupRemote); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, historyItemGroupBase); + + // node g + assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, historyItemGroupBase); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'h'); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, historyItemGroupBase); }); }); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 74f304ba7a3..65e79d238a7 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -87,7 +87,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | undefined = undefined; + picker: IQuickPick | undefined = undefined; editorViewState = this._register(this.instantiationService.createInstance(PickerEditorState)); @@ -109,7 +109,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider): void { + set(picker: IQuickPick): void { // Picker for this run this.picker = picker; @@ -188,7 +188,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider, token: CancellationToken, runOptions?: AnythingQuickAccessProviderRunOptions): IDisposable { + override provide(picker: IQuickPick, token: CancellationToken, runOptions?: AnythingQuickAccessProviderRunOptions): IDisposable { const disposables = new DisposableStore(); // Update the pick state for this run diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts index aa71685ae21..6a54da2e050 100644 --- a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -17,7 +17,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchCompressibleObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; import { FastAndSlowPicks, IPickerQuickAccessItem, IPickerQuickAccessSeparator, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { DefaultQuickAccessFilterValue, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; -import { IKeyMods, IQuickPick, IQuickPickItem, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPick, IQuickPickItem, QuickInputButtonLocation, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { searchDetailsIcon, searchOpenInFileIcon, searchActivityBarIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -32,6 +32,7 @@ import { PickerEditorState } from 'vs/workbench/browser/quickaccess'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { Sequencer } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; +import { Codicon } from 'vs/base/common/codicons'; export const TEXT_SEARCH_QUICK_ACCESS_PREFIX = '%'; @@ -99,15 +100,18 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { + override provide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const disposables = new DisposableStore(); if (TEXT_SEARCH_QUICK_ACCESS_PREFIX.length < picker.value.length) { picker.valueSelection = [TEXT_SEARCH_QUICK_ACCESS_PREFIX.length, picker.value.length]; } - picker.customButton = true; - picker.customLabel = '$(go-to-search)'; + picker.buttons = [{ + location: QuickInputButtonLocation.Inline, + iconClass: ThemeIcon.asClassName(Codicon.goToSearch), + tooltip: localize('goToSearch', "See in Search Panel") + }]; this.editorViewState.reset(); - disposables.add(picker.onDidCustom(() => { + disposables.add(picker.onDidTriggerButton(() => { if (this.searchModel.searchResult.count() > 0) { this.moveToSearchViewlet(undefined); } else { diff --git a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts index a16f5a19f44..a1fb65de21b 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts @@ -14,7 +14,6 @@ import * as Constants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; import { FileMatch, FolderMatchWithResource, Match, RenderableMatch } from 'vs/workbench/contrib/search/browser/searchModel'; import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; import { URI } from 'vs/base/common/uri'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -34,6 +33,7 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { Schemas } from 'vs/base/common/network'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; //#region Interfaces @@ -309,7 +309,6 @@ function expandSelectSubtree(accessor: ServicesAccessor) { } async function searchWithFolderCommand(accessor: ServicesAccessor, isFromExplorer: boolean, isIncludes: boolean, resource?: URI, folderMatch?: FolderMatchWithResource) { - const listService = accessor.get(IListService); const fileService = accessor.get(IFileService); const viewsService = accessor.get(IViewsService); const contextService = accessor.get(IWorkspaceContextService); @@ -320,9 +319,9 @@ async function searchWithFolderCommand(accessor: ServicesAccessor, isFromExplore let resources: URI[]; if (isFromExplorer) { - resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); + resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); } else { - const searchView = getSearchView(accessor.get(IViewsService)); + const searchView = getSearchView(viewsService); if (!searchView) { return; } diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 121512e94be..dd790ca14f5 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -235,7 +235,7 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe SearchContext.FileFocusKey.bindTo(contextKeyServiceMain).set(true); SearchContext.FolderFocusKey.bindTo(contextKeyServiceMain).set(false); - const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain])); + const instantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain]))); const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { menuOptions: { shouldForwardArgs: true @@ -327,7 +327,7 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender SearchContext.FileFocusKey.bindTo(contextKeyServiceMain).set(false); SearchContext.FolderFocusKey.bindTo(contextKeyServiceMain).set(false); - const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain])); + const instantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain]))); const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { menuOptions: { shouldForwardArgs: true diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 662d7fb9019..518b7555f10 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -480,9 +480,10 @@ export class SearchView extends ViewPane { this.queryDetails = dom.append(this.searchWidgetsContainerElement, $('.query-details')); // Toggle query details button + const toggleQueryDetailsLabel = nls.localize('moreSearch', "Toggle Search Details"); this.toggleQueryDetailsButton = dom.append(this.queryDetails, - $('.more' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button' })); - this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, nls.localize('moreSearch', "Toggle Search Details"))); + $('.more' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button', 'aria-label': toggleQueryDetailsLabel })); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, toggleQueryDetailsLabel)); this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.CLICK, e => { dom.EventHelper.stop(e); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index c3577b4e873..335c4d19590 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -165,8 +165,9 @@ export class SearchEditor extends AbstractTextCodeEditor this.includesExcludesContainer = DOM.append(container, DOM.$('.includes-excludes')); // Toggle query details button - this.toggleQueryDetailsButton = DOM.append(this.includesExcludesContainer, DOM.$('.expand' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button' })); - this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, localize('moreSearch', "Toggle Search Details"))); + const toggleQueryDetailsLabel = localize('moreSearch', "Toggle Search Details"); + this.toggleQueryDetailsButton = DOM.append(this.includesExcludesContainer, DOM.$('.expand' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button', 'aria-label': toggleQueryDetailsLabel })); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, toggleQueryDetailsLabel)); this._register(DOM.addDisposableListener(this.toggleQueryDetailsButton, DOM.EventType.CLICK, e => { DOM.EventHelper.stop(e); this.toggleIncludesExcludes(); diff --git a/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts b/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts index 192845aa528..30946f93485 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts @@ -77,7 +77,7 @@ export async function pickSnippet(accessor: ServicesAccessor, languageIdOrSnippe return result; }; - const picker = quickInputService.createQuickPick(); + const picker = quickInputService.createQuickPick({ useSeparators: true }); picker.placeholder = nls.localize('pick.placeholder', "Select a snippet"); picker.matchOnDetail = true; picker.ignoreFocusOut = false; diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index 1fd7dd05886..f83c86942d5 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -160,6 +160,7 @@ const ModulesToLookFor = [ '@azure/attestation', '@azure/data-tables', '@azure/arm-appservice', + '@azure-rest/ai-inference', '@azure-rest/arm-appservice', '@azure/arm-appcontainers', '@azure/arm-rediscache', @@ -229,8 +230,8 @@ const ModulesToLookFor = [ '@azure/cognitiveservices-customvision-training', '@azure/cognitiveservices-face', '@azure/cognitiveservices-translatortext', - 'microsoft-cognitiveservices-speech-sdk' - + 'microsoft-cognitiveservices-speech-sdk', + '@google/generative-ai' ]; const PyMetaModulesToLookFor = [ @@ -249,6 +250,7 @@ const PyMetaModulesToLookFor = [ const PyModulesToLookFor = [ 'azure', + 'azure-ai-inference', 'azure-ai-language-conversations', 'azure-ai-language-questionanswering', 'azure-ai-ml', @@ -394,7 +396,8 @@ const PyModulesToLookFor = [ 'azure-cognitiveservices-vision-contentmoderator', 'azure-cognitiveservices-vision-face', 'azure-mgmt-cognitiveservices', - 'azure-mgmt-search' + 'azure-mgmt-search', + 'google-generativeai' ]; const GoModulesToLookFor = [ @@ -670,6 +673,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.@azure/communication-administration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/attestation" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/data-tables" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-inference" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure-rest/arm-appservice" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/arm-appservice" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/arm-appcontainers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -691,6 +695,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.@azure/arm-kubernetesconfiguration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.react-native-macos" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.react-native-windows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@google/generative-ai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.bower" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.yeoman.code.ext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.cordova.high" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -759,6 +764,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.pulumi-azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ai-inference" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai-language-conversations" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai-language-questionanswering" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai-ml" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -911,6 +917,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.trulens" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.trulens-eval" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.wandb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.google-generativeai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index e11e8a81fc3..fdde273c878 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -55,7 +55,7 @@ import { getTemplates as getTaskTemplates } from 'vs/workbench/contrib/tasks/com import * as TaskConfig from '../common/taskConfiguration'; import { TerminalTaskSystem } from './terminalTaskSystem'; -import { IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickSeparator, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; @@ -234,6 +234,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private _onDidChangeTaskConfig: Emitter = new Emitter(); public onDidChangeTaskConfig: Event = this._onDidChangeTaskConfig.event; public get isReconnected(): boolean { return this._tasksReconnected; } + private _onDidChangeTaskProviders = this._register(new Emitter()); + public onDidChangeTaskProviders = this._onDidChangeTaskProviders.event; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -316,15 +318,22 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this._registerCommands().then(() => TaskCommandsRegistered.bindTo(this._contextKeyService).set(true)); ServerlessWebContext.bindTo(this._contextKeyService).set(Platform.isWeb && !remoteAgentService.getConnection()?.remoteAuthority); this._configurationResolverService.contributeVariable('defaultBuildTask', async (): Promise => { - let tasks = await this._getTasksForGroup(TaskGroup.Build); + // delay provider activation, we might find a single default build task in the tasks.json file + let tasks = await this._getTasksForGroup(TaskGroup.Build, true); if (tasks.length > 0) { const defaults = this._getDefaultTasks(tasks); if (defaults.length === 1) { return defaults[0]._label; - } else if (defaults.length) { - tasks = defaults; } } + // activate all providers, we haven't found the default build task in the tasks.json file + tasks = await this._getTasksForGroup(TaskGroup.Build); + const defaults = this._getDefaultTasks(tasks); + if (defaults.length === 1) { + return defaults[0]._label; + } else if (defaults.length) { + tasks = defaults; + } let entry: ITaskQuickPickEntry | null | undefined; if (tasks && tasks.length > 0) { @@ -608,6 +617,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer // We need to first wait for extensions to be registered because we might read // the `TaskDefinitionRegistry` in case `type` is `undefined` await this._extensionService.whenInstalledExtensionsRegistered(); + this._log('Activating task providers ' + (type ?? 'all')); await raceTimeout( Promise.all(this._getActivationEvents(type).map(activationEvent => this._extensionService.activateByEvent(activationEvent))), 5000, @@ -672,10 +682,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const handle = AbstractTaskService._nextHandle++; this._providers.set(handle, provider); this._providerTypes.set(handle, type); + this._onDidChangeTaskProviders.fire(); return { dispose: () => { this._providers.delete(handle); this._providerTypes.delete(handle); + this._onDidChangeTaskProviders.fire(); } }; } @@ -870,29 +882,15 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!this._versionAndEngineCompatible(filter)) { return Promise.resolve([]); } - return this._getGroupedTasks(filter).then((map) => { - if (!filter || !filter.type) { - return map.all(); - } - const result: Task[] = []; - map.forEach((tasks) => { - for (const task of tasks) { - if (ContributedTask.is(task) && ((task.defines.type === filter.type) || (task._source.label === filter.type))) { - result.push(task); - } else if (CustomTask.is(task)) { - if (task.type === filter.type) { - result.push(task); - } else { - const customizes = task.customizes(); - if (customizes && customizes.type === filter.type) { - result.push(task); - } - } - } - } - }); - return result; - }); + return this._getGroupedTasks(filter).then((map) => this.applyFilterToTaskMap(filter, map)); + } + + public async getKnownTasks(filter?: ITaskFilter): Promise { + if (!this._versionAndEngineCompatible(filter)) { + return Promise.resolve([]); + } + + return this._getGroupedTasks(filter, true, true).then((map) => this.applyFilterToTaskMap(filter, map)); } public taskTypes(): string[] { @@ -955,6 +953,30 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return this._recentlyUsedTasksV1; } + private applyFilterToTaskMap(filter: ITaskFilter | undefined, map: TaskMap): Task[] { + if (!filter || !filter.type) { + return map.all(); + } + const result: Task[] = []; + map.forEach((tasks) => { + for (const task of tasks) { + if (ContributedTask.is(task) && ((task.defines.type === filter.type) || (task._source.label === filter.type))) { + result.push(task); + } else if (CustomTask.is(task)) { + if (task.type === filter.type) { + result.push(task); + } else { + const customizes = task.customizes(); + if (customizes && customizes.type === filter.type) { + result.push(task); + } + } + } + } + }); + return result; + } + private _getTasksFromStorage(type: 'persistent' | 'historical'): LRUCache { return type === 'persistent' ? this._getPersistentTasks() : this._getRecentTasks(); } @@ -1418,8 +1440,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return task; } - private async _getTasksForGroup(group: TaskGroup): Promise { - const groups = await this._getGroupedTasks(); + private async _getTasksForGroup(group: TaskGroup, waitToActivate?: boolean): Promise { + const groups = await this._getGroupedTasks(undefined, waitToActivate); const result: Task[] = []; groups.forEach(tasks => { for (const task of tasks) { @@ -2001,11 +2023,13 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return !definition || !definition.when || this._contextKeyService.contextMatchesRules(definition.when); } - private async _getGroupedTasks(filter?: ITaskFilter): Promise { + private async _getGroupedTasks(filter?: ITaskFilter, waitToActivate?: boolean, knownOnlyOrTrusted?: boolean): Promise { await this._waitForAllSupportedExecutions; const type = filter?.type; const needsRecentTasksMigration = this._needsRecentTasksMigration(); - await this._activateTaskProviders(filter?.type); + if (!waitToActivate) { + await this._activateTaskProviders(filter?.type); + } const validTypes: IStringDictionary = Object.create(null); TaskDefinitionRegistry.all().forEach(definition => validTypes[definition.taskType] = true); validTypes['shell'] = true; @@ -2086,123 +2110,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } try { - const customTasks = await this.getWorkspaceTasks(); - const customTasksKeyValuePairs = Array.from(customTasks); - const customTasksPromises = customTasksKeyValuePairs.map(async ([key, folderTasks]) => { - const contributed = contributedTasks.get(key); - if (!folderTasks.set) { - if (contributed) { - result.add(key, ...contributed); - } - return; - } - - if (this._contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - result.add(key, ...folderTasks.set.tasks); - } else { - const configurations = folderTasks.configurations; - const legacyTaskConfigurations = folderTasks.set ? this._getLegacyTaskConfigurations(folderTasks.set) : undefined; - const customTasksToDelete: Task[] = []; - if (configurations || legacyTaskConfigurations) { - const unUsedConfigurations: Set = new Set(); - if (configurations) { - Object.keys(configurations.byIdentifier).forEach(key => unUsedConfigurations.add(key)); - } - for (const task of contributed) { - if (!ContributedTask.is(task)) { - continue; - } - if (configurations) { - const configuringTask = configurations.byIdentifier[task.defines._key]; - if (configuringTask) { - unUsedConfigurations.delete(task.defines._key); - result.add(key, TaskConfig.createCustomTask(task, configuringTask)); - } else { - result.add(key, task); - } - } else if (legacyTaskConfigurations) { - const configuringTask = legacyTaskConfigurations[task.defines._key]; - if (configuringTask) { - result.add(key, TaskConfig.createCustomTask(task, configuringTask)); - customTasksToDelete.push(configuringTask); - } else { - result.add(key, task); - } - } else { - result.add(key, task); - } - } - if (customTasksToDelete.length > 0) { - const toDelete = customTasksToDelete.reduce>((map, task) => { - map[task._id] = true; - return map; - }, Object.create(null)); - for (const task of folderTasks.set.tasks) { - if (toDelete[task._id]) { - continue; - } - result.add(key, task); - } - } else { - result.add(key, ...folderTasks.set.tasks); - } - - const unUsedConfigurationsAsArray = Array.from(unUsedConfigurations); - - const unUsedConfigurationPromises = unUsedConfigurationsAsArray.map(async (value) => { - const configuringTask = configurations!.byIdentifier[value]; - if (type && (type !== configuringTask.configures.type)) { - return; - } - - let requiredTaskProviderUnavailable: boolean = false; - - for (const [handle, provider] of this._providers) { - const providerType = this._providerTypes.get(handle); - if (configuringTask.type === providerType) { - if (providerType && !this._isTaskProviderEnabled(providerType)) { - requiredTaskProviderUnavailable = true; - continue; - } - - try { - const resolvedTask = await provider.resolveTask(configuringTask); - if (resolvedTask && (resolvedTask._id === configuringTask._id)) { - result.add(key, TaskConfig.createCustomTask(resolvedTask, configuringTask)); - return; - } - } catch (error) { - // Ignore errors. The task could not be provided by any of the providers. - } - } - } - - if (requiredTaskProviderUnavailable) { - this._log(nls.localize( - 'TaskService.providerUnavailable', - 'Warning: {0} tasks are unavailable in the current environment.', - configuringTask.configures.type - )); - } else { - this._log(nls.localize( - 'TaskService.noConfiguration', - 'Error: The {0} task detection didn\'t contribute a task for the following configuration:\n{1}\nThe task will be ignored.', - configuringTask.configures.type, - JSON.stringify(configuringTask._source.config.element, undefined, 4) - )); - this._showOutput(); - } - }); - - await Promise.all(unUsedConfigurationPromises); - } else { - result.add(key, ...folderTasks.set.tasks); - result.add(key, ...contributed); - } - } - }); - - await Promise.all(customTasksPromises); + let tasks: [string, IWorkspaceFolderTaskResult][] = []; + // prevent workspace trust dialog from being shown in unexpected cases #224881 + if (!knownOnlyOrTrusted || this._workspaceTrustManagementService.isWorkspaceTrusted()) { + tasks = Array.from(await this.getWorkspaceTasks()); + } + await Promise.all(this._getCustomTaskPromises(tasks, filter, result, contributedTasks, waitToActivate)); if (needsRecentTasksMigration) { // At this point we have all the tasks and can migrate the recently used tasks. await this._migrateRecentTasks(result.all()); @@ -2222,6 +2135,120 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return result; } } + private _getCustomTaskPromises(customTasksKeyValuePairs: [string, IWorkspaceFolderTaskResult][], filter: ITaskFilter | undefined, result: TaskMap, contributedTasks: TaskMap, waitToActivate: boolean | undefined) { + return customTasksKeyValuePairs.map(async ([key, folderTasks]) => { + const contributed = contributedTasks.get(key); + if (!folderTasks.set) { + if (contributed) { + result.add(key, ...contributed); + } + return; + } + + if (this._contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + result.add(key, ...folderTasks.set.tasks); + } else { + const configurations = folderTasks.configurations; + const legacyTaskConfigurations = folderTasks.set ? this._getLegacyTaskConfigurations(folderTasks.set) : undefined; + const customTasksToDelete: Task[] = []; + if (configurations || legacyTaskConfigurations) { + const unUsedConfigurations: Set = new Set(); + if (configurations) { + Object.keys(configurations.byIdentifier).forEach(key => unUsedConfigurations.add(key)); + } + for (const task of contributed) { + if (!ContributedTask.is(task)) { + continue; + } + if (configurations) { + const configuringTask = configurations.byIdentifier[task.defines._key]; + if (configuringTask) { + unUsedConfigurations.delete(task.defines._key); + result.add(key, TaskConfig.createCustomTask(task, configuringTask)); + } else { + result.add(key, task); + } + } else if (legacyTaskConfigurations) { + const configuringTask = legacyTaskConfigurations[task.defines._key]; + if (configuringTask) { + result.add(key, TaskConfig.createCustomTask(task, configuringTask)); + customTasksToDelete.push(configuringTask); + } else { + result.add(key, task); + } + } else { + result.add(key, task); + } + } + if (customTasksToDelete.length > 0) { + const toDelete = customTasksToDelete.reduce>((map, task) => { + map[task._id] = true; + return map; + }, Object.create(null)); + for (const task of folderTasks.set.tasks) { + if (toDelete[task._id]) { + continue; + } + result.add(key, task); + } + } else { + result.add(key, ...folderTasks.set.tasks); + } + + const unUsedConfigurationsAsArray = Array.from(unUsedConfigurations); + + const unUsedConfigurationPromises = unUsedConfigurationsAsArray.map(async (value) => { + const configuringTask = configurations!.byIdentifier[value]; + if (filter?.type && (filter.type !== configuringTask.configures.type)) { + return; + } + + let requiredTaskProviderUnavailable: boolean = false; + + for (const [handle, provider] of this._providers) { + const providerType = this._providerTypes.get(handle); + if (configuringTask.type === providerType) { + if (providerType && !this._isTaskProviderEnabled(providerType)) { + requiredTaskProviderUnavailable = true; + continue; + } + + try { + const resolvedTask = await provider.resolveTask(configuringTask); + if (resolvedTask && (resolvedTask._id === configuringTask._id)) { + result.add(key, TaskConfig.createCustomTask(resolvedTask, configuringTask)); + return; + } + } catch (error) { + // Ignore errors. The task could not be provided by any of the providers. + } + } + } + if (requiredTaskProviderUnavailable) { + this._log(nls.localize( + 'TaskService.providerUnavailable', + 'Warning: {0} tasks are unavailable in the current environment.', + configuringTask.configures.type + )); + } else if (!waitToActivate) { + this._log(nls.localize( + 'TaskService.noConfiguration', + 'Error: The {0} task detection didn\'t contribute a task for the following configuration:\n{1}\nThe task will be ignored.', + configuringTask.configures.type, + JSON.stringify(configuringTask._source.config.element, undefined, 4) + )); + this._showOutput(); + } + }); + + await Promise.all(unUsedConfigurationPromises); + } else { + result.add(key, ...folderTasks.set.tasks); + result.add(key, ...contributed); + } + } + }); + } private _getLegacyTaskConfigurations(workspaceTasks: ITaskSet): IStringDictionary | undefined { let result: IStringDictionary | undefined; @@ -2718,7 +2745,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer entries.push(additionalEntries[0]); } - const picker: IQuickPick = this._quickInputService.createQuickPick(); + const picker = this._quickInputService.createQuickPick({ useSeparators: true }); picker.placeholder = placeHolder; picker.matchOnDescription = true; if (name) { diff --git a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts index fe0e1641dc1..1d8ec3e1889 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts @@ -229,7 +229,7 @@ export class TaskQuickPick extends Disposable { } public async show(placeHolder: string, defaultEntry?: ITaskQuickPickEntry, startAtType?: string, name?: string): Promise { - const picker: IQuickPick = this._quickInputService.createQuickPick(); + const picker = this._quickInputService.createQuickPick({ useSeparators: true }); picker.placeholder = placeHolder; picker.matchOnDescription = true; picker.ignoreFocusOut = false; @@ -308,7 +308,7 @@ export class TaskQuickPick extends Disposable { - private async _doPickerFirstLevel(picker: IQuickPick, taskQuickPickEntries: QuickPickInput[]): Promise { + private async _doPickerFirstLevel(picker: IQuickPick, taskQuickPickEntries: QuickPickInput[]): Promise { picker.items = taskQuickPickEntries; showWithPinnedItems(this._storageService, runTaskStorageKey, picker, true); const firstLevelPickerResult = await new Promise(resolve => { @@ -319,7 +319,7 @@ export class TaskQuickPick extends Disposable { return firstLevelPickerResult?.task; } - public async doPickerSecondLevel(picker: IQuickPick, type: string, name?: string) { + public async doPickerSecondLevel(picker: IQuickPick, type: string, name?: string) { picker.busy = true; if (type === SHOW_ALL) { const items = (await this._taskService.tasks()).filter(t => !t.configurationProperties.hide).sort((a, b) => this._sorter.compare(a, b)).map(task => this._createTaskEntry(task)); diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 12ed79506ab..6919111b4c6 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -562,9 +562,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return { exitCode: 0 }; }); }).finally(() => { - if (this._activeTasks[mapKey] === activeTask) { - delete this._activeTasks[mapKey]; - } + delete this._activeTasks[mapKey]; }); const lastInstance = this._getInstances(task).pop(); const count = lastInstance?.count ?? { count: 0 }; @@ -1046,9 +1044,9 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { this._viewsService.openView(Markers.MARKERS_VIEW_ID); } else if (task.command.presentation && (task.command.presentation.focus || task.command.presentation.reveal === RevealKind.Always)) { this._terminalService.setActiveInstance(terminal); - await this._terminalService.revealActiveTerminal(); + await this._terminalService.revealTerminal(terminal); if (task.command.presentation.focus) { - this._terminalService.focusActiveInstance(); + this._terminalService.focusInstance(terminal); } } this._activeTasks[task.getMapKey()].terminal = terminal; diff --git a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts index 0bb6027fcb2..591f201162d 100644 --- a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts +++ b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts @@ -1195,6 +1195,212 @@ export namespace Schemas { } } }; + + export const WatchingPattern: IJSONSchema = { + type: 'object', + additionalProperties: false, + properties: { + regexp: { + type: 'string', + description: localize('WatchingPatternSchema.regexp', 'The regular expression to detect the begin or end of a background task.') + }, + file: { + type: 'integer', + description: localize('WatchingPatternSchema.file', 'The match group index of the filename. Can be omitted.') + }, + } + }; + + export const PatternType: IJSONSchema = { + anyOf: [ + { + type: 'string', + description: localize('PatternTypeSchema.name', 'The name of a contributed or predefined pattern') + }, + Schemas.ProblemPattern, + Schemas.MultiLineProblemPattern + ], + description: localize('PatternTypeSchema.description', 'A problem pattern or the name of a contributed or predefined problem pattern. Can be omitted if base is specified.') + }; + + export const ProblemMatcher: IJSONSchema = { + type: 'object', + additionalProperties: false, + properties: { + base: { + type: 'string', + description: localize('ProblemMatcherSchema.base', 'The name of a base problem matcher to use.') + }, + owner: { + type: 'string', + description: localize('ProblemMatcherSchema.owner', 'The owner of the problem inside Code. Can be omitted if base is specified. Defaults to \'external\' if omitted and base is not specified.') + }, + source: { + type: 'string', + description: localize('ProblemMatcherSchema.source', 'A human-readable string describing the source of this diagnostic, e.g. \'typescript\' or \'super lint\'.') + }, + severity: { + type: 'string', + enum: ['error', 'warning', 'info'], + description: localize('ProblemMatcherSchema.severity', 'The default severity for captures problems. Is used if the pattern doesn\'t define a match group for severity.') + }, + applyTo: { + type: 'string', + enum: ['allDocuments', 'openDocuments', 'closedDocuments'], + description: localize('ProblemMatcherSchema.applyTo', 'Controls if a problem reported on a text document is applied only to open, closed or all documents.') + }, + pattern: PatternType, + fileLocation: { + oneOf: [ + { + type: 'string', + enum: ['absolute', 'relative', 'autoDetect', 'search'] + }, + { + type: 'array', + prefixItems: [ + { + type: 'string', + enum: ['absolute', 'relative', 'autoDetect', 'search'] + }, + ], + minItems: 1, + maxItems: 1, + additionalItems: false + }, + { + type: 'array', + prefixItems: [ + { type: 'string', enum: ['relative', 'autoDetect'] }, + { type: 'string' }, + ], + minItems: 2, + maxItems: 2, + additionalItems: false, + examples: [ + ['relative', '${workspaceFolder}'], + ['autoDetect', '${workspaceFolder}'], + ] + }, + { + type: 'array', + prefixItems: [ + { type: 'string', enum: ['search'] }, + { + type: 'object', + properties: { + 'include': { + oneOf: [ + { type: 'string' }, + { type: 'array', items: { type: 'string' } } + ] + }, + 'exclude': { + oneOf: [ + { type: 'string' }, + { type: 'array', items: { type: 'string' } } + ] + }, + }, + required: ['include'] + } + ], + minItems: 2, + maxItems: 2, + additionalItems: false, + examples: [ + ['search', { 'include': ['${workspaceFolder}'] }], + ['search', { 'include': ['${workspaceFolder}'], 'exclude': [] }] + ], + } + ], + description: localize('ProblemMatcherSchema.fileLocation', 'Defines how file names reported in a problem pattern should be interpreted. A relative fileLocation may be an array, where the second element of the array is the path of the relative file location. The search fileLocation mode, performs a deep (and, possibly, heavy) file system search within the directories specified by the include/exclude properties of the second element (or the current workspace directory if not specified).') + }, + background: { + type: 'object', + additionalProperties: false, + description: localize('ProblemMatcherSchema.background', 'Patterns to track the begin and end of a matcher active on a background task.'), + properties: { + activeOnStart: { + type: 'boolean', + description: localize('ProblemMatcherSchema.background.activeOnStart', 'If set to true the background monitor is in active mode when the task starts. This is equals of issuing a line that matches the beginsPattern') + }, + beginsPattern: { + oneOf: [ + { + type: 'string' + }, + Schemas.WatchingPattern + ], + description: localize('ProblemMatcherSchema.background.beginsPattern', 'If matched in the output the start of a background task is signaled.') + }, + endsPattern: { + oneOf: [ + { + type: 'string' + }, + Schemas.WatchingPattern + ], + description: localize('ProblemMatcherSchema.background.endsPattern', 'If matched in the output the end of a background task is signaled.') + } + } + }, + watching: { + type: 'object', + additionalProperties: false, + deprecationMessage: localize('ProblemMatcherSchema.watching.deprecated', 'The watching property is deprecated. Use background instead.'), + description: localize('ProblemMatcherSchema.watching', 'Patterns to track the begin and end of a watching matcher.'), + properties: { + activeOnStart: { + type: 'boolean', + description: localize('ProblemMatcherSchema.watching.activeOnStart', 'If set to true the watcher is in active mode when the task starts. This is equals of issuing a line that matches the beginPattern') + }, + beginsPattern: { + oneOf: [ + { + type: 'string' + }, + Schemas.WatchingPattern + ], + description: localize('ProblemMatcherSchema.watching.beginsPattern', 'If matched in the output the start of a watching task is signaled.') + }, + endsPattern: { + oneOf: [ + { + type: 'string' + }, + Schemas.WatchingPattern + ], + description: localize('ProblemMatcherSchema.watching.endsPattern', 'If matched in the output the end of a watching task is signaled.') + } + } + } + } + }; + + export const LegacyProblemMatcher: IJSONSchema = Objects.deepClone(ProblemMatcher); + LegacyProblemMatcher.properties = Objects.deepClone(LegacyProblemMatcher.properties) || {}; + LegacyProblemMatcher.properties['watchedTaskBeginsRegExp'] = { + type: 'string', + deprecationMessage: localize('LegacyProblemMatcherSchema.watchedBegin.deprecated', 'This property is deprecated. Use the watching property instead.'), + description: localize('LegacyProblemMatcherSchema.watchedBegin', 'A regular expression signaling that a watched tasks begins executing triggered through file watching.') + }; + LegacyProblemMatcher.properties['watchedTaskEndsRegExp'] = { + type: 'string', + deprecationMessage: localize('LegacyProblemMatcherSchema.watchedEnd.deprecated', 'This property is deprecated. Use the watching property instead.'), + description: localize('LegacyProblemMatcherSchema.watchedEnd', 'A regular expression signaling that a watched tasks ends executing.') + }; + + export const NamedProblemMatcher: IJSONSchema = Objects.deepClone(ProblemMatcher); + NamedProblemMatcher.properties = Objects.deepClone(NamedProblemMatcher.properties) || {}; + NamedProblemMatcher.properties.name = { + type: 'string', + description: localize('NamedProblemMatcherSchema.name', 'The name of the problem matcher used to refer to it.') + }; + NamedProblemMatcher.properties.label = { + type: 'string', + description: localize('NamedProblemMatcherSchema.label', 'A human readable label of the problem matcher.') + }; } const problemPatternExtPoint = ExtensionsRegistry.registerExtensionPoint({ @@ -1630,216 +1836,6 @@ export class ProblemMatcherParser extends Parser { } } -export namespace Schemas { - - export const WatchingPattern: IJSONSchema = { - type: 'object', - additionalProperties: false, - properties: { - regexp: { - type: 'string', - description: localize('WatchingPatternSchema.regexp', 'The regular expression to detect the begin or end of a background task.') - }, - file: { - type: 'integer', - description: localize('WatchingPatternSchema.file', 'The match group index of the filename. Can be omitted.') - }, - } - }; - - - export const PatternType: IJSONSchema = { - anyOf: [ - { - type: 'string', - description: localize('PatternTypeSchema.name', 'The name of a contributed or predefined pattern') - }, - Schemas.ProblemPattern, - Schemas.MultiLineProblemPattern - ], - description: localize('PatternTypeSchema.description', 'A problem pattern or the name of a contributed or predefined problem pattern. Can be omitted if base is specified.') - }; - - export const ProblemMatcher: IJSONSchema = { - type: 'object', - additionalProperties: false, - properties: { - base: { - type: 'string', - description: localize('ProblemMatcherSchema.base', 'The name of a base problem matcher to use.') - }, - owner: { - type: 'string', - description: localize('ProblemMatcherSchema.owner', 'The owner of the problem inside Code. Can be omitted if base is specified. Defaults to \'external\' if omitted and base is not specified.') - }, - source: { - type: 'string', - description: localize('ProblemMatcherSchema.source', 'A human-readable string describing the source of this diagnostic, e.g. \'typescript\' or \'super lint\'.') - }, - severity: { - type: 'string', - enum: ['error', 'warning', 'info'], - description: localize('ProblemMatcherSchema.severity', 'The default severity for captures problems. Is used if the pattern doesn\'t define a match group for severity.') - }, - applyTo: { - type: 'string', - enum: ['allDocuments', 'openDocuments', 'closedDocuments'], - description: localize('ProblemMatcherSchema.applyTo', 'Controls if a problem reported on a text document is applied only to open, closed or all documents.') - }, - pattern: PatternType, - fileLocation: { - oneOf: [ - { - type: 'string', - enum: ['absolute', 'relative', 'autoDetect', 'search'] - }, - { - type: 'array', - prefixItems: [ - { - type: 'string', - enum: ['absolute', 'relative', 'autoDetect', 'search'] - }, - ], - minItems: 1, - maxItems: 1, - additionalItems: false - }, - { - type: 'array', - prefixItems: [ - { type: 'string', enum: ['relative', 'autoDetect'] }, - { type: 'string' }, - ], - minItems: 2, - maxItems: 2, - additionalItems: false, - examples: [ - ['relative', '${workspaceFolder}'], - ['autoDetect', '${workspaceFolder}'], - ] - }, - { - type: 'array', - prefixItems: [ - { type: 'string', enum: ['search'] }, - { - type: 'object', - properties: { - 'include': { - oneOf: [ - { type: 'string' }, - { type: 'array', items: { type: 'string' } } - ] - }, - 'exclude': { - oneOf: [ - { type: 'string' }, - { type: 'array', items: { type: 'string' } } - ] - }, - }, - required: ['include'] - } - ], - minItems: 2, - maxItems: 2, - additionalItems: false, - examples: [ - ['search', { 'include': ['${workspaceFolder}'] }], - ['search', { 'include': ['${workspaceFolder}'], 'exclude': [] }] - ], - } - ], - description: localize('ProblemMatcherSchema.fileLocation', 'Defines how file names reported in a problem pattern should be interpreted. A relative fileLocation may be an array, where the second element of the array is the path of the relative file location. The search fileLocation mode, performs a deep (and, possibly, heavy) file system search within the directories specified by the include/exclude properties of the second element (or the current workspace directory if not specified).') - }, - background: { - type: 'object', - additionalProperties: false, - description: localize('ProblemMatcherSchema.background', 'Patterns to track the begin and end of a matcher active on a background task.'), - properties: { - activeOnStart: { - type: 'boolean', - description: localize('ProblemMatcherSchema.background.activeOnStart', 'If set to true the background monitor is in active mode when the task starts. This is equals of issuing a line that matches the beginsPattern') - }, - beginsPattern: { - oneOf: [ - { - type: 'string' - }, - Schemas.WatchingPattern - ], - description: localize('ProblemMatcherSchema.background.beginsPattern', 'If matched in the output the start of a background task is signaled.') - }, - endsPattern: { - oneOf: [ - { - type: 'string' - }, - Schemas.WatchingPattern - ], - description: localize('ProblemMatcherSchema.background.endsPattern', 'If matched in the output the end of a background task is signaled.') - } - } - }, - watching: { - type: 'object', - additionalProperties: false, - deprecationMessage: localize('ProblemMatcherSchema.watching.deprecated', 'The watching property is deprecated. Use background instead.'), - description: localize('ProblemMatcherSchema.watching', 'Patterns to track the begin and end of a watching matcher.'), - properties: { - activeOnStart: { - type: 'boolean', - description: localize('ProblemMatcherSchema.watching.activeOnStart', 'If set to true the watcher is in active mode when the task starts. This is equals of issuing a line that matches the beginPattern') - }, - beginsPattern: { - oneOf: [ - { - type: 'string' - }, - Schemas.WatchingPattern - ], - description: localize('ProblemMatcherSchema.watching.beginsPattern', 'If matched in the output the start of a watching task is signaled.') - }, - endsPattern: { - oneOf: [ - { - type: 'string' - }, - Schemas.WatchingPattern - ], - description: localize('ProblemMatcherSchema.watching.endsPattern', 'If matched in the output the end of a watching task is signaled.') - } - } - } - } - }; - - export const LegacyProblemMatcher: IJSONSchema = Objects.deepClone(ProblemMatcher); - LegacyProblemMatcher.properties = Objects.deepClone(LegacyProblemMatcher.properties) || {}; - LegacyProblemMatcher.properties['watchedTaskBeginsRegExp'] = { - type: 'string', - deprecationMessage: localize('LegacyProblemMatcherSchema.watchedBegin.deprecated', 'This property is deprecated. Use the watching property instead.'), - description: localize('LegacyProblemMatcherSchema.watchedBegin', 'A regular expression signaling that a watched tasks begins executing triggered through file watching.') - }; - LegacyProblemMatcher.properties['watchedTaskEndsRegExp'] = { - type: 'string', - deprecationMessage: localize('LegacyProblemMatcherSchema.watchedEnd.deprecated', 'This property is deprecated. Use the watching property instead.'), - description: localize('LegacyProblemMatcherSchema.watchedEnd', 'A regular expression signaling that a watched tasks ends executing.') - }; - - export const NamedProblemMatcher: IJSONSchema = Objects.deepClone(ProblemMatcher); - NamedProblemMatcher.properties = Objects.deepClone(NamedProblemMatcher.properties) || {}; - NamedProblemMatcher.properties.name = { - type: 'string', - description: localize('NamedProblemMatcherSchema.name', 'The name of the problem matcher used to refer to it.') - }; - NamedProblemMatcher.properties.label = { - type: 'string', - description: localize('NamedProblemMatcherSchema.label', 'A human readable label of the problem matcher.') - }; -} - const problemMatchersExtPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'problemMatchers', deps: [problemPatternExtPoint], diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 0de3b0a3839..6670671e78d 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -133,6 +133,7 @@ export interface IPresentationOptionsConfig { /** * Controls whether the terminal that the task runs in is closed when the task completes. + * Note that if the terminal process exits with a non-zero exit code, it will not close. */ close?: boolean; } diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index 3db39c2267d..dab0931a4c1 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -64,6 +64,8 @@ export interface IWorkspaceFolderTaskResult extends IWorkspaceTaskResult { export interface ITaskService { readonly _serviceBrand: undefined; onDidStateChange: Event; + /** Fired when task providers are registered or unregistered */ + onDidChangeTaskProviders: Event; isReconnected: boolean; onDidReconnectToTasks: Event; supportsMultipleTaskExecutions: boolean; @@ -75,6 +77,11 @@ export interface ITaskService { getBusyTasks(): Promise; terminate(task: Task): Promise; tasks(filter?: ITaskFilter): Promise; + /** + * Gets tasks currently known to the task system. Unlike {@link tasks}, + * this does not activate extensions or prompt for workspace trust. + */ + getKnownTasks(filter?: ITaskFilter): Promise; taskTypes(): string[]; getWorkspaceTasks(runSource?: TaskRunSource): Promise>; getSavedTasks(type: 'persistent' | 'historical'): Promise<(Task | ConfiguringTask)[]>; diff --git a/src/vs/workbench/contrib/terminal/browser/media/CodeTabExpansion.psm1 b/src/vs/workbench/contrib/terminal/browser/media/CodeTabExpansion.psm1 new file mode 100644 index 00000000000..559c9d3c76b --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/media/CodeTabExpansion.psm1 @@ -0,0 +1,62 @@ +# --------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# --------------------------------------------------------------------------------------------- + +# TODO: Dynamically enable depending on quality? +Microsoft.PowerShell.Core\Register-ArgumentCompleter -CommandName "code","code-insiders" -Native -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + # TODO: These tooltips could be localized? + # TODO: Context aware suggestions + # TODO: Auto-generate this + # TODO: Subcommands + @( + [System.Management.Automation.CompletionResult]::new("--add", "--add", 'ParameterName', 'Add folder(s) to the last active window.'), + [System.Management.Automation.CompletionResult]::new("--category", "--category", 'ParameterName', 'Filters installed extensions by provided category, when using --list-extensions.'), + [System.Management.Automation.CompletionResult]::new("--diff", "--diff", 'ParameterName', 'Compare two files with each other.'), + [System.Management.Automation.CompletionResult]::new("--disable-chromium-sandbox", "--disable-chromium-sandbox", 'ParameterName', 'Use this option only when there is requirement to launch the application as sudo user on Linux or when running as an elevated user in an applocker environment on Windows.'), + [System.Management.Automation.CompletionResult]::new("--disable-extension", "--disable-extension", 'ParameterName', 'Disable the provided extension. This option is not persisted and is effective only when the command opens a new window.'), + [System.Management.Automation.CompletionResult]::new("--disable-extensions", "--disable-extensions", 'ParameterName', 'Disable all installed extensions. This option is not persisted and is effective only when the command opens a new window.'), + [System.Management.Automation.CompletionResult]::new("--disable-gpu", "--disable-gpu", 'ParameterName', 'Disable GPU hardware acceleration.'), + [System.Management.Automation.CompletionResult]::new("--disable-lcd-text", "--disable-lcd-text", 'ParameterName', 'Disable LCD font rendering.'), + [System.Management.Automation.CompletionResult]::new("--enable-proposed-api", "--enable-proposed-api", 'ParameterName', 'Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.'), + [System.Management.Automation.CompletionResult]::new("--extensions-dir", "--extensions-dir", 'ParameterName', 'Set the root path for extensions.'), + [System.Management.Automation.CompletionResult]::new("--goto", "--goto", 'ParameterName', 'Open a file at the path on the specified line and character position.'), + [System.Management.Automation.CompletionResult]::new("--help", "--help", 'ParameterName', 'Print usage.'), + [System.Management.Automation.CompletionResult]::new("--inspect-brk-extensions", "--inspect-brk-extensions", 'ParameterName', 'Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.'), + [System.Management.Automation.CompletionResult]::new("--inspect-extensions", "--inspect-extensions", 'ParameterName', 'Allow debugging and profiling of extensions. Check the developer tools for the connection URI.'), + [System.Management.Automation.CompletionResult]::new("--install-extension", "--install-extension", 'ParameterName', 'Installs or updates an extension. The argument is either an extension id or a path to a VSIX. The identifier of an extension is ''${publisher}.${name}''. Use ''--force'' argument to update to latest version. To install a specific version provide ''@${version}''. For example: ''vscode.csharp@1.2.3''.'), + [System.Management.Automation.CompletionResult]::new("--list-extensions", "--list-extensions", 'ParameterName', 'List the installed extensions.'), + [System.Management.Automation.CompletionResult]::new("--locale", "--locale", 'ParameterName', 'The locale to use (e.g. en-US or zh-TW).'), + [System.Management.Automation.CompletionResult]::new("--log", "--log", 'ParameterName', 'Log level to use. Default is ''info''. Allowed values are ''critical'', ''error'', ''warn'', ''info'', ''debug'', ''trace'', ''off''. You can also configure the log level of an extension by passing extension id and log level in the following format:\n\n ''${publisher}.${name}:${logLevel}''. For example: ''vscode.csharp:trace''. Can receive one or more such entries.'), + [System.Management.Automation.CompletionResult]::new("--merge", "--merge", 'ParameterName', 'Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results.'), + [System.Management.Automation.CompletionResult]::new("--new-window", "--new-window", 'ParameterName', 'Force to open a new window.'), + [System.Management.Automation.CompletionResult]::new("--pre-release", "--pre-release", 'ParameterName', 'Installs the pre-release version of the extension, when using'), + [System.Management.Automation.CompletionResult]::new("--prof-startup", "--prof-startup", 'ParameterName', 'Run CPU profiler during startup.'), + [System.Management.Automation.CompletionResult]::new("--profile", "--profile", 'ParameterName', 'Opens the provided folder or workspace with the given profile and associates the profile with the workspace. If the profile does not exist, a new empty one is created.'), + [System.Management.Automation.CompletionResult]::new("--reuse-window", "--reuse-window", 'ParameterName', 'Force to open a file or folder in an already opened window.'), + [System.Management.Automation.CompletionResult]::new("--show-versions", "--show-versions", 'ParameterName', 'Show versions of installed extensions, when using --list-extensions.'), + [System.Management.Automation.CompletionResult]::new("--status", "--status", 'ParameterName', 'Print process usage and diagnostics information.'), + [System.Management.Automation.CompletionResult]::new("--sync", "--sync", 'ParameterName', 'Turn sync on or off.'), + [System.Management.Automation.CompletionResult]::new("--telemetry", "--telemetry", 'ParameterName', 'Shows all telemetry events which VS code collects.'), + [System.Management.Automation.CompletionResult]::new("--uninstall-extension", "--uninstall-extension", 'ParameterName', 'Uninstalls an extension.'), + [System.Management.Automation.CompletionResult]::new("--update-extensions", "--update-extensions", 'ParameterName', 'Update the installed extensions.'), + [System.Management.Automation.CompletionResult]::new("--user-data-dir", "--user-data-dir", 'ParameterName', 'Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code.'), + [System.Management.Automation.CompletionResult]::new("--verbose", "--verbose", 'ParameterName', 'Print verbose output (implies --wait).'), + [System.Management.Automation.CompletionResult]::new("--version", "--version", 'ParameterName', 'Print version.'), + [System.Management.Automation.CompletionResult]::new("--wait", "--wait", 'ParameterName', 'Wait for the files to be closed before returning.'), + [System.Management.Automation.CompletionResult]::new("-a", "-a", 'ParameterName', 'Add folder(s) to the last active window.'), + [System.Management.Automation.CompletionResult]::new("-d", "-d", 'ParameterName', 'Compare two files with each other.'), + [System.Management.Automation.CompletionResult]::new("-g", "-g", 'ParameterName', 'Open a file at the path on the specified line and character position.'), + [System.Management.Automation.CompletionResult]::new("-h", "-h", 'ParameterName', 'Print usage.'), + [System.Management.Automation.CompletionResult]::new("-m", "-m", 'ParameterName', 'Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results.'), + [System.Management.Automation.CompletionResult]::new("-n", "-n", 'ParameterName', 'Force to open a new window.'), + [System.Management.Automation.CompletionResult]::new("-r", "-r", 'ParameterName', 'Force to open a file or folder in an already opened window.'), + [System.Management.Automation.CompletionResult]::new("-s", "-s", 'ParameterName', 'Print process usage and diagnostics information.'), + [System.Management.Automation.CompletionResult]::new("-v", "-v", 'ParameterName', 'Print version.'), + [System.Management.Automation.CompletionResult]::new("-w", "-w", 'ParameterName', 'Wait for the files to be closed before returning.'), + [System.Management.Automation.CompletionResult]::new("serve-web", "serve-web", 'Command', 'Run a server that displays the editor UI in browsers.') + [System.Management.Automation.CompletionResult]::new("tunnel", "tunnel", 'Command', 'Make the current machine accessible from vscode.dev or other machines through a secure tunnel') + ) +} diff --git a/src/vs/workbench/contrib/terminal/browser/media/GitTabExpansion.psm1 b/src/vs/workbench/contrib/terminal/browser/media/GitTabExpansion.psm1 new file mode 100644 index 00000000000..ca1dabba877 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/media/GitTabExpansion.psm1 @@ -0,0 +1,663 @@ +# --------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# --------------------------------------------------------------------------------------------- + +# This is a fork of posh-git that has been modified to add additional features and custom VS Code +# specific integrations. +# +# Copyright (c) 2010-2018 Keith Dahlby, Keith Hill, and contributors +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +function dbg($Message, [Diagnostics.Stopwatch]$Stopwatch) { + if ($Stopwatch) { + Write-Verbose ('{0:00000}:{1}' -f $Stopwatch.ElapsedMilliseconds,$Message) -Verbose # -ForegroundColor Yellow + } +} + +function Get-AliasPattern($cmd) { + $aliases = @($cmd) + @(Get-Alias | Where-Object { $_.Definition -match "^$cmd(\.exe)?$" } | Foreach-Object Name) + "($($aliases -join '|'))" +} + +$Global:GitTabSettings = New-Object PSObject -Property @{ + AllCommands = $false + KnownAliases = @{ + '!f() { exec vsts code pr "$@"; }; f' = 'vsts.pr' + } + EnableLogging = $false + LogPath = Join-Path ([System.IO.Path]::GetTempPath()) posh-git_tabexp.log + RegisteredCommands = "" +} + +$subcommands = @{ + bisect = "start bad good skip reset visualize replay log run" + notes = 'add append copy edit get-ref list merge prune remove show' + 'vsts.pr' = 'create update show list complete abandon reactivate reviewers work-items set-vote policies' + reflog = "show delete expire" + remote = " + add rename remove set-head set-branches + get-url set-url show prune update + " + rerere = "clear forget diff remaining status gc" + stash = 'push save list show apply clear drop pop create branch' + submodule = "add status init deinit update summary foreach sync" + svn = " + init fetch clone rebase dcommit log find-rev + set-tree commit-diff info create-ignore propget + proplist show-ignore show-externals branch tag blame + migrate mkdirs reset gc + " + tfs = " + list-remote-branches clone quick-clone bootstrap init + clone fetch pull quick-clone unshelve shelve-list labels + rcheckin checkin checkintool shelve shelve-delete + branch + info cleanup cleanup-workspaces help verify autotag subtree reset-remote checkout + " + flow = "init feature bugfix release hotfix support help version" + worktree = "add list lock move prune remove unlock" +} + +$gitflowsubcommands = @{ + init = 'help' + feature = 'list start finish publish track diff rebase checkout pull help delete' + bugfix = 'list start finish publish track diff rebase checkout pull help delete' + release = 'list start finish track publish help delete' + hotfix = 'list start finish track publish help delete' + support = 'list start help' + config = 'list set base' +} + +function script:gitCmdOperations($commands, $command, $filter) { + $commands[$command].Trim() -split '\s+' | Where-Object { $_ -like "$filter*" } +} + +$script:someCommands = @( + 'add','am','annotate','archive','bisect','blame','branch','bundle','checkout','cherry', + 'cherry-pick','citool','clean','clone','commit','config','describe','diff','difftool','fetch', + 'format-patch','gc','grep','gui','help','init','instaweb','log','merge','mergetool','mv', + 'notes','prune','pull','push','rebase','reflog','remote','rerere','reset','restore','revert','rm', + 'shortlog','show','stash','status','submodule','svn','switch','tag','whatchanged', 'worktree' +) + +# Based on git help -a output +$script:someCommandsDescriptions = @{ + # Main Porcelain Commands" + add = "Add file contents to the index" + am = "Apply a series of patches from a mailbox" + archive = "Create an archive of files from a named tree" + bisect = "Use binary search to find the commit that introduced a bug" + branch = "List, create, or delete branches" + bundle = "Move objects and refs by archive" + checkout = "Switch branches or restore working tree files" + "cherry-pick" = "Apply the changes introduced by some existing commits" + citool = "Graphical alternative to git-commit" + clean = "Remove untracked files from the working tree" + clone = "Clone a repository into a new directory" + commit = "Record changes to the repository" + describe = "Give an object a human readable name based on an available ref" + diff = "Show changes between commits, commit and working tree, etc" + fetch = "Download objects and refs from another repository" + "format-patch" = "Prepare patches for e-mail submission" + gc = "Cleanup unnecessary files and optimize the local repository" + grep = "Print lines matching a pattern" + gui = "A portable graphical interface to Git" + init = "Create an empty Git repository or reinitialize an existing one" + log = "Show commit logs" + merge = "Join two or more development histories together" + mv = "Move or rename a file, a directory, or a symlink" + notes = "Add or inspect object notes" + pull = "Fetch from and integrate with another repository or a local branch" + push = "Update remote refs along with associated objects" + rebase = "Reapply commits on top of another base tip" + reset = "Reset current HEAD to the specified state" + restore = "Restore working tree files" + revert = "Revert some existing commits" + rm = "Remove files from the working tree and from the index" + shortlog = "Summarize 'git log' output" + show = "Show various types of objects" + stash = "Stash the changes in a dirty working directory away" + status = "Show the working tree status" + submodule = "Initialize, update or inspect submodules" + switch = "Switch branches" + tag = "Create, list, delete or verify a tag object signed with GPG" + worktree = "Manage multiple working trees" + + # Ancillary Commands / Manipulators + config = "Get and set repository or global options" + mergetool = "Run merge conflict resolution tools to resolve merge conflicts" + "pack-refs" = "Pack heads and tags for efficient repository access" + prune = "Prune all unreachable objects from the object database" + reflog = "Manage reflog information" + remote = "Manage set of tracked repositories" + + # Ancillary Commands / Interrogators + annotate = "Annotate file lines with commit information" + blame = "Show what revision and author last modified each line of a file" + difftool = "Show changes using common diff tools" + help = "Display help information about Git" + rerere = "Reuse recorded resolution of conflicted merges" + whatchanged = "Show logs with differences each commit introduces" + + # Low-level Commands / Interrogators + cherry = "Find commits yet to be applied to upstream" +} + + + +if ((($PSVersionTable.PSVersion.Major -eq 5) -or $IsWindows) -and ($script:GitVersion -ge [System.Version]'2.16.2')) { + $script:someCommands += 'update-git-for-windows' +} + +$script:gitCommandsWithLongParams = $longGitParams.Keys -join '|' +$script:gitCommandsWithShortParams = $shortGitParams.Keys -join '|' +$script:gitCommandsWithParamValues = $gitParamValues.Keys -join '|' +$script:vstsCommandsWithShortParams = $shortVstsParams.Keys -join '|' +$script:vstsCommandsWithLongParams = $longVstsParams.Keys -join '|' + +try { + if ($null -ne (git help -a 2>&1 | Select-String flow)) { + $script:someCommands += 'flow' + } +} +catch { + Write-Debug "Search for 'flow' in 'git help' output failed with error: $_" +} + +filter quoteStringWithSpecialChars { + if ($_ -and ($_ -match '\s+|#|@|\$|;|,|''|\{|\}|\(|\)')) { + $str = $_ -replace "'", "''" + "'$str'" + } + else { + $_ + } +} + +function script:gitCommands($filter, $includeAliases) { + $cmdList = @() + if (-not $global:GitTabSettings.AllCommands) { + $cmdList += $someCommands -like "$filter*" + } + else { + $cmdList += git help --all | + Where-Object { $_ -match '^\s{2,}\S.*' } | + ForEach-Object { $_.Split(' ', [StringSplitOptions]::RemoveEmptyEntries) } | + Where-Object { $_ -like "$filter*" } + } + + $completions = $cmdList | Sort-Object | ForEach-Object { + $command = $_ + if ($script:someCommandsDescriptions.ContainsKey($command)) { + [System.Management.Automation.CompletionResult]::new($command, $command, 'Method', $script:someCommandsDescriptions[$command]) + } else { + [System.Management.Automation.CompletionResult]::new($command, $command, 'Method', $command) + } + } + + if ($includeAliases) { + $completions += gitAliases $filter + } + + $completions +} + +function script:gitRemotes($filter) { + git remote | + Where-Object { $_ -like "$filter*" } | + quoteStringWithSpecialChars +} + +function script:gitBranches($filter, $includeHEAD = $false, $prefix = '') { + if ($filter -match "^(?\S*\.{2,3})(?.*)") { + $prefix += $matches['from'] + $filter = $matches['to'] + } + + $branches = @(git branch --no-color | ForEach-Object { if (($_ -notmatch "^\* \(HEAD detached .+\)$") -and ($_ -match "^[\*\+]?\s*(?\S+)(?: -> .+)?")) { $matches['ref'] } }) + + @(git branch --no-color -r | ForEach-Object { if ($_ -match "^ (?\S+)(?: -> .+)?") { $matches['ref'] } }) + + @(if ($includeHEAD) { 'HEAD','FETCH_HEAD','ORIG_HEAD','MERGE_HEAD' }) + + $branches | + Where-Object { $_ -ne '(no branch)' -and $_ -like "$filter*" } | + ForEach-Object { $prefix + $_ } | + quoteStringWithSpecialChars +} + +function script:gitRemoteUniqueBranches($filter) { + git branch --no-color -r | + ForEach-Object { if ($_ -match "^ (?[^/]+)/(?\S+)(?! -> .+)?$") { $matches['branch'] } } | + Group-Object -NoElement | + Where-Object { $_.Count -eq 1 } | + Select-Object -ExpandProperty Name | + Where-Object { $_ -like "$filter*" } | + quoteStringWithSpecialChars +} + +function script:gitConfigKeys($section, $filter, $defaultOptions = '') { + $completions = @($defaultOptions -split ' ') + + git config --name-only --get-regexp ^$section\..* | + ForEach-Object { $completions += ($_ -replace "$section\.","") } + + return $completions | + Where-Object { $_ -like "$filter*" } | + Sort-Object | + quoteStringWithSpecialChars +} + +function script:gitTags($filter, $prefix = '') { + git tag | + Where-Object { $_ -like "$filter*" } | + ForEach-Object { $prefix + $_ } | + quoteStringWithSpecialChars +} + +function script:gitFeatures($filter, $command) { + $featurePrefix = git config --local --get "gitflow.prefix.$command" + $branches = @(git branch --no-color | ForEach-Object { if ($_ -match "^\*?\s*$featurePrefix(?.*)") { $matches['ref'] } }) + $branches | + Where-Object { $_ -ne '(no branch)' -and $_ -like "$filter*" } | + ForEach-Object { $featurePrefix + $_ } | + quoteStringWithSpecialChars +} + +function script:gitRemoteBranches($remote, $ref, $filter, $prefix = '') { + git branch --no-color -r | + Where-Object { $_ -like " $remote/$filter*" } | + ForEach-Object { $prefix + $ref + ($_ -replace " $remote/","") } | + quoteStringWithSpecialChars +} + +function script:gitStashes($filter) { + (git stash list) -replace ':.*','' | + Where-Object { $_ -like "$filter*" } | + quoteStringWithSpecialChars +} + +function script:gitTfsShelvesets($filter) { + (git tfs shelve-list) | + Where-Object { $_ -like "$filter*" } | + quoteStringWithSpecialChars +} + +function script:gitFiles($filter, $files) { + $files | Sort-Object | + Where-Object { $_ -like "$filter*" } | + quoteStringWithSpecialChars +} + +function script:gitIndex($GitStatus, $filter) { + gitFiles $filter $GitStatus.Index +} + +function script:gitAddFiles($GitStatus, $filter) { + gitFiles $filter (@($GitStatus.Working.Unmerged) + @($GitStatus.Working.Modified) + @($GitStatus.Working.Added)) +} + +function script:gitCheckoutFiles($GitStatus, $filter) { + gitFiles $filter (@($GitStatus.Working.Unmerged) + @($GitStatus.Working.Modified) + @($GitStatus.Working.Deleted)) +} + +function script:gitDeleted($GitStatus, $filter) { + gitFiles $filter $GitStatus.Working.Deleted +} + +function script:gitDiffFiles($GitStatus, $filter, $staged) { + if ($staged) { + gitFiles $filter $GitStatus.Index.Modified + } + else { + gitFiles $filter (@($GitStatus.Working.Unmerged) + @($GitStatus.Working.Modified) + @($GitStatus.Index.Modified)) + } +} + +function script:gitMergeFiles($GitStatus, $filter) { + gitFiles $filter $GitStatus.Working.Unmerged +} + +function script:gitRestoreFiles($GitStatus, $filter, $staged) { + if ($staged) { + gitFiles $filter (@($GitStatus.Index.Added) + @($GitStatus.Index.Modified) + @($GitStatus.Index.Deleted)) + } + else { + gitFiles $filter (@($GitStatus.Working.Unmerged) + @($GitStatus.Working.Modified) + @($GitStatus.Working.Deleted)) + } +} + +function script:gitAliases($filter) { + git config --get-regexp ^alias\. | ForEach-Object { + if ($_ -match "^alias\.(?\S+) (?.+)") { + $alias = $Matches['alias'] + $expanded = $Matches['expanded'] + if ($alias -like "$filter*") { + [System.Management.Automation.CompletionResult]::new($alias, $alias, 'Variable', $expanded) + } + } + } +} + +function script:expandGitAlias($cmd, $rest) { + $alias = git config "alias.$cmd" + + if ($alias) { + $known = $Global:GitTabSettings.KnownAliases[$alias] + if ($known) { + return "git $known$rest" + } + + return "git $alias$rest" + } + else { + return "git $cmd$rest" + } +} + +function script:expandLongParams($hash, $cmd, $filter) { + $hash[$cmd].Trim() -split ' ' | + Where-Object { $_ -like "$filter*" } | + Sort-Object | + ForEach-Object { -join ("--", $_) } +} + +function script:expandShortParams($hash, $cmd, $filter) { + $hash[$cmd].Trim() -split ' ' | + Where-Object { $_ -like "$filter*" } | + Sort-Object | + ForEach-Object { -join ("-", $_) } +} + +function script:expandParamValues($cmd, $param, $filter) { + $paramValues = $gitParamValues[$cmd][$param] + + $completions = if ($paramValues -is [scriptblock]) { + & $paramValues $filter + } + else { + $paramValues.Trim() -split ' ' | Where-Object { $_ -like "$filter*" } | Sort-Object + } + + $completions | ForEach-Object { -join ("--", $param, "=", $_) } +} + +function Expand-GitCommand($Command) { + # Parse all Git output as UTF8, including tab completion output - https://github.com/dahlbyk/posh-git/pull/359 + $res = GitTabExpansionInternal $Command $Global:GitStatus + $res +} + +function GitTabExpansionInternal($lastBlock, $GitStatus = $null) { + $ignoreGitParams = '(?\s+-(?:[aA-zZ0-9]+|-[aA-zZ0-9][aA-zZ0-9-]*)(?:=\S+)?)*' + + if ($lastBlock -match "^$(Get-AliasPattern git) (?\S+)(? .*)$") { + $lastBlock = expandGitAlias $Matches['cmd'] $Matches['args'] + } + + # Handles tgit (tortoisegit) + if ($lastBlock -match "^$(Get-AliasPattern tgit) (?\S*)$") { + # Need return statement to prevent fall-through. + return $Global:TortoiseGitSettings.TortoiseGitCommands.Keys.GetEnumerator() | Sort-Object | Where-Object { $_ -like "$($matches['cmd'])*" } + } + + # Handles gitk + if ($lastBlock -match "^$(Get-AliasPattern gitk).* (?\S*)$") { + return gitBranches $matches['ref'] $true + } + + switch -regex ($lastBlock -replace "^$(Get-AliasPattern git) ","") { + + # Handles git + "^(?$($subcommands.Keys -join '|'))\s+(?\S*)$" { + gitCmdOperations $subcommands $matches['cmd'] $matches['op'] + } + + # Handles git flow + "^flow (?$($gitflowsubcommands.Keys -join '|'))\s+(?\S*)$" { + gitCmdOperations $gitflowsubcommands $matches['cmd'] $matches['op'] + } + + # Handles git flow + "^flow (?\S*)\s+(?\S*)\s+(?\S*)$" { + gitFeatures $matches['name'] $matches['command'] + } + + # Handles git remote (rename|rm|remove|set-head|set-branches|set-url|show|prune) + "^remote.* (?:rename|rm|remove|set-head|set-branches|set-url|show|prune).* (?\S*)$" { + gitRemotes $matches['remote'] | ConvertTo-VscodeCompletion -Type 'remote' + } + + # Handles git stash (show|apply|drop|pop|branch) + "^stash (?:show|apply|drop|pop|branch).* (?\S*)$" { + gitStashes $matches['stash'] | ConvertTo-VscodeCompletion -Type 'stash' + } + + # Handles git bisect (bad|good|reset|skip) + "^bisect (?:bad|good|reset|skip).* (?\S*)$" { + gitBranches $matches['ref'] $true | ConvertTo-VscodeCompletion -Type 'branch' + } + + # Handles git tfs unshelve + "^tfs +unshelve.* (?\S*)$" { + gitTfsShelvesets $matches['shelveset'] + } + + # Handles git branch -d|-D|-m|-M + # Handles git branch + "^branch.* (?\S*)$" { + gitBranches $matches['branch'] | ConvertTo-VscodeCompletion -Type 'branch' + } + + # Handles git (commands & aliases) + "^(?\S*)$" { + gitCommands $matches['cmd'] $TRUE + } + + # Handles git help (commands only) + "^help (?\S*)$" { + gitCommands $matches['cmd'] $FALSE + } + + # Handles git push remote : + # Handles git push remote +: + "^push${ignoreGitParams}\s+(?[^\s-]\S*).*\s+(?\+?)(?[^\s\:]*\:)(?\S*)$" { + gitRemoteBranches $matches['remote'] $matches['ref'] $matches['branch'] -prefix $matches['force'] | ConvertTo-VscodeCompletion -Type 'branch' + } + + # Handles git push remote + # Handles git push remote + + # Handles git pull remote + "^(?:push|pull)${ignoreGitParams}\s+(?[^\s-]\S*).*\s+(?\+?)(?[^\s\:]*)$" { + gitBranches $matches['ref'] -prefix $matches['force'] | ConvertTo-VscodeCompletion -Type 'branch' + gitTags $matches['ref'] -prefix $matches['force'] | ConvertTo-VscodeCompletion -Type 'tag' + } + + # Handles git pull + # Handles git push + # Handles git fetch + "^(?:push|pull|fetch)${ignoreGitParams}\s+(?\S*)$" { + gitRemotes $matches['remote'] | ConvertTo-VscodeCompletion -Type 'remote' + } + + # Handles git reset HEAD + # Handles git reset HEAD -- + "^reset.* HEAD(?:\s+--)? (?\S*)$" { + gitIndex $GitStatus $matches['path'] + } + + # Handles git + "^commit.*-C\s+(?\S*)$" { + gitBranches $matches['ref'] $true | ConvertTo-VscodeCompletion -Type 'branch' + } + + # Handles git add + "^add.* (?\S*)$" { + gitAddFiles $GitStatus $matches['files'] + } + + # Handles git checkout -- + "^checkout.* -- (?\S*)$" { + gitCheckoutFiles $GitStatus $matches['files'] + } + + # Handles git restore -s / --source= - must come before the next regex case + "^restore.* (?-i)(-s\s*|(?--source=))(?\S*)$" { + gitBranches $matches['ref'] $true $matches['source'] + gitTags $matches['ref'] + break + } + + # Handles git restore + "^restore(?:.* (?(?:(?-i)-S|--staged))|.*) (?\S*)$" { + gitRestoreFiles $GitStatus $matches['files'] $matches['staged'] + } + + # Handles git rm + "^rm.* (?\S*)$" { + gitDeleted $GitStatus $matches['index'] + } + + # Handles git diff/difftool + "^(?:diff|difftool)(?:.* (?(?:--cached|--staged))|.*) (?\S*)$" { + gitDiffFiles $GitStatus $matches['files'] $matches['staged'] + } + + # Handles git merge/mergetool + "^(?:merge|mergetool).* (?\S*)$" { + gitMergeFiles $GitStatus $matches['files'] + } + + # Handles git checkout|switch + "^(?:checkout|switch).* (?\S*)$" { + if ($lastBlock -match "-b\s[^\s]*$") { + $null # Force zero results + } else { + [System.Management.Automation.CompletionResult]::new('.', '.', 'ParameterName', "Discard changes in working directory") + gitBranches $matches['ref'] $true | ConvertTo-VscodeCompletion -Type 'branch' + gitRemoteUniqueBranches $matches['ref'] | ConvertTo-VscodeCompletion -Type 'branch' + gitTags $matches['ref'] | ConvertTo-VscodeCompletion -Type 'tag' + } + } + + # Handles git worktree add + "^worktree add.* (?\S+) (?\S*)$" { + gitBranches $matches['ref'] | ConvertTo-VscodeCompletion -Type 'branch' + } + + # Handles git + "^(?:cherry|cherry-pick|diff|difftool|log|merge|rebase|reflog\s+show|reset|revert|show).* (?\S*)$" { + gitBranches $matches['ref'] $true | ConvertTo-VscodeCompletion -Type 'branch' + gitTags $matches['ref'] | ConvertTo-VscodeCompletion -Type 'tag' + } + + # Handles git --= + "^(?$gitCommandsWithParamValues).* --(?[^=]+)=(?\S*)$" { + expandParamValues $matches['cmd'] $matches['param'] $matches['value'] + } + + # Handles git -- + "^(?$gitCommandsWithLongParams).* --(?\S*)$" { + expandLongParams $longGitParams $matches['cmd'] $matches['param'] + } + + # Handles git - + "^(?$gitCommandsWithShortParams).* -(?\S*)$" { + expandShortParams $shortGitParams $matches['cmd'] $matches['shortparam'] + } + + # Handles git pr alias + "vsts\.pr\s+(?\S*)$" { + gitCmdOperations $subcommands 'vsts.pr' $matches['op'] + } + + # Handles git pr -- + "vsts\.pr\s+(?$vstsCommandsWithLongParams).*--(?\S*)$" + { + expandLongParams $longVstsParams $matches['cmd'] $matches['param'] + } + + # Handles git pr - + "vsts\.pr\s+(?$vstsCommandsWithShortParams).*-(?\S*)$" + { + expandShortParams $shortVstsParams $matches['cmd'] $matches['shortparam'] + } + } +} + +function ConvertTo-VscodeCompletion { + Param( + [Parameter(ValueFromPipeline=$true)] + $CompletionText, + [string] + $Type, + [string] + $CustomIcon + ) + + Process { + $completionMappings = @{ + "branch" = "gitBranch" + "stash" = "gitStash" + "remote" = "remote" + "tag" = "tag" + } + $CompletionText | ForEach-Object { + $result = [System.Management.Automation.CompletionResult]::new($_, $_, [System.Management.Automation.CompletionResultType]::DynamicKeyword, "$Type $_") + $result | Add-Member -NotePropertyName 'CustomIcon' -NotePropertyValue $completionMappings[$Type] + $result + } + } +} + +function WriteTabExpLog([string] $Message) { + if (!$global:GitTabSettings.EnableLogging) { return } + + $timestamp = Get-Date -Format HH:mm:ss + "[$timestamp] $Message" | Out-File -Append $global:GitTabSettings.LogPath +} + +# if ($PSVersionTable.PSVersion.Major -ge 6) { +$cmdNames = "git","tgit","gitk" + +# Create regex pattern from $cmdNames: ^(git|git\.exe|tgit|tgit\.exe|gitk|gitk\.exe)$ +$cmdNamesPattern = "^($($cmdNames -join '|'))(\.exe)?$" +$cmdNames += Get-Alias | Where-Object { $_.Definition -match $cmdNamesPattern } | Foreach-Object Name + +$global:GitTabSettings.RegisteredCommands = $cmdNames -join ", " + +Microsoft.PowerShell.Core\Register-ArgumentCompleter -CommandName $cmdNames -Native -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + # The PowerShell completion has a habit of stripping the trailing space when completing: + # git checkout + # The Expand-GitCommand expects this trailing space, so pad with a space if necessary. + $padLength = $cursorPosition - $commandAst.Extent.StartOffset + $textToComplete = $commandAst.ToString().PadRight($padLength, ' ').Substring(0, $padLength) + + WriteTabExpLog "Expand: command: '$($commandAst.Extent.Text)', padded: '$textToComplete', padlen: $padLength" + $result = Expand-GitCommand $textToComplete + if ($null -eq $result) { + ,@() + } else { + $result + } +} + + diff --git a/src/vs/workbench/contrib/terminal/browser/media/cgmanifest.json b/src/vs/workbench/contrib/terminal/browser/media/cgmanifest.json new file mode 100644 index 00000000000..0d773545f20 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/media/cgmanifest.json @@ -0,0 +1,48 @@ +{ + "registrations": [ + { + "component": { + "type": "other", + "other": { + "name": "posh-git", + "downloadUrl": "https://github.com/dahlbyk/posh-git", + "version": "70e44dc0c2cdaf10c0cc8eb9ef5a9ca65ab63dcf" + } + }, + "licenseDetail": [ + "Oniguruma LICENSE", + "-----------------", + "", + "Copyright (c) 2002-2020 K.Kosako ", + "All rights reserved.", + "", + "The BSD License", + "", + "Redistribution and use in source and binary forms, with or without", + "modification, are permitted provided that the following conditions", + "are met:", + "1. Redistributions of source code must retain the above copyright", + " notice, this list of conditions and the following disclaimer.", + "2. Redistributions in binary form must reproduce the above copyright", + " notice, this list of conditions and the following disclaimer in the", + " documentation and/or other materials provided with the distribution.", + "", + "THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND", + "ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE", + "IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE", + "ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE", + "FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL", + "DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS", + "OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)", + "HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT", + "LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY", + "OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF", + "SUCH DAMAGE." + ], + "isOnlyProductionDependency": true, + "license": "MIT", + "version": "70e44dc0c2cdaf10c0cc8eb9ef5a9ca65ab63dcf" + } + ], + "version": 1 +} diff --git a/src/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/shellIntegration.fish b/src/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/shellIntegration.fish index a771735ae52..0377d700096 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/shellIntegration.fish +++ b/src/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/shellIntegration.fish @@ -24,7 +24,7 @@ set --global VSCODE_SHELL_INTEGRATION 1 # Apply any explicit path prefix (see #99878) if status --is-login; and set -q VSCODE_PATH_PREFIX - fish_add_path -p $VSCODE_PATH_PREFIX + set -gx PATH "$VSCODE_PATH_PREFIX$PATH" end set -e VSCODE_PATH_PREFIX diff --git a/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css b/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css deleted file mode 100644 index 2a1f155fc8e..00000000000 --- a/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css +++ /dev/null @@ -1,45 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .xterm-viewport { - /* Use the hack presented in https://stackoverflow.com/a/38748186/1156119 to get opacity transitions working on the scrollbar */ - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - transition: background-color 800ms linear; -} - -.monaco-workbench .xterm-viewport { - scrollbar-width: thin; -} - -.monaco-workbench .xterm-viewport::-webkit-scrollbar { - width: 10px; -} - -.monaco-workbench .xterm-viewport::-webkit-scrollbar-track { - opacity: 0; -} - -.monaco-workbench .xterm-viewport::-webkit-scrollbar-thumb { - min-height: 20px; - background-color: inherit; -} - -.monaco-workbench .force-scrollbar .xterm .xterm-viewport, -.monaco-workbench .xterm.focus .xterm-viewport, -.monaco-workbench .xterm:focus .xterm-viewport, -.monaco-workbench .xterm:hover .xterm-viewport { - transition: opacity 100ms linear; - cursor: default; -} - -.monaco-workbench .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { - transition: opacity 0ms linear; -} - -.monaco-workbench .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive { - background-color: inherit; -} diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index 06036fcbaae..92c0e07f5ad 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -34,7 +34,7 @@ if [ "$VSCODE_INJECTION" == "1" ]; then # Apply any explicit path prefix (see #99878) if [ -n "${VSCODE_PATH_PREFIX:-}" ]; then - export PATH=$VSCODE_PATH_PREFIX$PATH + export PATH="$VSCODE_PATH_PREFIX$PATH" builtin unset VSCODE_PATH_PREFIX fi fi @@ -112,15 +112,15 @@ __vsc_escape_value() { fi # Process text byte by byte, not by codepoint. - local -r LC_ALL=C - local -r str="${1}" - local -ir len="${#str}" + builtin local -r LC_ALL=C + builtin local -r str="${1}" + builtin local -ir len="${#str}" - local -i i - local -i val - local byte - local token - local out='' + builtin local -i i + builtin local -i val + builtin local byte + builtin local token + builtin local out='' for (( i=0; i < "${#str}"; ++i )); do # Escape backslashes, semi-colons specially, then special ASCII chars below space (0x20). @@ -143,7 +143,8 @@ __vsc_escape_value() { } # Send the IsWindows property if the environment looks like Windows -if [[ "$(uname -s)" =~ ^CYGWIN*|MINGW*|MSYS* ]]; then +__vsc_regex_environment="^CYGWIN*|MINGW*|MSYS*" +if [[ "$(uname -s)" =~ $__vsc_regex_environment ]]; then builtin printf '\e]633;P;IsWindows=True\a' __vsc_is_windows=1 else @@ -152,12 +153,16 @@ fi # Allow verifying $BASH_COMMAND doesn't have aliases resolved via history when the right HISTCONTROL # configuration is used -if [[ "$HISTCONTROL" =~ .*(erasedups|ignoreboth|ignoredups).* ]]; then +__vsc_regex_histcontrol=".*(erasedups|ignoreboth|ignoredups).*" +if [[ "$HISTCONTROL" =~ $__vsc_regex_histcontrol ]]; then __vsc_history_verify=0 else __vsc_history_verify=1 fi +builtin unset __vsc_regex_environment +builtin unset __vsc_regex_histcontrol + __vsc_initialized=0 __vsc_original_PS1="$PS1" __vsc_original_PS2="$PS2" @@ -210,7 +215,7 @@ __vsc_update_cwd() { } __vsc_command_output_start() { - if [[ -z "$__vsc_first_prompt" ]]; then + if [[ -z "${__vsc_first_prompt-}" ]]; then builtin return fi builtin printf '\e]633;E;%s;%s\a' "$(__vsc_escape_value "${__vsc_current_command}")" $__vsc_nonce @@ -226,7 +231,7 @@ __vsc_continuation_end() { } __vsc_command_complete() { - if [[ -z "$__vsc_first_prompt" ]]; then + if [[ -z "${__vsc_first_prompt-}" ]]; then builtin return fi if [ "$__vsc_current_command" = "" ]; then @@ -326,7 +331,7 @@ __vsc_prompt_cmd_original() { __vsc_restore_exit_code "${__vsc_status}" # Evaluate the original PROMPT_COMMAND similarly to how bash would normally # See https://unix.stackexchange.com/a/672843 for technique - local cmd + builtin local cmd for cmd in "${__vsc_original_prompt_command[@]}"; do eval "${cmd:-}" done diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh index 5a45b076b22..c25ded3d7eb 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh @@ -2,15 +2,17 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # --------------------------------------------------------------------------------------------- -if [[ $options[norcs] = off && -o "login" && -f $USER_ZDOTDIR/.zprofile ]]; then - VSCODE_ZDOTDIR=$ZDOTDIR - ZDOTDIR=$USER_ZDOTDIR - . $USER_ZDOTDIR/.zprofile - ZDOTDIR=$VSCODE_ZDOTDIR +if [[ $options[norcs] = off && -o "login" ]]; then + if [[ -f $USER_ZDOTDIR/.zprofile ]]; then + VSCODE_ZDOTDIR=$ZDOTDIR + ZDOTDIR=$USER_ZDOTDIR + . $USER_ZDOTDIR/.zprofile + ZDOTDIR=$VSCODE_ZDOTDIR + fi # Apply any explicit path prefix (see #99878) if (( ${+VSCODE_PATH_PREFIX} )); then - export PATH=$VSCODE_PATH_PREFIX$PATH + export PATH="$VSCODE_PATH_PREFIX$PATH" fi builtin unset VSCODE_PATH_PREFIX fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh index c555d1ac157..54a13d575f1 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh @@ -159,7 +159,7 @@ __vsc_update_prompt() { } __vsc_precmd() { - local __vsc_status="$?" + builtin local __vsc_status="$?" if [ -z "${__vsc_in_command_execution-}" ]; then # not in command execution __vsc_command_output_start diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index 6c92130ec95..c624a5f0036 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -170,6 +170,13 @@ function Set-MappedKeyHandler { } } +function Get-KeywordCompletionResult( + $Keyword, + $Description = $null +) { + [System.Management.Automation.CompletionResult]::new($Keyword, $Keyword, [System.Management.Automation.CompletionResultType]::Keyword, $null -ne $Description ? $Description : $Keyword) +} + function Set-MappedKeyHandlers { Set-MappedKeyHandler -Chord Ctrl+Spacebar -Sequence 'F12,a' Set-MappedKeyHandler -Chord Alt+Spacebar -Sequence 'F12,b' @@ -178,7 +185,7 @@ function Set-MappedKeyHandlers { # Enable suggestions if the environment variable is set and Windows PowerShell is not being used # as APIs are not available to support this feature - if ($env:VSCODE_SUGGEST -eq '1' -and $PSVersionTable.PSVersion -ge "6.0") { + if ($env:VSCODE_SUGGEST -eq '1' -and $PSVersionTable.PSVersion -ge "7.0") { Remove-Item Env:VSCODE_SUGGEST # VS Code send completions request (may override Ctrl+Spacebar) @@ -186,20 +193,140 @@ function Set-MappedKeyHandlers { Send-Completions } - # TODO: When does this invalidate? Installing a new module could add new commands. We could expose a command to update? Track `(Get-Module).Count`? - # Commands are expensive to complete and send over, do this once for the empty string so we - # don't need to do it each time the user requests. Additionally we also want to do filtering - # and ranking on the client side with the full list of results. - $result = "$([char]0x1b)]633;CompletionsPwshCommands;commands;" - $result += [System.Management.Automation.CompletionCompleters]::CompleteCommand('') | ConvertTo-Json -Compress - $result += "`a" - Write-Host -NoNewLine $result + # VS Code send global completions request + Set-PSReadLineKeyHandler -Chord 'F12,f' -ScriptBlock { + # Get commands, convert to string array to reduce the payload size and send as JSON + $commands = @( + [System.Management.Automation.CompletionCompleters]::CompleteCommand('') + Get-KeywordCompletionResult -Keyword 'begin' + Get-KeywordCompletionResult -Keyword 'break' + Get-KeywordCompletionResult -Keyword 'catch' -Description "catch [[][',' ]*] {}" + Get-KeywordCompletionResult -Keyword 'class' -Description @" +class [: [][,]] { + [[] [hidden] [static] ...] + [([]) + {} ...] + [[] [hidden] [static] ...] +} +"@ + Get-KeywordCompletionResult -Keyword 'clean' + Get-KeywordCompletionResult -Keyword 'continue' + Get-KeywordCompletionResult -Keyword 'data' -Description @" +data [] [-supportedCommand ] { + +} +"@ + Get-KeywordCompletionResult -Keyword 'do' -Description @" +do {} while () +do {} until () +"@ + Get-KeywordCompletionResult -Keyword 'dynamicparam' -Description "dynamicparam {}" + Get-KeywordCompletionResult -Keyword 'else' -Description @" +if () + {} +[elseif () + {}] +[else + {}] +"@ + Get-KeywordCompletionResult -Keyword 'elseif' -Description @" +if () + {} +[elseif () + {}] +[else + {}] +"@ + Get-KeywordCompletionResult -Keyword 'end' + Get-KeywordCompletionResult -Keyword 'enum' -Description @" +[[]...] [Flag()] enum [ : ] { +