Unified · Split
| @@ -1,4 +1,4 @@ | |||
| 1 | name: Bug Report (AI Related) | 1 | name: Bug Report (AI) |
| 2 | description: Zed Agent Panel Bugs | 2 | description: Zed Agent Panel Bugs |
| 3 | type: "Bug" | 3 | type: "Bug" |
| 4 | labels: ["ai"] | 4 | labels: ["ai"] |
| @@ -19,15 +19,14 @@ body: · 14 unmodified lines | |||
| 19 | 2. | 19 | 2. |
| 20 | 3. | 20 | 3. |
| 21 | 21 | ||
| 22 | Actual Behavior: | 22 | **Expected Behavior**: |
| 23 | Expected Behavior: | 23 | **Actual Behavior**: |
| 24 | 24 | ||
| 25 | ### Model Provider Details | 25 | ### Model Provider Details |
| 26 | - Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc) | 26 | - Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc) |
| 27 | - Model Name: | 27 | - Model Name: |
| 28 | - Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads) | 28 | - Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads) |
| 29 | - MCP Servers in-use: | 29 | - Other Details (MCPs, other settings, etc): |
| 30 | - Other Details: | ||
| 31 | validations: | 30 | validations: |
| 32 | required: true | 31 | required: true |
| 33 | 32 | ||
| @@ -1,36 +0,0 @@ | |||
| 1 | name: Bug Report (Edit Predictions) | ||
| 2 | description: Zed Edit Predictions bugs | ||
| 3 | type: "Bug" | ||
| 4 | labels: ["ai", "inline completion", "zeta"] | ||
| 5 | title: "Edit Predictions: <a short description of the Edit Prediction bug>" | ||
| 6 | body: | ||
| 7 | - type: textarea | ||
| 8 | attributes: | ||
| 9 | label: Summary | ||
| 10 | description: Describe the bug with a one line summary, and provide detailed reproduction steps | ||
| 11 | value: | | ||
| 12 | <!-- Please insert a one line summary of the issue below --> | ||
| 13 | SUMMARY_SENTENCE_HERE | ||
| 14 | |||
| 15 | ### Description | ||
| 16 | <!-- Describe with sufficient detail to reproduce from a clean Zed install. --> | ||
| 17 | <!-- Please include the LLM provider and model name you are using --> | ||
| 18 | Steps to trigger the problem: | ||
| 19 | 1. | ||
| 20 | 2. | ||
| 21 | 3. | ||
| 22 | |||
| 23 | Actual Behavior: | ||
| 24 | Expected Behavior: | ||
| 25 | validations: | ||
| 26 | required: true | ||
| 27 | |||
| 28 | - type: textarea | ||
| 29 | id: environment | ||
| 30 | attributes: | ||
| 31 | label: Zed Version and System Specs | ||
| 32 | description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"' | ||
| 33 | placeholder: | | ||
| 34 | Output of "zed: copy system specs into clipboard" | ||
| 35 | validations: | ||
| 36 | required: true | ||
| @@ -1,35 +0,0 @@ | |||
| 1 | name: Bug Report (Git) | ||
| 2 | description: Zed Git-Related Bugs | ||
| 3 | type: "Bug" | ||
| 4 | labels: ["git"] | ||
| 5 | title: "Git: <a short description of the Git bug>" | ||
| 6 | body: | ||
| 7 | - type: textarea | ||
| 8 | attributes: | ||
| 9 | label: Summary | ||
| 10 | description: Describe the bug with a one line summary, and provide detailed reproduction steps | ||
| 11 | value: | | ||
| 12 | <!-- Please insert a one line summary of the issue below --> | ||
| 13 | SUMMARY_SENTENCE_HERE | ||
| 14 | |||
| 15 | ### Description | ||
| 16 | <!-- Describe with sufficient detail to reproduce from a clean Zed install. --> | ||
| 17 | Steps to trigger the problem: | ||
| 18 | 1. | ||
| 19 | 2. | ||
| 20 | 3. | ||
| 21 | |||
| 22 | Actual Behavior: | ||
| 23 | Expected Behavior: | ||
| 24 | |||
| 25 | validations: | ||
| 26 | required: true | ||
| 27 | - type: textarea | ||
| 28 | id: environment | ||
| 29 | attributes: | ||
| 30 | label: Zed Version and System Specs | ||
| 31 | description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"' | ||
| 32 | placeholder: | | ||
| 33 | Output of "zed: copy system specs into clipboard" | ||
| 34 | validations: | ||
| 35 | required: true | ||
| @@ -19,8 +19,8 @@ body: · 18 unmodified lines | |||
| 19 | 2. | 19 | 2. |
| 20 | 3. | 20 | 3. |
| 21 | 21 | ||
| 22 | Actual Behavior: | 22 | **Expected Behavior**: |
| 23 | Expected Behavior: | 23 | **Actual Behavior**: |
| 24 | 24 | ||
| 25 | validations: | 25 | validations: |
| 26 | required: true | 26 | required: true |
| @@ -18,14 +18,16 @@ body: · 17 unmodified lines | |||
| 18 | - Issues with insufficient detail may be summarily closed. | 18 | - Issues with insufficient detail may be summarily closed. |
| 19 | --> | 19 | --> |
| 20 | 20 | ||
| 21 | DESCRIPTION_HERE | ||
| 22 | |||
| 21 | Steps to reproduce: | 23 | Steps to reproduce: |
| 22 | 1. | 24 | 1. |
| 23 | 2. | 25 | 2. |
| 24 | 3. | 26 | 3. |
| 25 | 4. | 27 | 4. |
| 26 | 28 | ||
| 27 | Expected Behavior: | 29 | **Expected Behavior**: |
| 28 | Actual Behavior: | 30 | **Actual Behavior**: |
| 29 | 31 | ||
| 30 | <!-- Before Submitting, did you: | 32 | <!-- Before Submitting, did you: |
| 31 | 1. Include settings.json, keymap.json, .editorconfig if relevant? | 33 | 1. Include settings.json, keymap.json, .editorconfig if relevant? |
| @@ -22,7 +22,7 @@ runs: · 21 unmodified lines | |||
| 22 | - name: Check for broken links | 22 | - name: Check for broken links |
| 23 | uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1 | 23 | uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1 |
| 24 | with: | 24 | with: |
| 25 | args: --no-progress './docs/src/**/*' | 25 | args: --no-progress --exclude '^http' './docs/src/**/*' |
| 26 | fail: true | 26 | fail: true |
| 27 | 27 | ||
| 28 | - name: Build book | 28 | - name: Build book |
| @@ -498,6 +498,7 @@ jobs: · 497 unmodified lines | |||
| 498 | needs: | 498 | needs: |
| 499 | - job_spec | 499 | - job_spec |
| 500 | - style | 500 | - style |
| 501 | - check_docs | ||
| 501 | - migration_checks | 502 | - migration_checks |
| 502 | # run_tests: If adding required tests, add them here and to script below. | 503 | # run_tests: If adding required tests, add them here and to script below. |
| 503 | - workspace_hack | 504 | - workspace_hack |
| @@ -515,7 +516,8 @@ jobs: · 11 unmodified lines | |||
| 515 | # Check dependent jobs... | 516 | # Check dependent jobs... |
| 516 | RET_CODE=0 | 517 | RET_CODE=0 |
| 517 | # Always check style | 518 | # Always check style |
| 518 | [[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; } | 519 | [[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; } |
| 520 | [[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; } | ||
| 519 | 521 | ||
| 520 | # Only check test jobs if they were supposed to run | 522 | # Only check test jobs if they were supposed to run |
| 521 | if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then | 523 | if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then |
| @@ -801,6 +803,7 @@ jobs: · 279 unmodified lines | |||
| 801 | name: Build with Nix | 803 | name: Build with Nix |
| 802 | uses: ./.github/workflows/nix.yml | 804 | uses: ./.github/workflows/nix.yml |
| 803 | if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix') | 805 | if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix') |
| 806 | secrets: inherit | ||
| 804 | with: | 807 | with: |
| 805 | flake-output: debug | 808 | flake-output: debug |
| 806 | # excludes the final package to only cache dependencies | 809 | # excludes the final package to only cache dependencies |
| @@ -30,6 +30,7 @@ jobs: · 29 unmodified lines | |||
| 30 | noop: | 30 | noop: |
| 31 | name: No-op | 31 | name: No-op |
| 32 | runs-on: ubuntu-latest | 32 | runs-on: ubuntu-latest |
| 33 | if: github.repository_owner == 'zed-industries' | ||
| 33 | steps: | 34 | steps: |
| 34 | - name: No-op | 35 | - name: No-op |
| 35 | run: echo "Nothing to do" | 36 | run: echo "Nothing to do" |
| @@ -214,6 +214,7 @@ jobs: · 213 unmodified lines | |||
| 214 | bundle-nix: | 214 | bundle-nix: |
| 215 | name: Build and cache Nix package | 215 | name: Build and cache Nix package |
| 216 | needs: tests | 216 | needs: tests |
| 217 | secrets: inherit | ||
| 217 | uses: ./.github/workflows/nix.yml | 218 | uses: ./.github/workflows/nix.yml |
| 218 | 219 | ||
| 219 | update-nightly-tag: | 220 | update-nightly-tag: |
| @@ -19,6 +19,7 @@ env: · 18 unmodified lines | |||
| 19 | 19 | ||
| 20 | jobs: | 20 | jobs: |
| 21 | unit_evals: | 21 | unit_evals: |
| 22 | if: github.repository_owner == 'zed-industries' | ||
| 22 | timeout-minutes: 60 | 23 | timeout-minutes: 60 |
| 23 | name: Run unit evals | 24 | name: Run unit evals |
| 24 | runs-on: | 25 | runs-on: |
| @@ -62,7 +63,7 @@ jobs: · 37 unmodified lines | |||
| 62 | 63 | ||
| 63 | - name: Run unit evals | 64 | - name: Run unit evals |
| 64 | shell: bash -euxo pipefail {0} | 65 | shell: bash -euxo pipefail {0} |
| 65 | run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)' --test-threads 1 | 66 | run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)' |
| 66 | env: | 67 | env: |
| 67 | ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | 68 | ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} |
| 68 | 69 | ||
| @@ -99,6 +99,7 @@ dependencies = [ · 98 unmodified lines | |||
| 99 | "paths", | 99 | "paths", |
| 100 | "picker", | 100 | "picker", |
| 101 | "postage", | 101 | "postage", |
| 102 | "pretty_assertions", | ||
| 102 | "project", | 103 | "project", |
| 103 | "prompt_store", | 104 | "prompt_store", |
| 104 | "proto", | 105 | "proto", |
| @@ -490,7 +491,6 @@ dependencies = [ · 385 unmodified lines | |||
| 490 | "anyhow", | 491 | "anyhow", |
| 491 | "futures 0.3.31", | 492 | "futures 0.3.31", |
| 492 | "gpui", | 493 | "gpui", |
| 493 | "shlex", | ||
| 494 | "smol", | 494 | "smol", |
| 495 | "tempfile", | 495 | "tempfile", |
| 496 | "util", | 496 | "util", |
| @@ -704,6 +704,7 @@ dependencies = [ · 207 unmodified lines | |||
| 704 | "serde_json", | 704 | "serde_json", |
| 705 | "settings", | 705 | "settings", |
| 706 | "smallvec", | 706 | "smallvec", |
| 707 | "smol", | ||
| 707 | "streaming_diff", | 708 | "streaming_diff", |
| 708 | "strsim", | 709 | "strsim", |
| 709 | "task", | 710 | "task", |
| @@ -2821,9 +2822,11 @@ dependencies = [ · 2111 unmodified lines | |||
| 2821 | "collections", | 2822 | "collections", |
| 2822 | "credentials_provider", | 2823 | "credentials_provider", |
| 2823 | "feature_flags", | 2824 | "feature_flags", |
| 2825 | "fs", | ||
| 2824 | "futures 0.3.31", | 2826 | "futures 0.3.31", |
| 2825 | "gpui", | 2827 | "gpui", |
| 2826 | "gpui_tokio", | 2828 | "gpui_tokio", |
| 2829 | "hickory-resolver", | ||
| 2827 | "http_client", | 2830 | "http_client", |
| 2828 | "http_client_tls", | 2831 | "http_client_tls", |
| 2829 | "httparse", | 2832 | "httparse", |
| @@ -2832,6 +2835,7 @@ dependencies = [ · 2 unmodified lines | |||
| 2832 | "paths", | 2835 | "paths", |
| 2833 | "postage", | 2836 | "postage", |
| 2834 | "rand 0.8.5", | 2837 | "rand 0.8.5", |
| 2838 | "regex", | ||
| 2835 | "release_channel", | 2839 | "release_channel", |
| 2836 | "rpc", | 2840 | "rpc", |
| 2837 | "rustls-pki-types", | 2841 | "rustls-pki-types", |
| @@ -3159,6 +3163,16 @@ dependencies = [ · 321 unmodified lines | |||
| 3159 | "memchr", | 3163 | "memchr", |
| 3160 | ] | 3164 | ] |
| 3161 | 3165 | ||
| 3166 | [[package]] | ||
| 3167 | name = "command-fds" | ||
| 3168 | version = "0.3.1" | ||
| 3169 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3170 | checksum = "2ec1052629a80c28594777d1252efc8a6b005d13f9edfd8c3fc0f44d5b32489a" | ||
| 3171 | dependencies = [ | ||
| 3172 | "nix 0.30.1", | ||
| 3173 | "thiserror 2.0.12", | ||
| 3174 | ] | ||
| 3175 | |||
| 3162 | [[package]] | 3176 | [[package]] |
| 3163 | name = "command_palette" | 3177 | name = "command_palette" |
| 3164 | version = "0.1.0" | 3178 | version = "0.1.0" |
| @@ -3528,6 +3542,20 @@ dependencies = [ · 363 unmodified lines | |||
| 3528 | "coreaudio-sys", | 3542 | "coreaudio-sys", |
| 3529 | ] | 3543 | ] |
| 3530 | 3544 | ||
| 3545 | [[package]] | ||
| 3546 | name = "coreaudio-rs" | ||
| 3547 | version = "0.13.0" | ||
| 3548 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3549 | checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" | ||
| 3550 | dependencies = [ | ||
| 3551 | "bitflags 1.3.2", | ||
| 3552 | "libc", | ||
| 3553 | "objc2-audio-toolbox", | ||
| 3554 | "objc2-core-audio", | ||
| 3555 | "objc2-core-audio-types", | ||
| 3556 | "objc2-core-foundation", | ||
| 3557 | ] | ||
| 3558 | |||
| 3531 | [[package]] | 3559 | [[package]] |
| 3532 | name = "coreaudio-sys" | 3560 | name = "coreaudio-sys" |
| 3533 | version = "0.2.16" | 3561 | version = "0.2.16" |
| @@ -3563,7 +3591,8 @@ dependencies = [ · 29 unmodified lines | |||
| 3563 | [[package]] | 3591 | [[package]] |
| 3564 | name = "cpal" | 3592 | name = "cpal" |
| 3565 | version = "0.15.3" | 3593 | version = "0.15.3" |
| 3566 | source = "git+https://github.com/zed-industries/cpal?rev=fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50#fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" | 3594 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3595 | checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" | ||
| 3567 | dependencies = [ | 3596 | dependencies = [ |
| 3568 | "alsa", | 3597 | "alsa", |
| 3569 | "core-foundation-sys", | 3598 | "core-foundation-sys", |
| @@ -3573,7 +3602,7 @@ dependencies = [ · 3 unmodified lines | |||
| 3573 | "js-sys", | 3602 | "js-sys", |
| 3574 | "libc", | 3603 | "libc", |
| 3575 | "mach2", | 3604 | "mach2", |
| 3576 | "ndk", | 3605 | "ndk 0.8.0", |
| 3577 | "ndk-context", | 3606 | "ndk-context", |
| 3578 | "oboe", | 3607 | "oboe", |
| 3579 | "wasm-bindgen", | 3608 | "wasm-bindgen", |
| @@ -3582,6 +3611,32 @@ dependencies = [ · 2 unmodified lines | |||
| 3582 | "windows 0.54.0", | 3611 | "windows 0.54.0", |
| 3583 | ] | 3612 | ] |
| 3584 | 3613 | ||
| 3614 | [[package]] | ||
| 3615 | name = "cpal" | ||
| 3616 | version = "0.16.0" | ||
| 3617 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3618 | checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" | ||
| 3619 | dependencies = [ | ||
| 3620 | "alsa", | ||
| 3621 | "coreaudio-rs 0.13.0", | ||
| 3622 | "dasp_sample", | ||
| 3623 | "jni", | ||
| 3624 | "js-sys", | ||
| 3625 | "libc", | ||
| 3626 | "mach2", | ||
| 3627 | "ndk 0.9.0", | ||
| 3628 | "ndk-context", | ||
| 3629 | "num-derive", | ||
| 3630 | "num-traits", | ||
| 3631 | "objc2-audio-toolbox", | ||
| 3632 | "objc2-core-audio", | ||
| 3633 | "objc2-core-audio-types", | ||
| 3634 | "wasm-bindgen", | ||
| 3635 | "wasm-bindgen-futures", | ||
| 3636 | "web-sys", | ||
| 3637 | "windows 0.54.0", | ||
| 3638 | ] | ||
| 3639 | |||
| 3585 | [[package]] | 3640 | [[package]] |
| 3586 | name = "cpp_demangle" | 3641 | name = "cpp_demangle" |
| 3587 | version = "0.4.4" | 3642 | version = "0.4.4" |
| @@ -4015,6 +4070,7 @@ dependencies = [ · 427 unmodified lines | |||
| 4015 | "gpui", | 4070 | "gpui", |
| 4016 | "http_client", | 4071 | "http_client", |
| 4017 | "language", | 4072 | "language", |
| 4073 | "libc", | ||
| 4018 | "log", | 4074 | "log", |
| 4019 | "node_runtime", | 4075 | "node_runtime", |
| 4020 | "parking_lot", | 4076 | "parking_lot", |
| @@ -4038,7 +4094,7 @@ dependencies = [ · 17 unmodified lines | |||
| 4038 | [[package]] | 4094 | [[package]] |
| 4039 | name = "dap-types" | 4095 | name = "dap-types" |
| 4040 | version = "0.0.1" | 4096 | version = "0.0.1" |
| 4041 | source = "git+https://github.com/zed-industries/dap-types?rev=68516de327fa1be15214133a0a2e52a12982ce75#68516de327fa1be15214133a0a2e52a12982ce75" | 4097 | source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308" |
| 4042 | dependencies = [ | 4098 | dependencies = [ |
| 4043 | "schemars", | 4099 | "schemars", |
| 4044 | "serde", | 4100 | "serde", |
| @@ -4051,6 +4107,7 @@ version = "0.1.0" · 6 unmodified lines | |||
| 4051 | dependencies = [ | 4107 | dependencies = [ |
| 4052 | "anyhow", | 4108 | "anyhow", |
| 4053 | "async-trait", | 4109 | "async-trait", |
| 4110 | "collections", | ||
| 4054 | "dap", | 4111 | "dap", |
| 4055 | "futures 0.3.31", | 4112 | "futures 0.3.31", |
| 4056 | "gpui", | 4113 | "gpui", |
| @@ -4682,7 +4739,6 @@ dependencies = [ · 625 unmodified lines | |||
| 4682 | "client", | 4739 | "client", |
| 4683 | "clock", | 4740 | "clock", |
| 4684 | "collections", | 4741 | "collections", |
| 4685 | "command_palette_hooks", | ||
| 4686 | "convert_case 0.8.0", | 4742 | "convert_case 0.8.0", |
| 4687 | "ctor", | 4743 | "ctor", |
| 4688 | "dap", | 4744 | "dap", |
| @@ -4851,6 +4907,18 @@ version = "1.1.0" · 162 unmodified lines | |||
| 4851 | source = "registry+https://github.com/rust-lang/crates.io-index" | 4907 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 4852 | checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" | 4908 | checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" |
| 4853 | 4909 | ||
| 4910 | [[package]] | ||
| 4911 | name = "enum-as-inner" | ||
| 4912 | version = "0.6.1" | ||
| 4913 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 4914 | checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" | ||
| 4915 | dependencies = [ | ||
| 4916 | "heck 0.5.0", | ||
| 4917 | "proc-macro2", | ||
| 4918 | "quote", | ||
| 4919 | "syn 2.0.101", | ||
| 4920 | ] | ||
| 4921 | |||
| 4854 | [[package]] | 4922 | [[package]] |
| 4855 | name = "enumflags2" | 4923 | name = "enumflags2" |
| 4856 | version = "0.7.11" | 4924 | version = "0.7.11" |
| @@ -6108,6 +6176,7 @@ dependencies = [ · 1251 unmodified lines | |||
| 6108 | "anyhow", | 6176 | "anyhow", |
| 6109 | "askpass", | 6177 | "askpass", |
| 6110 | "buffer_diff", | 6178 | "buffer_diff", |
| 6179 | "call", | ||
| 6111 | "chrono", | 6180 | "chrono", |
| 6112 | "collections", | 6181 | "collections", |
| 6113 | "command_palette_hooks", | 6182 | "command_palette_hooks", |
| @@ -7428,6 +7497,51 @@ version = "0.2.1" · 1314 unmodified lines | |||
| 7428 | source = "registry+https://github.com/rust-lang/crates.io-index" | 7497 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 7429 | checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" | 7498 | checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" |
| 7430 | 7499 | ||
| 7500 | [[package]] | ||
| 7501 | name = "hickory-proto" | ||
| 7502 | version = "0.24.4" | ||
| 7503 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 7504 | checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" | ||
| 7505 | dependencies = [ | ||
| 7506 | "async-trait", | ||
| 7507 | "cfg-if", | ||
| 7508 | "data-encoding", | ||
| 7509 | "enum-as-inner", | ||
| 7510 | "futures-channel", | ||
| 7511 | "futures-io", | ||
| 7512 | "futures-util", | ||
| 7513 | "idna", | ||
| 7514 | "ipnet", | ||
| 7515 | "once_cell", | ||
| 7516 | "rand 0.8.5", | ||
| 7517 | "thiserror 1.0.69", | ||
| 7518 | "tinyvec", | ||
| 7519 | "tokio", | ||
| 7520 | "tracing", | ||
| 7521 | "url", | ||
| 7522 | ] | ||
| 7523 | |||
| 7524 | [[package]] | ||
| 7525 | name = "hickory-resolver" | ||
| 7526 | version = "0.24.4" | ||
| 7527 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 7528 | checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" | ||
| 7529 | dependencies = [ | ||
| 7530 | "cfg-if", | ||
| 7531 | "futures-util", | ||
| 7532 | "hickory-proto", | ||
| 7533 | "ipconfig", | ||
| 7534 | "lru-cache", | ||
| 7535 | "once_cell", | ||
| 7536 | "parking_lot", | ||
| 7537 | "rand 0.8.5", | ||
| 7538 | "resolv-conf", | ||
| 7539 | "smallvec", | ||
| 7540 | "thiserror 1.0.69", | ||
| 7541 | "tokio", | ||
| 7542 | "tracing", | ||
| 7543 | ] | ||
| 7544 | |||
| 7431 | [[package]] | 7545 | [[package]] |
| 7432 | name = "hidden-trait" | 7546 | name = "hidden-trait" |
| 7433 | version = "0.1.2" | 7547 | version = "0.1.2" |
| @@ -8297,6 +8411,18 @@ dependencies = [ · 863 unmodified lines | |||
| 8297 | "windows 0.58.0", | 8411 | "windows 0.58.0", |
| 8298 | ] | 8412 | ] |
| 8299 | 8413 | ||
| 8414 | [[package]] | ||
| 8415 | name = "ipconfig" | ||
| 8416 | version = "0.3.2" | ||
| 8417 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 8418 | checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" | ||
| 8419 | dependencies = [ | ||
| 8420 | "socket2", | ||
| 8421 | "widestring", | ||
| 8422 | "windows-sys 0.48.0", | ||
| 8423 | "winreg 0.50.0", | ||
| 8424 | ] | ||
| 8425 | |||
| 8300 | [[package]] | 8426 | [[package]] |
| 8301 | name = "ipnet" | 8427 | name = "ipnet" |
| 8302 | version = "2.11.0" | 8428 | version = "2.11.0" |
| @@ -8990,7 +9116,6 @@ dependencies = [ · 687 unmodified lines | |||
| 8990 | "tree-sitter-yaml", | 9116 | "tree-sitter-yaml", |
| 8991 | "unindent", | 9117 | "unindent", |
| 8992 | "util", | 9118 | "util", |
| 8993 | "which 6.0.3", | ||
| 8994 | "workspace", | 9119 | "workspace", |
| 8995 | "workspace-hack", | 9120 | "workspace-hack", |
| 8996 | ] | 9121 | ] |
| @@ -9182,6 +9307,12 @@ dependencies = [ · 185 unmodified lines | |||
| 9182 | "cc", | 9307 | "cc", |
| 9183 | ] | 9308 | ] |
| 9184 | 9309 | ||
| 9310 | [[package]] | ||
| 9311 | name = "linked-hash-map" | ||
| 9312 | version = "0.5.6" | ||
| 9313 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 9314 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" | ||
| 9315 | |||
| 9185 | [[package]] | 9316 | [[package]] |
| 9186 | name = "linkify" | 9317 | name = "linkify" |
| 9187 | version = "0.10.0" | 9318 | version = "0.10.0" |
| @@ -9308,7 +9439,7 @@ dependencies = [ · 120 unmodified lines | |||
| 9308 | "core-foundation 0.10.0", | 9439 | "core-foundation 0.10.0", |
| 9309 | "core-video", | 9440 | "core-video", |
| 9310 | "coreaudio-rs 0.12.1", | 9441 | "coreaudio-rs 0.12.1", |
| 9311 | "cpal", | 9442 | "cpal 0.16.0", |
| 9312 | "futures 0.3.31", | 9443 | "futures 0.3.31", |
| 9313 | "gpui", | 9444 | "gpui", |
| 9314 | "gpui_tokio", | 9445 | "gpui_tokio", |
| @@ -9442,6 +9573,15 @@ dependencies = [ · 127 unmodified lines | |||
| 9442 | "hashbrown 0.15.3", | 9573 | "hashbrown 0.15.3", |
| 9443 | ] | 9574 | ] |
| 9444 | 9575 | ||
| 9576 | [[package]] | ||
| 9577 | name = "lru-cache" | ||
| 9578 | version = "0.1.2" | ||
| 9579 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 9580 | checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" | ||
| 9581 | dependencies = [ | ||
| 9582 | "linked-hash-map", | ||
| 9583 | ] | ||
| 9584 | |||
| 9445 | [[package]] | 9585 | [[package]] |
| 9446 | name = "lsp" | 9586 | name = "lsp" |
| 9447 | version = "0.1.0" | 9587 | version = "0.1.0" |
| @@ -10078,7 +10218,21 @@ dependencies = [ · 630 unmodified lines | |||
| 10078 | "bitflags 2.9.0", | 10218 | "bitflags 2.9.0", |
| 10079 | "jni-sys", | 10219 | "jni-sys", |
| 10080 | "log", | 10220 | "log", |
| 10081 | "ndk-sys", | 10221 | "ndk-sys 0.5.0+25.2.9519653", |
| 10222 | "num_enum", | ||
| 10223 | "thiserror 1.0.69", | ||
| 10224 | ] | ||
| 10225 | |||
| 10226 | [[package]] | ||
| 10227 | name = "ndk" | ||
| 10228 | version = "0.9.0" | ||
| 10229 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 10230 | checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" | ||
| 10231 | dependencies = [ | ||
| 10232 | "bitflags 2.9.0", | ||
| 10233 | "jni-sys", | ||
| 10234 | "log", | ||
| 10235 | "ndk-sys 0.6.0+11769913", | ||
| 10082 | "num_enum", | 10236 | "num_enum", |
| 10083 | "thiserror 1.0.69", | 10237 | "thiserror 1.0.69", |
| 10084 | ] | 10238 | ] |
| @@ -10098,6 +10252,15 @@ dependencies = [ · 13 unmodified lines | |||
| 10098 | "jni-sys", | 10252 | "jni-sys", |
| 10099 | ] | 10253 | ] |
| 10100 | 10254 | ||
| 10255 | [[package]] | ||
| 10256 | name = "ndk-sys" | ||
| 10257 | version = "0.6.0+11769913" | ||
| 10258 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 10259 | checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" | ||
| 10260 | dependencies = [ | ||
| 10261 | "jni-sys", | ||
| 10262 | ] | ||
| 10263 | |||
| 10101 | [[package]] | 10264 | [[package]] |
| 10102 | name = "new_debug_unreachable" | 10265 | name = "new_debug_unreachable" |
| 10103 | version = "1.0.6" | 10266 | version = "1.0.6" |
| @@ -10129,6 +10292,18 @@ dependencies = [ · 25 unmodified lines | |||
| 10129 | "memoffset", | 10292 | "memoffset", |
| 10130 | ] | 10293 | ] |
| 10131 | 10294 | ||
| 10295 | [[package]] | ||
| 10296 | name = "nix" | ||
| 10297 | version = "0.30.1" | ||
| 10298 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 10299 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" | ||
| 10300 | dependencies = [ | ||
| 10301 | "bitflags 2.9.0", | ||
| 10302 | "cfg-if", | ||
| 10303 | "cfg_aliases 0.2.1", | ||
| 10304 | "libc", | ||
| 10305 | ] | ||
| 10306 | |||
| 10132 | [[package]] | 10307 | [[package]] |
| 10133 | name = "node_runtime" | 10308 | name = "node_runtime" |
| 10134 | version = "0.1.0" | 10309 | version = "0.1.0" |
| @@ -10499,6 +10674,43 @@ dependencies = [ · 364 unmodified lines | |||
| 10499 | "objc2-quartz-core", | 10674 | "objc2-quartz-core", |
| 10500 | ] | 10675 | ] |
| 10501 | 10676 | ||
| 10677 | [[package]] | ||
| 10678 | name = "objc2-audio-toolbox" | ||
| 10679 | version = "0.3.1" | ||
| 10680 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 10681 | checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" | ||
| 10682 | dependencies = [ | ||
| 10683 | "bitflags 2.9.0", | ||
| 10684 | "libc", | ||
| 10685 | "objc2", | ||
| 10686 | "objc2-core-audio", | ||
| 10687 | "objc2-core-audio-types", | ||
| 10688 | "objc2-core-foundation", | ||
| 10689 | "objc2-foundation", | ||
| 10690 | ] | ||
| 10691 | |||
| 10692 | [[package]] | ||
| 10693 | name = "objc2-core-audio" | ||
| 10694 | version = "0.3.1" | ||
| 10695 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 10696 | checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" | ||
| 10697 | dependencies = [ | ||
| 10698 | "dispatch2", | ||
| 10699 | "objc2", | ||
| 10700 | "objc2-core-audio-types", | ||
| 10701 | "objc2-core-foundation", | ||
| 10702 | ] | ||
| 10703 | |||
| 10704 | [[package]] | ||
| 10705 | name = "objc2-core-audio-types" | ||
| 10706 | version = "0.3.1" | ||
| 10707 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 10708 | checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" | ||
| 10709 | dependencies = [ | ||
| 10710 | "bitflags 2.9.0", | ||
| 10711 | "objc2", | ||
| 10712 | ] | ||
| 10713 | |||
| 10502 | [[package]] | 10714 | [[package]] |
| 10503 | name = "objc2-core-foundation" | 10715 | name = "objc2-core-foundation" |
| 10504 | version = "0.3.1" | 10716 | version = "0.3.1" |
| @@ -10604,7 +10816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" · 99 unmodified lines | |||
| 10604 | checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" | 10816 | checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" |
| 10605 | dependencies = [ | 10817 | dependencies = [ |
| 10606 | "jni", | 10818 | "jni", |
| 10607 | "ndk", | 10819 | "ndk 0.8.0", |
| 10608 | "ndk-context", | 10820 | "ndk-context", |
| 10609 | "num-derive", | 10821 | "num-derive", |
| 10610 | "num-traits", | 10822 | "num-traits", |
| @@ -12109,7 +12321,6 @@ dependencies = [ · 1498 unmodified lines | |||
| 12109 | "unindent", | 12321 | "unindent", |
| 12110 | "url", | 12322 | "url", |
| 12111 | "util", | 12323 | "util", |
| 12112 | "uuid", | ||
| 12113 | "which 6.0.3", | 12324 | "which 6.0.3", |
| 12114 | "workspace-hack", | 12325 | "workspace-hack", |
| 12115 | "worktree", | 12326 | "worktree", |
| @@ -13202,6 +13413,7 @@ dependencies = [ · 1086 unmodified lines | |||
| 13202 | "futures-core", | 13413 | "futures-core", |
| 13203 | "futures-util", | 13414 | "futures-util", |
| 13204 | "h2 0.4.9", | 13415 | "h2 0.4.9", |
| 13416 | "hickory-resolver", | ||
| 13205 | "http 1.3.1", | 13417 | "http 1.3.1", |
| 13206 | "http-body 1.0.1", | 13418 | "http-body 1.0.1", |
| 13207 | "http-body-util", | 13419 | "http-body-util", |
| @@ -13258,6 +13470,12 @@ dependencies = [ · 50 unmodified lines | |||
| 13258 | "workspace-hack", | 13470 | "workspace-hack", |
| 13259 | ] | 13471 | ] |
| 13260 | 13472 | ||
| 13473 | [[package]] | ||
| 13474 | name = "resolv-conf" | ||
| 13475 | version = "0.7.4" | ||
| 13476 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 13477 | checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" | ||
| 13478 | |||
| 13261 | [[package]] | 13479 | [[package]] |
| 13262 | name = "resvg" | 13480 | name = "resvg" |
| 13263 | version = "0.45.1" | 13481 | version = "0.45.1" |
| @@ -13377,7 +13595,7 @@ version = "0.20.1" · 113 unmodified lines | |||
| 13377 | source = "registry+https://github.com/rust-lang/crates.io-index" | 13595 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 13378 | checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" | 13596 | checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" |
| 13379 | dependencies = [ | 13597 | dependencies = [ |
| 13380 | "cpal", | 13598 | "cpal 0.15.3", |
| 13381 | "hound", | 13599 | "hound", |
| 13382 | ] | 13600 | ] |
| 13383 | 13601 | ||
| @@ -14389,12 +14607,12 @@ dependencies = [ · 1005 unmodified lines | |||
| 14389 | "fs", | 14607 | "fs", |
| 14390 | "gpui", | 14608 | "gpui", |
| 14391 | "log", | 14609 | "log", |
| 14392 | "paths", | ||
| 14393 | "schemars", | 14610 | "schemars", |
| 14394 | "serde", | 14611 | "serde", |
| 14395 | "settings", | 14612 | "settings", |
| 14396 | "theme", | 14613 | "theme", |
| 14397 | "ui", | 14614 | "ui", |
| 14615 | "util", | ||
| 14398 | "workspace", | 14616 | "workspace", |
| 14399 | "workspace-hack", | 14617 | "workspace-hack", |
| 14400 | ] | 14618 | ] |
| @@ -15725,6 +15943,7 @@ dependencies = [ · 1324 unmodified lines | |||
| 15725 | "theme", | 15943 | "theme", |
| 15726 | "thiserror 2.0.12", | 15944 | "thiserror 2.0.12", |
| 15727 | "url", | 15945 | "url", |
| 15946 | "urlencoding", | ||
| 15728 | "util", | 15947 | "util", |
| 15729 | "windows 0.61.1", | 15948 | "windows 0.61.1", |
| 15730 | "workspace-hack", | 15949 | "workspace-hack", |
| @@ -17121,6 +17340,7 @@ dependencies = [ · 1390 unmodified lines | |||
| 17121 | "async-fs", | 17340 | "async-fs", |
| 17122 | "async_zip", | 17341 | "async_zip", |
| 17123 | "collections", | 17342 | "collections", |
| 17343 | "command-fds", | ||
| 17124 | "dirs 4.0.0", | 17344 | "dirs 4.0.0", |
| 17125 | "dunce", | 17345 | "dunce", |
| 17126 | "futures 0.3.31", | 17346 | "futures 0.3.31", |
| @@ -17131,12 +17351,14 @@ dependencies = [ · 4 unmodified lines | |||
| 17131 | "itertools 0.14.0", | 17351 | "itertools 0.14.0", |
| 17132 | "libc", | 17352 | "libc", |
| 17133 | "log", | 17353 | "log", |
| 17354 | "nix 0.29.0", | ||
| 17134 | "rand 0.8.5", | 17355 | "rand 0.8.5", |
| 17135 | "regex", | 17356 | "regex", |
| 17136 | "rust-embed", | 17357 | "rust-embed", |
| 17137 | "serde", | 17358 | "serde", |
| 17138 | "serde_json", | 17359 | "serde_json", |
| 17139 | "serde_json_lenient", | 17360 | "serde_json_lenient", |
| 17361 | "shlex", | ||
| 17140 | "smol", | 17362 | "smol", |
| 17141 | "take-until", | 17363 | "take-until", |
| 17142 | "tempfile", | 17364 | "tempfile", |
| @@ -18178,6 +18400,12 @@ dependencies = [ · 1035 unmodified lines | |||
| 18178 | "wasite", | 18400 | "wasite", |
| 18179 | ] | 18401 | ] |
| 18180 | 18402 | ||
| 18403 | [[package]] | ||
| 18404 | name = "widestring" | ||
| 18405 | version = "1.2.0" | ||
| 18406 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 18407 | checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" | ||
| 18408 | |||
| 18181 | [[package]] | 18409 | [[package]] |
| 18182 | name = "wiggle" | 18410 | name = "wiggle" |
| 18183 | version = "29.0.1" | 18411 | version = "29.0.1" |
| @@ -19298,6 +19526,7 @@ dependencies = [ · 1114 unmodified lines | |||
| 19298 | "num-rational", | 19526 | "num-rational", |
| 19299 | "num-traits", | 19527 | "num-traits", |
| 19300 | "objc2", | 19528 | "objc2", |
| 19529 | "objc2-core-foundation", | ||
| 19301 | "objc2-foundation", | 19530 | "objc2-foundation", |
| 19302 | "objc2-metal", | 19531 | "objc2-metal", |
| 19303 | "object", | 19532 | "object", |
| @@ -19718,7 +19947,7 @@ dependencies = [ · 414 unmodified lines | |||
| 19718 | 19947 | ||
| 19719 | [[package]] | 19948 | [[package]] |
| 19720 | name = "zed" | 19949 | name = "zed" |
| 19721 | version = "0.191.0" | 19950 | version = "0.192.0" |
| 19722 | dependencies = [ | 19951 | dependencies = [ |
| 19723 | "activity_indicator", | 19952 | "activity_indicator", |
| 19724 | "agent", | 19953 | "agent", |
| @@ -433,9 +433,10 @@ convert_case = "0.8.0" · 432 unmodified lines | |||
| 433 | core-foundation = "0.10.0" | 433 | core-foundation = "0.10.0" |
| 434 | core-foundation-sys = "0.8.6" | 434 | core-foundation-sys = "0.8.6" |
| 435 | core-video = { version = "0.4.3", features = ["metal"] } | 435 | core-video = { version = "0.4.3", features = ["metal"] } |
| 436 | cpal = "0.16" | ||
| 436 | criterion = { version = "0.5", features = ["html_reports"] } | 437 | criterion = { version = "0.5", features = ["html_reports"] } |
| 437 | ctor = "0.4.0" | 438 | ctor = "0.4.0" |
| 438 | dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" } | 439 | dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" } |
| 439 | dashmap = "6.0" | 440 | dashmap = "6.0" |
| 440 | derive_more = "0.99.17" | 441 | derive_more = "0.99.17" |
| 441 | dirs = "4.0" | 442 | dirs = "4.0" |
| @@ -523,6 +524,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 · 81 unmodified lines | |||
| 523 | "rustls-tls-native-roots", | 524 | "rustls-tls-native-roots", |
| 524 | "socks", | 525 | "socks", |
| 525 | "stream", | 526 | "stream", |
| 527 | "hickory-dns", | ||
| 526 | ] } | 528 | ] } |
| 527 | rsa = "0.9.6" | 529 | rsa = "0.9.6" |
| 528 | runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [ | 530 | runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [ |
| @@ -682,9 +684,7 @@ features = [ · 153 unmodified lines | |||
| 682 | "Win32_UI_WindowsAndMessaging", | 684 | "Win32_UI_WindowsAndMessaging", |
| 683 | ] | 685 | ] |
| 684 | 686 | ||
| 685 | # TODO livekit https://github.com/RustAudio/cpal/pull/891 | ||
| 686 | [patch.crates-io] | 687 | [patch.crates-io] |
| 687 | cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" } | ||
| 688 | notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } | 688 | notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } |
| 689 | notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } | 689 | notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } |
| 690 | 690 | ||
| @@ -698,6 +698,8 @@ codegen-units = 16 · 7 unmodified lines | |||
| 698 | [profile.dev.package] | 698 | [profile.dev.package] |
| 699 | taffy = { opt-level = 3 } | 699 | taffy = { opt-level = 3 } |
| 700 | cranelift-codegen = { opt-level = 3 } | 700 | cranelift-codegen = { opt-level = 3 } |
| 701 | cranelift-codegen-meta = { opt-level = 3 } | ||
| 702 | cranelift-codegen-shared = { opt-level = 3 } | ||
| 701 | resvg = { opt-level = 3 } | 703 | resvg = { opt-level = 3 } |
| 702 | rustybuzz = { opt-level = 3 } | 704 | rustybuzz = { opt-level = 3 } |
| 703 | ttf-parser = { opt-level = 3 } | 705 | ttf-parser = { opt-level = 3 } |
| @@ -0,0 +1 @@ | |||
| 1 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-help-icon lucide-circle-help"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg> | ||
| @@ -115,6 +115,7 @@ · 114 unmodified lines | |||
| 115 | "ctrl-\"": "editor::ExpandAllDiffHunks", | 115 | "ctrl-\"": "editor::ExpandAllDiffHunks", |
| 116 | "ctrl-i": "editor::ShowSignatureHelp", | 116 | "ctrl-i": "editor::ShowSignatureHelp", |
| 117 | "alt-g b": "git::Blame", | 117 | "alt-g b": "git::Blame", |
| 118 | "alt-g m": "git::OpenModifiedFiles", | ||
| 118 | "menu": "editor::OpenContextMenu", | 119 | "menu": "editor::OpenContextMenu", |
| 119 | "shift-f10": "editor::OpenContextMenu", | 120 | "shift-f10": "editor::OpenContextMenu", |
| 120 | "ctrl-shift-e": "editor::ToggleEditPrediction", | 121 | "ctrl-shift-e": "editor::ToggleEditPrediction", |
| @@ -139,6 +139,7 @@ · 138 unmodified lines | |||
| 139 | "cmd-'": "editor::ToggleSelectedDiffHunks", | 139 | "cmd-'": "editor::ToggleSelectedDiffHunks", |
| 140 | "cmd-\"": "editor::ExpandAllDiffHunks", | 140 | "cmd-\"": "editor::ExpandAllDiffHunks", |
| 141 | "cmd-alt-g b": "git::Blame", | 141 | "cmd-alt-g b": "git::Blame", |
| 142 | "cmd-alt-g m": "git::OpenModifiedFiles", | ||
| 142 | "cmd-i": "editor::ShowSignatureHelp", | 143 | "cmd-i": "editor::ShowSignatureHelp", |
| 143 | "f9": "editor::ToggleBreakpoint", | 144 | "f9": "editor::ToggleBreakpoint", |
| 144 | "shift-f9": "editor::EditLogBreakpoint", | 145 | "shift-f9": "editor::EditLogBreakpoint", |
| @@ -184,6 +184,8 @@ · 183 unmodified lines | |||
| 184 | "z f": "editor::FoldSelectedRanges", | 184 | "z f": "editor::FoldSelectedRanges", |
| 185 | "z shift-m": "editor::FoldAll", | 185 | "z shift-m": "editor::FoldAll", |
| 186 | "z shift-r": "editor::UnfoldAll", | 186 | "z shift-r": "editor::UnfoldAll", |
| 187 | "z l": "vim::ColumnRight", | ||
| 188 | "z h": "vim::ColumnLeft", | ||
| 187 | "shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }], | 189 | "shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }], |
| 188 | "shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }], | 190 | "shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }], |
| 189 | // Count support | 191 | // Count support |
| @@ -395,6 +397,8 @@ · 205 unmodified lines | |||
| 395 | "ctrl-pagedown": "pane::ActivateNextItem", | 397 | "ctrl-pagedown": "pane::ActivateNextItem", |
| 396 | "ctrl-pageup": "pane::ActivatePreviousItem", | 398 | "ctrl-pageup": "pane::ActivatePreviousItem", |
| 397 | "insert": "vim::InsertBefore", | 399 | "insert": "vim::InsertBefore", |
| 400 | ".": "vim::Repeat", | ||
| 401 | "alt-.": "vim::RepeatFind", | ||
| 398 | // tree-sitter related commands | 402 | // tree-sitter related commands |
| 399 | "[ x": "editor::SelectLargerSyntaxNode", | 403 | "[ x": "editor::SelectLargerSyntaxNode", |
| 400 | "] x": "editor::SelectSmallerSyntaxNode", | 404 | "] x": "editor::SelectSmallerSyntaxNode", |
| @@ -421,6 +425,7 @@ · 20 unmodified lines | |||
| 421 | 425 | ||
| 422 | "x": "editor::SelectLine", | 426 | "x": "editor::SelectLine", |
| 423 | "shift-x": "editor::SelectLine", | 427 | "shift-x": "editor::SelectLine", |
| 428 | "%": "editor::SelectAll", | ||
| 424 | // Window mode | 429 | // Window mode |
| 425 | "space w h": "workspace::ActivatePaneLeft", | 430 | "space w h": "workspace::ActivatePaneLeft", |
| 426 | "space w l": "workspace::ActivatePaneRight", | 431 | "space w l": "workspace::ActivatePaneRight", |
| @@ -450,7 +455,8 @@ · 23 unmodified lines | |||
| 450 | "ctrl-c": "editor::ToggleComments", | 455 | "ctrl-c": "editor::ToggleComments", |
| 451 | "d": "vim::HelixDelete", | 456 | "d": "vim::HelixDelete", |
| 452 | "c": "vim::Substitute", | 457 | "c": "vim::Substitute", |
| 453 | "shift-c": "editor::AddSelectionBelow" | 458 | "shift-c": "editor::AddSelectionBelow", |
| 459 | "alt-shift-c": "editor::AddSelectionAbove" | ||
| 454 | } | 460 | } |
| 455 | }, | 461 | }, |
| 456 | { | 462 | { |
| @@ -27,11 +27,11 @@ If you are unsure how to fulfill the user's request, gather more information wit · 26 unmodified lines | |||
| 27 | If appropriate, use tool calls to explore the current project, which contains the following root directories: | 27 | If appropriate, use tool calls to explore the current project, which contains the following root directories: |
| 28 | 28 | ||
| 29 | {{#each worktrees}} | 29 | {{#each worktrees}} |
| 30 | - `{{root_name}}` | 30 | - `{{abs_path}}` |
| 31 | {{/each}} | 31 | {{/each}} |
| 32 | 32 | ||
| 33 | - Bias towards not asking the user for help if you can find the answer yourself. | 33 | - Bias towards not asking the user for help if you can find the answer yourself. |
| 34 | - When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above. | 34 | - When providing paths to tools, the path should always start with the name of a project root directory listed above. |
| 35 | - Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path! | 35 | - Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path! |
| 36 | {{# if (has_tool 'grep') }} | 36 | {{# if (has_tool 'grep') }} |
| 37 | - When looking for symbols in the project, prefer the `grep` tool. | 37 | - When looking for symbols in the project, prefer the `grep` tool. |
| @@ -307,6 +307,8 @@ · 306 unmodified lines | |||
| 307 | // "all" | 307 | // "all" |
| 308 | // 4. Draw whitespaces at boundaries only: | 308 | // 4. Draw whitespaces at boundaries only: |
| 309 | // "boundary" | 309 | // "boundary" |
| 310 | // 5. Draw whitespaces only after non-whitespace characters: | ||
| 311 | // "trailing" | ||
| 310 | // For a whitespace to be on a boundary, any of the following conditions need to be met: | 312 | // For a whitespace to be on a boundary, any of the following conditions need to be met: |
| 311 | // - It is a tab | 313 | // - It is a tab |
| 312 | // - It is adjacent to an edge (start or end) | 314 | // - It is adjacent to an edge (start or end) |
| @@ -445,7 +447,9 @@ · 132 unmodified lines | |||
| 445 | // Whether to show breakpoints in the gutter. | 447 | // Whether to show breakpoints in the gutter. |
| 446 | "breakpoints": true, | 448 | "breakpoints": true, |
| 447 | // Whether to show fold buttons in the gutter. | 449 | // Whether to show fold buttons in the gutter. |
| 448 | "folds": true | 450 | "folds": true, |
| 451 | // Minimum number of characters to reserve space for in the gutter. | ||
| 452 | "min_line_number_digits": 4 | ||
| 449 | }, | 453 | }, |
| 450 | "indent_guides": { | 454 | "indent_guides": { |
| 451 | // Whether to show indent guides in the editor. | 455 | // Whether to show indent guides in the editor. |
| @@ -604,7 +608,9 @@ · 152 unmodified lines | |||
| 604 | // 2. Never show indent guides: | 608 | // 2. Never show indent guides: |
| 605 | // "never" | 609 | // "never" |
| 606 | "show": "always" | 610 | "show": "always" |
| 607 | } | 611 | }, |
| 612 | // Whether to hide the root entry when only one folder is open in the window. | ||
| 613 | "hide_root": false | ||
| 608 | }, | 614 | }, |
| 609 | "outline_panel": { | 615 | "outline_panel": { |
| 610 | // Whether to show the outline panel button in the status bar | 616 | // Whether to show the outline panel button in the status bar |
| @@ -1476,7 +1482,8 @@ · 865 unmodified lines | |||
| 1476 | "Go": { | 1482 | "Go": { |
| 1477 | "code_actions_on_format": { | 1483 | "code_actions_on_format": { |
| 1478 | "source.organizeImports": true | 1484 | "source.organizeImports": true |
| 1479 | } | 1485 | }, |
| 1486 | "debuggers": ["Delve"] | ||
| 1480 | }, | 1487 | }, |
| 1481 | "GraphQL": { | 1488 | "GraphQL": { |
| 1482 | "prettier": { | 1489 | "prettier": { |
| @@ -1541,9 +1548,15 @@ · 58 unmodified lines | |||
| 1541 | "Plain Text": { | 1548 | "Plain Text": { |
| 1542 | "allow_rewrap": "anywhere" | 1549 | "allow_rewrap": "anywhere" |
| 1543 | }, | 1550 | }, |
| 1551 | "Python": { | ||
| 1552 | "debuggers": ["Debugpy"] | ||
| 1553 | }, | ||
| 1544 | "Ruby": { | 1554 | "Ruby": { |
| 1545 | "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."] | 1555 | "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."] |
| 1546 | }, | 1556 | }, |
| 1557 | "Rust": { | ||
| 1558 | "debuggers": ["CodeLLDB"] | ||
| 1559 | }, | ||
| 1547 | "SCSS": { | 1560 | "SCSS": { |
| 1548 | "prettier": { | 1561 | "prettier": { |
| 1549 | "allowed": true | 1562 | "allowed": true |
| @@ -1,4 +1,4 @@ | |||
| 1 | // Static tasks configuration. | 1 | // Project tasks configuration. See https://zed.dev/docs/tasks for documentation. |
| 2 | // | 2 | // |
| 3 | // Example: | 3 | // Example: |
| 4 | [ | 4 | [ |
| @@ -99,6 +99,8 @@ · 98 unmodified lines | |||
| 99 | "version_control.added": "#27a657ff", | 99 | "version_control.added": "#27a657ff", |
| 100 | "version_control.modified": "#d3b020ff", | 100 | "version_control.modified": "#d3b020ff", |
| 101 | "version_control.deleted": "#e06c76ff", | 101 | "version_control.deleted": "#e06c76ff", |
| 102 | "version_control.conflict_marker.ours": "#a1c1811a", | ||
| 103 | "version_control.conflict_marker.theirs": "#74ade81a", | ||
| 102 | "conflict": "#dec184ff", | 104 | "conflict": "#dec184ff", |
| 103 | "conflict.background": "#dec1841a", | 105 | "conflict.background": "#dec1841a", |
| 104 | "conflict.border": "#5d4c2fff", | 106 | "conflict.border": "#5d4c2fff", |
| @@ -7,7 +7,10 @@ use gpui::{ · 6 unmodified lines | |||
| 7 | InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement, | 7 | InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement, |
| 8 | Styled, Transformation, Window, actions, percentage, | 8 | Styled, Transformation, Window, actions, percentage, |
| 9 | }; | 9 | }; |
| 10 | use language::{BinaryStatus, LanguageRegistry, LanguageServerId}; | 10 | use language::{ |
| 11 | BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName, | ||
| 12 | LanguageServerStatusUpdate, ServerHealth, | ||
| 13 | }; | ||
| 11 | use project::{ | 14 | use project::{ |
| 12 | EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project, | 15 | EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project, |
| 13 | ProjectEnvironmentEvent, | 16 | ProjectEnvironmentEvent, |
| @@ -16,6 +19,7 @@ use project::{ · 2 unmodified lines | |||
| 16 | use smallvec::SmallVec; | 19 | use smallvec::SmallVec; |
| 17 | use std::{ | 20 | use std::{ |
| 18 | cmp::Reverse, | 21 | cmp::Reverse, |
| 22 | collections::HashSet, | ||
| 19 | fmt::Write, | 23 | fmt::Write, |
| 20 | path::Path, | 24 | path::Path, |
| 21 | sync::Arc, | 25 | sync::Arc, |
| @@ -30,9 +34,9 @@ const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0); · 8 unmodified lines | |||
| 30 | actions!(activity_indicator, [ShowErrorMessage]); | 34 | actions!(activity_indicator, [ShowErrorMessage]); |
| 31 | 35 | ||
| 32 | pub enum Event { | 36 | pub enum Event { |
| 33 | ShowError { | 37 | ShowStatus { |
| 34 | server_name: SharedString, | 38 | server_name: LanguageServerName, |
| 35 | error: String, | 39 | status: SharedString, |
| 36 | }, | 40 | }, |
| 37 | } | 41 | } |
| 38 | 42 | ||
| @@ -45,8 +49,8 @@ pub struct ActivityIndicator { · 6 unmodified lines | |||
| 45 | 49 | ||
| 46 | #[derive(Debug)] | 50 | #[derive(Debug)] |
| 47 | struct ServerStatus { | 51 | struct ServerStatus { |
| 48 | name: SharedString, | 52 | name: LanguageServerName, |
| 49 | status: BinaryStatus, | 53 | status: LanguageServerStatusUpdate, |
| 50 | } | 54 | } |
| 51 | 55 | ||
| 52 | struct PendingWork<'a> { | 56 | struct PendingWork<'a> { |
| @@ -145,19 +149,19 @@ impl ActivityIndicator { · 92 unmodified lines | |||
| 145 | }); | 149 | }); |
| 146 | 150 | ||
| 147 | cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event { | 151 | cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event { |
| 148 | Event::ShowError { server_name, error } => { | 152 | Event::ShowStatus { |
| 153 | server_name, | ||
| 154 | status, | ||
| 155 | } => { | ||
| 149 | let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx)); | 156 | let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx)); |
| 150 | let project = project.clone(); | 157 | let project = project.clone(); |
| 151 | let error = error.clone(); | 158 | let status = status.clone(); |
| 152 | let server_name = server_name.clone(); | 159 | let server_name = server_name.clone(); |
| 153 | cx.spawn_in(window, async move |workspace, cx| { | 160 | cx.spawn_in(window, async move |workspace, cx| { |
| 154 | let buffer = create_buffer.await?; | 161 | let buffer = create_buffer.await?; |
| 155 | buffer.update(cx, |buffer, cx| { | 162 | buffer.update(cx, |buffer, cx| { |
| 156 | buffer.edit( | 163 | buffer.edit( |
| 157 | [( | 164 | [(0..0, format!("Language server {server_name}:\n\n{status}"))], |
| 158 | 0..0, | ||
| 159 | format!("Language server error: {}\n\n{}", server_name, error), | ||
| 160 | )], | ||
| 161 | None, | 165 | None, |
| 162 | cx, | 166 | cx, |
| 163 | ); | 167 | ); |
| @@ -166,7 +170,10 @@ impl ActivityIndicator { · 2 unmodified lines | |||
| 166 | workspace.update_in(cx, |workspace, window, cx| { | 170 | workspace.update_in(cx, |workspace, window, cx| { |
| 167 | workspace.add_item_to_active_pane( | 171 | workspace.add_item_to_active_pane( |
| 168 | Box::new(cx.new(|cx| { | 172 | Box::new(cx.new(|cx| { |
| 169 | Editor::for_buffer(buffer, Some(project.clone()), window, cx) | 173 | let mut editor = |
| 174 | Editor::for_buffer(buffer, Some(project.clone()), window, cx); | ||
| 175 | editor.set_read_only(true); | ||
| 176 | editor | ||
| 170 | })), | 177 | })), |
| 171 | None, | 178 | None, |
| 172 | true, | 179 | true, |
| @@ -185,19 +192,34 @@ impl ActivityIndicator { · 12 unmodified lines | |||
| 185 | } | 192 | } |
| 186 | 193 | ||
| 187 | fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) { | 194 | fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) { |
| 188 | self.statuses.retain(|status| { | 195 | let mut status_message_shown = false; |
| 189 | if let BinaryStatus::Failed { error } = &status.status { | 196 | self.statuses.retain(|status| match &status.status { |
| 190 | cx.emit(Event::ShowError { | 197 | LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error }) |
| 198 | if !status_message_shown => | ||
| 199 | { | ||
| 200 | cx.emit(Event::ShowStatus { | ||
| 191 | server_name: status.name.clone(), | 201 | server_name: status.name.clone(), |
| 192 | error: error.clone(), | 202 | status: SharedString::from(error), |
| 193 | }); | 203 | }); |
| 204 | status_message_shown = true; | ||
| 194 | false | 205 | false |
| 195 | } else { | ||
| 196 | true | ||
| 197 | } | 206 | } |
| 207 | LanguageServerStatusUpdate::Health( | ||
| 208 | ServerHealth::Error | ServerHealth::Warning, | ||
| 209 | status_string, | ||
| 210 | ) if !status_message_shown => match status_string { | ||
| 211 | Some(error) => { | ||
| 212 | cx.emit(Event::ShowStatus { | ||
| 213 | server_name: status.name.clone(), | ||
| 214 | status: error.clone(), | ||
| 215 | }); | ||
| 216 | status_message_shown = true; | ||
| 217 | false | ||
| 218 | } | ||
| 219 | None => false, | ||
| 220 | }, | ||
| 221 | _ => true, | ||
| 198 | }); | 222 | }); |
| 199 | |||
| 200 | cx.notify(); | ||
| 201 | } | 223 | } |
| 202 | 224 | ||
| 203 | fn dismiss_error_message( | 225 | fn dismiss_error_message( |
| @@ -267,48 +289,52 @@ impl ActivityIndicator { · 63 unmodified lines | |||
| 267 | }); | 289 | }); |
| 268 | } | 290 | } |
| 269 | // Show any language server has pending activity. | 291 | // Show any language server has pending activity. |
| 270 | let mut pending_work = self.pending_language_server_work(cx); | ||
| 271 | if let Some(PendingWork { | ||
| 272 | progress_token, | ||
| 273 | progress, | ||
| 274 | .. | ||
| 275 | }) = pending_work.next() | ||
| 276 | { | 292 | { |
| 277 | let mut message = progress | 293 | let mut pending_work = self.pending_language_server_work(cx); |
| 278 | .title | 294 | if let Some(PendingWork { |
| 279 | .as_deref() | 295 | progress_token, |
| 280 | .unwrap_or(progress_token) | 296 | progress, |
| 281 | .to_string(); | 297 | .. |
| 282 | 298 | }) = pending_work.next() | |
| 283 | if let Some(percentage) = progress.percentage { | 299 | { |
| 284 | write!(&mut message, " ({}%)", percentage).unwrap(); | 300 | let mut message = progress |
| 285 | } | 301 | .title |
| 302 | .as_deref() | ||
| 303 | .unwrap_or(progress_token) | ||
| 304 | .to_string(); | ||
| 305 | |||
| 306 | if let Some(percentage) = progress.percentage { | ||
| 307 | write!(&mut message, " ({}%)", percentage).unwrap(); | ||
| 308 | } | ||
| 286 | 309 | ||
| 287 | if let Some(progress_message) = progress.message.as_ref() { | 310 | if let Some(progress_message) = progress.message.as_ref() { |
| 288 | message.push_str(": "); | 311 | message.push_str(": "); |
| 289 | message.push_str(progress_message); | 312 | message.push_str(progress_message); |
| 290 | } | 313 | } |
| 291 | 314 | ||
| 292 | let additional_work_count = pending_work.count(); | 315 | let additional_work_count = pending_work.count(); |
| 293 | if additional_work_count > 0 { | 316 | if additional_work_count > 0 { |
| 294 | write!(&mut message, " + {} more", additional_work_count).unwrap(); | 317 | write!(&mut message, " + {} more", additional_work_count).unwrap(); |
| 295 | } | 318 | } |
| 296 | 319 | ||
| 297 | return Some(Content { | 320 | return Some(Content { |
| 298 | icon: Some( | 321 | icon: Some( |
| 299 | Icon::new(IconName::ArrowCircle) | 322 | Icon::new(IconName::ArrowCircle) |
| 300 | .size(IconSize::Small) | 323 | .size(IconSize::Small) |
| 301 | .with_animation( | 324 | .with_animation( |
| 302 | "arrow-circle", | 325 | "arrow-circle", |
| 303 | Animation::new(Duration::from_secs(2)).repeat(), | 326 | Animation::new(Duration::from_secs(2)).repeat(), |
| 304 | |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), | 327 | |icon, delta| { |
| 305 | ) | 328 | icon.transform(Transformation::rotate(percentage(delta))) |
| 306 | .into_any_element(), | 329 | }, |
| 307 | ), | 330 | ) |
| 308 | message, | 331 | .into_any_element(), |
| 309 | on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)), | 332 | ), |
| 310 | tooltip_message: None, | 333 | message, |
| 311 | }); | 334 | on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)), |
| 335 | tooltip_message: None, | ||
| 336 | }); | ||
| 337 | } | ||
| 312 | } | 338 | } |
| 313 | 339 | ||
| 314 | if let Some(session) = self | 340 | if let Some(session) = self |
| @@ -369,14 +395,38 @@ impl ActivityIndicator { · 54 unmodified lines | |||
| 369 | let mut downloading = SmallVec::<[_; 3]>::new(); | 395 | let mut downloading = SmallVec::<[_; 3]>::new(); |
| 370 | let mut checking_for_update = SmallVec::<[_; 3]>::new(); | 396 | let mut checking_for_update = SmallVec::<[_; 3]>::new(); |
| 371 | let mut failed = SmallVec::<[_; 3]>::new(); | 397 | let mut failed = SmallVec::<[_; 3]>::new(); |
| 398 | let mut health_messages = SmallVec::<[_; 3]>::new(); | ||
| 399 | let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default(); | ||
| 372 | for status in &self.statuses { | 400 | for status in &self.statuses { |
| 373 | match status.status { | 401 | match &status.status { |
| 374 | BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()), | 402 | LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => { |
| 375 | BinaryStatus::Downloading => downloading.push(status.name.clone()), | 403 | checking_for_update.push(status.name.clone()); |
| 376 | BinaryStatus::Failed { .. } => failed.push(status.name.clone()), | 404 | } |
| 377 | BinaryStatus::None => {} | 405 | LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => { |
| 406 | downloading.push(status.name.clone()); | ||
| 407 | } | ||
| 408 | LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => { | ||
| 409 | failed.push(status.name.clone()); | ||
| 410 | } | ||
| 411 | LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {} | ||
| 412 | LanguageServerStatusUpdate::Health(health, server_status) => match server_status { | ||
| 413 | Some(server_status) => { | ||
| 414 | health_messages.push((status.name.clone(), *health, server_status.clone())); | ||
| 415 | } | ||
| 416 | None => { | ||
| 417 | servers_to_clear_statuses.insert(status.name.clone()); | ||
| 418 | } | ||
| 419 | }, | ||
| 378 | } | 420 | } |
| 379 | } | 421 | } |
| 422 | self.statuses | ||
| 423 | .retain(|status| !servers_to_clear_statuses.contains(&status.name)); | ||
| 424 | |||
| 425 | health_messages.sort_by_key(|(_, health, _)| match health { | ||
| 426 | ServerHealth::Error => 2, | ||
| 427 | ServerHealth::Warning => 1, | ||
| 428 | ServerHealth::Ok => 0, | ||
| 429 | }); | ||
| 380 | 430 | ||
| 381 | if !downloading.is_empty() { | 431 | if !downloading.is_empty() { |
| 382 | return Some(Content { | 432 | return Some(Content { |
| @@ -457,7 +507,7 @@ impl ActivityIndicator { · 74 unmodified lines | |||
| 457 | }), | 507 | }), |
| 458 | ), | 508 | ), |
| 459 | on_click: Some(Arc::new(|this, window, cx| { | 509 | on_click: Some(Arc::new(|this, window, cx| { |
| 460 | this.show_error_message(&Default::default(), window, cx) | 510 | this.show_error_message(&ShowErrorMessage, window, cx) |
| 461 | })), | 511 | })), |
| 462 | tooltip_message: None, | 512 | tooltip_message: None, |
| 463 | }); | 513 | }); |
| @@ -471,7 +521,7 @@ impl ActivityIndicator { · 7 unmodified lines | |||
| 471 | .size(IconSize::Small) | 521 | .size(IconSize::Small) |
| 472 | .into_any_element(), | 522 | .into_any_element(), |
| 473 | ), | 523 | ), |
| 474 | message: format!("Formatting failed: {}. Click to see logs.", failure), | 524 | message: format!("Formatting failed: {failure}. Click to see logs."), |
| 475 | on_click: Some(Arc::new(|indicator, window, cx| { | 525 | on_click: Some(Arc::new(|indicator, window, cx| { |
| 476 | indicator.project.update(cx, |project, cx| { | 526 | indicator.project.update(cx, |project, cx| { |
| 477 | project.reset_last_formatting_failure(cx); | 527 | project.reset_last_formatting_failure(cx); |
| @@ -482,6 +532,56 @@ impl ActivityIndicator { · 4 unmodified lines | |||
| 482 | }); | 532 | }); |
| 483 | } | 533 | } |
| 484 | 534 | ||
| 535 | // Show any health messages for the language servers | ||
| 536 | if let Some((server_name, health, message)) = health_messages.pop() { | ||
| 537 | let health_str = match health { | ||
| 538 | ServerHealth::Ok => format!("({server_name}) "), | ||
| 539 | ServerHealth::Warning => format!("({server_name}) Warning: "), | ||
| 540 | ServerHealth::Error => format!("({server_name}) Error: "), | ||
| 541 | }; | ||
| 542 | let single_line_message = message | ||
| 543 | .lines() | ||
| 544 | .filter_map(|line| { | ||
| 545 | let line = line.trim(); | ||
| 546 | if line.is_empty() { None } else { Some(line) } | ||
| 547 | }) | ||
| 548 | .collect::<Vec<_>>() | ||
| 549 | .join(" "); | ||
| 550 | let mut altered_message = single_line_message != message; | ||
| 551 | let truncated_message = truncate_and_trailoff( | ||
| 552 | &single_line_message, | ||
| 553 | MAX_MESSAGE_LEN.saturating_sub(health_str.len()), | ||
| 554 | ); | ||
| 555 | altered_message |= truncated_message != single_line_message; | ||
| 556 | let final_message = format!("{health_str}{truncated_message}"); | ||
| 557 | |||
| 558 | let tooltip_message = if altered_message { | ||
| 559 | Some(format!("{health_str}{message}")) | ||
| 560 | } else { | ||
| 561 | None | ||
| 562 | }; | ||
| 563 | |||
| 564 | return Some(Content { | ||
| 565 | icon: Some( | ||
| 566 | Icon::new(IconName::Warning) | ||
| 567 | .size(IconSize::Small) | ||
| 568 | .into_any_element(), | ||
| 569 | ), | ||
| 570 | message: final_message, | ||
| 571 | tooltip_message, | ||
| 572 | on_click: Some(Arc::new(move |activity_indicator, window, cx| { | ||
| 573 | if altered_message { | ||
| 574 | activity_indicator.show_error_message(&ShowErrorMessage, window, cx) | ||
| 575 | } else { | ||
| 576 | activity_indicator | ||
| 577 | .statuses | ||
| 578 | .retain(|status| status.name != server_name); | ||
| 579 | cx.notify(); | ||
| 580 | } | ||
| 581 | })), | ||
| 582 | }); | ||
| 583 | } | ||
| 584 | |||
| 485 | // Show any application auto-update info. | 585 | // Show any application auto-update info. |
| 486 | if let Some(updater) = &self.auto_updater { | 586 | if let Some(updater) = &self.auto_updater { |
| 487 | return match &updater.read(cx).status() { | 587 | return match &updater.read(cx).status() { |
| @@ -109,5 +109,6 @@ gpui = { workspace = true, "features" = ["test-support"] } · 108 unmodified lines | |||
| 109 | indoc.workspace = true | 109 | indoc.workspace = true |
| 110 | language = { workspace = true, "features" = ["test-support"] } | 110 | language = { workspace = true, "features" = ["test-support"] } |
| 111 | language_model = { workspace = true, "features" = ["test-support"] } | 111 | language_model = { workspace = true, "features" = ["test-support"] } |
| 112 | pretty_assertions.workspace = true | ||
| 112 | project = { workspace = true, features = ["test-support"] } | 113 | project = { workspace = true, features = ["test-support"] } |
| 113 | rand.workspace = true | 114 | rand.workspace = true |
| @@ -1605,6 +1605,7 @@ impl ActiveThread { · 1604 unmodified lines | |||
| 1605 | 1605 | ||
| 1606 | this.thread.update(cx, |thread, cx| { | 1606 | this.thread.update(cx, |thread, cx| { |
| 1607 | thread.advance_prompt_id(); | 1607 | thread.advance_prompt_id(); |
| 1608 | thread.cancel_last_completion(Some(window.window_handle()), cx); | ||
| 1608 | thread.send_to_model( | 1609 | thread.send_to_model( |
| 1609 | model.model, | 1610 | model.model, |
| 1610 | CompletionIntent::UserPrompt, | 1611 | CompletionIntent::UserPrompt, |
| @@ -1680,7 +1681,10 @@ impl ActiveThread { · 69 unmodified lines | |||
| 1680 | 1681 | ||
| 1681 | let editor = cx.new(|cx| { | 1682 | let editor = cx.new(|cx| { |
| 1682 | let mut editor = Editor::new( | 1683 | let mut editor = Editor::new( |
| 1683 | editor::EditorMode::AutoHeight { max_lines: 4 }, | 1684 | editor::EditorMode::AutoHeight { |
| 1685 | min_lines: 1, | ||
| 1686 | max_lines: 4, | ||
| 1687 | }, | ||
| 1684 | buffer, | 1688 | buffer, |
| 1685 | None, | 1689 | None, |
| 1686 | window, | 1690 | window, |
| @@ -1788,12 +1792,31 @@ impl ActiveThread { · 101 unmodified lines | |||
| 1788 | 1792 | ||
| 1789 | fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { | 1793 | fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { |
| 1790 | let message_id = self.messages[ix]; | 1794 | let message_id = self.messages[ix]; |
| 1791 | let Some(message) = self.thread.read(cx).message(message_id) else { | 1795 | let workspace = self.workspace.clone(); |
| 1796 | let thread = self.thread.read(cx); | ||
| 1797 | |||
| 1798 | let is_first_message = ix == 0; | ||
| 1799 | let is_last_message = ix == self.messages.len() - 1; | ||
| 1800 | |||
| 1801 | let Some(message) = thread.message(message_id) else { | ||
| 1792 | return Empty.into_any(); | 1802 | return Empty.into_any(); |
| 1793 | }; | 1803 | }; |
| 1794 | 1804 | ||
| 1805 | let is_generating = thread.is_generating(); | ||
| 1806 | let is_generating_stale = thread.is_generation_stale().unwrap_or(false); | ||
| 1807 | |||
| 1808 | let loading_dots = (is_generating && is_last_message).then(|| { | ||
| 1809 | h_flex() | ||
| 1810 | .h_8() | ||
| 1811 | .my_3() | ||
| 1812 | .mx_5() | ||
| 1813 | .when(is_generating_stale || message.is_hidden, |this| { | ||
| 1814 | this.child(AnimatedLabel::new("").size(LabelSize::Small)) | ||
| 1815 | }) | ||
| 1816 | }); | ||
| 1817 | |||
| 1795 | if message.is_hidden { | 1818 | if message.is_hidden { |
| 1796 | return Empty.into_any(); | 1819 | return div().children(loading_dots).into_any(); |
| 1797 | } | 1820 | } |
| 1798 | 1821 | ||
| 1799 | let message_creases = message.creases.clone(); | 1822 | let message_creases = message.creases.clone(); |
| @@ -1802,9 +1825,6 @@ impl ActiveThread { · 2 unmodified lines | |||
| 1802 | return Empty.into_any(); | 1825 | return Empty.into_any(); |
| 1803 | }; | 1826 | }; |
| 1804 | 1827 | ||
| 1805 | let workspace = self.workspace.clone(); | ||
| 1806 | let thread = self.thread.read(cx); | ||
| 1807 | |||
| 1808 | // Get all the data we need from thread before we start using it in closures | 1828 | // Get all the data we need from thread before we start using it in closures |
| 1809 | let checkpoint = thread.checkpoint_for_message(message_id); | 1829 | let checkpoint = thread.checkpoint_for_message(message_id); |
| 1810 | let configured_model = thread.configured_model().map(|m| m.model); | 1830 | let configured_model = thread.configured_model().map(|m| m.model); |
| @@ -1815,14 +1835,6 @@ impl ActiveThread { · 4 unmodified lines | |||
| 1815 | 1835 | ||
| 1816 | let tool_uses = thread.tool_uses_for_message(message_id, cx); | 1836 | let tool_uses = thread.tool_uses_for_message(message_id, cx); |
| 1817 | let has_tool_uses = !tool_uses.is_empty(); | 1837 | let has_tool_uses = !tool_uses.is_empty(); |
| 1818 | let is_generating = thread.is_generating(); | ||
| 1819 | let is_generating_stale = thread.is_generation_stale().unwrap_or(false); | ||
| 1820 | |||
| 1821 | let is_first_message = ix == 0; | ||
| 1822 | let is_last_message = ix == self.messages.len() - 1; | ||
| 1823 | |||
| 1824 | let loading_dots = (is_generating_stale && is_last_message) | ||
| 1825 | .then(|| AnimatedLabel::new("").size(LabelSize::Small)); | ||
| 1826 | 1838 | ||
| 1827 | let editing_message_state = self | 1839 | let editing_message_state = self |
| 1828 | .editing_message | 1840 | .editing_message |
| @@ -2238,17 +2250,7 @@ impl ActiveThread { · 409 unmodified lines | |||
| 2238 | parent.child(self.render_rules_item(cx)) | 2250 | parent.child(self.render_rules_item(cx)) |
| 2239 | }) | 2251 | }) |
| 2240 | .child(styled_message) | 2252 | .child(styled_message) |
| 2241 | .when(is_generating && is_last_message, |this| { | 2253 | .children(loading_dots) |
| 2242 | this.child( | ||
| 2243 | h_flex() | ||
| 2244 | .h_8() | ||
| 2245 | .mt_2() | ||
| 2246 | .mb_4() | ||
| 2247 | .ml_4() | ||
| 2248 | .py_1p5() | ||
| 2249 | .when_some(loading_dots, |this, loading_dots| this.child(loading_dots)), | ||
| 2250 | ) | ||
| 2251 | }) | ||
| 2252 | .when(show_feedback, move |parent| { | 2254 | .when(show_feedback, move |parent| { |
| 2253 | parent.child(feedback_items).when_some( | 2255 | parent.child(feedback_items).when_some( |
| 2254 | self.open_feedback_editors.get(&message_id), | 2256 | self.open_feedback_editors.get(&message_id), |
| @@ -3708,7 +3710,7 @@ mod tests { · 1453 unmodified lines | |||
| 3708 | use util::path; | 3710 | use util::path; |
| 3709 | use workspace::CollaboratorId; | 3711 | use workspace::CollaboratorId; |
| 3710 | 3712 | ||
| 3711 | use crate::{ContextLoadResult, thread_store}; | 3713 | use crate::{ContextLoadResult, thread::MessageSegment, thread_store}; |
| 3712 | 3714 | ||
| 3713 | use super::*; | 3715 | use super::*; |
| 3714 | 3716 | ||
| @@ -3842,6 +3844,114 @@ mod tests { · 127 unmodified lines | |||
| 3842 | }); | 3844 | }); |
| 3843 | } | 3845 | } |
| 3844 | 3846 | ||
| 3847 | #[gpui::test] | ||
| 3848 | async fn test_editing_message_cancels_previous_completion(cx: &mut TestAppContext) { | ||
| 3849 | init_test_settings(cx); | ||
| 3850 | |||
| 3851 | let project = create_test_project(cx, json!({})).await; | ||
| 3852 | |||
| 3853 | let (cx, active_thread, _, thread, model) = | ||
| 3854 | setup_test_environment(cx, project.clone()).await; | ||
| 3855 | |||
| 3856 | cx.update(|_, cx| { | ||
| 3857 | LanguageModelRegistry::global(cx).update(cx, |registry, cx| { | ||
| 3858 | registry.set_default_model( | ||
| 3859 | Some(ConfiguredModel { | ||
| 3860 | provider: Arc::new(FakeLanguageModelProvider), | ||
| 3861 | model: model.clone(), | ||
| 3862 | }), | ||
| 3863 | cx, | ||
| 3864 | ); | ||
| 3865 | }); | ||
| 3866 | }); | ||
| 3867 | |||
| 3868 | // Track thread events to verify cancellation | ||
| 3869 | let cancellation_events = Arc::new(std::sync::Mutex::new(Vec::new())); | ||
| 3870 | let new_request_events = Arc::new(std::sync::Mutex::new(Vec::new())); | ||
| 3871 | |||
| 3872 | let _subscription = cx.update(|_, cx| { | ||
| 3873 | let cancellation_events = cancellation_events.clone(); | ||
| 3874 | let new_request_events = new_request_events.clone(); | ||
| 3875 | cx.subscribe( | ||
| 3876 | &thread, | ||
| 3877 | move |_thread, event: &ThreadEvent, _cx| match event { | ||
| 3878 | ThreadEvent::CompletionCanceled => { | ||
| 3879 | cancellation_events.lock().unwrap().push(()); | ||
| 3880 | } | ||
| 3881 | ThreadEvent::NewRequest => { | ||
| 3882 | new_request_events.lock().unwrap().push(()); | ||
| 3883 | } | ||
| 3884 | _ => {} | ||
| 3885 | }, | ||
| 3886 | ) | ||
| 3887 | }); | ||
| 3888 | |||
| 3889 | // Insert a user message and start streaming a response | ||
| 3890 | let message = thread.update(cx, |thread, cx| { | ||
| 3891 | let message_id = thread.insert_user_message( | ||
| 3892 | "Hello, how are you?", | ||
| 3893 | ContextLoadResult::default(), | ||
| 3894 | None, | ||
| 3895 | vec![], | ||
| 3896 | cx, | ||
| 3897 | ); | ||
| 3898 | thread.advance_prompt_id(); | ||
| 3899 | thread.send_to_model( | ||
| 3900 | model.clone(), | ||
| 3901 | CompletionIntent::UserPrompt, | ||
| 3902 | cx.active_window(), | ||
| 3903 | cx, | ||
| 3904 | ); | ||
| 3905 | thread.message(message_id).cloned().unwrap() | ||
| 3906 | }); | ||
| 3907 | |||
| 3908 | cx.run_until_parked(); | ||
| 3909 | |||
| 3910 | // Verify that a completion is in progress | ||
| 3911 | assert!(cx.read(|cx| thread.read(cx).is_generating())); | ||
| 3912 | assert_eq!(new_request_events.lock().unwrap().len(), 1); | ||
| 3913 | |||
| 3914 | // Edit the message while the completion is still running | ||
| 3915 | active_thread.update_in(cx, |active_thread, window, cx| { | ||
| 3916 | active_thread.start_editing_message( | ||
| 3917 | message.id, | ||
| 3918 | message.segments.as_slice(), | ||
| 3919 | message.creases.as_slice(), | ||
| 3920 | window, | ||
| 3921 | cx, | ||
| 3922 | ); | ||
| 3923 | let editor = active_thread | ||
| 3924 | .editing_message | ||
| 3925 | .as_ref() | ||
| 3926 | .unwrap() | ||
| 3927 | .1 | ||
| 3928 | .editor | ||
| 3929 | .clone(); | ||
| 3930 | editor.update(cx, |editor, cx| { | ||
| 3931 | editor.set_text("What is the weather like?", window, cx); | ||
| 3932 | }); | ||
| 3933 | active_thread.confirm_editing_message(&Default::default(), window, cx); | ||
| 3934 | }); | ||
| 3935 | |||
| 3936 | cx.run_until_parked(); | ||
| 3937 | |||
| 3938 | // Verify that the previous completion was cancelled | ||
| 3939 | assert_eq!(cancellation_events.lock().unwrap().len(), 1); | ||
| 3940 | |||
| 3941 | // Verify that a new request was started after cancellation | ||
| 3942 | assert_eq!(new_request_events.lock().unwrap().len(), 2); | ||
| 3943 | |||
| 3944 | // Verify that the edited message contains the new text | ||
| 3945 | let edited_message = | ||
| 3946 | thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap()); | ||
| 3947 | match &edited_message.segments[0] { | ||
| 3948 | MessageSegment::Text(text) => { | ||
| 3949 | assert_eq!(text, "What is the weather like?"); | ||
| 3950 | } | ||
| 3951 | _ => panic!("Expected text segment"), | ||
| 3952 | } | ||
| 3953 | } | ||
| 3954 | |||
| 3845 | fn init_test_settings(cx: &mut TestAppContext) { | 3955 | fn init_test_settings(cx: &mut TestAppContext) { |
| 3846 | cx.update(|cx| { | 3956 | cx.update(|cx| { |
| 3847 | let settings_store = SettingsStore::test(cx); | 3957 | let settings_store = SettingsStore::test(cx); |
| @@ -162,7 +162,7 @@ pub fn init( · 161 unmodified lines | |||
| 162 | assistant_slash_command::init(cx); | 162 | assistant_slash_command::init(cx); |
| 163 | thread_store::init(cx); | 163 | thread_store::init(cx); |
| 164 | agent_panel::init(cx); | 164 | agent_panel::init(cx); |
| 165 | context_server_configuration::init(language_registry, cx); | 165 | context_server_configuration::init(language_registry, fs.clone(), cx); |
| 166 | 166 | ||
| 167 | register_slash_commands(cx); | 167 | register_slash_commands(cx); |
| 168 | inline_assistant::init( | 168 | inline_assistant::init( |
| @@ -12,7 +12,7 @@ use context_server::ContextServerId; · 11 unmodified lines | |||
| 12 | use fs::Fs; | 12 | use fs::Fs; |
| 13 | use gpui::{ | 13 | use gpui::{ |
| 14 | Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle, | 14 | Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle, |
| 15 | Focusable, ScrollHandle, Subscription, pulsating_between, | 15 | Focusable, ScrollHandle, Subscription, Transformation, percentage, |
| 16 | }; | 16 | }; |
| 17 | use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; | 17 | use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; |
| 18 | use project::context_server_store::{ContextServerStatus, ContextServerStore}; | 18 | use project::context_server_store::{ContextServerStatus, ContextServerStore}; |
| @@ -475,7 +475,6 @@ impl AgentConfiguration { · 456 unmodified lines | |||
| 475 | .get(&context_server_id) | 475 | .get(&context_server_id) |
| 476 | .copied() | 476 | .copied() |
| 477 | .unwrap_or_default(); | 477 | .unwrap_or_default(); |
| 478 | |||
| 479 | let tools = tools_by_source | 478 | let tools = tools_by_source |
| 480 | .get(&ToolSource::ContextServer { | 479 | .get(&ToolSource::ContextServer { |
| 481 | id: context_server_id.0.clone().into(), | 480 | id: context_server_id.0.clone().into(), |
| @@ -484,25 +483,23 @@ impl AgentConfiguration { · 2 unmodified lines | |||
| 484 | let tool_count = tools.len(); | 483 | let tool_count = tools.len(); |
| 485 | 484 | ||
| 486 | let border_color = cx.theme().colors().border.opacity(0.6); | 485 | let border_color = cx.theme().colors().border.opacity(0.6); |
| 487 | let success_color = Color::Success.color(cx); | ||
| 488 | 486 | ||
| 489 | let (status_indicator, tooltip_text) = match server_status { | 487 | let (status_indicator, tooltip_text) = match server_status { |
| 490 | ContextServerStatus::Starting => ( | 488 | ContextServerStatus::Starting => ( |
| 491 | Indicator::dot() | 489 | Icon::new(IconName::LoadCircle) |
| 492 | .color(Color::Success) | 490 | .size(IconSize::XSmall) |
| 491 | .color(Color::Accent) | ||
| 493 | .with_animation( | 492 | .with_animation( |
| 494 | SharedString::from(format!("{}-starting", context_server_id.0.clone(),)), | 493 | SharedString::from(format!("{}-starting", context_server_id.0.clone(),)), |
| 495 | Animation::new(Duration::from_secs(2)) | 494 | Animation::new(Duration::from_secs(3)).repeat(), |
| 496 | .repeat() | 495 | |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), |
| 497 | .with_easing(pulsating_between(0.4, 1.)), | ||
| 498 | move |this, delta| this.color(success_color.alpha(delta).into()), | ||
| 499 | ) | 496 | ) |
| 500 | .into_any_element(), | 497 | .into_any_element(), |
| 501 | "Server is starting.", | 498 | "Server is starting.", |
| 502 | ), | 499 | ), |
| 503 | ContextServerStatus::Running => ( | 500 | ContextServerStatus::Running => ( |
| 504 | Indicator::dot().color(Color::Success).into_any_element(), | 501 | Indicator::dot().color(Color::Success).into_any_element(), |
| 505 | "Server is running.", | 502 | "Server is active.", |
| 506 | ), | 503 | ), |
| 507 | ContextServerStatus::Error(_) => ( | 504 | ContextServerStatus::Error(_) => ( |
| 508 | Indicator::dot().color(Color::Error).into_any_element(), | 505 | Indicator::dot().color(Color::Error).into_any_element(), |
| @@ -526,12 +523,11 @@ impl AgentConfiguration { · 17 unmodified lines | |||
| 526 | .p_1() | 523 | .p_1() |
| 527 | .justify_between() | 524 | .justify_between() |
| 528 | .when( | 525 | .when( |
| 529 | error.is_some() || are_tools_expanded && tool_count > 1, | 526 | error.is_some() || are_tools_expanded && tool_count >= 1, |
| 530 | |element| element.border_b_1().border_color(border_color), | 527 | |element| element.border_b_1().border_color(border_color), |
| 531 | ) | 528 | ) |
| 532 | .child( | 529 | .child( |
| 533 | h_flex() | 530 | h_flex() |
| 534 | .gap_1p5() | ||
| 535 | .child( | 531 | .child( |
| 536 | Disclosure::new( | 532 | Disclosure::new( |
| 537 | "tool-list-disclosure", | 533 | "tool-list-disclosure", |
| @@ -551,12 +547,16 @@ impl AgentConfiguration { · 13 unmodified lines | |||
| 551 | })), | 547 | })), |
| 552 | ) | 548 | ) |
| 553 | .child( | 549 | .child( |
| 554 | div() | 550 | h_flex() |
| 555 | .id(item_id.clone()) | 551 | .id(SharedString::from(format!("tooltip-{}", item_id))) |
| 552 | .h_full() | ||
| 553 | .w_3() | ||
| 554 | .mx_1() | ||
| 555 | .justify_center() | ||
| 556 | .tooltip(Tooltip::text(tooltip_text)) | 556 | .tooltip(Tooltip::text(tooltip_text)) |
| 557 | .child(status_indicator), | 557 | .child(status_indicator), |
| 558 | ) | 558 | ) |
| 559 | .child(Label::new(context_server_id.0.clone()).ml_0p5()) | 559 | .child(Label::new(item_id).ml_0p5().mr_1p5()) |
| 560 | .when(is_running, |this| { | 560 | .when(is_running, |this| { |
| 561 | this.child( | 561 | this.child( |
| 562 | Label::new(if tool_count == 1 { | 562 | Label::new(if tool_count == 1 { |
| @@ -89,7 +89,7 @@ impl ConfigureContextServerModal { · 88 unmodified lines | |||
| 89 | }), | 89 | }), |
| 90 | settings_validator, | 90 | settings_validator, |
| 91 | settings_editor: cx.new(|cx| { | 91 | settings_editor: cx.new(|cx| { |
| 92 | let mut editor = Editor::auto_height(16, window, cx); | 92 | let mut editor = Editor::auto_height(1, 16, window, cx); |
| 93 | editor.set_text(config.default_settings.trim(), window, cx); | 93 | editor.set_text(config.default_settings.trim(), window, cx); |
| 94 | editor.set_show_gutter(false, cx); | 94 | editor.set_show_gutter(false, cx); |
| 95 | editor.set_soft_wrap_mode( | 95 | editor.set_soft_wrap_mode( |
| @@ -31,7 +31,7 @@ use util::ResultExt; · 30 unmodified lines | |||
| 31 | use workspace::{ | 31 | use workspace::{ |
| 32 | Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, | 32 | Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, |
| 33 | Workspace, | 33 | Workspace, |
| 34 | item::{BreadcrumbText, ItemEvent, TabContentParams}, | 34 | item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams}, |
| 35 | searchable::SearchableItemHandle, | 35 | searchable::SearchableItemHandle, |
| 36 | }; | 36 | }; |
| 37 | use zed_actions::assistant::ToggleFocus; | 37 | use zed_actions::assistant::ToggleFocus; |
| @@ -532,12 +532,12 @@ impl Item for AgentDiffPane { · 494 unmodified lines | |||
| 532 | 532 | ||
| 533 | fn save( | 533 | fn save( |
| 534 | &mut self, | 534 | &mut self, |
| 535 | format: bool, | 535 | options: SaveOptions, |
| 536 | project: Entity<Project>, | 536 | project: Entity<Project>, |
| 537 | window: &mut Window, | 537 | window: &mut Window, |
| 538 | cx: &mut Context<Self>, | 538 | cx: &mut Context<Self>, |
| 539 | ) -> Task<Result<()>> { | 539 | ) -> Task<Result<()>> { |
| 540 | self.editor.save(format, project, window, cx) | 540 | self.editor.save(options, project, window, cx) |
| 541 | } | 541 | } |
| 542 | 542 | ||
| 543 | fn save_as( | 543 | fn save_as( |
| @@ -1513,7 +1513,7 @@ impl AgentDiff { · 969 unmodified lines | |||
| 1513 | multibuffer.add_diff(diff_handle.clone(), cx); | 1513 | multibuffer.add_diff(diff_handle.clone(), cx); |
| 1514 | }); | 1514 | }); |
| 1515 | 1515 | ||
| 1516 | let new_state = if thread.read(cx).has_pending_edit_tool_uses() { | 1516 | let new_state = if thread.read(cx).is_generating() { |
| 1517 | EditorState::Generating | 1517 | EditorState::Generating |
| 1518 | } else { | 1518 | } else { |
| 1519 | EditorState::Reviewing | 1519 | EditorState::Reviewing |
| @@ -91,12 +91,13 @@ impl AgentModelSelector { · 90 unmodified lines | |||
| 91 | 91 | ||
| 92 | impl Render for AgentModelSelector { | 92 | impl Render for AgentModelSelector { |
| 93 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | 93 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| 94 | let focus_handle = self.focus_handle.clone(); | ||
| 95 | |||
| 96 | let model = self.selector.read(cx).delegate.active_model(cx); | 94 | let model = self.selector.read(cx).delegate.active_model(cx); |
| 97 | let model_name = model | 95 | let model_name = model |
| 98 | .map(|model| model.model.name().0) | 96 | .map(|model| model.model.name().0) |
| 99 | .unwrap_or_else(|| SharedString::from("No model selected")); | 97 | .unwrap_or_else(|| SharedString::from("No model selected")); |
| 98 | |||
| 99 | let focus_handle = self.focus_handle.clone(); | ||
| 100 | |||
| 100 | PickerPopoverMenu::new( | 101 | PickerPopoverMenu::new( |
| 101 | self.selector.clone(), | 102 | self.selector.clone(), |
| 102 | Button::new("active-model", model_name) | 103 | Button::new("active-model", model_name) |
| @@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize}; · 9 unmodified lines | |||
| 10 | use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView}; | 10 | use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView}; |
| 11 | use anyhow::{Result, anyhow}; | 11 | use anyhow::{Result, anyhow}; |
| 12 | use assistant_context_editor::{ | 12 | use assistant_context_editor::{ |
| 13 | AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent, | 13 | AgentPanelDelegate, AssistantContext, ContextEditor, ContextEvent, ContextSummary, |
| 14 | ContextSummary, SlashCommandCompletionProvider, humanize_token_count, | 14 | SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate, |
| 15 | make_lsp_adapter_delegate, render_remaining_tokens, | 15 | render_remaining_tokens, |
| 16 | }; | 16 | }; |
| 17 | use assistant_slash_command::SlashCommandWorkingSet; | 17 | use assistant_slash_command::SlashCommandWorkingSet; |
| 18 | use assistant_tool::ToolWorkingSet; | 18 | use assistant_tool::ToolWorkingSet; |
| @@ -29,7 +29,8 @@ use gpui::{ · 10 unmodified lines | |||
| 29 | }; | 29 | }; |
| 30 | use language::LanguageRegistry; | 30 | use language::LanguageRegistry; |
| 31 | use language_model::{ | 31 | use language_model::{ |
| 32 | LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID, | 32 | ConfigurationError, LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, |
| 33 | ZED_CLOUD_PROVIDER_ID, | ||
| 33 | }; | 34 | }; |
| 34 | use project::{Project, ProjectPath, Worktree}; | 35 | use project::{Project, ProjectPath, Worktree}; |
| 35 | use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; | 36 | use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; |
| @@ -2353,24 +2354,6 @@ impl AgentPanel { · 2317 unmodified lines | |||
| 2353 | self.thread.clone().into_any_element() | 2354 | self.thread.clone().into_any_element() |
| 2354 | } | 2355 | } |
| 2355 | 2356 | ||
| 2356 | fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> { | ||
| 2357 | let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else { | ||
| 2358 | return Some(ConfigurationError::NoProvider); | ||
| 2359 | }; | ||
| 2360 | |||
| 2361 | if !model.provider.is_authenticated(cx) { | ||
| 2362 | return Some(ConfigurationError::ProviderNotAuthenticated); | ||
| 2363 | } | ||
| 2364 | |||
| 2365 | if model.provider.must_accept_terms(cx) { | ||
| 2366 | return Some(ConfigurationError::ProviderPendingTermsAcceptance( | ||
| 2367 | model.provider, | ||
| 2368 | )); | ||
| 2369 | } | ||
| 2370 | |||
| 2371 | None | ||
| 2372 | } | ||
| 2373 | |||
| 2374 | fn render_thread_empty_state( | 2357 | fn render_thread_empty_state( |
| 2375 | &self, | 2358 | &self, |
| 2376 | window: &mut Window, | 2359 | window: &mut Window, |
| @@ -2380,7 +2363,9 @@ impl AgentPanel { · 3 unmodified lines | |||
| 2380 | .history_store | 2363 | .history_store |
| 2381 | .update(cx, |this, cx| this.recent_entries(6, cx)); | 2364 | .update(cx, |this, cx| this.recent_entries(6, cx)); |
| 2382 | 2365 | ||
| 2383 | let configuration_error = self.configuration_error(cx); | 2366 | let model_registry = LanguageModelRegistry::read_global(cx); |
| 2367 | let configuration_error = | ||
| 2368 | model_registry.configuration_error(model_registry.default_model(), cx); | ||
| 2384 | let no_error = configuration_error.is_none(); | 2369 | let no_error = configuration_error.is_none(); |
| 2385 | let focus_handle = self.focus_handle(cx); | 2370 | let focus_handle = self.focus_handle(cx); |
| 2386 | 2371 | ||
| @@ -2397,11 +2382,7 @@ impl AgentPanel { · 10 unmodified lines | |||
| 2397 | .justify_center() | 2382 | .justify_center() |
| 2398 | .items_center() | 2383 | .items_center() |
| 2399 | .gap_1() | 2384 | .gap_1() |
| 2400 | .child( | 2385 | .child(h_flex().child(Headline::new("Welcome to the Agent Panel"))) |
| 2401 | h_flex().child( | ||
| 2402 | Headline::new("Welcome to the Agent Panel") | ||
| 2403 | ), | ||
| 2404 | ) | ||
| 2405 | .when(no_error, |parent| { | 2386 | .when(no_error, |parent| { |
| 2406 | parent | 2387 | parent |
| 2407 | .child( | 2388 | .child( |
| @@ -2425,7 +2406,10 @@ impl AgentPanel { · 17 unmodified lines | |||
| 2425 | cx, | 2406 | cx, |
| 2426 | )) | 2407 | )) |
| 2427 | .on_click(|_event, window, cx| { | 2408 | .on_click(|_event, window, cx| { |
| 2428 | window.dispatch_action(NewThread::default().boxed_clone(), cx) | 2409 | window.dispatch_action( |
| 2410 | NewThread::default().boxed_clone(), | ||
| 2411 | cx, | ||
| 2412 | ) | ||
| 2429 | }), | 2413 | }), |
| 2430 | ) | 2414 | ) |
| 2431 | .child( | 2415 | .child( |
| @@ -2442,7 +2426,10 @@ impl AgentPanel { · 10 unmodified lines | |||
| 2442 | cx, | 2426 | cx, |
| 2443 | )) | 2427 | )) |
| 2444 | .on_click(|_event, window, cx| { | 2428 | .on_click(|_event, window, cx| { |
| 2445 | window.dispatch_action(ToggleContextPicker.boxed_clone(), cx) | 2429 | window.dispatch_action( |
| 2430 | ToggleContextPicker.boxed_clone(), | ||
| 2431 | cx, | ||
| 2432 | ) | ||
| 2446 | }), | 2433 | }), |
| 2447 | ) | 2434 | ) |
| 2448 | .child( | 2435 | .child( |
| @@ -2459,7 +2446,10 @@ impl AgentPanel { · 10 unmodified lines | |||
| 2459 | cx, | 2446 | cx, |
| 2460 | )) | 2447 | )) |
| 2461 | .on_click(|_event, window, cx| { | 2448 | .on_click(|_event, window, cx| { |
| 2462 | window.dispatch_action(ToggleModelSelector.boxed_clone(), cx) | 2449 | window.dispatch_action( |
| 2450 | ToggleModelSelector.boxed_clone(), | ||
| 2451 | cx, | ||
| 2452 | ) | ||
| 2463 | }), | 2453 | }), |
| 2464 | ) | 2454 | ) |
| 2465 | .child( | 2455 | .child( |
| @@ -2476,51 +2466,50 @@ impl AgentPanel { · 10 unmodified lines | |||
| 2476 | cx, | 2466 | cx, |
| 2477 | )) | 2467 | )) |
| 2478 | .on_click(|_event, window, cx| { | 2468 | .on_click(|_event, window, cx| { |
| 2479 | window.dispatch_action(OpenConfiguration.boxed_clone(), cx) | 2469 | window.dispatch_action( |
| 2470 | OpenConfiguration.boxed_clone(), | ||
| 2471 | cx, | ||
| 2472 | ) | ||
| 2480 | }), | 2473 | }), |
| 2481 | ) | 2474 | ) |
| 2482 | }) | 2475 | }) |
| 2483 | .map(|parent| { | 2476 | .map(|parent| match configuration_error_ref { |
| 2484 | match configuration_error_ref { | 2477 | Some( |
| 2485 | Some(ConfigurationError::ProviderNotAuthenticated) | 2478 | err @ (ConfigurationError::ModelNotFound |
| 2486 | | Some(ConfigurationError::NoProvider) => { | 2479 | | ConfigurationError::ProviderNotAuthenticated(_) |
| 2487 | parent | 2480 | | ConfigurationError::NoProvider), |
| 2488 | .child( | 2481 | ) => parent |
| 2489 | h_flex().child( | 2482 | .child(h_flex().child( |
| 2490 | Label::new("To start using the agent, configure at least one LLM provider.") | 2483 | Label::new(err.to_string()).color(Color::Muted).mb_2p5(), |
| 2491 | .color(Color::Muted) | 2484 | )) |
| 2492 | .mb_2p5() | 2485 | .child( |
| 2493 | ) | 2486 | Button::new("settings", "Configure a Provider") |
| 2494 | ) | 2487 | .icon(IconName::Settings) |
| 2495 | .child( | 2488 | .icon_position(IconPosition::Start) |
| 2496 | Button::new("settings", "Configure a Provider") | 2489 | .icon_size(IconSize::Small) |
| 2497 | .icon(IconName::Settings) | 2490 | .icon_color(Color::Muted) |
| 2498 | .icon_position(IconPosition::Start) | 2491 | .full_width() |
| 2499 | .icon_size(IconSize::Small) | 2492 | .key_binding(KeyBinding::for_action_in( |
| 2500 | .icon_color(Color::Muted) | 2493 | &OpenConfiguration, |
| 2501 | .full_width() | 2494 | &focus_handle, |
| 2502 | .key_binding(KeyBinding::for_action_in( | 2495 | window, |
| 2503 | &OpenConfiguration, | ||
| 2504 | &focus_handle, | ||
| 2505 | window, | ||
| 2506 | cx, | ||
| 2507 | )) | ||
| 2508 | .on_click(|_event, window, cx| { | ||
| 2509 | window.dispatch_action(OpenConfiguration.boxed_clone(), cx) | ||
| 2510 | }), | ||
| 2511 | ) | ||
| 2512 | } | ||
| 2513 | Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { | ||
| 2514 | parent.children( | ||
| 2515 | provider.render_accept_terms( | ||
| 2516 | LanguageModelProviderTosView::ThreadFreshStart, | ||
| 2517 | cx, | 2496 | cx, |
| 2518 | ), | 2497 | )) |
| 2519 | ) | 2498 | .on_click(|_event, window, cx| { |
| 2520 | } | 2499 | window.dispatch_action( |
| 2521 | None => parent, | 2500 | OpenConfiguration.boxed_clone(), |
| 2501 | cx, | ||
| 2502 | ) | ||
| 2503 | }), | ||
| 2504 | ), | ||
| 2505 | Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { | ||
| 2506 | parent.children(provider.render_accept_terms( | ||
| 2507 | LanguageModelProviderTosView::ThreadFreshStart, | ||
| 2508 | cx, | ||
| 2509 | )) | ||
| 2522 | } | 2510 | } |
| 2523 | }) | 2511 | None => parent, |
| 2512 | }), | ||
| 2524 | ) | 2513 | ) |
| 2525 | }) | 2514 | }) |
| 2526 | .when(!recent_history.is_empty(), |parent| { | 2515 | .when(!recent_history.is_empty(), |parent| { |
| @@ -2555,7 +2544,8 @@ impl AgentPanel { · 28 unmodified lines | |||
| 2555 | &self.focus_handle(cx), | 2544 | &self.focus_handle(cx), |
| 2556 | window, | 2545 | window, |
| 2557 | cx, | 2546 | cx, |
| 2558 | ).map(|kb| kb.size(rems_from_px(12.))), | 2547 | ) |
| 2548 | .map(|kb| kb.size(rems_from_px(12.))), | ||
| 2559 | ) | 2549 | ) |
| 2560 | .on_click(move |_event, window, cx| { | 2550 | .on_click(move |_event, window, cx| { |
| 2561 | window.dispatch_action(OpenHistory.boxed_clone(), cx); | 2551 | window.dispatch_action(OpenHistory.boxed_clone(), cx); |
| @@ -2565,79 +2555,68 @@ impl AgentPanel { · 3 unmodified lines | |||
| 2565 | .child( | 2555 | .child( |
| 2566 | v_flex() | 2556 | v_flex() |
| 2567 | .gap_1() | 2557 | .gap_1() |
| 2568 | .children( | 2558 | .children(recent_history.into_iter().enumerate().map( |
| 2569 | recent_history.into_iter().enumerate().map(|(index, entry)| { | 2559 | |(index, entry)| { |
| 2570 | // TODO: Add keyboard navigation. | 2560 | // TODO: Add keyboard navigation. |
| 2571 | let is_hovered = self.hovered_recent_history_item == Some(index); | 2561 | let is_hovered = |
| 2562 | self.hovered_recent_history_item == Some(index); | ||
| 2572 | HistoryEntryElement::new(entry.clone(), cx.entity().downgrade()) | 2563 | HistoryEntryElement::new(entry.clone(), cx.entity().downgrade()) |
| 2573 | .hovered(is_hovered) | 2564 | .hovered(is_hovered) |
| 2574 | .on_hover(cx.listener(move |this, is_hovered, _window, cx| { | 2565 | .on_hover(cx.listener( |
| 2575 | if *is_hovered { | 2566 | move |this, is_hovered, _window, cx| { |
| 2576 | this.hovered_recent_history_item = Some(index); | 2567 | if *is_hovered { |
| 2577 | } else if this.hovered_recent_history_item == Some(index) { | 2568 | this.hovered_recent_history_item = Some(index); |
| 2578 | this.hovered_recent_history_item = None; | 2569 | } else if this.hovered_recent_history_item |
| 2579 | } | 2570 | == Some(index) |
| 2580 | cx.notify(); | 2571 | { |
| 2581 | })) | 2572 | this.hovered_recent_history_item = None; |
| 2573 | } | ||
| 2574 | cx.notify(); | ||
| 2575 | }, | ||
| 2576 | )) | ||
| 2582 | .into_any_element() | 2577 | .into_any_element() |
| 2583 | }), | 2578 | }, |
| 2584 | ) | 2579 | )), |
| 2585 | ) | 2580 | ) |
| 2586 | .map(|parent| { | 2581 | .map(|parent| match configuration_error_ref { |
| 2587 | match configuration_error_ref { | 2582 | Some( |
| 2588 | Some(ConfigurationError::ProviderNotAuthenticated) | 2583 | err @ (ConfigurationError::ModelNotFound |
| 2589 | | Some(ConfigurationError::NoProvider) => { | 2584 | | ConfigurationError::ProviderNotAuthenticated(_) |
| 2590 | parent | 2585 | | ConfigurationError::NoProvider), |
| 2591 | .child( | 2586 | ) => parent.child( |
| 2592 | Banner::new() | 2587 | Banner::new() |
| 2593 | .severity(ui::Severity::Warning) | 2588 | .severity(ui::Severity::Warning) |
| 2594 | .child( | 2589 | .child(Label::new(err.to_string()).size(LabelSize::Small)) |
| 2595 | Label::new( | 2590 | .action_slot( |
| 2596 | "Configure at least one LLM provider to start using the panel.", | 2591 | Button::new("settings", "Configure Provider") |
| 2597 | ) | 2592 | .style(ButtonStyle::Tinted(ui::TintColor::Warning)) |
| 2598 | .size(LabelSize::Small), | 2593 | .label_size(LabelSize::Small) |
| 2594 | .key_binding( | ||
| 2595 | KeyBinding::for_action_in( | ||
| 2596 | &OpenConfiguration, | ||
| 2597 | &focus_handle, | ||
| 2598 | window, | ||
| 2599 | cx, | ||
| 2599 | ) | 2600 | ) |
| 2600 | .action_slot( | 2601 | .map(|kb| kb.size(rems_from_px(12.))), |
| 2601 | Button::new("settings", "Configure Provider") | 2602 | ) |
| 2602 | .style(ButtonStyle::Tinted(ui::TintColor::Warning)) | 2603 | .on_click(|_event, window, cx| { |
| 2603 | .label_size(LabelSize::Small) | 2604 | window.dispatch_action( |
| 2604 | .key_binding( | 2605 | OpenConfiguration.boxed_clone(), |
| 2605 | KeyBinding::for_action_in( | 2606 | cx, |
| 2606 | &OpenConfiguration, | 2607 | ) |
| 2607 | &focus_handle, | 2608 | }), |
| 2608 | window, | 2609 | ), |
| 2609 | cx, | 2610 | ), |
| 2610 | ) | 2611 | Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { |
| 2611 | .map(|kb| kb.size(rems_from_px(12.))), | 2612 | parent.child(Banner::new().severity(ui::Severity::Warning).child( |
| 2612 | ) | 2613 | h_flex().w_full().children(provider.render_accept_terms( |
| 2613 | .on_click(|_event, window, cx| { | 2614 | LanguageModelProviderTosView::ThreadtEmptyState, |
| 2614 | window.dispatch_action( | 2615 | cx, |
| 2615 | OpenConfiguration.boxed_clone(), | 2616 | )), |
| 2616 | cx, | 2617 | )) |
| 2617 | ) | ||
| 2618 | }), | ||
| 2619 | ), | ||
| 2620 | ) | ||
| 2621 | } | ||
| 2622 | Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { | ||
| 2623 | parent | ||
| 2624 | .child( | ||
| 2625 | Banner::new() | ||
| 2626 | .severity(ui::Severity::Warning) | ||
| 2627 | .child( | ||
| 2628 | h_flex() | ||
| 2629 | .w_full() | ||
| 2630 | .children( | ||
| 2631 | provider.render_accept_terms( | ||
| 2632 | LanguageModelProviderTosView::ThreadtEmptyState, | ||
| 2633 | cx, | ||
| 2634 | ), | ||
| 2635 | ), | ||
| 2636 | ), | ||
| 2637 | ) | ||
| 2638 | } | ||
| 2639 | None => parent, | ||
| 2640 | } | 2618 | } |
| 2619 | None => parent, | ||
| 2641 | }) | 2620 | }) |
| 2642 | }) | 2621 | }) |
| 2643 | } | 2622 | } |
| @@ -386,8 +386,10 @@ impl CodegenAlternative { · 385 unmodified lines | |||
| 386 | async { Ok(LanguageModelTextStream::default()) }.boxed_local() | 386 | async { Ok(LanguageModelTextStream::default()) }.boxed_local() |
| 387 | } else { | 387 | } else { |
| 388 | let request = self.build_request(&model, user_prompt, cx)?; | 388 | let request = self.build_request(&model, user_prompt, cx)?; |
| 389 | cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await) | 389 | cx.spawn(async move |_, cx| { |
| 390 | .boxed_local() | 390 | Ok(model.stream_completion_text(request.await, &cx).await?) |
| 391 | }) | ||
| 392 | .boxed_local() | ||
| 391 | }; | 393 | }; |
| 392 | self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx); | 394 | self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx); |
| 393 | Ok(()) | 395 | Ok(()) |
| @@ -1066,7 +1066,7 @@ mod tests { · 1065 unmodified lines | |||
| 1066 | use serde_json::json; | 1066 | use serde_json::json; |
| 1067 | use settings::SettingsStore; | 1067 | use settings::SettingsStore; |
| 1068 | use std::{ops::Deref, rc::Rc}; | 1068 | use std::{ops::Deref, rc::Rc}; |
| 1069 | use util::{path, separator}; | 1069 | use util::path; |
| 1070 | use workspace::{AppState, Item}; | 1070 | use workspace::{AppState, Item}; |
| 1071 | 1071 | ||
| 1072 | #[test] | 1072 | #[test] |
| @@ -1217,14 +1217,14 @@ mod tests { · 144 unmodified lines | |||
| 1217 | let mut cx = VisualTestContext::from_window(*window.deref(), cx); | 1217 | let mut cx = VisualTestContext::from_window(*window.deref(), cx); |
| 1218 | 1218 | ||
| 1219 | let paths = vec![ | 1219 | let paths = vec![ |
| 1220 | separator!("a/one.txt"), | 1220 | path!("a/one.txt"), |
| 1221 | separator!("a/two.txt"), | 1221 | path!("a/two.txt"), |
| 1222 | separator!("a/three.txt"), | 1222 | path!("a/three.txt"), |
| 1223 | separator!("a/four.txt"), | 1223 | path!("a/four.txt"), |
| 1224 | separator!("b/five.txt"), | 1224 | path!("b/five.txt"), |
| 1225 | separator!("b/six.txt"), | 1225 | path!("b/six.txt"), |
| 1226 | separator!("b/seven.txt"), | 1226 | path!("b/seven.txt"), |
| 1227 | separator!("b/eight.txt"), | 1227 | path!("b/eight.txt"), |
| 1228 | ]; | 1228 | ]; |
| 1229 | 1229 | ||
| 1230 | let mut opened_editors = Vec::new(); | 1230 | let mut opened_editors = Vec::new(); |
| @@ -3,16 +3,21 @@ use std::sync::Arc; · 2 unmodified lines | |||
| 3 | use anyhow::Context as _; | 3 | use anyhow::Context as _; |
| 4 | use context_server::ContextServerId; | 4 | use context_server::ContextServerId; |
| 5 | use extension::{ContextServerConfiguration, ExtensionManifest}; | 5 | use extension::{ContextServerConfiguration, ExtensionManifest}; |
| 6 | use fs::Fs; | ||
| 6 | use gpui::Task; | 7 | use gpui::Task; |
| 7 | use language::LanguageRegistry; | 8 | use language::LanguageRegistry; |
| 8 | use project::context_server_store::registry::ContextServerDescriptorRegistry; | 9 | use project::{ |
| 10 | context_server_store::registry::ContextServerDescriptorRegistry, | ||
| 11 | project_settings::ProjectSettings, | ||
| 12 | }; | ||
| 13 | use settings::update_settings_file; | ||
| 9 | use ui::prelude::*; | 14 | use ui::prelude::*; |
| 10 | use util::ResultExt; | 15 | use util::ResultExt; |
| 11 | use workspace::Workspace; | 16 | use workspace::Workspace; |
| 12 | 17 | ||
| 13 | use crate::agent_configuration::ConfigureContextServerModal; | 18 | use crate::agent_configuration::ConfigureContextServerModal; |
| 14 | 19 | ||
| 15 | pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { | 20 | pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx: &mut App) { |
| 16 | cx.observe_new(move |_: &mut Workspace, window, cx| { | 21 | cx.observe_new(move |_: &mut Workspace, window, cx| { |
| 17 | let Some(window) = window else { | 22 | let Some(window) = window else { |
| 18 | return; | 23 | return; |
| @@ -21,6 +26,7 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 2 unmodified lines | |||
| 21 | if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() { | 26 | if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() { |
| 22 | cx.subscribe_in(extension_events, window, { | 27 | cx.subscribe_in(extension_events, window, { |
| 23 | let language_registry = language_registry.clone(); | 28 | let language_registry = language_registry.clone(); |
| 29 | let fs = fs.clone(); | ||
| 24 | move |workspace, _, event, window, cx| match event { | 30 | move |workspace, _, event, window, cx| match event { |
| 25 | extension::Event::ExtensionInstalled(manifest) => { | 31 | extension::Event::ExtensionInstalled(manifest) => { |
| 26 | show_configure_mcp_modal( | 32 | show_configure_mcp_modal( |
| @@ -31,6 +37,13 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 4 unmodified lines | |||
| 31 | cx, | 37 | cx, |
| 32 | ); | 38 | ); |
| 33 | } | 39 | } |
| 40 | extension::Event::ExtensionUninstalled(manifest) => { | ||
| 41 | remove_context_server_settings( | ||
| 42 | manifest.context_servers.keys().cloned().collect(), | ||
| 43 | fs.clone(), | ||
| 44 | cx, | ||
| 45 | ); | ||
| 46 | } | ||
| 34 | extension::Event::ConfigureExtensionRequested(manifest) => { | 47 | extension::Event::ConfigureExtensionRequested(manifest) => { |
| 35 | if !manifest.context_servers.is_empty() { | 48 | if !manifest.context_servers.is_empty() { |
| 36 | show_configure_mcp_modal( | 49 | show_configure_mcp_modal( |
| @@ -55,6 +68,18 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 18 unmodified lines | |||
| 55 | .detach(); | 68 | .detach(); |
| 56 | } | 69 | } |
| 57 | 70 | ||
| 71 | fn remove_context_server_settings( | ||
| 72 | context_server_ids: Vec<Arc<str>>, | ||
| 73 | fs: Arc<dyn Fs>, | ||
| 74 | cx: &mut App, | ||
| 75 | ) { | ||
| 76 | update_settings_file::<ProjectSettings>(fs, cx, move |settings, _| { | ||
| 77 | settings | ||
| 78 | .context_servers | ||
| 79 | .retain(|server_id, _| !context_server_ids.contains(server_id)); | ||
| 80 | }); | ||
| 81 | } | ||
| 82 | |||
| 58 | pub enum Configuration { | 83 | pub enum Configuration { |
| 59 | NotAvailable(ContextServerId, Option<SharedString>), | 84 | NotAvailable(ContextServerId, Option<SharedString>), |
| 60 | Required( | 85 | Required( |
| @@ -71,6 +96,10 @@ fn show_configure_mcp_modal( · 10 unmodified lines | |||
| 71 | window: &mut Window, | 96 | window: &mut Window, |
| 72 | cx: &mut Context<'_, Workspace>, | 97 | cx: &mut Context<'_, Workspace>, |
| 73 | ) { | 98 | ) { |
| 99 | if !window.is_window_active() { | ||
| 100 | return; | ||
| 101 | } | ||
| 102 | |||
| 74 | let context_server_store = workspace.project().read(cx).context_server_store(); | 103 | let context_server_store = workspace.project().read(cx).context_server_store(); |
| 75 | let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into()); | 104 | let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into()); |
| 76 | 105 | ||
| @@ -105,7 +105,7 @@ impl Tool for ContextServerTool { · 104 unmodified lines | |||
| 105 | arguments | 105 | arguments |
| 106 | ); | 106 | ); |
| 107 | let response = protocol | 107 | let response = protocol |
| 108 | .request::<context_server::types::request::CallTool>( | 108 | .request::<context_server::types::requests::CallTool>( |
| 109 | context_server::types::CallToolParams { | 109 | context_server::types::CallToolParams { |
| 110 | name: tool_name, | 110 | name: tool_name, |
| 111 | arguments, | 111 | arguments, |
| @@ -123,6 +123,9 @@ impl Tool for ContextServerTool { · 11 unmodified lines | |||
| 123 | types::ToolResponseContent::Image { .. } => { | 123 | types::ToolResponseContent::Image { .. } => { |
| 124 | log::warn!("Ignoring image content from tool response"); | 124 | log::warn!("Ignoring image content from tool response"); |
| 125 | } | 125 | } |
| 126 | types::ToolResponseContent::Audio { .. } => { | ||
| 127 | log::warn!("Ignoring audio content from tool response"); | ||
| 128 | } | ||
| 126 | types::ToolResponseContent::Resource { .. } => { | 129 | types::ToolResponseContent::Resource { .. } => { |
| 127 | log::warn!("Ignoring resource content from tool response"); | 130 | log::warn!("Ignoring resource content from tool response"); |
| 128 | } | 131 | } |
| @@ -24,6 +24,7 @@ use gpui::{ · 23 unmodified lines | |||
| 24 | WeakEntity, Window, point, | 24 | WeakEntity, Window, point, |
| 25 | }; | 25 | }; |
| 26 | use language::{Buffer, Point, Selection, TransactionId}; | 26 | use language::{Buffer, Point, Selection, TransactionId}; |
| 27 | use language_model::ConfigurationError; | ||
| 27 | use language_model::ConfiguredModel; | 28 | use language_model::ConfiguredModel; |
| 28 | use language_model::{LanguageModelRegistry, report_assistant_event}; | 29 | use language_model::{LanguageModelRegistry, report_assistant_event}; |
| 29 | use multi_buffer::MultiBufferRow; | 30 | use multi_buffer::MultiBufferRow; |
| @@ -38,8 +39,7 @@ use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; · 8 unmodified lines | |||
| 38 | use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; | 39 | use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; |
| 39 | use text::{OffsetRangeExt, ToPoint as _}; | 40 | use text::{OffsetRangeExt, ToPoint as _}; |
| 40 | use ui::prelude::*; | 41 | use ui::prelude::*; |
| 41 | use util::RangeExt; | 42 | use util::{RangeExt, ResultExt, maybe}; |
| 42 | use util::ResultExt; | ||
| 43 | use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId}; | 43 | use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId}; |
| 44 | use zed_actions::agent::OpenConfiguration; | 44 | use zed_actions::agent::OpenConfiguration; |
| 45 | 45 | ||
| @@ -233,10 +233,9 @@ impl InlineAssistant { · 187 unmodified lines | |||
| 233 | return; | 233 | return; |
| 234 | }; | 234 | }; |
| 235 | 235 | ||
| 236 | let is_authenticated = || { | 236 | let configuration_error = || { |
| 237 | LanguageModelRegistry::read_global(cx) | 237 | let model_registry = LanguageModelRegistry::read_global(cx); |
| 238 | .inline_assistant_model() | 238 | model_registry.configuration_error(model_registry.inline_assistant_model(), cx) |
| 239 | .map_or(false, |model| model.provider.is_authenticated(cx)) | ||
| 240 | }; | 239 | }; |
| 241 | 240 | ||
| 242 | let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else { | 241 | let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else { |
| @@ -284,20 +283,23 @@ impl InlineAssistant { · 41 unmodified lines | |||
| 284 | } | 283 | } |
| 285 | }; | 284 | }; |
| 286 | 285 | ||
| 287 | if is_authenticated() { | 286 | if let Some(error) = configuration_error() { |
| 288 | handle_assist(window, cx); | 287 | if let ConfigurationError::ProviderNotAuthenticated(provider) = error { |
| 289 | } else { | 288 | cx.spawn(async move |_, cx| { |
| 290 | cx.spawn_in(window, async move |_workspace, cx| { | 289 | cx.update(|cx| provider.authenticate(cx))?.await?; |
| 291 | let Some(task) = cx.update(|_, cx| { | 290 | anyhow::Ok(()) |
| 292 | LanguageModelRegistry::read_global(cx) | 291 | }) |
| 293 | .inline_assistant_model() | 292 | .detach_and_log_err(cx); |
| 294 | .map_or(None, |model| Some(model.provider.authenticate(cx))) | 293 | |
| 295 | })? | 294 | if configuration_error().is_none() { |
| 296 | else { | 295 | handle_assist(window, cx); |
| 296 | } | ||
| 297 | } else { | ||
| 298 | cx.spawn_in(window, async move |_, cx| { | ||
| 297 | let answer = cx | 299 | let answer = cx |
| 298 | .prompt( | 300 | .prompt( |
| 299 | gpui::PromptLevel::Warning, | 301 | gpui::PromptLevel::Warning, |
| 300 | "No language model provider configured", | 302 | &error.to_string(), |
| 301 | None, | 303 | None, |
| 302 | &["Configure", "Cancel"], | 304 | &["Configure", "Cancel"], |
| 303 | ) | 305 | ) |
| @@ -311,17 +313,12 @@ impl InlineAssistant { · 7 unmodified lines | |||
| 311 | .ok(); | 313 | .ok(); |
| 312 | } | 314 | } |
| 313 | } | 315 | } |
| 314 | return Ok(()); | 316 | anyhow::Ok(()) |
| 315 | }; | 317 | }) |
| 316 | task.await?; | 318 | .detach_and_log_err(cx); |
| 317 | |||
| 318 | anyhow::Ok(()) | ||
| 319 | }) | ||
| 320 | .detach_and_log_err(cx); | ||
| 321 | |||
| 322 | if is_authenticated() { | ||
| 323 | handle_assist(window, cx); | ||
| 324 | } | 319 | } |
| 320 | } else { | ||
| 321 | handle_assist(window, cx); | ||
| 325 | } | 322 | } |
| 326 | } | 323 | } |
| 327 | 324 | ||
| @@ -1171,27 +1168,31 @@ impl InlineAssistant { · 843 unmodified lines | |||
| 1171 | selections.select_anchor_ranges([position..position]) | 1168 | selections.select_anchor_ranges([position..position]) |
| 1172 | }); | 1169 | }); |
| 1173 | 1170 | ||
| 1174 | let mut scroll_target_top; | 1171 | let mut scroll_target_range = None; |
| 1175 | let mut scroll_target_bottom; | ||
| 1176 | if let Some(decorations) = assist.decorations.as_ref() { | 1172 | if let Some(decorations) = assist.decorations.as_ref() { |
| 1177 | scroll_target_top = editor | 1173 | scroll_target_range = maybe!({ |
| 1178 | .row_for_block(decorations.prompt_block_id, cx) | 1174 | let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32; |
| 1179 | .unwrap() | 1175 | let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f32; |
| 1180 | .0 as f32; | 1176 | Some((top, bottom)) |
| 1181 | scroll_target_bottom = editor | 1177 | }); |
| 1182 | .row_for_block(decorations.end_block_id, cx) | 1178 | if scroll_target_range.is_none() { |
| 1183 | .unwrap() | 1179 | log::error!("bug: failed to find blocks for scrolling to inline assist"); |
| 1184 | .0 as f32; | 1180 | } |
| 1185 | } else { | 1181 | } |
| 1182 | let scroll_target_range = scroll_target_range.unwrap_or_else(|| { | ||
| 1186 | let snapshot = editor.snapshot(window, cx); | 1183 | let snapshot = editor.snapshot(window, cx); |
| 1187 | let start_row = assist | 1184 | let start_row = assist |
| 1188 | .range | 1185 | .range |
| 1189 | .start | 1186 | .start |
| 1190 | .to_display_point(&snapshot.display_snapshot) | 1187 | .to_display_point(&snapshot.display_snapshot) |
| 1191 | .row(); | 1188 | .row(); |
| 1192 | scroll_target_top = start_row.0 as f32; | 1189 | let top = start_row.0 as f32; |
| 1193 | scroll_target_bottom = scroll_target_top + 1.; | 1190 | let bottom = top + 1.0; |
| 1194 | } | 1191 | (top, bottom) |
| 1192 | }); | ||
| 1193 | let mut scroll_target_top = scroll_target_range.0; | ||
| 1194 | let mut scroll_target_bottom = scroll_target_range.1; | ||
| 1195 | |||
| 1195 | scroll_target_top -= editor.vertical_scroll_margin() as f32; | 1196 | scroll_target_top -= editor.vertical_scroll_margin() as f32; |
| 1196 | scroll_target_bottom += editor.vertical_scroll_margin() as f32; | 1197 | scroll_target_bottom += editor.vertical_scroll_margin() as f32; |
| 1197 | 1198 | ||
| @@ -1331,7 +1332,7 @@ impl InlineAssistant { · 133 unmodified lines | |||
| 1331 | editor.clear_gutter_highlights::<GutterPendingRange>(cx); | 1332 | editor.clear_gutter_highlights::<GutterPendingRange>(cx); |
| 1332 | } else { | 1333 | } else { |
| 1333 | editor.highlight_gutter::<GutterPendingRange>( | 1334 | editor.highlight_gutter::<GutterPendingRange>( |
| 1334 | &gutter_pending_ranges, | 1335 | gutter_pending_ranges, |
| 1335 | |cx| cx.theme().status().info_background, | 1336 | |cx| cx.theme().status().info_background, |
| 1336 | cx, | 1337 | cx, |
| 1337 | ) | 1338 | ) |
| @@ -1342,7 +1343,7 @@ impl InlineAssistant { · 4 unmodified lines | |||
| 1342 | editor.clear_gutter_highlights::<GutterTransformedRange>(cx); | 1343 | editor.clear_gutter_highlights::<GutterTransformedRange>(cx); |
| 1343 | } else { | 1344 | } else { |
| 1344 | editor.highlight_gutter::<GutterTransformedRange>( | 1345 | editor.highlight_gutter::<GutterTransformedRange>( |
| 1345 | &gutter_transformed_ranges, | 1346 | gutter_transformed_ranges, |
| 1346 | |cx| cx.theme().status().info, | 1347 | |cx| cx.theme().status().info, |
| 1347 | cx, | 1348 | cx, |
| 1348 | ) | 1349 | ) |
| @@ -261,7 +261,7 @@ impl<T: 'static> PromptEditor<T> { · 260 unmodified lines | |||
| 261 | 261 | ||
| 262 | let focus = self.editor.focus_handle(cx).contains_focused(window, cx); | 262 | let focus = self.editor.focus_handle(cx).contains_focused(window, cx); |
| 263 | self.editor = cx.new(|cx| { | 263 | self.editor = cx.new(|cx| { |
| 264 | let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx); | 264 | let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx); |
| 265 | editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); | 265 | editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); |
| 266 | editor.set_placeholder_text("Add a prompt…", cx); | 266 | editor.set_placeholder_text("Add a prompt…", cx); |
| 267 | editor.set_text(prompt, window, cx); | 267 | editor.set_text(prompt, window, cx); |
| @@ -872,6 +872,7 @@ impl PromptEditor<BufferCodegen> { · 604 unmodified lines | |||
| 872 | let prompt_editor = cx.new(|cx| { | 872 | let prompt_editor = cx.new(|cx| { |
| 873 | let mut editor = Editor::new( | 873 | let mut editor = Editor::new( |
| 874 | EditorMode::AutoHeight { | 874 | EditorMode::AutoHeight { |
| 875 | min_lines: 1, | ||
| 875 | max_lines: Self::MAX_LINES as usize, | 876 | max_lines: Self::MAX_LINES as usize, |
| 876 | }, | 877 | }, |
| 877 | prompt_buffer, | 878 | prompt_buffer, |
| @@ -1050,6 +1051,7 @@ impl PromptEditor<TerminalCodegen> { · 172 unmodified lines | |||
| 1050 | let prompt_editor = cx.new(|cx| { | 1051 | let prompt_editor = cx.new(|cx| { |
| 1051 | let mut editor = Editor::new( | 1052 | let mut editor = Editor::new( |
| 1052 | EditorMode::AutoHeight { | 1053 | EditorMode::AutoHeight { |
| 1054 | min_lines: 1, | ||
| 1053 | max_lines: Self::MAX_LINES as usize, | 1055 | max_lines: Self::MAX_LINES as usize, |
| 1054 | }, | 1056 | }, |
| 1055 | prompt_buffer, | 1057 | prompt_buffer, |
| @@ -39,7 +39,7 @@ use proto::Plan; · 38 unmodified lines | |||
| 39 | use settings::Settings; | 39 | use settings::Settings; |
| 40 | use std::time::Duration; | 40 | use std::time::Duration; |
| 41 | use theme::ThemeSettings; | 41 | use theme::ThemeSettings; |
| 42 | use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*}; | 42 | use ui::{Callout, Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*}; |
| 43 | use util::{ResultExt as _, maybe}; | 43 | use util::{ResultExt as _, maybe}; |
| 44 | use workspace::{CollaboratorId, Workspace}; | 44 | use workspace::{CollaboratorId, Workspace}; |
| 45 | use zed_llm_client::CompletionIntent; | 45 | use zed_llm_client::CompletionIntent; |
| @@ -79,6 +79,7 @@ pub struct MessageEditor { · 33 unmodified lines | |||
| 79 | _subscriptions: Vec<Subscription>, | 79 | _subscriptions: Vec<Subscription>, |
| 80 | } | 80 | } |
| 81 | 81 | ||
| 82 | const MIN_EDITOR_LINES: usize = 4; | ||
| 82 | const MAX_EDITOR_LINES: usize = 8; | 83 | const MAX_EDITOR_LINES: usize = 8; |
| 83 | 84 | ||
| 84 | pub(crate) fn create_editor( | 85 | pub(crate) fn create_editor( |
| @@ -102,6 +103,7 @@ pub(crate) fn create_editor( · 17 unmodified lines | |||
| 102 | let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); | 103 | let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); |
| 103 | let mut editor = Editor::new( | 104 | let mut editor = Editor::new( |
| 104 | editor::EditorMode::AutoHeight { | 105 | editor::EditorMode::AutoHeight { |
| 106 | min_lines: MIN_EDITOR_LINES, | ||
| 105 | max_lines: MAX_EDITOR_LINES, | 107 | max_lines: MAX_EDITOR_LINES, |
| 106 | }, | 108 | }, |
| 107 | buffer, | 109 | buffer, |
| @@ -253,6 +255,7 @@ impl MessageEditor { · 145 unmodified lines | |||
| 253 | }) | 255 | }) |
| 254 | } else { | 256 | } else { |
| 255 | editor.set_mode(EditorMode::AutoHeight { | 257 | editor.set_mode(EditorMode::AutoHeight { |
| 258 | min_lines: MIN_EDITOR_LINES, | ||
| 256 | max_lines: MAX_EDITOR_LINES, | 259 | max_lines: MAX_EDITOR_LINES, |
| 257 | }) | 260 | }) |
| 258 | } | 261 | } |
| @@ -428,10 +431,6 @@ impl MessageEditor { · 169 unmodified lines | |||
| 428 | } | 431 | } |
| 429 | 432 | ||
| 430 | fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) { | 433 | fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) { |
| 431 | if self.thread.read(cx).has_pending_edit_tool_uses() { | ||
| 432 | return; | ||
| 433 | } | ||
| 434 | |||
| 435 | self.edits_expanded = true; | 434 | self.edits_expanded = true; |
| 436 | AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err(); | 435 | AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err(); |
| 437 | cx.notify(); | 436 | cx.notify(); |
| @@ -506,7 +505,7 @@ impl MessageEditor { · 68 unmodified lines | |||
| 506 | cx.notify(); | 505 | cx.notify(); |
| 507 | } | 506 | } |
| 508 | 507 | ||
| 509 | fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> { | 508 | fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> { |
| 510 | let thread = self.thread.read(cx); | 509 | let thread = self.thread.read(cx); |
| 511 | let model = thread.configured_model(); | 510 | let model = thread.configured_model(); |
| 512 | if !model?.model.supports_max_mode() { | 511 | if !model?.model.supports_max_mode() { |
| @@ -641,96 +640,87 @@ impl MessageEditor { · 128 unmodified lines | |||
| 641 | .border_color(cx.theme().colors().border) | 640 | .border_color(cx.theme().colors().border) |
| 642 | .child( | 641 | .child( |
| 643 | h_flex() | 642 | h_flex() |
| 644 | .items_start() | ||
| 645 | .justify_between() | 643 | .justify_between() |
| 646 | .child(self.context_strip.clone()) | 644 | .child(self.context_strip.clone()) |
| 647 | .child( | 645 | .when(focus_handle.is_focused(window), |this| { |
| 648 | h_flex() | 646 | this.child( |
| 649 | .gap_1() | 647 | IconButton::new("toggle-height", expand_icon) |
| 650 | .when(focus_handle.is_focused(window), |this| { | 648 | .icon_size(IconSize::XSmall) |
| 651 | this.child( | 649 | .icon_color(Color::Muted) |
| 652 | IconButton::new("toggle-height", expand_icon) | 650 | .tooltip({ |
| 653 | .icon_size(IconSize::XSmall) | 651 | let focus_handle = focus_handle.clone(); |
| 654 | .icon_color(Color::Muted) | 652 | move |window, cx| { |
| 655 | .tooltip({ | 653 | let expand_label = if is_editor_expanded { |
| 656 | let focus_handle = focus_handle.clone(); | 654 | "Minimize Message Editor".to_string() |
| 657 | move |window, cx| { | 655 | } else { |
| 658 | let expand_label = if is_editor_expanded { | 656 | "Expand Message Editor".to_string() |
| 659 | "Minimize Message Editor".to_string() | 657 | }; |
| 660 | } else { | 658 | |
| 661 | "Expand Message Editor".to_string() | 659 | Tooltip::for_action_in( |
| 662 | }; | 660 | expand_label, |
| 663 | 661 | &ExpandMessageEditor, | |
| 664 | Tooltip::for_action_in( | 662 | &focus_handle, |
| 665 | expand_label, | 663 | window, |
| 666 | &ExpandMessageEditor, | 664 | cx, |
| 667 | &focus_handle, | 665 | ) |
| 668 | window, | 666 | } |
| 669 | cx, | 667 | }) |
| 670 | ) | 668 | .on_click(cx.listener(|_, _, window, cx| { |
| 671 | } | 669 | window.dispatch_action(Box::new(ExpandMessageEditor), cx); |
| 672 | }) | 670 | })), |
| 673 | .on_click(cx.listener(|_, _, window, cx| { | 671 | ) |
| 674 | window | 672 | }), |
| 675 | .dispatch_action(Box::new(ExpandMessageEditor), cx); | ||
| 676 | })), | ||
| 677 | ) | ||
| 678 | }), | ||
| 679 | ), | ||
| 680 | ) | 673 | ) |
| 681 | .child( | 674 | .child( |
| 682 | v_flex() | 675 | v_flex() |
| 683 | .size_full() | 676 | .size_full() |
| 684 | .gap_4() | 677 | .gap_1() |
| 685 | .when(is_editor_expanded, |this| { | 678 | .when(is_editor_expanded, |this| { |
| 686 | this.h(vh(0.8, window)).justify_between() | 679 | this.h(vh(0.8, window)).justify_between() |
| 687 | }) | 680 | }) |
| 688 | .child( | 681 | .child({ |
| 689 | v_flex() | 682 | let settings = ThemeSettings::get_global(cx); |
| 690 | .min_h_16() | 683 | let font_size = TextSize::Small |
| 691 | .when(is_editor_expanded, |this| this.h_full()) | 684 | .rems(cx) |
| 692 | .child({ | 685 | .to_pixels(settings.agent_font_size(cx)); |
| 693 | let settings = ThemeSettings::get_global(cx); | 686 | let line_height = settings.buffer_line_height.value() * font_size; |
| 694 | let font_size = TextSize::Small | 687 | |
| 695 | .rems(cx) | 688 | let text_style = TextStyle { |
| 696 | .to_pixels(settings.agent_font_size(cx)); | 689 | color: cx.theme().colors().text, |
| 697 | let line_height = settings.buffer_line_height.value() * font_size; | 690 | font_family: settings.buffer_font.family.clone(), |
| 698 | 691 | font_fallbacks: settings.buffer_font.fallbacks.clone(), | |
| 699 | let text_style = TextStyle { | 692 | font_features: settings.buffer_font.features.clone(), |
| 700 | color: cx.theme().colors().text, | 693 | font_size: font_size.into(), |
| 701 | font_family: settings.buffer_font.family.clone(), | 694 | line_height: line_height.into(), |
| 702 | font_fallbacks: settings.buffer_font.fallbacks.clone(), | 695 | ..Default::default() |
| 703 | font_features: settings.buffer_font.features.clone(), | 696 | }; |
| 704 | font_size: font_size.into(), | 697 | |
| 705 | line_height: line_height.into(), | 698 | EditorElement::new( |
| 706 | ..Default::default() | 699 | &self.editor, |
| 707 | }; | 700 | EditorStyle { |
| 708 | 701 | background: editor_bg_color, | |
| 709 | EditorElement::new( | 702 | local_player: cx.theme().players().local(), |
| 710 | &self.editor, | 703 | text: text_style, |
| 711 | EditorStyle { | 704 | syntax: cx.theme().syntax().clone(), |
| 712 | background: editor_bg_color, | 705 | ..Default::default() |
| 713 | local_player: cx.theme().players().local(), | 706 | }, |
| 714 | text: text_style, | 707 | ) |
| 715 | syntax: cx.theme().syntax().clone(), | 708 | .into_any() |
| 716 | ..Default::default() | 709 | }) |
| 717 | }, | ||
| 718 | ) | ||
| 719 | .into_any() | ||
| 720 | }), | ||
| 721 | ) | ||
| 722 | .child( | 710 | .child( |
| 723 | h_flex() | 711 | h_flex() |
| 724 | .flex_none() | 712 | .flex_none() |
| 713 | .flex_wrap() | ||
| 725 | .justify_between() | 714 | .justify_between() |
| 726 | .child( | 715 | .child( |
| 727 | h_flex() | 716 | h_flex() |
| 728 | .child(self.render_follow_toggle(cx)) | 717 | .child(self.render_follow_toggle(cx)) |
| 729 | .children(self.render_max_mode_toggle(cx)), | 718 | .children(self.render_burn_mode_toggle(cx)), |
| 730 | ) | 719 | ) |
| 731 | .child( | 720 | .child( |
| 732 | h_flex() | 721 | h_flex() |
| 733 | .gap_1() | 722 | .gap_1() |
| 723 | .flex_wrap() | ||
| 734 | .when(!incompatible_tools.is_empty(), |this| { | 724 | .when(!incompatible_tools.is_empty(), |this| { |
| 735 | this.child( | 725 | this.child( |
| 736 | IconButton::new( | 726 | IconButton::new( |
| @@ -1183,6 +1173,7 @@ impl MessageEditor { · 446 unmodified lines | |||
| 1183 | .map_or(false, |model| { | 1173 | .map_or(false, |model| { |
| 1184 | model.provider.id().0 == ZED_CLOUD_PROVIDER_ID | 1174 | model.provider.id().0 == ZED_CLOUD_PROVIDER_ID |
| 1185 | }); | 1175 | }); |
| 1176 | |||
| 1186 | if !is_using_zed_provider { | 1177 | if !is_using_zed_provider { |
| 1187 | return None; | 1178 | return None; |
| 1188 | } | 1179 | } |
| @@ -1237,14 +1228,6 @@ impl MessageEditor { · 48 unmodified lines | |||
| 1237 | token_usage_ratio: TokenUsageRatio, | 1228 | token_usage_ratio: TokenUsageRatio, |
| 1238 | cx: &mut Context<Self>, | 1229 | cx: &mut Context<Self>, |
| 1239 | ) -> Option<Div> { | 1230 | ) -> Option<Div> { |
| 1240 | let title = if token_usage_ratio == TokenUsageRatio::Exceeded { | ||
| 1241 | "Thread reached the token limit" | ||
| 1242 | } else { | ||
| 1243 | "Thread reaching the token limit soon" | ||
| 1244 | }; | ||
| 1245 | |||
| 1246 | let message = "Start a new thread from a summary to continue the conversation."; | ||
| 1247 | |||
| 1248 | let icon = if token_usage_ratio == TokenUsageRatio::Exceeded { | 1231 | let icon = if token_usage_ratio == TokenUsageRatio::Exceeded { |
| 1249 | Icon::new(IconName::X) | 1232 | Icon::new(IconName::X) |
| 1250 | .color(Color::Error) | 1233 | .color(Color::Error) |
| @@ -1255,19 +1238,43 @@ impl MessageEditor { · 4 unmodified lines | |||
| 1255 | .size(IconSize::XSmall) | 1238 | .size(IconSize::XSmall) |
| 1256 | }; | 1239 | }; |
| 1257 | 1240 | ||
| 1241 | let title = if token_usage_ratio == TokenUsageRatio::Exceeded { | ||
| 1242 | "Thread reached the token limit" | ||
| 1243 | } else { | ||
| 1244 | "Thread reaching the token limit soon" | ||
| 1245 | }; | ||
| 1246 | |||
| 1258 | Some( | 1247 | Some( |
| 1259 | div() | 1248 | div() |
| 1260 | .child(ui::Callout::multi_line( | 1249 | .border_t_1() |
| 1261 | title, | 1250 | .border_color(cx.theme().colors().border) |
| 1262 | message, | 1251 | .child( |
| 1263 | icon, | 1252 | Callout::new() |
| 1264 | "Start New Thread", | 1253 | .line_height(line_height) |
| 1265 | Box::new(cx.listener(|this, _, window, cx| { | 1254 | .icon(icon) |
| 1266 | let from_thread_id = Some(this.thread.read(cx).id().clone()); | 1255 | .title(title) |
| 1267 | window.dispatch_action(Box::new(NewThread { from_thread_id }), cx); | 1256 | .description( |
| 1268 | })), | 1257 | "To continue, start a new thread from a summary or turn burn mode on.", |
| 1269 | )) | 1258 | ) |
| 1270 | .line_height(line_height), | 1259 | .primary_action( |
| 1260 | Button::new("start-new-thread", "Start New Thread") | ||
| 1261 | .label_size(LabelSize::Small) | ||
| 1262 | .on_click(cx.listener(|this, _, window, cx| { | ||
| 1263 | let from_thread_id = Some(this.thread.read(cx).id().clone()); | ||
| 1264 | window.dispatch_action( | ||
| 1265 | Box::new(NewThread { from_thread_id }), | ||
| 1266 | cx, | ||
| 1267 | ); | ||
| 1268 | })), | ||
| 1269 | ) | ||
| 1270 | .secondary_action( | ||
| 1271 | IconButton::new("burn-mode-callout", IconName::ZedBurnMode) | ||
| 1272 | .icon_size(IconSize::XSmall) | ||
| 1273 | .on_click(cx.listener(|this, _event, window, cx| { | ||
| 1274 | this.toggle_burn_mode(&ToggleBurnMode, window, cx); | ||
| 1275 | })), | ||
| 1276 | ), | ||
| 1277 | ), | ||
| 1271 | ) | 1278 | ) |
| 1272 | } | 1279 | } |
| 1273 | 1280 | ||
| @@ -1464,6 +1471,8 @@ impl Render for MessageEditor { · 190 unmodified lines | |||
| 1464 | total_token_usage.ratio() | 1471 | total_token_usage.ratio() |
| 1465 | }); | 1472 | }); |
| 1466 | 1473 | ||
| 1474 | let burn_mode_enabled = thread.completion_mode() == CompletionMode::Burn; | ||
| 1475 | |||
| 1467 | let action_log = self.thread.read(cx).action_log(); | 1476 | let action_log = self.thread.read(cx).action_log(); |
| 1468 | let changed_buffers = action_log.read(cx).changed_buffers(cx); | 1477 | let changed_buffers = action_log.read(cx).changed_buffers(cx); |
| 1469 | 1478 | ||
| @@ -1480,7 +1489,7 @@ impl Render for MessageEditor { · 10 unmodified lines | |||
| 1480 | 1489 | ||
| 1481 | if usage_callout.is_some() { | 1490 | if usage_callout.is_some() { |
| 1482 | usage_callout | 1491 | usage_callout |
| 1483 | } else if token_usage_ratio != TokenUsageRatio::Normal { | 1492 | } else if token_usage_ratio != TokenUsageRatio::Normal && !burn_mode_enabled { |
| 1484 | self.render_token_limit_callout(line_height, token_usage_ratio, cx) | 1493 | self.render_token_limit_callout(line_height, token_usage_ratio, cx) |
| 1485 | } else { | 1494 | } else { |
| 1486 | None | 1495 | None |
| @@ -195,20 +195,20 @@ impl MessageSegment { · 194 unmodified lines | |||
| 195 | } | 195 | } |
| 196 | } | 196 | } |
| 197 | 197 | ||
| 198 | #[derive(Debug, Clone, Serialize, Deserialize)] | 198 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| 199 | pub struct ProjectSnapshot { | 199 | pub struct ProjectSnapshot { |
| 200 | pub worktree_snapshots: Vec<WorktreeSnapshot>, | 200 | pub worktree_snapshots: Vec<WorktreeSnapshot>, |
| 201 | pub unsaved_buffer_paths: Vec<String>, | 201 | pub unsaved_buffer_paths: Vec<String>, |
| 202 | pub timestamp: DateTime<Utc>, | 202 | pub timestamp: DateTime<Utc>, |
| 203 | } | 203 | } |
| 204 | 204 | ||
| 205 | #[derive(Debug, Clone, Serialize, Deserialize)] | 205 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| 206 | pub struct WorktreeSnapshot { | 206 | pub struct WorktreeSnapshot { |
| 207 | pub worktree_path: String, | 207 | pub worktree_path: String, |
| 208 | pub git_state: Option<GitState>, | 208 | pub git_state: Option<GitState>, |
| 209 | } | 209 | } |
| 210 | 210 | ||
| 211 | #[derive(Debug, Clone, Serialize, Deserialize)] | 211 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| 212 | pub struct GitState { | 212 | pub struct GitState { |
| 213 | pub remote_url: Option<String>, | 213 | pub remote_url: Option<String>, |
| 214 | pub head_sha: Option<String>, | 214 | pub head_sha: Option<String>, |
| @@ -247,7 +247,7 @@ impl LastRestoreCheckpoint { · 32 unmodified lines | |||
| 247 | } | 247 | } |
| 248 | } | 248 | } |
| 249 | 249 | ||
| 250 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | 250 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] |
| 251 | pub enum DetailedSummaryState { | 251 | pub enum DetailedSummaryState { |
| 252 | #[default] | 252 | #[default] |
| 253 | NotGenerated, | 253 | NotGenerated, |
| @@ -391,7 +391,7 @@ impl ThreadSummary { · 137 unmodified lines | |||
| 391 | } | 391 | } |
| 392 | } | 392 | } |
| 393 | 393 | ||
| 394 | #[derive(Debug, Clone, Serialize, Deserialize)] | 394 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| 395 | pub struct ExceededWindowError { | 395 | pub struct ExceededWindowError { |
| 396 | /// Model used when last message exceeded context window | 396 | /// Model used when last message exceeded context window |
| 397 | model_id: LanguageModelId, | 397 | model_id: LanguageModelId, |
| @@ -1563,6 +1563,9 @@ impl Thread { · 1165 unmodified lines | |||
| 1563 | Err(LanguageModelCompletionError::Other(error)) => { | 1563 | Err(LanguageModelCompletionError::Other(error)) => { |
| 1564 | return Err(error); | 1564 | return Err(error); |
| 1565 | } | 1565 | } |
| 1566 | Err(err @ LanguageModelCompletionError::RateLimit(..)) => { | ||
| 1567 | return Err(err.into()); | ||
| 1568 | } | ||
| 1566 | }; | 1569 | }; |
| 1567 | 1570 | ||
| 1568 | match event { | 1571 | match event { |
| @@ -594,10 +594,11 @@ impl Render for ThreadHistory { · 593 unmodified lines | |||
| 594 | view.pr_5() | 594 | view.pr_5() |
| 595 | .child( | 595 | .child( |
| 596 | uniform_list( | 596 | uniform_list( |
| 597 | cx.entity().clone(), | ||
| 598 | "thread-history", | 597 | "thread-history", |
| 599 | self.list_item_count(), | 598 | self.list_item_count(), |
| 600 | Self::list_items, | 599 | cx.processor(|this, range: Range<usize>, window, cx| { |
| 600 | this.list_items(range, window, cx) | ||
| 601 | }), | ||
| 601 | ) | 602 | ) |
| 602 | .p_1() | 603 | .p_1() |
| 603 | .track_scroll(self.scroll_handle.clone()) | 604 | .track_scroll(self.scroll_handle.clone()) |
| @@ -89,7 +89,7 @@ pub fn init(cx: &mut App) { · 88 unmodified lines | |||
| 89 | pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>); | 89 | pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>); |
| 90 | 90 | ||
| 91 | impl SharedProjectContext { | 91 | impl SharedProjectContext { |
| 92 | pub fn borrow(&self) -> Ref<Option<ProjectContext>> { | 92 | pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> { |
| 93 | self.0.borrow() | 93 | self.0.borrow() |
| 94 | } | 94 | } |
| 95 | } | 95 | } |
| @@ -305,17 +305,19 @@ impl ThreadStore { · 209 unmodified lines | |||
| 305 | project: Entity<Project>, | 305 | project: Entity<Project>, |
| 306 | cx: &mut App, | 306 | cx: &mut App, |
| 307 | ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> { | 307 | ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> { |
| 308 | let root_name = worktree.read(cx).root_name().into(); | 308 | let tree = worktree.read(cx); |
| 309 | let root_name = tree.root_name().into(); | ||
| 310 | let abs_path = tree.abs_path(); | ||
| 311 | |||
| 312 | let mut context = WorktreeContext { | ||
| 313 | root_name, | ||
| 314 | abs_path, | ||
| 315 | rules_file: None, | ||
| 316 | }; | ||
| 309 | 317 | ||
| 310 | let rules_task = Self::load_worktree_rules_file(worktree, project, cx); | 318 | let rules_task = Self::load_worktree_rules_file(worktree, project, cx); |
| 311 | let Some(rules_task) = rules_task else { | 319 | let Some(rules_task) = rules_task else { |
| 312 | return Task::ready(( | 320 | return Task::ready((context, None)); |
| 313 | WorktreeContext { | ||
| 314 | root_name, | ||
| 315 | rules_file: None, | ||
| 316 | }, | ||
| 317 | None, | ||
| 318 | )); | ||
| 319 | }; | 321 | }; |
| 320 | 322 | ||
| 321 | cx.spawn(async move |_| { | 323 | cx.spawn(async move |_| { |
| @@ -328,11 +330,8 @@ impl ThreadStore { · 6 unmodified lines | |||
| 328 | }), | 330 | }), |
| 329 | ), | 331 | ), |
| 330 | }; | 332 | }; |
| 331 | let worktree_info = WorktreeContext { | 333 | context.rules_file = rules_file; |
| 332 | root_name, | 334 | (context, rules_file_error) |
| 333 | rules_file, | ||
| 334 | }; | ||
| 335 | (worktree_info, rules_file_error) | ||
| 336 | }) | 335 | }) |
| 337 | } | 336 | } |
| 338 | 337 | ||
| @@ -341,12 +340,12 @@ impl ThreadStore { · 2 unmodified lines | |||
| 341 | project: Entity<Project>, | 340 | project: Entity<Project>, |
| 342 | cx: &mut App, | 341 | cx: &mut App, |
| 343 | ) -> Option<Task<Result<RulesFileContext>>> { | 342 | ) -> Option<Task<Result<RulesFileContext>>> { |
| 344 | let worktree_ref = worktree.read(cx); | 343 | let worktree = worktree.read(cx); |
| 345 | let worktree_id = worktree_ref.id(); | 344 | let worktree_id = worktree.id(); |
| 346 | let selected_rules_file = RULES_FILE_NAMES | 345 | let selected_rules_file = RULES_FILE_NAMES |
| 347 | .into_iter() | 346 | .into_iter() |
| 348 | .filter_map(|name| { | 347 | .filter_map(|name| { |
| 349 | worktree_ref | 348 | worktree |
| 350 | .entry_for_path(name) | 349 | .entry_for_path(name) |
| 351 | .filter(|entry| entry.is_file()) | 350 | .filter(|entry| entry.is_file()) |
| 352 | .map(|entry| entry.path.clone()) | 351 | .map(|entry| entry.path.clone()) |
| @@ -562,7 +561,7 @@ impl ThreadStore { · 209 unmodified lines | |||
| 562 | 561 | ||
| 563 | if protocol.capable(context_server::protocol::ServerCapability::Tools) { | 562 | if protocol.capable(context_server::protocol::ServerCapability::Tools) { |
| 564 | if let Some(response) = protocol | 563 | if let Some(response) = protocol |
| 565 | .request::<context_server::types::request::ListTools>(()) | 564 | .request::<context_server::types::requests::ListTools>(()) |
| 566 | .await | 565 | .await |
| 567 | .log_err() | 566 | .log_err() |
| 568 | { | 567 | { |
| @@ -603,7 +602,7 @@ pub struct SerializedThreadMetadata { · 34 unmodified lines | |||
| 603 | pub updated_at: DateTime<Utc>, | 602 | pub updated_at: DateTime<Utc>, |
| 604 | } | 603 | } |
| 605 | 604 | ||
| 606 | #[derive(Serialize, Deserialize, Debug)] | 605 | #[derive(Serialize, Deserialize, Debug, PartialEq)] |
| 607 | pub struct SerializedThread { | 606 | pub struct SerializedThread { |
| 608 | pub version: String, | 607 | pub version: String, |
| 609 | pub summary: SharedString, | 608 | pub summary: SharedString, |
| @@ -629,7 +628,7 @@ pub struct SerializedThread { · 19 unmodified lines | |||
| 629 | pub profile: Option<AgentProfileId>, | 628 | pub profile: Option<AgentProfileId>, |
| 630 | } | 629 | } |
| 631 | 630 | ||
| 632 | #[derive(Serialize, Deserialize, Debug)] | 631 | #[derive(Serialize, Deserialize, Debug, PartialEq)] |
| 633 | pub struct SerializedLanguageModel { | 632 | pub struct SerializedLanguageModel { |
| 634 | pub provider: String, | 633 | pub provider: String, |
| 635 | pub model: String, | 634 | pub model: String, |
| @@ -690,11 +689,15 @@ impl SerializedThreadV0_1_0 { · 54 unmodified lines | |||
| 690 | messages.push(message); | 689 | messages.push(message); |
| 691 | } | 690 | } |
| 692 | 691 | ||
| 693 | SerializedThread { messages, ..self.0 } | 692 | SerializedThread { |
| 693 | messages, | ||
| 694 | version: SerializedThread::VERSION.to_string(), | ||
| 695 | ..self.0 | ||
| 696 | } | ||
| 694 | } | 697 | } |
| 695 | } | 698 | } |
| 696 | 699 | ||
| 697 | #[derive(Debug, Serialize, Deserialize)] | 700 | #[derive(Debug, Serialize, Deserialize, PartialEq)] |
| 698 | pub struct SerializedMessage { | 701 | pub struct SerializedMessage { |
| 699 | pub id: MessageId, | 702 | pub id: MessageId, |
| 700 | pub role: Role, | 703 | pub role: Role, |
| @@ -712,7 +715,7 @@ pub struct SerializedMessage { · 11 unmodified lines | |||
| 712 | pub is_hidden: bool, | 715 | pub is_hidden: bool, |
| 713 | } | 716 | } |
| 714 | 717 | ||
| 715 | #[derive(Debug, Serialize, Deserialize)] | 718 | #[derive(Debug, Serialize, Deserialize, PartialEq)] |
| 716 | #[serde(tag = "type")] | 719 | #[serde(tag = "type")] |
| 717 | pub enum SerializedMessageSegment { | 720 | pub enum SerializedMessageSegment { |
| 718 | #[serde(rename = "text")] | 721 | #[serde(rename = "text")] |
| @@ -730,14 +733,14 @@ pub enum SerializedMessageSegment { · 11 unmodified lines | |||
| 730 | }, | 733 | }, |
| 731 | } | 734 | } |
| 732 | 735 | ||
| 733 | #[derive(Debug, Serialize, Deserialize)] | 736 | #[derive(Debug, Serialize, Deserialize, PartialEq)] |
| 734 | pub struct SerializedToolUse { | 737 | pub struct SerializedToolUse { |
| 735 | pub id: LanguageModelToolUseId, | 738 | pub id: LanguageModelToolUseId, |
| 736 | pub name: SharedString, | 739 | pub name: SharedString, |
| 737 | pub input: serde_json::Value, | 740 | pub input: serde_json::Value, |
| 738 | } | 741 | } |
| 739 | 742 | ||
| 740 | #[derive(Debug, Serialize, Deserialize)] | 743 | #[derive(Debug, Serialize, Deserialize, PartialEq)] |
| 741 | pub struct SerializedToolResult { | 744 | pub struct SerializedToolResult { |
| 742 | pub tool_use_id: LanguageModelToolUseId, | 745 | pub tool_use_id: LanguageModelToolUseId, |
| 743 | pub is_error: bool, | 746 | pub is_error: bool, |
| @@ -800,7 +803,7 @@ impl LegacySerializedMessage { · 56 unmodified lines | |||
| 800 | } | 803 | } |
| 801 | } | 804 | } |
| 802 | 805 | ||
| 803 | #[derive(Debug, Serialize, Deserialize)] | 806 | #[derive(Debug, Serialize, Deserialize, PartialEq)] |
| 804 | pub struct SerializedCrease { | 807 | pub struct SerializedCrease { |
| 805 | pub start: usize, | 808 | pub start: usize, |
| 806 | pub end: usize, | 809 | pub end: usize, |
| @@ -919,7 +922,7 @@ impl ThreadsDatabase { · 112 unmodified lines | |||
| 919 | 922 | ||
| 920 | fn bytes_encode( | 923 | fn bytes_encode( |
| 921 | item: &Self::EItem, | 924 | item: &Self::EItem, |
| 922 | ) -> Result<std::borrow::Cow<[u8]>, heed::BoxedError> { | 925 | ) -> Result<std::borrow::Cow<'_, [u8]>, heed::BoxedError> { |
| 923 | serde_json::to_vec(&item.0) | 926 | serde_json::to_vec(&item.0) |
| 924 | .map(std::borrow::Cow::Owned) | 927 | .map(std::borrow::Cow::Owned) |
| 925 | .map_err(Into::into) | 928 | .map_err(Into::into) |
| @@ -1057,3 +1060,181 @@ impl ThreadsDatabase { · 131 unmodified lines | |||
| 1057 | }) | 1060 | }) |
| 1058 | } | 1061 | } |
| 1059 | } | 1062 | } |
| 1063 | |||
| 1064 | #[cfg(test)] | ||
| 1065 | mod tests { | ||
| 1066 | use super::*; | ||
| 1067 | use crate::thread::{DetailedSummaryState, MessageId}; | ||
| 1068 | use chrono::Utc; | ||
| 1069 | use language_model::{Role, TokenUsage}; | ||
| 1070 | use pretty_assertions::assert_eq; | ||
| 1071 | |||
| 1072 | #[test] | ||
| 1073 | fn test_legacy_serialized_thread_upgrade() { | ||
| 1074 | let updated_at = Utc::now(); | ||
| 1075 | let legacy_thread = LegacySerializedThread { | ||
| 1076 | summary: "Test conversation".into(), | ||
| 1077 | updated_at, | ||
| 1078 | messages: vec![LegacySerializedMessage { | ||
| 1079 | id: MessageId(1), | ||
| 1080 | role: Role::User, | ||
| 1081 | text: "Hello, world!".to_string(), | ||
| 1082 | tool_uses: vec![], | ||
| 1083 | tool_results: vec![], | ||
| 1084 | }], | ||
| 1085 | initial_project_snapshot: None, | ||
| 1086 | }; | ||
| 1087 | |||
| 1088 | let upgraded = legacy_thread.upgrade(); | ||
| 1089 | |||
| 1090 | assert_eq!( | ||
| 1091 | upgraded, | ||
| 1092 | SerializedThread { | ||
| 1093 | summary: "Test conversation".into(), | ||
| 1094 | updated_at, | ||
| 1095 | messages: vec![SerializedMessage { | ||
| 1096 | id: MessageId(1), | ||
| 1097 | role: Role::User, | ||
| 1098 | segments: vec![SerializedMessageSegment::Text { | ||
| 1099 | text: "Hello, world!".to_string() | ||
| 1100 | }], | ||
| 1101 | tool_uses: vec![], | ||
| 1102 | tool_results: vec![], | ||
| 1103 | context: "".to_string(), | ||
| 1104 | creases: vec![], | ||
| 1105 | is_hidden: false | ||
| 1106 | }], | ||
| 1107 | version: SerializedThread::VERSION.to_string(), | ||
| 1108 | initial_project_snapshot: None, | ||
| 1109 | cumulative_token_usage: TokenUsage::default(), | ||
| 1110 | request_token_usage: vec![], | ||
| 1111 | detailed_summary_state: DetailedSummaryState::default(), | ||
| 1112 | exceeded_window_error: None, | ||
| 1113 | model: None, | ||
| 1114 | completion_mode: None, | ||
| 1115 | tool_use_limit_reached: false, | ||
| 1116 | profile: None | ||
| 1117 | } | ||
| 1118 | ) | ||
| 1119 | } | ||
| 1120 | |||
| 1121 | #[test] | ||
| 1122 | fn test_serialized_threadv0_1_0_upgrade() { | ||
| 1123 | let updated_at = Utc::now(); | ||
| 1124 | let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread { | ||
| 1125 | summary: "Test conversation".into(), | ||
| 1126 | updated_at, | ||
| 1127 | messages: vec![ | ||
| 1128 | SerializedMessage { | ||
| 1129 | id: MessageId(1), | ||
| 1130 | role: Role::User, | ||
| 1131 | segments: vec![SerializedMessageSegment::Text { | ||
| 1132 | text: "Use tool_1".to_string(), | ||
| 1133 | }], | ||
| 1134 | tool_uses: vec![], | ||
| 1135 | tool_results: vec![], | ||
| 1136 | context: "".to_string(), | ||
| 1137 | creases: vec![], | ||
| 1138 | is_hidden: false, | ||
| 1139 | }, | ||
| 1140 | SerializedMessage { | ||
| 1141 | id: MessageId(2), | ||
| 1142 | role: Role::Assistant, | ||
| 1143 | segments: vec![SerializedMessageSegment::Text { | ||
| 1144 | text: "I want to use a tool".to_string(), | ||
| 1145 | }], | ||
| 1146 | tool_uses: vec![SerializedToolUse { | ||
| 1147 | id: "abc".into(), | ||
| 1148 | name: "tool_1".into(), | ||
| 1149 | input: serde_json::Value::Null, | ||
| 1150 | }], | ||
| 1151 | tool_results: vec![], | ||
| 1152 | context: "".to_string(), | ||
| 1153 | creases: vec![], | ||
| 1154 | is_hidden: false, | ||
| 1155 | }, | ||
| 1156 | SerializedMessage { | ||
| 1157 | id: MessageId(1), | ||
| 1158 | role: Role::User, | ||
| 1159 | segments: vec![SerializedMessageSegment::Text { | ||
| 1160 | text: "Here is the tool result".to_string(), | ||
| 1161 | }], | ||
| 1162 | tool_uses: vec![], | ||
| 1163 | tool_results: vec![SerializedToolResult { | ||
| 1164 | tool_use_id: "abc".into(), | ||
| 1165 | is_error: false, | ||
| 1166 | content: LanguageModelToolResultContent::Text("abcdef".into()), | ||
| 1167 | output: Some(serde_json::Value::Null), | ||
| 1168 | }], | ||
| 1169 | context: "".to_string(), | ||
| 1170 | creases: vec![], | ||
| 1171 | is_hidden: false, | ||
| 1172 | }, | ||
| 1173 | ], | ||
| 1174 | version: SerializedThreadV0_1_0::VERSION.to_string(), | ||
| 1175 | initial_project_snapshot: None, | ||
| 1176 | cumulative_token_usage: TokenUsage::default(), | ||
| 1177 | request_token_usage: vec![], | ||
| 1178 | detailed_summary_state: DetailedSummaryState::default(), | ||
| 1179 | exceeded_window_error: None, | ||
| 1180 | model: None, | ||
| 1181 | completion_mode: None, | ||
| 1182 | tool_use_limit_reached: false, | ||
| 1183 | profile: None, | ||
| 1184 | }); | ||
| 1185 | let upgraded = thread_v0_1_0.upgrade(); | ||
| 1186 | |||
| 1187 | assert_eq!( | ||
| 1188 | upgraded, | ||
| 1189 | SerializedThread { | ||
| 1190 | summary: "Test conversation".into(), | ||
| 1191 | updated_at, | ||
| 1192 | messages: vec![ | ||
| 1193 | SerializedMessage { | ||
| 1194 | id: MessageId(1), | ||
| 1195 | role: Role::User, | ||
| 1196 | segments: vec![SerializedMessageSegment::Text { | ||
| 1197 | text: "Use tool_1".to_string() | ||
| 1198 | }], | ||
| 1199 | tool_uses: vec![], | ||
| 1200 | tool_results: vec![], | ||
| 1201 | context: "".to_string(), | ||
| 1202 | creases: vec![], | ||
| 1203 | is_hidden: false | ||
| 1204 | }, | ||
| 1205 | SerializedMessage { | ||
| 1206 | id: MessageId(2), | ||
| 1207 | role: Role::Assistant, | ||
| 1208 | segments: vec![SerializedMessageSegment::Text { | ||
| 1209 | text: "I want to use a tool".to_string(), | ||
| 1210 | }], | ||
| 1211 | tool_uses: vec![SerializedToolUse { | ||
| 1212 | id: "abc".into(), | ||
| 1213 | name: "tool_1".into(), | ||
| 1214 | input: serde_json::Value::Null, | ||
| 1215 | }], | ||
| 1216 | tool_results: vec![SerializedToolResult { | ||
| 1217 | tool_use_id: "abc".into(), | ||
| 1218 | is_error: false, | ||
| 1219 | content: LanguageModelToolResultContent::Text("abcdef".into()), | ||
| 1220 | output: Some(serde_json::Value::Null), | ||
| 1221 | }], | ||
| 1222 | context: "".to_string(), | ||
| 1223 | creases: vec![], | ||
| 1224 | is_hidden: false, | ||
| 1225 | }, | ||
| 1226 | ], | ||
| 1227 | version: SerializedThread::VERSION.to_string(), | ||
| 1228 | initial_project_snapshot: None, | ||
| 1229 | cumulative_token_usage: TokenUsage::default(), | ||
| 1230 | request_token_usage: vec![], | ||
| 1231 | detailed_summary_state: DetailedSummaryState::default(), | ||
| 1232 | exceeded_window_error: None, | ||
| 1233 | model: None, | ||
| 1234 | completion_mode: None, | ||
| 1235 | tool_use_limit_reached: false, | ||
| 1236 | profile: None | ||
| 1237 | } | ||
| 1238 | ) | ||
| 1239 | } | ||
| 1240 | } | ||
| @@ -2,7 +2,7 @@ use client::zed_urls; · 1 unmodified line | |||
| 2 | use component::{empty_example, example_group_with_title, single_example}; | 2 | use component::{empty_example, example_group_with_title, single_example}; |
| 3 | use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; | 3 | use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; |
| 4 | use language_model::RequestUsage; | 4 | use language_model::RequestUsage; |
| 5 | use ui::{Callout, Color, Icon, IconName, IconSize, prelude::*}; | 5 | use ui::{Callout, prelude::*}; |
| 6 | use zed_llm_client::{Plan, UsageLimit}; | 6 | use zed_llm_client::{Plan, UsageLimit}; |
| 7 | 7 | ||
| 8 | #[derive(IntoElement, RegisterComponent)] | 8 | #[derive(IntoElement, RegisterComponent)] |
| @@ -91,16 +91,23 @@ impl RenderOnce for UsageCallout { · 82 unmodified lines | |||
| 91 | .size(IconSize::XSmall) | 91 | .size(IconSize::XSmall) |
| 92 | }; | 92 | }; |
| 93 | 93 | ||
| 94 | Callout::multi_line( | 94 | div() |
| 95 | title, | 95 | .border_t_1() |
| 96 | message, | 96 | .border_color(cx.theme().colors().border) |
| 97 | icon, | 97 | .child( |
| 98 | button_text, | 98 | Callout::new() |
| 99 | Box::new(move |_, _, cx| { | 99 | .icon(icon) |
| 100 | cx.open_url(&url); | 100 | .title(title) |
| 101 | }), | 101 | .description(message) |
| 102 | ) | 102 | .primary_action( |
| 103 | .into_any_element() | 103 | Button::new("upgrade", button_text) |
| 104 | .label_size(LabelSize::Small) | ||
| 105 | .on_click(move |_, _, cx| { | ||
| 106 | cx.open_url(&url); | ||
| 107 | }), | ||
| 108 | ), | ||
| 109 | ) | ||
| 110 | .into_any_element() | ||
| 104 | } | 111 | } |
| 105 | } | 112 | } |
| 106 | 113 | ||
| @@ -189,10 +196,8 @@ impl Component for UsageCallout { · 82 unmodified lines | |||
| 189 | ); | 196 | ); |
| 190 | 197 | ||
| 191 | Some( | 198 | Some( |
| 192 | div() | 199 | v_flex() |
| 193 | .p_4() | 200 | .p_4() |
| 194 | .flex() | ||
| 195 | .flex_col() | ||
| 196 | .gap_4() | 201 | .gap_4() |
| 197 | .child(free_examples) | 202 | .child(free_examples) |
| 198 | .child(trial_examples) | 203 | .child(trial_examples) |
| @@ -1,4 +1,5 @@ | |||
| 1 | use std::str::FromStr; | 1 | use std::str::FromStr; |
| 2 | use std::time::Duration; | ||
| 2 | 3 | ||
| 3 | use anyhow::{Context as _, Result, anyhow}; | 4 | use anyhow::{Context as _, Result, anyhow}; |
| 4 | use chrono::{DateTime, Utc}; | 5 | use chrono::{DateTime, Utc}; |
| @@ -32,15 +33,6 @@ pub enum AnthropicModelMode { · 27 unmodified lines | |||
| 32 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] | 33 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] |
| 33 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] | 34 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] |
| 34 | pub enum Model { | 35 | pub enum Model { |
| 35 | #[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")] | ||
| 36 | Claude3_5Sonnet, | ||
| 37 | #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")] | ||
| 38 | Claude3_7Sonnet, | ||
| 39 | #[serde( | ||
| 40 | rename = "claude-3-7-sonnet-thinking", | ||
| 41 | alias = "claude-3-7-sonnet-thinking-latest" | ||
| 42 | )] | ||
| 43 | Claude3_7SonnetThinking, | ||
| 44 | #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")] | 36 | #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")] |
| 45 | ClaudeOpus4, | 37 | ClaudeOpus4, |
| 46 | #[serde( | 38 | #[serde( |
| @@ -56,6 +48,15 @@ pub enum Model { · 9 unmodified lines | |||
| 56 | alias = "claude-sonnet-4-thinking-latest" | 48 | alias = "claude-sonnet-4-thinking-latest" |
| 57 | )] | 49 | )] |
| 58 | ClaudeSonnet4Thinking, | 50 | ClaudeSonnet4Thinking, |
| 51 | #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")] | ||
| 52 | Claude3_7Sonnet, | ||
| 53 | #[serde( | ||
| 54 | rename = "claude-3-7-sonnet-thinking", | ||
| 55 | alias = "claude-3-7-sonnet-thinking-latest" | ||
| 56 | )] | ||
| 57 | Claude3_7SonnetThinking, | ||
| 58 | #[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")] | ||
| 59 | Claude3_5Sonnet, | ||
| 59 | #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")] | 60 | #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")] |
| 60 | Claude3_5Haiku, | 61 | Claude3_5Haiku, |
| 61 | #[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")] | 62 | #[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")] |
| @@ -89,46 +90,66 @@ impl Model { · 27 unmodified lines | |||
| 89 | } | 90 | } |
| 90 | 91 | ||
| 91 | pub fn from_id(id: &str) -> Result<Self> { | 92 | pub fn from_id(id: &str) -> Result<Self> { |
| 93 | if id.starts_with("claude-opus-4-thinking") { | ||
| 94 | return Ok(Self::ClaudeOpus4Thinking); | ||
| 95 | } | ||
| 96 | |||
| 97 | if id.starts_with("claude-opus-4") { | ||
| 98 | return Ok(Self::ClaudeOpus4); | ||
| 99 | } | ||
| 100 | |||
| 101 | if id.starts_with("claude-sonnet-4-thinking") { | ||
| 102 | return Ok(Self::ClaudeSonnet4Thinking); | ||
| 103 | } | ||
| 104 | |||
| 105 | if id.starts_with("claude-sonnet-4") { | ||
| 106 | return Ok(Self::ClaudeSonnet4); | ||
| 107 | } | ||
| 108 | |||
| 109 | if id.starts_with("claude-3-7-sonnet-thinking") { | ||
| 110 | return Ok(Self::Claude3_7SonnetThinking); | ||
| 111 | } | ||
| 112 | |||
| 113 | if id.starts_with("claude-3-7-sonnet") { | ||
| 114 | return Ok(Self::Claude3_7Sonnet); | ||
| 115 | } | ||
| 116 | |||
| 92 | if id.starts_with("claude-3-5-sonnet") { | 117 | if id.starts_with("claude-3-5-sonnet") { |
| 93 | Ok(Self::Claude3_5Sonnet) | 118 | return Ok(Self::Claude3_5Sonnet); |
| 94 | } else if id.starts_with("claude-3-7-sonnet-thinking") { | 119 | } |
| 95 | Ok(Self::Claude3_7SonnetThinking) | 120 | |
| 96 | } else if id.starts_with("claude-3-7-sonnet") { | 121 | if id.starts_with("claude-3-5-haiku") { |
| 97 | Ok(Self::Claude3_7Sonnet) | 122 | return Ok(Self::Claude3_5Haiku); |
| 98 | } else if id.starts_with("claude-3-5-haiku") { | 123 | } |
| 99 | Ok(Self::Claude3_5Haiku) | 124 | |
| 100 | } else if id.starts_with("claude-3-opus") { | 125 | if id.starts_with("claude-3-opus") { |
| 101 | Ok(Self::Claude3Opus) | 126 | return Ok(Self::Claude3Opus); |
| 102 | } else if id.starts_with("claude-3-sonnet") { | ||
| 103 | Ok(Self::Claude3Sonnet) | ||
| 104 | } else if id.starts_with("claude-3-haiku") { | ||
| 105 | Ok(Self::Claude3Haiku) | ||
| 106 | } else if id.starts_with("claude-opus-4-thinking") { | ||
| 107 | Ok(Self::ClaudeOpus4Thinking) | ||
| 108 | } else if id.starts_with("claude-opus-4") { | ||
| 109 | Ok(Self::ClaudeOpus4) | ||
| 110 | } else if id.starts_with("claude-sonnet-4-thinking") { | ||
| 111 | Ok(Self::ClaudeSonnet4Thinking) | ||
| 112 | } else if id.starts_with("claude-sonnet-4") { | ||
| 113 | Ok(Self::ClaudeSonnet4) | ||
| 114 | } else { | ||
| 115 | anyhow::bail!("invalid model id {id}"); | ||
| 116 | } | 127 | } |
| 128 | |||
| 129 | if id.starts_with("claude-3-sonnet") { | ||
| 130 | return Ok(Self::Claude3Sonnet); | ||
| 131 | } | ||
| 132 | |||
| 133 | if id.starts_with("claude-3-haiku") { | ||
| 134 | return Ok(Self::Claude3Haiku); | ||
| 135 | } | ||
| 136 | |||
| 137 | Err(anyhow!("invalid model ID: {id}")) | ||
| 117 | } | 138 | } |
| 118 | 139 | ||
| 119 | pub fn id(&self) -> &str { | 140 | pub fn id(&self) -> &str { |
| 120 | match self { | 141 | match self { |
| 121 | Model::ClaudeOpus4 => "claude-opus-4-latest", | 142 | Self::ClaudeOpus4 => "claude-opus-4-latest", |
| 122 | Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest", | 143 | Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest", |
| 123 | Model::ClaudeSonnet4 => "claude-sonnet-4-latest", | 144 | Self::ClaudeSonnet4 => "claude-sonnet-4-latest", |
| 124 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest", | 145 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest", |
| 125 | Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest", | 146 | Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", |
| 126 | Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest", | 147 | Self::Claude3_7Sonnet => "claude-3-7-sonnet-latest", |
| 127 | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest", | 148 | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest", |
| 128 | Model::Claude3_5Haiku => "claude-3-5-haiku-latest", | 149 | Self::Claude3_5Haiku => "claude-3-5-haiku-latest", |
| 129 | Model::Claude3Opus => "claude-3-opus-latest", | 150 | Self::Claude3Opus => "claude-3-opus-latest", |
| 130 | Model::Claude3Sonnet => "claude-3-sonnet-20240229", | 151 | Self::Claude3Sonnet => "claude-3-sonnet-20240229", |
| 131 | Model::Claude3Haiku => "claude-3-haiku-20240307", | 152 | Self::Claude3Haiku => "claude-3-haiku-20240307", |
| 132 | Self::Custom { name, .. } => name, | 153 | Self::Custom { name, .. } => name, |
| 133 | } | 154 | } |
| 134 | } | 155 | } |
| @@ -136,24 +157,24 @@ impl Model { · 1 unmodified line | |||
| 136 | /// The id of the model that should be used for making API requests | 157 | /// The id of the model that should be used for making API requests |
| 137 | pub fn request_id(&self) -> &str { | 158 | pub fn request_id(&self) -> &str { |
| 138 | match self { | 159 | match self { |
| 139 | Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514", | 160 | Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514", |
| 140 | Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514", | 161 | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514", |
| 141 | Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest", | 162 | Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", |
| 142 | Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest", | 163 | Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest", |
| 143 | Model::Claude3_5Haiku => "claude-3-5-haiku-latest", | 164 | Self::Claude3_5Haiku => "claude-3-5-haiku-latest", |
| 144 | Model::Claude3Opus => "claude-3-opus-latest", | 165 | Self::Claude3Opus => "claude-3-opus-latest", |
| 145 | Model::Claude3Sonnet => "claude-3-sonnet-20240229", | 166 | Self::Claude3Sonnet => "claude-3-sonnet-20240229", |
| 146 | Model::Claude3Haiku => "claude-3-haiku-20240307", | 167 | Self::Claude3Haiku => "claude-3-haiku-20240307", |
| 147 | Self::Custom { name, .. } => name, | 168 | Self::Custom { name, .. } => name, |
| 148 | } | 169 | } |
| 149 | } | 170 | } |
| 150 | 171 | ||
| 151 | pub fn display_name(&self) -> &str { | 172 | pub fn display_name(&self) -> &str { |
| 152 | match self { | 173 | match self { |
| 153 | Model::ClaudeOpus4 => "Claude Opus 4", | 174 | Self::ClaudeOpus4 => "Claude Opus 4", |
| 154 | Model::ClaudeOpus4Thinking => "Claude Opus 4 Thinking", | 175 | Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking", |
| 155 | Model::ClaudeSonnet4 => "Claude Sonnet 4", | 176 | Self::ClaudeSonnet4 => "Claude Sonnet 4", |
| 156 | Model::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking", | 177 | Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking", |
| 157 | Self::Claude3_7Sonnet => "Claude 3.7 Sonnet", | 178 | Self::Claude3_7Sonnet => "Claude 3.7 Sonnet", |
| 158 | Self::Claude3_5Sonnet => "Claude 3.5 Sonnet", | 179 | Self::Claude3_5Sonnet => "Claude 3.5 Sonnet", |
| 159 | Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking", | 180 | Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking", |
| @@ -209,15 +230,15 @@ impl Model { · 49 unmodified lines | |||
| 209 | 230 | ||
| 210 | pub fn max_output_tokens(&self) -> u32 { | 231 | pub fn max_output_tokens(&self) -> u32 { |
| 211 | match self { | 232 | match self { |
| 212 | Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096, | 233 | Self::ClaudeOpus4 |
| 213 | Self::Claude3_5Sonnet | ||
| 214 | | Self::Claude3_7Sonnet | ||
| 215 | | Self::Claude3_7SonnetThinking | ||
| 216 | | Self::Claude3_5Haiku | ||
| 217 | | Self::ClaudeOpus4 | ||
| 218 | | Self::ClaudeOpus4Thinking | 234 | | Self::ClaudeOpus4Thinking |
| 219 | | Self::ClaudeSonnet4 | 235 | | Self::ClaudeSonnet4 |
| 220 | | Self::ClaudeSonnet4Thinking => 8_192, | 236 | | Self::ClaudeSonnet4Thinking |
| 237 | | Self::Claude3_5Sonnet | ||
| 238 | | Self::Claude3_7Sonnet | ||
| 239 | | Self::Claude3_7SonnetThinking | ||
| 240 | | Self::Claude3_5Haiku => 8_192, | ||
| 241 | Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096, | ||
| 221 | Self::Custom { | 242 | Self::Custom { |
| 222 | max_output_tokens, .. | 243 | max_output_tokens, .. |
| 223 | } => max_output_tokens.unwrap_or(4_096), | 244 | } => max_output_tokens.unwrap_or(4_096), |
| @@ -246,17 +267,17 @@ impl Model { · 22 unmodified lines | |||
| 246 | 267 | ||
| 247 | pub fn mode(&self) -> AnthropicModelMode { | 268 | pub fn mode(&self) -> AnthropicModelMode { |
| 248 | match self { | 269 | match self { |
| 249 | Self::Claude3_5Sonnet | 270 | Self::ClaudeOpus4 |
| 271 | | Self::ClaudeSonnet4 | ||
| 272 | | Self::Claude3_5Sonnet | ||
| 250 | | Self::Claude3_7Sonnet | 273 | | Self::Claude3_7Sonnet |
| 251 | | Self::Claude3_5Haiku | 274 | | Self::Claude3_5Haiku |
| 252 | | Self::ClaudeOpus4 | ||
| 253 | | Self::ClaudeSonnet4 | ||
| 254 | | Self::Claude3Opus | 275 | | Self::Claude3Opus |
| 255 | | Self::Claude3Sonnet | 276 | | Self::Claude3Sonnet |
| 256 | | Self::Claude3Haiku => AnthropicModelMode::Default, | 277 | | Self::Claude3Haiku => AnthropicModelMode::Default, |
| 257 | Self::Claude3_7SonnetThinking | 278 | Self::ClaudeOpus4Thinking |
| 258 | | Self::ClaudeOpus4Thinking | 279 | | Self::ClaudeSonnet4Thinking |
| 259 | | Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking { | 280 | | Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking { |
| 260 | budget_tokens: Some(4_096), | 281 | budget_tokens: Some(4_096), |
| 261 | }, | 282 | }, |
| 262 | Self::Custom { mode, .. } => mode.clone(), | 283 | Self::Custom { mode, .. } => mode.clone(), |
| @@ -267,7 +288,7 @@ impl Model { · 4 unmodified lines | |||
| 267 | 288 | ||
| 268 | pub fn beta_headers(&self) -> String { | 289 | pub fn beta_headers(&self) -> String { |
| 269 | let mut headers = Self::DEFAULT_BETA_HEADERS | 290 | let mut headers = Self::DEFAULT_BETA_HEADERS |
| 270 | .into_iter() | 291 | .iter() |
| 271 | .map(|header| header.to_string()) | 292 | .map(|header| header.to_string()) |
| 272 | .collect::<Vec<_>>(); | 293 | .collect::<Vec<_>>(); |
| 273 | 294 | ||
| @@ -406,6 +427,7 @@ impl RateLimit { · 132 unmodified lines | |||
| 406 | /// <https://docs.anthropic.com/en/api/rate-limits#response-headers> | 427 | /// <https://docs.anthropic.com/en/api/rate-limits#response-headers> |
| 407 | #[derive(Debug)] | 428 | #[derive(Debug)] |
| 408 | pub struct RateLimitInfo { | 429 | pub struct RateLimitInfo { |
| 430 | pub retry_after: Option<Duration>, | ||
| 409 | pub requests: Option<RateLimit>, | 431 | pub requests: Option<RateLimit>, |
| 410 | pub tokens: Option<RateLimit>, | 432 | pub tokens: Option<RateLimit>, |
| 411 | pub input_tokens: Option<RateLimit>, | 433 | pub input_tokens: Option<RateLimit>, |
| @@ -417,10 +439,11 @@ impl RateLimitInfo { · 5 unmodified lines | |||
| 417 | // Check if any rate limit headers exist | 439 | // Check if any rate limit headers exist |
| 418 | let has_rate_limit_headers = headers | 440 | let has_rate_limit_headers = headers |
| 419 | .keys() | 441 | .keys() |
| 420 | .any(|k| k.as_str().starts_with("anthropic-ratelimit-")); | 442 | .any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-")); |
| 421 | 443 | ||
| 422 | if !has_rate_limit_headers { | 444 | if !has_rate_limit_headers { |
| 423 | return Self { | 445 | return Self { |
| 446 | retry_after: None, | ||
| 424 | requests: None, | 447 | requests: None, |
| 425 | tokens: None, | 448 | tokens: None, |
| 426 | input_tokens: None, | 449 | input_tokens: None, |
| @@ -429,6 +452,11 @@ impl RateLimitInfo { · 2 unmodified lines | |||
| 429 | } | 452 | } |
| 430 | 453 | ||
| 431 | Self { | 454 | Self { |
| 455 | retry_after: headers | ||
| 456 | .get("retry-after") | ||
| 457 | .and_then(|v| v.to_str().ok()) | ||
| 458 | .and_then(|v| v.parse::<u64>().ok()) | ||
| 459 | .map(Duration::from_secs), | ||
| 432 | requests: RateLimit::from_headers("requests", headers).ok(), | 460 | requests: RateLimit::from_headers("requests", headers).ok(), |
| 433 | tokens: RateLimit::from_headers("tokens", headers).ok(), | 461 | tokens: RateLimit::from_headers("tokens", headers).ok(), |
| 434 | input_tokens: RateLimit::from_headers("input-tokens", headers).ok(), | 462 | input_tokens: RateLimit::from_headers("input-tokens", headers).ok(), |
| @@ -481,8 +509,8 @@ pub async fn stream_completion_with_rate_limit_info( · 46 unmodified lines | |||
| 481 | .send(request) | 509 | .send(request) |
| 482 | .await | 510 | .await |
| 483 | .context("failed to send request to Anthropic")?; | 511 | .context("failed to send request to Anthropic")?; |
| 512 | let rate_limits = RateLimitInfo::from_headers(response.headers()); | ||
| 484 | if response.status().is_success() { | 513 | if response.status().is_success() { |
| 485 | let rate_limits = RateLimitInfo::from_headers(response.headers()); | ||
| 486 | let reader = BufReader::new(response.into_body()); | 514 | let reader = BufReader::new(response.into_body()); |
| 487 | let stream = reader | 515 | let stream = reader |
| 488 | .lines() | 516 | .lines() |
| @@ -500,6 +528,8 @@ pub async fn stream_completion_with_rate_limit_info( · 11 unmodified lines | |||
| 500 | }) | 528 | }) |
| 501 | .boxed(); | 529 | .boxed(); |
| 502 | Ok((stream, Some(rate_limits))) | 530 | Ok((stream, Some(rate_limits))) |
| 531 | } else if let Some(retry_after) = rate_limits.retry_after { | ||
| 532 | Err(AnthropicError::RateLimit(retry_after)) | ||
| 503 | } else { | 533 | } else { |
| 504 | let mut body = Vec::new(); | 534 | let mut body = Vec::new(); |
| 505 | response | 535 | response |
| @@ -769,6 +799,8 @@ pub struct MessageDelta { · 263 unmodified lines | |||
| 769 | 799 | ||
| 770 | #[derive(Error, Debug)] | 800 | #[derive(Error, Debug)] |
| 771 | pub enum AnthropicError { | 801 | pub enum AnthropicError { |
| 802 | #[error("rate limit exceeded, retry after {0:?}")] | ||
| 803 | RateLimit(Duration), | ||
| 772 | #[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)] | 804 | #[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)] |
| 773 | ApiError(ApiError), | 805 | ApiError(ApiError), |
| 774 | #[error("{0}")] | 806 | #[error("{0}")] |
| @@ -15,7 +15,6 @@ path = "src/askpass.rs" · 14 unmodified lines | |||
| 15 | anyhow.workspace = true | 15 | anyhow.workspace = true |
| 16 | futures.workspace = true | 16 | futures.workspace = true |
| 17 | gpui.workspace = true | 17 | gpui.workspace = true |
| 18 | shlex.workspace = true | ||
| 19 | smol.workspace = true | 18 | smol.workspace = true |
| 20 | tempfile.workspace = true | 19 | tempfile.workspace = true |
| 21 | util.workspace = true | 20 | util.workspace = true |
| @@ -16,6 +16,8 @@ use smol::fs; · 15 unmodified lines | |||
| 16 | use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener}; | 16 | use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener}; |
| 17 | #[cfg(unix)] | 17 | #[cfg(unix)] |
| 18 | use util::ResultExt as _; | 18 | use util::ResultExt as _; |
| 19 | #[cfg(unix)] | ||
| 20 | use util::get_shell_safe_zed_path; | ||
| 19 | 21 | ||
| 20 | #[derive(PartialEq, Eq)] | 22 | #[derive(PartialEq, Eq)] |
| 21 | pub enum AskPassResult { | 23 | pub enum AskPassResult { |
| @@ -160,38 +162,6 @@ impl AskPassSession { · 138 unmodified lines | |||
| 160 | } | 162 | } |
| 161 | } | 163 | } |
| 162 | 164 | ||
| 163 | #[cfg(unix)] | ||
| 164 | fn get_shell_safe_zed_path() -> anyhow::Result<String> { | ||
| 165 | let zed_path = std::env::current_exe() | ||
| 166 | .context("Failed to determine current executable path for use in askpass")? | ||
| 167 | .to_string_lossy() | ||
| 168 | // see https://github.com/rust-lang/rust/issues/69343 | ||
| 169 | .trim_end_matches(" (deleted)") | ||
| 170 | .to_string(); | ||
| 171 | |||
| 172 | // NOTE: this was previously enabled, however, it caused errors when it shouldn't have | ||
| 173 | // (see https://github.com/zed-industries/zed/issues/29819) | ||
| 174 | // The zed path failing to execute within the askpass script results in very vague ssh | ||
| 175 | // authentication failed errors, so this was done to try and surface a better error | ||
| 176 | // | ||
| 177 | // use std::os::unix::fs::MetadataExt; | ||
| 178 | // let metadata = std::fs::metadata(&zed_path) | ||
| 179 | // .context("Failed to check metadata of Zed executable path for use in askpass")?; | ||
| 180 | // let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0; | ||
| 181 | // anyhow::ensure!( | ||
| 182 | // is_executable, | ||
| 183 | // "Failed to verify Zed executable path for use in askpass" | ||
| 184 | // ); | ||
| 185 | |||
| 186 | // As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible | ||
| 187 | // but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other | ||
| 188 | // errors are introduced in the future :( | ||
| 189 | let zed_path_escaped = shlex::try_quote(&zed_path) | ||
| 190 | .context("Failed to shell-escape Zed executable path for use in askpass")?; | ||
| 191 | |||
| 192 | return Ok(zed_path_escaped.to_string()); | ||
| 193 | } | ||
| 194 | |||
| 195 | /// The main function for when Zed is running in netcat mode for use in askpass. | 165 | /// The main function for when Zed is running in netcat mode for use in askpass. |
| 196 | /// Called from both the remote server binary and the zed binary in their respective main functions. | 166 | /// Called from both the remote server binary and the zed binary in their respective main functions. |
| 197 | #[cfg(unix)] | 167 | #[cfg(unix)] |
| @@ -39,7 +39,7 @@ use language::{ · 38 unmodified lines | |||
| 39 | language_settings::{SoftWrap, all_language_settings}, | 39 | language_settings::{SoftWrap, all_language_settings}, |
| 40 | }; | 40 | }; |
| 41 | use language_model::{ | 41 | use language_model::{ |
| 42 | LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry, | 42 | ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry, |
| 43 | Role, | 43 | Role, |
| 44 | }; | 44 | }; |
| 45 | use multi_buffer::MultiBufferRow; | 45 | use multi_buffer::MultiBufferRow; |
| @@ -1887,6 +1887,8 @@ impl ContextEditor { · 1841 unmodified lines | |||
| 1887 | // value to not show the nudge. | 1887 | // value to not show the nudge. |
| 1888 | let nudge = Some(false); | 1888 | let nudge = Some(false); |
| 1889 | 1889 | ||
| 1890 | let model_registry = LanguageModelRegistry::read_global(cx); | ||
| 1891 | |||
| 1890 | if nudge.map_or(false, |value| value) { | 1892 | if nudge.map_or(false, |value| value) { |
| 1891 | Some( | 1893 | Some( |
| 1892 | h_flex() | 1894 | h_flex() |
| @@ -1935,14 +1937,9 @@ impl ContextEditor { · 42 unmodified lines | |||
| 1935 | ) | 1937 | ) |
| 1936 | .into_any_element(), | 1938 | .into_any_element(), |
| 1937 | ) | 1939 | ) |
| 1938 | } else if let Some(configuration_error) = configuration_error(cx) { | 1940 | } else if let Some(configuration_error) = |
| 1939 | let label = match configuration_error { | 1941 | model_registry.configuration_error(model_registry.default_model(), cx) |
| 1940 | ConfigurationError::NoProvider => "No LLM provider selected.", | 1942 | { |
| 1941 | ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.", | ||
| 1942 | ConfigurationError::ProviderPendingTermsAcceptance(_) => { | ||
| 1943 | "LLM provider requires accepting the Terms of Service." | ||
| 1944 | } | ||
| 1945 | }; | ||
| 1946 | Some( | 1943 | Some( |
| 1947 | h_flex() | 1944 | h_flex() |
| 1948 | .px_3() | 1945 | .px_3() |
| @@ -1959,7 +1956,7 @@ impl ContextEditor { · 10 unmodified lines | |||
| 1959 | .size(IconSize::Small) | 1956 | .size(IconSize::Small) |
| 1960 | .color(Color::Warning), | 1957 | .color(Color::Warning), |
| 1961 | ) | 1958 | ) |
| 1962 | .child(Label::new(label)), | 1959 | .child(Label::new(configuration_error.to_string())), |
| 1963 | ) | 1960 | ) |
| 1964 | .child( | 1961 | .child( |
| 1965 | Button::new("open-configuration", "Configure Providers") | 1962 | Button::new("open-configuration", "Configure Providers") |
| @@ -2034,14 +2031,19 @@ impl ContextEditor { · 68 unmodified lines | |||
| 2034 | /// Will return false if the selected provided has a configuration error or | 2031 | /// Will return false if the selected provided has a configuration error or |
| 2035 | /// if the user has not accepted the terms of service for this provider. | 2032 | /// if the user has not accepted the terms of service for this provider. |
| 2036 | fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool { | 2033 | fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool { |
| 2037 | let model = LanguageModelRegistry::read_global(cx).default_model(); | 2034 | let model_registry = LanguageModelRegistry::read_global(cx); |
| 2035 | let Some(configuration_error) = | ||
| 2036 | model_registry.configuration_error(model_registry.default_model(), cx) | ||
| 2037 | else { | ||
| 2038 | return false; | ||
| 2039 | }; | ||
| 2038 | 2040 | ||
| 2039 | let has_configuration_error = configuration_error(cx).is_some(); | 2041 | match configuration_error { |
| 2040 | let needs_to_accept_terms = self.show_accept_terms | 2042 | ConfigurationError::NoProvider |
| 2041 | && model | 2043 | | ConfigurationError::ModelNotFound |
| 2042 | .as_ref() | 2044 | | ConfigurationError::ProviderNotAuthenticated(_) => true, |
| 2043 | .map_or(false, |model| model.provider.must_accept_terms(cx)); | 2045 | ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms, |
| 2044 | has_configuration_error || needs_to_accept_terms | 2046 | } |
| 2045 | } | 2047 | } |
| 2046 | 2048 | ||
| 2047 | fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement { | 2049 | fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement { |
| @@ -3180,33 +3182,6 @@ fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> { · 1132 unmodified lines | |||
| 3180 | } | 3182 | } |
| 3181 | } | 3183 | } |
| 3182 | 3184 | ||
| 3183 | pub enum ConfigurationError { | ||
| 3184 | NoProvider, | ||
| 3185 | ProviderNotAuthenticated, | ||
| 3186 | ProviderPendingTermsAcceptance(Arc<dyn LanguageModelProvider>), | ||
| 3187 | } | ||
| 3188 | |||
| 3189 | fn configuration_error(cx: &App) -> Option<ConfigurationError> { | ||
| 3190 | let model = LanguageModelRegistry::read_global(cx).default_model(); | ||
| 3191 | let is_authenticated = model | ||
| 3192 | .as_ref() | ||
| 3193 | .map_or(false, |model| model.provider.is_authenticated(cx)); | ||
| 3194 | |||
| 3195 | if model.is_some() && is_authenticated { | ||
| 3196 | return None; | ||
| 3197 | } | ||
| 3198 | |||
| 3199 | if model.is_none() { | ||
| 3200 | return Some(ConfigurationError::NoProvider); | ||
| 3201 | } | ||
| 3202 | |||
| 3203 | if !is_authenticated { | ||
| 3204 | return Some(ConfigurationError::ProviderNotAuthenticated); | ||
| 3205 | } | ||
| 3206 | |||
| 3207 | None | ||
| 3208 | } | ||
| 3209 | |||
| 3210 | pub fn humanize_token_count(count: usize) -> String { | 3185 | pub fn humanize_token_count(count: usize) -> String { |
| 3211 | match count { | 3186 | match count { |
| 3212 | 0..=999 => count.to_string(), | 3187 | 0..=999 => count.to_string(), |
| @@ -869,7 +869,7 @@ impl ContextStore { · 868 unmodified lines | |||
| 869 | 869 | ||
| 870 | if protocol.capable(context_server::protocol::ServerCapability::Prompts) { | 870 | if protocol.capable(context_server::protocol::ServerCapability::Prompts) { |
| 871 | if let Some(response) = protocol | 871 | if let Some(response) = protocol |
| 872 | .request::<context_server::types::request::PromptsList>(()) | 872 | .request::<context_server::types::requests::PromptsList>(()) |
| 873 | .await | 873 | .await |
| 874 | .log_err() | 874 | .log_err() |
| 875 | { | 875 | { |
| @@ -682,11 +682,12 @@ mod tests { · 681 unmodified lines | |||
| 682 | _: &AsyncApp, | 682 | _: &AsyncApp, |
| 683 | ) -> BoxFuture< | 683 | ) -> BoxFuture< |
| 684 | 'static, | 684 | 'static, |
| 685 | http_client::Result< | 685 | Result< |
| 686 | BoxStream< | 686 | BoxStream< |
| 687 | 'static, | 687 | 'static, |
| 688 | http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>, | 688 | Result<LanguageModelCompletionEvent, LanguageModelCompletionError>, |
| 689 | >, | 689 | >, |
| 690 | LanguageModelCompletionError, | ||
| 690 | >, | 691 | >, |
| 691 | > { | 692 | > { |
| 692 | unimplemented!() | 693 | unimplemented!() |
| @@ -87,7 +87,7 @@ impl SlashCommand for ContextServerSlashCommand { · 86 unmodified lines | |||
| 87 | let protocol = server.client().context("Context server not initialized")?; | 87 | let protocol = server.client().context("Context server not initialized")?; |
| 88 | 88 | ||
| 89 | let response = protocol | 89 | let response = protocol |
| 90 | .request::<context_server::types::request::CompletionComplete>( | 90 | .request::<context_server::types::requests::CompletionComplete>( |
| 91 | context_server::types::CompletionCompleteParams { | 91 | context_server::types::CompletionCompleteParams { |
| 92 | reference: context_server::types::CompletionReference::Prompt( | 92 | reference: context_server::types::CompletionReference::Prompt( |
| 93 | context_server::types::PromptReference { | 93 | context_server::types::PromptReference { |
| @@ -145,7 +145,7 @@ impl SlashCommand for ContextServerSlashCommand { · 51 unmodified lines | |||
| 145 | cx.foreground_executor().spawn(async move { | 145 | cx.foreground_executor().spawn(async move { |
| 146 | let protocol = server.client().context("Context server not initialized")?; | 146 | let protocol = server.client().context("Context server not initialized")?; |
| 147 | let response = protocol | 147 | let response = protocol |
| 148 | .request::<context_server::types::request::PromptsGet>( | 148 | .request::<context_server::types::requests::PromptsGet>( |
| 149 | context_server::types::PromptsGetParams { | 149 | context_server::types::PromptsGetParams { |
| 150 | name: prompt_name.clone(), | 150 | name: prompt_name.clone(), |
| 151 | arguments: Some(prompt_args), | 151 | arguments: Some(prompt_args), |
| @@ -582,7 +582,7 @@ mod test { · 581 unmodified lines | |||
| 582 | use serde_json::json; | 582 | use serde_json::json; |
| 583 | use settings::SettingsStore; | 583 | use settings::SettingsStore; |
| 584 | use smol::stream::StreamExt; | 584 | use smol::stream::StreamExt; |
| 585 | use util::{path, separator}; | 585 | use util::path; |
| 586 | 586 | ||
| 587 | use super::collect_files; | 587 | use super::collect_files; |
| 588 | 588 | ||
| @@ -627,7 +627,7 @@ mod test { · 38 unmodified lines | |||
| 627 | .await | 627 | .await |
| 628 | .unwrap(); | 628 | .unwrap(); |
| 629 | 629 | ||
| 630 | assert!(result_1.text.starts_with(separator!("root/dir"))); | 630 | assert!(result_1.text.starts_with(path!("root/dir"))); |
| 631 | // 4 files + 2 directories | 631 | // 4 files + 2 directories |
| 632 | assert_eq!(result_1.sections.len(), 6); | 632 | assert_eq!(result_1.sections.len(), 6); |
| 633 | 633 | ||
| @@ -643,7 +643,7 @@ mod test { · 9 unmodified lines | |||
| 643 | cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed()); | 643 | cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed()); |
| 644 | let result = SlashCommandOutput::from_event_stream(result).await.unwrap(); | 644 | let result = SlashCommandOutput::from_event_stream(result).await.unwrap(); |
| 645 | 645 | ||
| 646 | assert!(result.text.starts_with(separator!("root/dir"))); | 646 | assert!(result.text.starts_with(path!("root/dir"))); |
| 647 | // 5 files + 2 directories | 647 | // 5 files + 2 directories |
| 648 | assert_eq!(result.sections.len(), 7); | 648 | assert_eq!(result.sections.len(), 7); |
| 649 | 649 | ||
| @@ -691,24 +691,20 @@ mod test { · 41 unmodified lines | |||
| 691 | .unwrap(); | 691 | .unwrap(); |
| 692 | 692 | ||
| 693 | // Sanity check | 693 | // Sanity check |
| 694 | assert!(result.text.starts_with(separator!("zed/assets/themes\n"))); | 694 | assert!(result.text.starts_with(path!("zed/assets/themes\n"))); |
| 695 | assert_eq!(result.sections.len(), 7); | 695 | assert_eq!(result.sections.len(), 7); |
| 696 | 696 | ||
| 697 | // Ensure that full file paths are included in the real output | 697 | // Ensure that full file paths are included in the real output |
| 698 | assert!( | 698 | assert!( |
| 699 | result | 699 | result |
| 700 | .text | 700 | .text |
| 701 | .contains(separator!("zed/assets/themes/andromeda/LICENSE")) | 701 | .contains(path!("zed/assets/themes/andromeda/LICENSE")) |
| 702 | ); | 702 | ); |
| 703 | assert!(result.text.contains(path!("zed/assets/themes/ayu/LICENSE"))); | ||
| 703 | assert!( | 704 | assert!( |
| 704 | result | 705 | result |
| 705 | .text | 706 | .text |
| 706 | .contains(separator!("zed/assets/themes/ayu/LICENSE")) | 707 | .contains(path!("zed/assets/themes/summercamp/LICENSE")) |
| 707 | ); | ||
| 708 | assert!( | ||
| 709 | result | ||
| 710 | .text | ||
| 711 | .contains(separator!("zed/assets/themes/summercamp/LICENSE")) | ||
| 712 | ); | 708 | ); |
| 713 | 709 | ||
| 714 | assert_eq!(result.sections[5].label, "summercamp"); | 710 | assert_eq!(result.sections[5].label, "summercamp"); |
| @@ -716,17 +712,17 @@ mod test { · 1 unmodified line | |||
| 716 | // Ensure that things are in descending order, with properly relativized paths | 712 | // Ensure that things are in descending order, with properly relativized paths |
| 717 | assert_eq!( | 713 | assert_eq!( |
| 718 | result.sections[0].label, | 714 | result.sections[0].label, |
| 719 | separator!("zed/assets/themes/andromeda/LICENSE") | 715 | path!("zed/assets/themes/andromeda/LICENSE") |
| 720 | ); | 716 | ); |
| 721 | assert_eq!(result.sections[1].label, "andromeda"); | 717 | assert_eq!(result.sections[1].label, "andromeda"); |
| 722 | assert_eq!( | 718 | assert_eq!( |
| 723 | result.sections[2].label, | 719 | result.sections[2].label, |
| 724 | separator!("zed/assets/themes/ayu/LICENSE") | 720 | path!("zed/assets/themes/ayu/LICENSE") |
| 725 | ); | 721 | ); |
| 726 | assert_eq!(result.sections[3].label, "ayu"); | 722 | assert_eq!(result.sections[3].label, "ayu"); |
| 727 | assert_eq!( | 723 | assert_eq!( |
| 728 | result.sections[4].label, | 724 | result.sections[4].label, |
| 729 | separator!("zed/assets/themes/summercamp/LICENSE") | 725 | path!("zed/assets/themes/summercamp/LICENSE") |
| 730 | ); | 726 | ); |
| 731 | 727 | ||
| 732 | // Ensure that the project lasts until after the last await | 728 | // Ensure that the project lasts until after the last await |
| @@ -767,31 +763,28 @@ mod test { · 34 unmodified lines | |||
| 767 | .await | 763 | .await |
| 768 | .unwrap(); | 764 | .unwrap(); |
| 769 | 765 | ||
| 770 | assert!(result.text.starts_with(separator!("zed/assets/themes\n"))); | 766 | assert!(result.text.starts_with(path!("zed/assets/themes\n"))); |
| 771 | assert_eq!( | 767 | assert_eq!(result.sections[0].label, path!("zed/assets/themes/LICENSE")); |
| 772 | result.sections[0].label, | ||
| 773 | separator!("zed/assets/themes/LICENSE") | ||
| 774 | ); | ||
| 775 | assert_eq!( | 768 | assert_eq!( |
| 776 | result.sections[1].label, | 769 | result.sections[1].label, |
| 777 | separator!("zed/assets/themes/summercamp/LICENSE") | 770 | path!("zed/assets/themes/summercamp/LICENSE") |
| 778 | ); | 771 | ); |
| 779 | assert_eq!( | 772 | assert_eq!( |
| 780 | result.sections[2].label, | 773 | result.sections[2].label, |
| 781 | separator!("zed/assets/themes/summercamp/subdir/LICENSE") | 774 | path!("zed/assets/themes/summercamp/subdir/LICENSE") |
| 782 | ); | 775 | ); |
| 783 | assert_eq!( | 776 | assert_eq!( |
| 784 | result.sections[3].label, | 777 | result.sections[3].label, |
| 785 | separator!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE") | 778 | path!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE") |
| 786 | ); | 779 | ); |
| 787 | assert_eq!(result.sections[4].label, "subsubdir"); | 780 | assert_eq!(result.sections[4].label, "subsubdir"); |
| 788 | assert_eq!(result.sections[5].label, "subdir"); | 781 | assert_eq!(result.sections[5].label, "subdir"); |
| 789 | assert_eq!(result.sections[6].label, "summercamp"); | 782 | assert_eq!(result.sections[6].label, "summercamp"); |
| 790 | assert_eq!(result.sections[7].label, separator!("zed/assets/themes")); | 783 | assert_eq!(result.sections[7].label, path!("zed/assets/themes")); |
| 791 | 784 | ||
| 792 | assert_eq!( | 785 | assert_eq!( |
| 793 | result.text, | 786 | result.text, |
| 794 | separator!( | 787 | path!( |
| 795 | "zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n" | 788 | "zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n" |
| 796 | ) | 789 | ) |
| 797 | ); | 790 | ); |
| @@ -80,6 +80,7 @@ rand.workspace = true · 79 unmodified lines | |||
| 80 | pretty_assertions.workspace = true | 80 | pretty_assertions.workspace = true |
| 81 | reqwest_client.workspace = true | 81 | reqwest_client.workspace = true |
| 82 | settings = { workspace = true, features = ["test-support"] } | 82 | settings = { workspace = true, features = ["test-support"] } |
| 83 | smol.workspace = true | ||
| 83 | task = { workspace = true, features = ["test-support"]} | 84 | task = { workspace = true, features = ["test-support"]} |
| 84 | tempfile.workspace = true | 85 | tempfile.workspace = true |
| 85 | theme.workspace = true | 86 | theme.workspace = true |
| @@ -286,7 +286,13 @@ impl EditAgent { · 285 unmodified lines | |||
| 286 | _ => { | 286 | _ => { |
| 287 | let ranges = resolved_old_text | 287 | let ranges = resolved_old_text |
| 288 | .into_iter() | 288 | .into_iter() |
| 289 | .map(|text| text.range) | 289 | .map(|text| { |
| 290 | let start_line = | ||
| 291 | (snapshot.offset_to_point(text.range.start).row + 1) as usize; | ||
| 292 | let end_line = | ||
| 293 | (snapshot.offset_to_point(text.range.end).row + 1) as usize; | ||
| 294 | start_line..end_line | ||
| 295 | }) | ||
| 290 | .collect(); | 296 | .collect(); |
| 291 | output_events | 297 | output_events |
| 292 | .unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges)) | 298 | .unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges)) |
| @@ -429,25 +435,25 @@ impl EditAgent { · 136 unmodified lines | |||
| 429 | let task = cx.background_spawn(async move { | 435 | let task = cx.background_spawn(async move { |
| 430 | let mut matcher = StreamingFuzzyMatcher::new(snapshot); | 436 | let mut matcher = StreamingFuzzyMatcher::new(snapshot); |
| 431 | while let Some(edit_event) = edit_events.next().await { | 437 | while let Some(edit_event) = edit_events.next().await { |
| 432 | let EditParserEvent::OldTextChunk { chunk, done } = edit_event? else { | 438 | let EditParserEvent::OldTextChunk { |
| 439 | chunk, | ||
| 440 | done, | ||
| 441 | line_hint, | ||
| 442 | } = edit_event? | ||
| 443 | else { | ||
| 433 | break; | 444 | break; |
| 434 | }; | 445 | }; |
| 435 | 446 | ||
| 436 | old_range_tx.send(matcher.push(&chunk))?; | 447 | old_range_tx.send(matcher.push(&chunk, line_hint))?; |
| 437 | if done { | 448 | if done { |
| 438 | break; | 449 | break; |
| 439 | } | 450 | } |
| 440 | } | 451 | } |
| 441 | 452 | ||
| 442 | let matches = matcher.finish(); | 453 | let matches = matcher.finish(); |
| 454 | let best_match = matcher.select_best_match(); | ||
| 443 | 455 | ||
| 444 | let old_range = if matches.len() == 1 { | 456 | old_range_tx.send(best_match.clone())?; |
| 445 | matches.first() | ||
| 446 | } else { | ||
| 447 | // No matches or multiple ambiguous matches | ||
| 448 | None | ||
| 449 | }; | ||
| 450 | old_range_tx.send(old_range.cloned())?; | ||
| 451 | 457 | ||
| 452 | let indent = LineIndent::from_iter( | 458 | let indent = LineIndent::from_iter( |
| 453 | matcher | 459 | matcher |
| @@ -456,10 +462,18 @@ impl EditAgent { · 2 unmodified lines | |||
| 456 | .unwrap_or(&String::new()) | 462 | .unwrap_or(&String::new()) |
| 457 | .chars(), | 463 | .chars(), |
| 458 | ); | 464 | ); |
| 459 | let resolved_old_texts = matches | 465 | |
| 460 | .into_iter() | 466 | let resolved_old_texts = if let Some(best_match) = best_match { |
| 461 | .map(|range| ResolvedOldText { range, indent }) | 467 | vec![ResolvedOldText { |
| 462 | .collect::<Vec<_>>(); | 468 | range: best_match, |
| 469 | indent, | ||
| 470 | }] | ||
| 471 | } else { | ||
| 472 | matches | ||
| 473 | .into_iter() | ||
| 474 | .map(|range| ResolvedOldText { range, indent }) | ||
| 475 | .collect::<Vec<_>>() | ||
| 476 | }; | ||
| 463 | 477 | ||
| 464 | Ok((edit_events, resolved_old_texts)) | 478 | Ok((edit_events, resolved_old_texts)) |
| 465 | }); | 479 | }); |
| @@ -1374,10 +1388,12 @@ mod tests { · 908 unmodified lines | |||
| 1374 | &agent, | 1388 | &agent, |
| 1375 | indoc! {" | 1389 | indoc! {" |
| 1376 | <old_text> | 1390 | <old_text> |
| 1377 | return 42; | 1391 | return 42; |
| 1392 | } | ||
| 1378 | </old_text> | 1393 | </old_text> |
| 1379 | <new_text> | 1394 | <new_text> |
| 1380 | return 100; | 1395 | return 100; |
| 1396 | } | ||
| 1381 | </new_text> | 1397 | </new_text> |
| 1382 | "}, | 1398 | "}, |
| 1383 | &mut rng, | 1399 | &mut rng, |
| @@ -1407,7 +1423,7 @@ mod tests { · 23 unmodified lines | |||
| 1407 | 1423 | ||
| 1408 | // And AmbiguousEditRange even should be emitted | 1424 | // And AmbiguousEditRange even should be emitted |
| 1409 | let events = drain_events(&mut events); | 1425 | let events = drain_events(&mut events); |
| 1410 | let ambiguous_ranges = vec![17..31, 52..66, 87..101]; | 1426 | let ambiguous_ranges = vec![2..3, 6..7, 10..11]; |
| 1411 | assert!( | 1427 | assert!( |
| 1412 | events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)), | 1428 | events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)), |
| 1413 | "Should emit AmbiguousEditRange for non-unique text" | 1429 | "Should emit AmbiguousEditRange for non-unique text" |
| @@ -1,4 +1,5 @@ | |||
| 1 | use derive_more::{Add, AddAssign}; | 1 | use derive_more::{Add, AddAssign}; |
| 2 | use regex::Regex; | ||
| 2 | use schemars::JsonSchema; | 3 | use schemars::JsonSchema; |
| 3 | use serde::{Deserialize, Serialize}; | 4 | use serde::{Deserialize, Serialize}; |
| 4 | use smallvec::SmallVec; | 5 | use smallvec::SmallVec; |
| @@ -11,8 +12,15 @@ const END_TAGS: [&str; 3] = [OLD_TEXT_END_TAG, NEW_TEXT_END_TAG, EDITS_END_TAG]; · 6 unmodified lines | |||
| 11 | 12 | ||
| 12 | #[derive(Debug)] | 13 | #[derive(Debug)] |
| 13 | pub enum EditParserEvent { | 14 | pub enum EditParserEvent { |
| 14 | OldTextChunk { chunk: String, done: bool }, | 15 | OldTextChunk { |
| 15 | NewTextChunk { chunk: String, done: bool }, | 16 | chunk: String, |
| 17 | done: bool, | ||
| 18 | line_hint: Option<u32>, | ||
| 19 | }, | ||
| 20 | NewTextChunk { | ||
| 21 | chunk: String, | ||
| 22 | done: bool, | ||
| 23 | }, | ||
| 16 | } | 24 | } |
| 17 | 25 | ||
| 18 | #[derive( | 26 | #[derive( |
| @@ -33,7 +41,7 @@ pub struct EditParser { · 14 unmodified lines | |||
| 33 | #[derive(Debug, PartialEq)] | 41 | #[derive(Debug, PartialEq)] |
| 34 | enum EditParserState { | 42 | enum EditParserState { |
| 35 | Pending, | 43 | Pending, |
| 36 | WithinOldText { start: bool }, | 44 | WithinOldText { start: bool, line_hint: Option<u32> }, |
| 37 | AfterOldText, | 45 | AfterOldText, |
| 38 | WithinNewText { start: bool }, | 46 | WithinNewText { start: bool }, |
| 39 | } | 47 | } |
| @@ -54,14 +62,24 @@ impl EditParser { · 14 unmodified lines | |||
| 54 | loop { | 62 | loop { |
| 55 | match &mut self.state { | 63 | match &mut self.state { |
| 56 | EditParserState::Pending => { | 64 | EditParserState::Pending => { |
| 57 | if let Some(start) = self.buffer.find("<old_text>") { | 65 | if let Some(start) = self.buffer.find("<old_text") { |
| 58 | self.buffer.drain(..start + "<old_text>".len()); | 66 | if let Some(tag_end) = self.buffer[start..].find('>') { |
| 59 | self.state = EditParserState::WithinOldText { start: true }; | 67 | let tag_end = start + tag_end + 1; |
| 68 | let tag = &self.buffer[start..tag_end]; | ||
| 69 | let line_hint = self.parse_line_hint(tag); | ||
| 70 | self.buffer.drain(..tag_end); | ||
| 71 | self.state = EditParserState::WithinOldText { | ||
| 72 | start: true, | ||
| 73 | line_hint, | ||
| 74 | }; | ||
| 75 | } else { | ||
| 76 | break; | ||
| 77 | } | ||
| 60 | } else { | 78 | } else { |
| 61 | break; | 79 | break; |
| 62 | } | 80 | } |
| 63 | } | 81 | } |
| 64 | EditParserState::WithinOldText { start } => { | 82 | EditParserState::WithinOldText { start, line_hint } => { |
| 65 | if !self.buffer.is_empty() { | 83 | if !self.buffer.is_empty() { |
| 66 | if *start && self.buffer.starts_with('\n') { | 84 | if *start && self.buffer.starts_with('\n') { |
| 67 | self.buffer.remove(0); | 85 | self.buffer.remove(0); |
| @@ -69,6 +87,7 @@ impl EditParser { · 1 unmodified line | |||
| 69 | *start = false; | 87 | *start = false; |
| 70 | } | 88 | } |
| 71 | 89 | ||
| 90 | let line_hint = *line_hint; | ||
| 72 | if let Some(tag_range) = self.find_end_tag() { | 91 | if let Some(tag_range) = self.find_end_tag() { |
| 73 | let mut chunk = self.buffer[..tag_range.start].to_string(); | 92 | let mut chunk = self.buffer[..tag_range.start].to_string(); |
| 74 | if chunk.ends_with('\n') { | 93 | if chunk.ends_with('\n') { |
| @@ -82,12 +101,17 @@ impl EditParser { · 7 unmodified lines | |||
| 82 | 101 | ||
| 83 | self.buffer.drain(..tag_range.end); | 102 | self.buffer.drain(..tag_range.end); |
| 84 | self.state = EditParserState::AfterOldText; | 103 | self.state = EditParserState::AfterOldText; |
| 85 | edit_events.push(EditParserEvent::OldTextChunk { chunk, done: true }); | 104 | edit_events.push(EditParserEvent::OldTextChunk { |
| 105 | chunk, | ||
| 106 | done: true, | ||
| 107 | line_hint, | ||
| 108 | }); | ||
| 86 | } else { | 109 | } else { |
| 87 | if !self.ends_with_tag_prefix() { | 110 | if !self.ends_with_tag_prefix() { |
| 88 | edit_events.push(EditParserEvent::OldTextChunk { | 111 | edit_events.push(EditParserEvent::OldTextChunk { |
| 89 | chunk: mem::take(&mut self.buffer), | 112 | chunk: mem::take(&mut self.buffer), |
| 90 | done: false, | 113 | done: false, |
| 114 | line_hint, | ||
| 91 | }); | 115 | }); |
| 92 | } | 116 | } |
| 93 | break; | 117 | break; |
| @@ -154,6 +178,16 @@ impl EditParser { · 60 unmodified lines | |||
| 154 | end_prefixes.any(|prefix| self.buffer.ends_with(&prefix)) | 178 | end_prefixes.any(|prefix| self.buffer.ends_with(&prefix)) |
| 155 | } | 179 | } |
| 156 | 180 | ||
| 181 | fn parse_line_hint(&self, tag: &str) -> Option<u32> { | ||
| 182 | static LINE_HINT_REGEX: std::sync::LazyLock<Regex> = | ||
| 183 | std::sync::LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap()); | ||
| 184 | |||
| 185 | LINE_HINT_REGEX | ||
| 186 | .captures(tag) | ||
| 187 | .and_then(|caps| caps.get(1)) | ||
| 188 | .and_then(|m| m.as_str().parse::<u32>().ok()) | ||
| 189 | } | ||
| 190 | |||
| 157 | pub fn finish(self) -> EditParserMetrics { | 191 | pub fn finish(self) -> EditParserMetrics { |
| 158 | self.metrics | 192 | self.metrics |
| 159 | } | 193 | } |
| @@ -178,6 +212,7 @@ mod tests { · 18 unmodified lines | |||
| 178 | vec![Edit { | 212 | vec![Edit { |
| 179 | old_text: "original".to_string(), | 213 | old_text: "original".to_string(), |
| 180 | new_text: "updated".to_string(), | 214 | new_text: "updated".to_string(), |
| 215 | line_hint: None, | ||
| 181 | }] | 216 | }] |
| 182 | ); | 217 | ); |
| 183 | assert_eq!( | 218 | assert_eq!( |
| @@ -209,10 +244,12 @@ mod tests { · 25 unmodified lines | |||
| 209 | Edit { | 244 | Edit { |
| 210 | old_text: "first old".to_string(), | 245 | old_text: "first old".to_string(), |
| 211 | new_text: "first new".to_string(), | 246 | new_text: "first new".to_string(), |
| 247 | line_hint: None, | ||
| 212 | }, | 248 | }, |
| 213 | Edit { | 249 | Edit { |
| 214 | old_text: "second old".to_string(), | 250 | old_text: "second old".to_string(), |
| 215 | new_text: "second new".to_string(), | 251 | new_text: "second new".to_string(), |
| 252 | line_hint: None, | ||
| 216 | }, | 253 | }, |
| 217 | ] | 254 | ] |
| 218 | ); | 255 | ); |
| @@ -244,14 +281,17 @@ mod tests { · 25 unmodified lines | |||
| 244 | Edit { | 281 | Edit { |
| 245 | old_text: "content".to_string(), | 282 | old_text: "content".to_string(), |
| 246 | new_text: "updated content".to_string(), | 283 | new_text: "updated content".to_string(), |
| 284 | line_hint: None, | ||
| 247 | }, | 285 | }, |
| 248 | Edit { | 286 | Edit { |
| 249 | old_text: "second item".to_string(), | 287 | old_text: "second item".to_string(), |
| 250 | new_text: "modified second item".to_string(), | 288 | new_text: "modified second item".to_string(), |
| 289 | line_hint: None, | ||
| 251 | }, | 290 | }, |
| 252 | Edit { | 291 | Edit { |
| 253 | old_text: "third case".to_string(), | 292 | old_text: "third case".to_string(), |
| 254 | new_text: "improved third case".to_string(), | 293 | new_text: "improved third case".to_string(), |
| 294 | line_hint: None, | ||
| 255 | }, | 295 | }, |
| 256 | ] | 296 | ] |
| 257 | ); | 297 | ); |
| @@ -276,6 +316,7 @@ mod tests { · 18 unmodified lines | |||
| 276 | vec![Edit { | 316 | vec![Edit { |
| 277 | old_text: "code with <tag>nested</tag> elements".to_string(), | 317 | old_text: "code with <tag>nested</tag> elements".to_string(), |
| 278 | new_text: "new <code>content</code>".to_string(), | 318 | new_text: "new <code>content</code>".to_string(), |
| 319 | line_hint: None, | ||
| 279 | }] | 320 | }] |
| 280 | ); | 321 | ); |
| 281 | assert_eq!( | 322 | assert_eq!( |
| @@ -299,6 +340,7 @@ mod tests { · 17 unmodified lines | |||
| 299 | vec![Edit { | 340 | vec![Edit { |
| 300 | old_text: "".to_string(), | 341 | old_text: "".to_string(), |
| 301 | new_text: "".to_string(), | 342 | new_text: "".to_string(), |
| 343 | line_hint: None, | ||
| 302 | }] | 344 | }] |
| 303 | ); | 345 | ); |
| 304 | assert_eq!( | 346 | assert_eq!( |
| @@ -322,6 +364,7 @@ mod tests { · 17 unmodified lines | |||
| 322 | vec![Edit { | 364 | vec![Edit { |
| 323 | old_text: "line1\nline2\nline3".to_string(), | 365 | old_text: "line1\nline2\nline3".to_string(), |
| 324 | new_text: "line1\nmodified line2\nline3".to_string(), | 366 | new_text: "line1\nmodified line2\nline3".to_string(), |
| 367 | line_hint: None, | ||
| 325 | }] | 368 | }] |
| 326 | ); | 369 | ); |
| 327 | assert_eq!( | 370 | assert_eq!( |
| @@ -368,10 +411,12 @@ mod tests { · 40 unmodified lines | |||
| 368 | Edit { | 411 | Edit { |
| 369 | old_text: "a\nb\nc".to_string(), | 412 | old_text: "a\nb\nc".to_string(), |
| 370 | new_text: "a\nB\nc".to_string(), | 413 | new_text: "a\nB\nc".to_string(), |
| 414 | line_hint: None, | ||
| 371 | }, | 415 | }, |
| 372 | Edit { | 416 | Edit { |
| 373 | old_text: "d\ne\nf".to_string(), | 417 | old_text: "d\ne\nf".to_string(), |
| 374 | new_text: "D\ne\nF".to_string(), | 418 | new_text: "D\ne\nF".to_string(), |
| 419 | line_hint: None, | ||
| 375 | } | 420 | } |
| 376 | ] | 421 | ] |
| 377 | ); | 422 | ); |
| @@ -402,6 +447,7 @@ mod tests { · 24 unmodified lines | |||
| 402 | vec![Edit { | 447 | vec![Edit { |
| 403 | old_text: "Lorem".to_string(), | 448 | old_text: "Lorem".to_string(), |
| 404 | new_text: "LOREM".to_string(), | 449 | new_text: "LOREM".to_string(), |
| 450 | line_hint: None, | ||
| 405 | },] | 451 | },] |
| 406 | ); | 452 | ); |
| 407 | assert_eq!( | 453 | assert_eq!( |
| @@ -413,10 +459,77 @@ mod tests { · 5 unmodified lines | |||
| 413 | ); | 459 | ); |
| 414 | } | 460 | } |
| 415 | 461 | ||
| 462 | #[gpui::test(iterations = 100)] | ||
| 463 | fn test_line_hints(mut rng: StdRng) { | ||
| 464 | // Line hint is a single quoted line number | ||
| 465 | let mut parser = EditParser::new(); | ||
| 466 | |||
| 467 | let edits = parse_random_chunks( | ||
| 468 | r#" | ||
| 469 | <old_text line="23">original code</old_text> | ||
| 470 | <new_text>updated code</new_text>"#, | ||
| 471 | &mut parser, | ||
| 472 | &mut rng, | ||
| 473 | ); | ||
| 474 | |||
| 475 | assert_eq!(edits.len(), 1); | ||
| 476 | assert_eq!(edits[0].old_text, "original code"); | ||
| 477 | assert_eq!(edits[0].line_hint, Some(23)); | ||
| 478 | assert_eq!(edits[0].new_text, "updated code"); | ||
| 479 | |||
| 480 | // Line hint is a single unquoted line number | ||
| 481 | let mut parser = EditParser::new(); | ||
| 482 | |||
| 483 | let edits = parse_random_chunks( | ||
| 484 | r#" | ||
| 485 | <old_text line=45>original code</old_text> | ||
| 486 | <new_text>updated code</new_text>"#, | ||
| 487 | &mut parser, | ||
| 488 | &mut rng, | ||
| 489 | ); | ||
| 490 | |||
| 491 | assert_eq!(edits.len(), 1); | ||
| 492 | assert_eq!(edits[0].old_text, "original code"); | ||
| 493 | assert_eq!(edits[0].line_hint, Some(45)); | ||
| 494 | assert_eq!(edits[0].new_text, "updated code"); | ||
| 495 | |||
| 496 | // Line hint is a range | ||
| 497 | let mut parser = EditParser::new(); | ||
| 498 | |||
| 499 | let edits = parse_random_chunks( | ||
| 500 | r#" | ||
| 501 | <old_text line="23:50">original code</old_text> | ||
| 502 | <new_text>updated code</new_text>"#, | ||
| 503 | &mut parser, | ||
| 504 | &mut rng, | ||
| 505 | ); | ||
| 506 | |||
| 507 | assert_eq!(edits.len(), 1); | ||
| 508 | assert_eq!(edits[0].old_text, "original code"); | ||
| 509 | assert_eq!(edits[0].line_hint, Some(23)); | ||
| 510 | assert_eq!(edits[0].new_text, "updated code"); | ||
| 511 | |||
| 512 | // No line hint | ||
| 513 | let mut parser = EditParser::new(); | ||
| 514 | let edits = parse_random_chunks( | ||
| 515 | r#" | ||
| 516 | <old_text>old</old_text> | ||
| 517 | <new_text>new</new_text>"#, | ||
| 518 | &mut parser, | ||
| 519 | &mut rng, | ||
| 520 | ); | ||
| 521 | |||
| 522 | assert_eq!(edits.len(), 1); | ||
| 523 | assert_eq!(edits[0].old_text, "old"); | ||
| 524 | assert_eq!(edits[0].line_hint, None); | ||
| 525 | assert_eq!(edits[0].new_text, "new"); | ||
| 526 | } | ||
| 527 | |||
| 416 | #[derive(Default, Debug, PartialEq, Eq)] | 528 | #[derive(Default, Debug, PartialEq, Eq)] |
| 417 | struct Edit { | 529 | struct Edit { |
| 418 | old_text: String, | 530 | old_text: String, |
| 419 | new_text: String, | 531 | new_text: String, |
| 532 | line_hint: Option<u32>, | ||
| 420 | } | 533 | } |
| 421 | 534 | ||
| 422 | fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> { | 535 | fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> { |
| @@ -433,10 +546,15 @@ mod tests { · 10 unmodified lines | |||
| 433 | for chunk_ix in chunk_indices { | 546 | for chunk_ix in chunk_indices { |
| 434 | for event in parser.push(&input[last_ix..chunk_ix]) { | 547 | for event in parser.push(&input[last_ix..chunk_ix]) { |
| 435 | match event { | 548 | match event { |
| 436 | EditParserEvent::OldTextChunk { chunk, done } => { | 549 | EditParserEvent::OldTextChunk { |
| 550 | chunk, | ||
| 551 | done, | ||
| 552 | line_hint, | ||
| 553 | } => { | ||
| 437 | old_text.as_mut().unwrap().push_str(&chunk); | 554 | old_text.as_mut().unwrap().push_str(&chunk); |
| 438 | if done { | 555 | if done { |
| 439 | pending_edit.old_text = old_text.take().unwrap(); | 556 | pending_edit.old_text = old_text.take().unwrap(); |
| 557 | pending_edit.line_hint = line_hint; | ||
| 440 | new_text = Some(String::new()); | 558 | new_text = Some(String::new()); |
| 441 | } | 559 | } |
| 442 | } | 560 | } |
| @@ -11,7 +11,7 @@ use client::{Client, UserStore}; · 10 unmodified lines | |||
| 11 | use collections::HashMap; | 11 | use collections::HashMap; |
| 12 | use fs::FakeFs; | 12 | use fs::FakeFs; |
| 13 | use futures::{FutureExt, future::LocalBoxFuture}; | 13 | use futures::{FutureExt, future::LocalBoxFuture}; |
| 14 | use gpui::{AppContext, TestAppContext}; | 14 | use gpui::{AppContext, TestAppContext, Timer}; |
| 15 | use indoc::{formatdoc, indoc}; | 15 | use indoc::{formatdoc, indoc}; |
| 16 | use language_model::{ | 16 | use language_model::{ |
| 17 | LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, | 17 | LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, |
| @@ -26,6 +26,7 @@ use std::{ · 8 unmodified lines | |||
| 26 | cmp::Reverse, | 26 | cmp::Reverse, |
| 27 | fmt::{self, Display}, | 27 | fmt::{self, Display}, |
| 28 | io::Write as _, | 28 | io::Write as _, |
| 29 | path::Path, | ||
| 29 | str::FromStr, | 30 | str::FromStr, |
| 30 | sync::mpsc, | 31 | sync::mpsc, |
| 31 | }; | 32 | }; |
| @@ -38,10 +39,11 @@ fn eval_extract_handle_command_output() { · 6 unmodified lines | |||
| 38 | // | 39 | // |
| 39 | // Model | Pass rate | 40 | // Model | Pass rate |
| 40 | // ----------------------------|---------- | 41 | // ----------------------------|---------- |
| 41 | // claude-3.7-sonnet | 0.98 | 42 | // claude-3.7-sonnet | 0.99 (2025-06-14) |
| 42 | // gemini-2.5-pro-06-05 | 0.77 | 43 | // claude-sonnet-4 | 0.97 (2025-06-14) |
| 43 | // gemini-2.5-flash | 0.11 | 44 | // gemini-2.5-pro-06-05 | 0.77 (2025-05-22) |
| 44 | // gpt-4.1 | 1.00 | 45 | // gemini-2.5-flash | 0.11 (2025-05-22) |
| 46 | // gpt-4.1 | 1.00 (2025-05-22) | ||
| 45 | 47 | ||
| 46 | let input_file_path = "root/blame.rs"; | 48 | let input_file_path = "root/blame.rs"; |
| 47 | let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs"); | 49 | let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs"); |
| @@ -110,6 +112,13 @@ fn eval_extract_handle_command_output() { · 62 unmodified lines | |||
| 110 | #[test] | 112 | #[test] |
| 111 | #[cfg_attr(not(feature = "eval"), ignore)] | 113 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 112 | fn eval_delete_run_git_blame() { | 114 | fn eval_delete_run_git_blame() { |
| 115 | // Model | Pass rate | ||
| 116 | // ----------------------------|---------- | ||
| 117 | // claude-3.7-sonnet | 1.0 (2025-06-14) | ||
| 118 | // claude-sonnet-4 | 0.96 (2025-06-14) | ||
| 119 | // gemini-2.5-pro-06-05 | | ||
| 120 | // gemini-2.5-flash | | ||
| 121 | // gpt-4.1 | | ||
| 113 | let input_file_path = "root/blame.rs"; | 122 | let input_file_path = "root/blame.rs"; |
| 114 | let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs"); | 123 | let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs"); |
| 115 | let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs"); | 124 | let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs"); |
| @@ -165,13 +174,12 @@ fn eval_delete_run_git_blame() { · 49 unmodified lines | |||
| 165 | #[test] | 174 | #[test] |
| 166 | #[cfg_attr(not(feature = "eval"), ignore)] | 175 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 167 | fn eval_translate_doc_comments() { | 176 | fn eval_translate_doc_comments() { |
| 168 | // Results for 2025-05-22 | ||
| 169 | // | ||
| 170 | // Model | Pass rate | 177 | // Model | Pass rate |
| 171 | // ============================================ | 178 | // ============================================ |
| 172 | // | 179 | // |
| 173 | // claude-3.7-sonnet | | 180 | // claude-3.7-sonnet | 1.0 (2025-06-14) |
| 174 | // gemini-2.5-pro-preview-03-25 | 1.0 | 181 | // claude-sonnet-4 | 1.0 (2025-06-14) |
| 182 | // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) | ||
| 175 | // gemini-2.5-flash-preview-04-17 | | 183 | // gemini-2.5-flash-preview-04-17 | |
| 176 | // gpt-4.1 | | 184 | // gpt-4.1 | |
| 177 | let input_file_path = "root/canvas.rs"; | 185 | let input_file_path = "root/canvas.rs"; |
| @@ -228,13 +236,12 @@ fn eval_translate_doc_comments() { · 50 unmodified lines | |||
| 228 | #[test] | 236 | #[test] |
| 229 | #[cfg_attr(not(feature = "eval"), ignore)] | 237 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 230 | fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { | 238 | fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { |
| 231 | // Results for 2025-05-22 | ||
| 232 | // | ||
| 233 | // Model | Pass rate | 239 | // Model | Pass rate |
| 234 | // ============================================ | 240 | // ============================================ |
| 235 | // | 241 | // |
| 236 | // claude-3.7-sonnet | 0.98 | 242 | // claude-3.7-sonnet | 0.96 (2025-06-14) |
| 237 | // gemini-2.5-pro-preview-03-25 | 0.99 | 243 | // claude-sonnet-4 | 0.11 (2025-06-14) |
| 244 | // gemini-2.5-pro-preview-03-25 | 0.99 (2025-05-22) | ||
| 238 | // gemini-2.5-flash-preview-04-17 | | 245 | // gemini-2.5-flash-preview-04-17 | |
| 239 | // gpt-4.1 | | 246 | // gpt-4.1 | |
| 240 | let input_file_path = "root/lib.rs"; | 247 | let input_file_path = "root/lib.rs"; |
| @@ -354,13 +361,12 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { · 113 unmodified lines | |||
| 354 | #[test] | 361 | #[test] |
| 355 | #[cfg_attr(not(feature = "eval"), ignore)] | 362 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 356 | fn eval_disable_cursor_blinking() { | 363 | fn eval_disable_cursor_blinking() { |
| 357 | // Results for 2025-05-22 | ||
| 358 | // | ||
| 359 | // Model | Pass rate | 364 | // Model | Pass rate |
| 360 | // ============================================ | 365 | // ============================================ |
| 361 | // | 366 | // |
| 362 | // claude-3.7-sonnet | | 367 | // claude-3.7-sonnet | 0.99 (2025-06-14) |
| 363 | // gemini-2.5-pro-preview-03-25 | 1.0 | 368 | // claude-sonnet-4 | 0.85 (2025-06-14) |
| 369 | // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) | ||
| 364 | // gemini-2.5-flash-preview-04-17 | | 370 | // gemini-2.5-flash-preview-04-17 | |
| 365 | // gpt-4.1 | | 371 | // gpt-4.1 | |
| 366 | let input_file_path = "root/editor.rs"; | 372 | let input_file_path = "root/editor.rs"; |
| @@ -438,14 +444,21 @@ fn eval_disable_cursor_blinking() { · 71 unmodified lines | |||
| 438 | #[test] | 444 | #[test] |
| 439 | #[cfg_attr(not(feature = "eval"), ignore)] | 445 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 440 | fn eval_from_pixels_constructor() { | 446 | fn eval_from_pixels_constructor() { |
| 441 | // Results for 2025-05-22 | 447 | // Results for 2025-06-13 |
| 448 | // | ||
| 449 | // The outcome of this evaluation depends heavily on the LINE_HINT_TOLERANCE | ||
| 450 | // value. Higher values improve the pass rate but may sometimes cause | ||
| 451 | // edits to be misapplied. In the context of this eval, this means | ||
| 452 | // the agent might add from_pixels tests in incorrect locations | ||
| 453 | // (e.g., at the beginning of the file), yet the evaluation may still | ||
| 454 | // rate it highly. | ||
| 442 | // | 455 | // |
| 443 | // Model | Pass rate | 456 | // Model | Pass rate |
| 444 | // ============================================ | 457 | // ============================================ |
| 445 | // | 458 | // |
| 446 | // claude-3.7-sonnet | | 459 | // claude-4.0-sonnet | 0.99 |
| 447 | // gemini-2.5-pro-preview-03-25 | 0.94 | 460 | // claude-3.7-sonnet | 0.88 |
| 448 | // gemini-2.5-flash-preview-04-17 | | 461 | // gemini-2.5-pro-preview-03-25 | 0.96 |
| 449 | // gpt-4.1 | | 462 | // gpt-4.1 | |
| 450 | let input_file_path = "root/canvas.rs"; | 463 | let input_file_path = "root/canvas.rs"; |
| 451 | let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs"); | 464 | let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs"); |
| @@ -455,7 +468,7 @@ fn eval_from_pixels_constructor() { · 3 unmodified lines | |||
| 455 | 0.95, | 468 | 0.95, |
| 456 | // For whatever reason, this eval produces more mismatched tags. | 469 | // For whatever reason, this eval produces more mismatched tags. |
| 457 | // Increasing for now, let's see if we can bring this down. | 470 | // Increasing for now, let's see if we can bring this down. |
| 458 | 0.2, | 471 | 0.25, |
| 459 | EvalInput::from_conversation( | 472 | EvalInput::from_conversation( |
| 460 | vec![ | 473 | vec![ |
| 461 | message( | 474 | message( |
| @@ -641,15 +654,14 @@ fn eval_from_pixels_constructor() { · 179 unmodified lines | |||
| 641 | #[test] | 654 | #[test] |
| 642 | #[cfg_attr(not(feature = "eval"), ignore)] | 655 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 643 | fn eval_zode() { | 656 | fn eval_zode() { |
| 644 | // Results for 2025-05-22 | ||
| 645 | // | ||
| 646 | // Model | Pass rate | 657 | // Model | Pass rate |
| 647 | // ============================================ | 658 | // ============================================ |
| 648 | // | 659 | // |
| 649 | // claude-3.7-sonnet | 1.0 | 660 | // claude-3.7-sonnet | 1.0 (2025-06-14) |
| 650 | // gemini-2.5-pro-preview-03-25 | 1.0 | 661 | // claude-sonnet-4 | 1.0 (2025-06-14) |
| 651 | // gemini-2.5-flash-preview-04-17 | 1.0 | 662 | // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) |
| 652 | // gpt-4.1 | 1.0 | 663 | // gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22) |
| 664 | // gpt-4.1 | 1.0 (2025-05-22) | ||
| 653 | let input_file_path = "root/zode.py"; | 665 | let input_file_path = "root/zode.py"; |
| 654 | let input_content = None; | 666 | let input_content = None; |
| 655 | let edit_description = "Create the main Zode CLI script"; | 667 | let edit_description = "Create the main Zode CLI script"; |
| @@ -748,13 +760,12 @@ fn eval_zode() { · 92 unmodified lines | |||
| 748 | #[test] | 760 | #[test] |
| 749 | #[cfg_attr(not(feature = "eval"), ignore)] | 761 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 750 | fn eval_add_overwrite_test() { | 762 | fn eval_add_overwrite_test() { |
| 751 | // Results for 2025-05-22 | ||
| 752 | // | ||
| 753 | // Model | Pass rate | 763 | // Model | Pass rate |
| 754 | // ============================================ | 764 | // ============================================ |
| 755 | // | 765 | // |
| 756 | // claude-3.7-sonnet | 0.16 | 766 | // claude-3.7-sonnet | 0.65 (2025-06-14) |
| 757 | // gemini-2.5-pro-preview-03-25 | 0.35 | 767 | // claude-sonnet-4 | 0.07 (2025-06-14) |
| 768 | // gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22) | ||
| 758 | // gemini-2.5-flash-preview-04-17 | | 769 | // gemini-2.5-flash-preview-04-17 | |
| 759 | // gpt-4.1 | | 770 | // gpt-4.1 | |
| 760 | let input_file_path = "root/action_log.rs"; | 771 | let input_file_path = "root/action_log.rs"; |
| @@ -984,15 +995,14 @@ fn eval_create_empty_file() { · 223 unmodified lines | |||
| 984 | // thoughts into it. This issue is not specific to empty files, but | 995 | // thoughts into it. This issue is not specific to empty files, but |
| 985 | // it's easier to reproduce with them. | 996 | // it's easier to reproduce with them. |
| 986 | // | 997 | // |
| 987 | // Results for 2025-05-21: | ||
| 988 | // | ||
| 989 | // Model | Pass rate | 998 | // Model | Pass rate |
| 990 | // ============================================ | 999 | // ============================================ |
| 991 | // | 1000 | // |
| 992 | // claude-3.7-sonnet | 1.00 | 1001 | // claude-3.7-sonnet | 1.00 (2025-06-14) |
| 993 | // gemini-2.5-pro-preview-03-25 | 1.00 | 1002 | // claude-sonnet-4 | 1.00 (2025-06-14) |
| 994 | // gemini-2.5-flash-preview-04-17 | 1.00 | 1003 | // gemini-2.5-pro-preview-03-25 | 1.00 (2025-05-21) |
| 995 | // gpt-4.1 | 1.00 | 1004 | // gemini-2.5-flash-preview-04-17 | 1.00 (2025-05-21) |
| 1005 | // gpt-4.1 | 1.00 (2025-05-21) | ||
| 996 | // | 1006 | // |
| 997 | // | 1007 | // |
| 998 | // TODO: gpt-4.1-mini errored 38 times: | 1008 | // TODO: gpt-4.1-mini errored 38 times: |
| @@ -1255,9 +1265,12 @@ impl EvalAssertion { · 256 unmodified lines | |||
| 1255 | }], | 1265 | }], |
| 1256 | ..Default::default() | 1266 | ..Default::default() |
| 1257 | }; | 1267 | }; |
| 1258 | let mut response = judge | 1268 | let mut response = retry_on_rate_limit(async || { |
| 1259 | .stream_completion_text(request, &cx.to_async()) | 1269 | Ok(judge |
| 1260 | .await?; | 1270 | .stream_completion_text(request.clone(), &cx.to_async()) |
| 1271 | .await?) | ||
| 1272 | }) | ||
| 1273 | .await?; | ||
| 1261 | let mut output = String::new(); | 1274 | let mut output = String::new(); |
| 1262 | while let Some(chunk) = response.stream.next().await { | 1275 | while let Some(chunk) = response.stream.next().await { |
| 1263 | let chunk = chunk?; | 1276 | let chunk = chunk?; |
| @@ -1308,10 +1321,17 @@ fn eval( · 44 unmodified lines | |||
| 1308 | run_eval(eval.clone(), tx.clone()); | 1321 | run_eval(eval.clone(), tx.clone()); |
| 1309 | 1322 | ||
| 1310 | let executor = gpui::background_executor(); | 1323 | let executor = gpui::background_executor(); |
| 1324 | let semaphore = Arc::new(smol::lock::Semaphore::new(32)); | ||
| 1311 | for _ in 1..iterations { | 1325 | for _ in 1..iterations { |
| 1312 | let eval = eval.clone(); | 1326 | let eval = eval.clone(); |
| 1313 | let tx = tx.clone(); | 1327 | let tx = tx.clone(); |
| 1314 | executor.spawn(async move { run_eval(eval, tx) }).detach(); | 1328 | let semaphore = semaphore.clone(); |
| 1329 | executor | ||
| 1330 | .spawn(async move { | ||
| 1331 | let _guard = semaphore.acquire().await; | ||
| 1332 | run_eval(eval, tx) | ||
| 1333 | }) | ||
| 1334 | .detach(); | ||
| 1315 | } | 1335 | } |
| 1316 | drop(tx); | 1336 | drop(tx); |
| 1317 | 1337 | ||
| @@ -1539,6 +1559,7 @@ impl EditAgentTest { · 221 unmodified lines | |||
| 1539 | .collect::<Vec<_>>(); | 1559 | .collect::<Vec<_>>(); |
| 1540 | let worktrees = vec![WorktreeContext { | 1560 | let worktrees = vec![WorktreeContext { |
| 1541 | root_name: "root".to_string(), | 1561 | root_name: "root".to_string(), |
| 1562 | abs_path: Path::new("/path/to/root").into(), | ||
| 1542 | rules_file: None, | 1563 | rules_file: None, |
| 1543 | }]; | 1564 | }]; |
| 1544 | let prompt_builder = PromptBuilder::new(None)?; | 1565 | let prompt_builder = PromptBuilder::new(None)?; |
| @@ -1577,21 +1598,31 @@ impl EditAgentTest { · 32 unmodified lines | |||
| 1577 | if let Some(input_content) = eval.input_content.as_deref() { | 1598 | if let Some(input_content) = eval.input_content.as_deref() { |
| 1578 | buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx)); | 1599 | buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx)); |
| 1579 | } | 1600 | } |
| 1580 | let (edit_output, _) = self.agent.edit( | 1601 | retry_on_rate_limit(async || { |
| 1581 | buffer.clone(), | 1602 | self.agent |
| 1582 | eval.edit_file_input.display_description, | 1603 | .edit( |
| 1583 | &conversation, | 1604 | buffer.clone(), |
| 1584 | &mut cx.to_async(), | 1605 | eval.edit_file_input.display_description.clone(), |
| 1585 | ); | 1606 | &conversation, |
| 1586 | edit_output.await? | 1607 | &mut cx.to_async(), |
| 1608 | ) | ||
| 1609 | .0 | ||
| 1610 | .await | ||
| 1611 | }) | ||
| 1612 | .await? | ||
| 1587 | } else { | 1613 | } else { |
| 1588 | let (edit_output, _) = self.agent.overwrite( | 1614 | retry_on_rate_limit(async || { |
| 1589 | buffer.clone(), | 1615 | self.agent |
| 1590 | eval.edit_file_input.display_description, | 1616 | .overwrite( |
| 1591 | &conversation, | 1617 | buffer.clone(), |
| 1592 | &mut cx.to_async(), | 1618 | eval.edit_file_input.display_description.clone(), |
| 1593 | ); | 1619 | &conversation, |
| 1594 | edit_output.await? | 1620 | &mut cx.to_async(), |
| 1621 | ) | ||
| 1622 | .0 | ||
| 1623 | .await | ||
| 1624 | }) | ||
| 1625 | .await? | ||
| 1595 | }; | 1626 | }; |
| 1596 | 1627 | ||
| 1597 | let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text()); | 1628 | let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text()); |
| @@ -1613,6 +1644,31 @@ impl EditAgentTest { · 15 unmodified lines | |||
| 1613 | } | 1644 | } |
| 1614 | } | 1645 | } |
| 1615 | 1646 | ||
| 1647 | async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) -> Result<R> { | ||
| 1648 | let mut attempt = 0; | ||
| 1649 | loop { | ||
| 1650 | attempt += 1; | ||
| 1651 | match request().await { | ||
| 1652 | Ok(result) => return Ok(result), | ||
| 1653 | Err(err) => match err.downcast::<LanguageModelCompletionError>() { | ||
| 1654 | Ok(err) => match err { | ||
| 1655 | LanguageModelCompletionError::RateLimit(duration) => { | ||
| 1656 | // Wait for the duration supplied, with some jitter to avoid all requests being made at the same time. | ||
| 1657 | let jitter = duration.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); | ||
| 1658 | eprintln!( | ||
| 1659 | "Attempt #{attempt}: Rate limit exceeded. Retry after {duration:?} + jitter of {jitter:?}" | ||
| 1660 | ); | ||
| 1661 | Timer::after(duration + jitter).await; | ||
| 1662 | continue; | ||
| 1663 | } | ||
| 1664 | _ => return Err(err.into()), | ||
| 1665 | }, | ||
| 1666 | Err(err) => return Err(err), | ||
| 1667 | }, | ||
| 1668 | } | ||
| 1669 | } | ||
| 1670 | } | ||
| 1671 | |||
| 1616 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] | 1672 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] |
| 1617 | struct EvalAssertionOutcome { | 1673 | struct EvalAssertionOutcome { |
| 1618 | score: usize, | 1674 | score: usize, |
| @@ -10,8 +10,9 @@ const DELETION_COST: u32 = 10; · 9 unmodified lines | |||
| 10 | pub struct StreamingFuzzyMatcher { | 10 | pub struct StreamingFuzzyMatcher { |
| 11 | snapshot: TextBufferSnapshot, | 11 | snapshot: TextBufferSnapshot, |
| 12 | query_lines: Vec<String>, | 12 | query_lines: Vec<String>, |
| 13 | line_hint: Option<u32>, | ||
| 13 | incomplete_line: String, | 14 | incomplete_line: String, |
| 14 | best_matches: Vec<Range<usize>>, | 15 | matches: Vec<Range<usize>>, |
| 15 | matrix: SearchMatrix, | 16 | matrix: SearchMatrix, |
| 16 | } | 17 | } |
| 17 | 18 | ||
| @@ -21,8 +22,9 @@ impl StreamingFuzzyMatcher { · 3 unmodified lines | |||
| 21 | Self { | 22 | Self { |
| 22 | snapshot, | 23 | snapshot, |
| 23 | query_lines: Vec::new(), | 24 | query_lines: Vec::new(), |
| 25 | line_hint: None, | ||
| 24 | incomplete_line: String::new(), | 26 | incomplete_line: String::new(), |
| 25 | best_matches: Vec::new(), | 27 | matches: Vec::new(), |
| 26 | matrix: SearchMatrix::new(buffer_line_count + 1), | 28 | matrix: SearchMatrix::new(buffer_line_count + 1), |
| 27 | } | 29 | } |
| 28 | } | 30 | } |
| @@ -41,9 +43,10 @@ impl StreamingFuzzyMatcher { · 12 unmodified lines | |||
| 41 | /// | 43 | /// |
| 42 | /// Returns `Some(range)` if a match has been found with the accumulated | 44 | /// Returns `Some(range)` if a match has been found with the accumulated |
| 43 | /// query so far, or `None` if no suitable match exists yet. | 45 | /// query so far, or `None` if no suitable match exists yet. |
| 44 | pub fn push(&mut self, chunk: &str) -> Option<Range<usize>> { | 46 | pub fn push(&mut self, chunk: &str, line_hint: Option<u32>) -> Option<Range<usize>> { |
| 45 | // Add the chunk to our incomplete line buffer | 47 | // Add the chunk to our incomplete line buffer |
| 46 | self.incomplete_line.push_str(chunk); | 48 | self.incomplete_line.push_str(chunk); |
| 49 | self.line_hint = line_hint; | ||
| 47 | 50 | ||
| 48 | if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() { | 51 | if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() { |
| 49 | let complete_part = &self.incomplete_line[..=last_pos]; | 52 | let complete_part = &self.incomplete_line[..=last_pos]; |
| @@ -55,20 +58,11 @@ impl StreamingFuzzyMatcher { · 5 unmodified lines | |||
| 55 | 58 | ||
| 56 | self.incomplete_line.replace_range(..last_pos + 1, ""); | 59 | self.incomplete_line.replace_range(..last_pos + 1, ""); |
| 57 | 60 | ||
| 58 | self.best_matches = self.resolve_location_fuzzy(); | 61 | self.matches = self.resolve_location_fuzzy(); |
| 59 | |||
| 60 | if let Some(first_match) = self.best_matches.first() { | ||
| 61 | Some(first_match.clone()) | ||
| 62 | } else { | ||
| 63 | None | ||
| 64 | } | ||
| 65 | } else { | ||
| 66 | if let Some(first_match) = self.best_matches.first() { | ||
| 67 | Some(first_match.clone()) | ||
| 68 | } else { | ||
| 69 | None | ||
| 70 | } | ||
| 71 | } | 62 | } |
| 63 | |||
| 64 | let best_match = self.select_best_match(); | ||
| 65 | best_match.or_else(|| self.matches.first().cloned()) | ||
| 72 | } | 66 | } |
| 73 | 67 | ||
| 74 | /// Finish processing and return the final best match(es). | 68 | /// Finish processing and return the final best match(es). |
| @@ -80,9 +74,9 @@ impl StreamingFuzzyMatcher { · 5 unmodified lines | |||
| 80 | if !self.incomplete_line.is_empty() { | 74 | if !self.incomplete_line.is_empty() { |
| 81 | self.query_lines.push(self.incomplete_line.clone()); | 75 | self.query_lines.push(self.incomplete_line.clone()); |
| 82 | self.incomplete_line.clear(); | 76 | self.incomplete_line.clear(); |
| 83 | self.best_matches = self.resolve_location_fuzzy(); | 77 | self.matches = self.resolve_location_fuzzy(); |
| 84 | } | 78 | } |
| 85 | self.best_matches.clone() | 79 | self.matches.clone() |
| 86 | } | 80 | } |
| 87 | 81 | ||
| 88 | fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> { | 82 | fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> { |
| @@ -198,6 +192,43 @@ impl StreamingFuzzyMatcher { · 109 unmodified lines | |||
| 198 | 192 | ||
| 199 | valid_matches.into_iter().map(|(_, range)| range).collect() | 193 | valid_matches.into_iter().map(|(_, range)| range).collect() |
| 200 | } | 194 | } |
| 195 | |||
| 196 | /// Return the best match with starting position close enough to line_hint. | ||
| 197 | pub fn select_best_match(&self) -> Option<Range<usize>> { | ||
| 198 | // Allow line hint to be off by that many lines. | ||
| 199 | // Higher values increase probability of applying edits to a wrong place, | ||
| 200 | // Lower values increase edits failures and overall conversation length. | ||
| 201 | const LINE_HINT_TOLERANCE: u32 = 200; | ||
| 202 | |||
| 203 | if self.matches.is_empty() { | ||
| 204 | return None; | ||
| 205 | } | ||
| 206 | |||
| 207 | if self.matches.len() == 1 { | ||
| 208 | return self.matches.first().cloned(); | ||
| 209 | } | ||
| 210 | |||
| 211 | let Some(line_hint) = self.line_hint else { | ||
| 212 | // Multiple ambiguous matches | ||
| 213 | return None; | ||
| 214 | }; | ||
| 215 | |||
| 216 | let mut best_match = None; | ||
| 217 | let mut best_distance = u32::MAX; | ||
| 218 | |||
| 219 | for range in &self.matches { | ||
| 220 | let start_point = self.snapshot.offset_to_point(range.start); | ||
| 221 | let start_line = start_point.row; | ||
| 222 | let distance = start_line.abs_diff(line_hint); | ||
| 223 | |||
| 224 | if distance <= LINE_HINT_TOLERANCE && distance < best_distance { | ||
| 225 | best_distance = distance; | ||
| 226 | best_match = Some(range.clone()); | ||
| 227 | } | ||
| 228 | } | ||
| 229 | |||
| 230 | best_match | ||
| 231 | } | ||
| 201 | } | 232 | } |
| 202 | 233 | ||
| 203 | fn fuzzy_eq(left: &str, right: &str) -> bool { | 234 | fn fuzzy_eq(left: &str, right: &str) -> bool { |
| @@ -640,6 +671,52 @@ mod tests { · 436 unmodified lines | |||
| 640 | ); | 671 | ); |
| 641 | } | 672 | } |
| 642 | 673 | ||
| 674 | #[gpui::test] | ||
| 675 | fn test_line_hint_selection() { | ||
| 676 | let text = indoc! {r#" | ||
| 677 | fn first_function() { | ||
| 678 | return 42; | ||
| 679 | } | ||
| 680 | |||
| 681 | fn second_function() { | ||
| 682 | return 42; | ||
| 683 | } | ||
| 684 | |||
| 685 | fn third_function() { | ||
| 686 | return 42; | ||
| 687 | } | ||
| 688 | "#}; | ||
| 689 | |||
| 690 | let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.to_string()); | ||
| 691 | let snapshot = buffer.snapshot(); | ||
| 692 | let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); | ||
| 693 | |||
| 694 | // Given a query that matches all three functions | ||
| 695 | let query = "return 42;\n"; | ||
| 696 | |||
| 697 | // Test with line hint pointing to second function (around line 5) | ||
| 698 | let best_match = matcher.push(query, Some(5)).expect("Failed to match query"); | ||
| 699 | |||
| 700 | let matched_text = snapshot | ||
| 701 | .text_for_range(best_match.clone()) | ||
| 702 | .collect::<String>(); | ||
| 703 | assert!(matched_text.contains("return 42;")); | ||
| 704 | assert_eq!( | ||
| 705 | best_match, | ||
| 706 | 63..77, | ||
| 707 | "Expected to match `second_function` based on the line hint" | ||
| 708 | ); | ||
| 709 | |||
| 710 | let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); | ||
| 711 | matcher.push(query, None); | ||
| 712 | matcher.finish(); | ||
| 713 | let best_match = matcher.select_best_match(); | ||
| 714 | assert!( | ||
| 715 | best_match.is_none(), | ||
| 716 | "Best match should be None when query cannot be uniquely resolved" | ||
| 717 | ); | ||
| 718 | } | ||
| 719 | |||
| 643 | #[track_caller] | 720 | #[track_caller] |
| 644 | fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) { | 721 | fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) { |
| 645 | let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false); | 722 | let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false); |
| @@ -653,7 +730,7 @@ mod tests { · 7 unmodified lines | |||
| 653 | 730 | ||
| 654 | // Push chunks incrementally | 731 | // Push chunks incrementally |
| 655 | for chunk in &chunks { | 732 | for chunk in &chunks { |
| 656 | matcher.push(chunk); | 733 | matcher.push(chunk, None); |
| 657 | } | 734 | } |
| 658 | 735 | ||
| 659 | let actual_ranges = matcher.finish(); | 736 | let actual_ranges = matcher.finish(); |
| @@ -706,7 +783,7 @@ mod tests { · 46 unmodified lines | |||
| 706 | 783 | ||
| 707 | fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> { | 784 | fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> { |
| 708 | finder | 785 | finder |
| 709 | .push(chunk) | 786 | .push(chunk, None) |
| 710 | .map(|range| finder.snapshot.text_for_range(range).collect::<String>()) | 787 | .map(|range| finder.snapshot.text_for_range(range).collect::<String>()) |
| 711 | } | 788 | } |
| 712 | 789 | ||
| @@ -10,7 +10,7 @@ use assistant_tool::{ · 9 unmodified lines | |||
| 10 | ToolUseStatus, | 10 | ToolUseStatus, |
| 11 | }; | 11 | }; |
| 12 | use buffer_diff::{BufferDiff, BufferDiffSnapshot}; | 12 | use buffer_diff::{BufferDiff, BufferDiffSnapshot}; |
| 13 | use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey}; | 13 | use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll}; |
| 14 | use futures::StreamExt; | 14 | use futures::StreamExt; |
| 15 | use gpui::{ | 15 | use gpui::{ |
| 16 | Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, | 16 | Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, |
| @@ -69,13 +69,13 @@ pub struct EditFileToolInput { · 52 unmodified lines | |||
| 69 | /// start each path with one of the project's root directories. | 69 | /// start each path with one of the project's root directories. |
| 70 | /// | 70 | /// |
| 71 | /// The following examples assume we have two root directories in the project: | 71 | /// The following examples assume we have two root directories in the project: |
| 72 | /// - backend | 72 | /// - /a/b/backend |
| 73 | /// - frontend | 73 | /// - /c/d/frontend |
| 74 | /// | 74 | /// |
| 75 | /// <example> | 75 | /// <example> |
| 76 | /// `backend/src/main.rs` | 76 | /// `backend/src/main.rs` |
| 77 | /// | 77 | /// |
| 78 | /// Notice how the file path starts with root-1. Without that, the path | 78 | /// Notice how the file path starts with `backend`. Without that, the path |
| 79 | /// would be ambiguous and the call would fail! | 79 | /// would be ambiguous and the call would fail! |
| 80 | /// </example> | 80 | /// </example> |
| 81 | /// | 81 | /// |
| @@ -333,14 +333,18 @@ impl Tool for EditFileTool { · 251 unmodified lines | |||
| 333 | ); | 333 | ); |
| 334 | anyhow::ensure!( | 334 | anyhow::ensure!( |
| 335 | ambiguous_ranges.is_empty(), | 335 | ambiguous_ranges.is_empty(), |
| 336 | // TODO: Include ambiguous_ranges, converted to line numbers. | 336 | { |
| 337 | // This would work best if we add `line_hint` parameter | 337 | let line_numbers = ambiguous_ranges |
| 338 | // to edit_file_tool | 338 | .iter() |
| 339 | formatdoc! {" | 339 | .map(|range| range.start.to_string()) |
| 340 | <old_text> matches more than one position in the file. Read the | 340 | .collect::<Vec<_>>() |
| 341 | relevant sections of {input_path} again and extend <old_text> so | 341 | .join(", "); |
| 342 | that I can perform the requested edits. | 342 | formatdoc! {" |
| 343 | "} | 343 | <old_text> matches more than one position in the file (lines: {line_numbers}). Read the |
| 344 | relevant sections of {input_path} again and extend <old_text> so | ||
| 345 | that I can perform the requested edits. | ||
| 346 | "} | ||
| 347 | } | ||
| 344 | ); | 348 | ); |
| 345 | Ok(ToolResultOutput { | 349 | Ok(ToolResultOutput { |
| 346 | content: ToolResultContent::Text("No edits were made.".into()), | 350 | content: ToolResultContent::Text("No edits were made.".into()), |
| @@ -800,11 +804,30 @@ impl ToolCard for EditFileToolCard { · 453 unmodified lines | |||
| 800 | if let Some(active_editor) = item.downcast::<Editor>() { | 804 | if let Some(active_editor) = item.downcast::<Editor>() { |
| 801 | active_editor | 805 | active_editor |
| 802 | .update_in(cx, |editor, window, cx| { | 806 | .update_in(cx, |editor, window, cx| { |
| 803 | editor.go_to_singleton_buffer_point( | 807 | let snapshot = |
| 804 | language::Point::new(0, 0), | 808 | editor.buffer().read(cx).snapshot(cx); |
| 805 | window, | 809 | let first_hunk = editor |
| 806 | cx, | 810 | .diff_hunks_in_ranges( |
| 807 | ); | 811 | &[editor::Anchor::min() |
| 812 | ..editor::Anchor::max()], | ||
| 813 | &snapshot, | ||
| 814 | ) | ||
| 815 | .next(); | ||
| 816 | if let Some(first_hunk) = first_hunk { | ||
| 817 | let first_hunk_start = | ||
| 818 | first_hunk.multi_buffer_range().start; | ||
| 819 | editor.change_selections( | ||
| 820 | Some(Autoscroll::fit()), | ||
| 821 | window, | ||
| 822 | cx, | ||
| 823 | |selections| { | ||
| 824 | selections.select_anchor_ranges([ | ||
| 825 | first_hunk_start | ||
| 826 | ..first_hunk_start, | ||
| 827 | ]); | ||
| 828 | }, | ||
| 829 | ) | ||
| 830 | } | ||
| 808 | }) | 831 | }) |
| 809 | .log_err(); | 832 | .log_err(); |
| 810 | } | 833 | } |
| @@ -31,8 +31,8 @@ pub struct ReadFileToolInput { · 30 unmodified lines | |||
| 31 | /// <example> | 31 | /// <example> |
| 32 | /// If the project has the following root directories: | 32 | /// If the project has the following root directories: |
| 33 | /// | 33 | /// |
| 34 | /// - directory1 | 34 | /// - /a/b/directory1 |
| 35 | /// - directory2 | 35 | /// - /c/d/directory2 |
| 36 | /// | 36 | /// |
| 37 | /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`. | 37 | /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`. |
| 38 | /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`. | 38 | /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`. |
| @@ -3,21 +3,21 @@ You MUST respond with a series of edits to a file, using the following format: · 2 unmodified lines | |||
| 3 | ``` | 3 | ``` |
| 4 | <edits> | 4 | <edits> |
| 5 | 5 | ||
| 6 | <old_text> | 6 | <old_text line=10> |
| 7 | OLD TEXT 1 HERE | 7 | OLD TEXT 1 HERE |
| 8 | </old_text> | 8 | </old_text> |
| 9 | <new_text> | 9 | <new_text> |
| 10 | NEW TEXT 1 HERE | 10 | NEW TEXT 1 HERE |
| 11 | </new_text> | 11 | </new_text> |
| 12 | 12 | ||
| 13 | <old_text> | 13 | <old_text line=456> |
| 14 | OLD TEXT 2 HERE | 14 | OLD TEXT 2 HERE |
| 15 | </old_text> | 15 | </old_text> |
| 16 | <new_text> | 16 | <new_text> |
| 17 | NEW TEXT 2 HERE | 17 | NEW TEXT 2 HERE |
| 18 | </new_text> | 18 | </new_text> |
| 19 | 19 | ||
| 20 | <old_text> | 20 | <old_text line=42> |
| 21 | OLD TEXT 3 HERE | 21 | OLD TEXT 3 HERE |
| 22 | </old_text> | 22 | </old_text> |
| 23 | <new_text> | 23 | <new_text> |
| @@ -33,6 +33,7 @@ NEW TEXT 3 HERE · 9 unmodified lines | |||
| 33 | - `<old_text>` must exactly match existing file content, including indentation | 33 | - `<old_text>` must exactly match existing file content, including indentation |
| 34 | - `<old_text>` must come from the actual file, not an outline | 34 | - `<old_text>` must come from the actual file, not an outline |
| 35 | - `<old_text>` cannot be empty | 35 | - `<old_text>` cannot be empty |
| 36 | - `line` should be a starting line number for the text to be replaced | ||
| 36 | - Be minimal with replacements: | 37 | - Be minimal with replacements: |
| 37 | - For unique lines, include only those lines | 38 | - For unique lines, include only those lines |
| 38 | - For non-unique lines, include enough context to identify them | 39 | - For non-unique lines, include enough context to identify them |
| @@ -48,7 +49,7 @@ Claude and gpt-4.1 don't really need it. --}} · 9 unmodified lines | |||
| 48 | <example> | 49 | <example> |
| 49 | <edits> | 50 | <edits> |
| 50 | 51 | ||
| 51 | <old_text> | 52 | <old_text line=3> |
| 52 | struct User { | 53 | struct User { |
| 53 | name: String, | 54 | name: String, |
| 54 | email: String, | 55 | email: String, |
| @@ -62,7 +63,7 @@ struct User { · 7 unmodified lines | |||
| 62 | } | 63 | } |
| 63 | </new_text> | 64 | </new_text> |
| 64 | 65 | ||
| 65 | <old_text> | 66 | <old_text line=25> |
| 66 | let user = User { | 67 | let user = User { |
| 67 | name: String::from("John"), | 68 | name: String::from("John"), |
| 68 | email: String::from("john@example.com"), | 69 | email: String::from("john@example.com"), |
| @@ -638,29 +638,36 @@ impl ToolCard for TerminalToolCard { · 637 unmodified lines | |||
| 638 | .bg(cx.theme().colors().editor_background) | 638 | .bg(cx.theme().colors().editor_background) |
| 639 | .rounded_b_md() | 639 | .rounded_b_md() |
| 640 | .text_ui_sm(cx) | 640 | .text_ui_sm(cx) |
| 641 | .child( | 641 | .child({ |
| 642 | ToolOutputPreview::new( | 642 | let content_mode = terminal.read(cx).content_mode(window, cx); |
| 643 | terminal.clone().into_any_element(), | 643 | |
| 644 | terminal.entity_id(), | 644 | if content_mode.is_scrollable() { |
| 645 | ) | 645 | div().h_72().child(terminal.clone()).into_any_element() |
| 646 | .with_total_lines(self.content_line_count) | 646 | } else { |
| 647 | .toggle_state(!terminal.read(cx).is_content_limited(window)) | 647 | ToolOutputPreview::new( |
| 648 | .on_toggle({ | 648 | terminal.clone().into_any_element(), |
| 649 | let terminal = terminal.clone(); | 649 | terminal.entity_id(), |
| 650 | move |is_expanded, _, cx| { | 650 | ) |
| 651 | terminal.update(cx, |terminal, cx| { | 651 | .with_total_lines(self.content_line_count) |
| 652 | terminal.set_embedded_mode( | 652 | .toggle_state(!content_mode.is_limited()) |
| 653 | if is_expanded { | 653 | .on_toggle({ |
| 654 | None | 654 | let terminal = terminal.clone(); |
| 655 | } else { | 655 | move |is_expanded, _, cx| { |
| 656 | Some(COLLAPSED_LINES) | 656 | terminal.update(cx, |terminal, cx| { |
| 657 | }, | 657 | terminal.set_embedded_mode( |
| 658 | cx, | 658 | if is_expanded { |
| 659 | ); | 659 | None |
| 660 | }); | 660 | } else { |
| 661 | } | 661 | Some(COLLAPSED_LINES) |
| 662 | }), | 662 | }, |
| 663 | ), | 663 | cx, |
| 664 | ); | ||
| 665 | }); | ||
| 666 | } | ||
| 667 | }) | ||
| 668 | .into_any_element() | ||
| 669 | } | ||
| 670 | }), | ||
| 664 | ) | 671 | ) |
| 665 | }, | 672 | }, |
| 666 | ) | 673 | ) |
| @@ -221,7 +221,7 @@ pub fn check(_: &Check, window: &mut Window, cx: &mut App) { · 220 unmodified lines | |||
| 221 | } | 221 | } |
| 222 | 222 | ||
| 223 | if let Some(updater) = AutoUpdater::get(cx) { | 223 | if let Some(updater) = AutoUpdater::get(cx) { |
| 224 | updater.update(cx, |updater, cx| updater.poll(cx)); | 224 | updater.update(cx, |updater, cx| updater.poll(UpdateCheckType::Manual, cx)); |
| 225 | } else { | 225 | } else { |
| 226 | drop(window.prompt( | 226 | drop(window.prompt( |
| 227 | gpui::PromptLevel::Info, | 227 | gpui::PromptLevel::Info, |
| @@ -296,6 +296,11 @@ impl InstallerDir { · 68 unmodified lines | |||
| 296 | } | 296 | } |
| 297 | } | 297 | } |
| 298 | 298 | ||
| 299 | pub enum UpdateCheckType { | ||
| 300 | Automatic, | ||
| 301 | Manual, | ||
| 302 | } | ||
| 303 | |||
| 299 | impl AutoUpdater { | 304 | impl AutoUpdater { |
| 300 | pub fn get(cx: &mut App) -> Option<Entity<Self>> { | 305 | pub fn get(cx: &mut App) -> Option<Entity<Self>> { |
| 301 | cx.default_global::<GlobalAutoUpdate>().0.clone() | 306 | cx.default_global::<GlobalAutoUpdate>().0.clone() |
| @@ -313,13 +318,13 @@ impl AutoUpdater { · 11 unmodified lines | |||
| 313 | pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> { | 318 | pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> { |
| 314 | cx.spawn(async move |this, cx| { | 319 | cx.spawn(async move |this, cx| { |
| 315 | loop { | 320 | loop { |
| 316 | this.update(cx, |this, cx| this.poll(cx))?; | 321 | this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?; |
| 317 | cx.background_executor().timer(POLL_INTERVAL).await; | 322 | cx.background_executor().timer(POLL_INTERVAL).await; |
| 318 | } | 323 | } |
| 319 | }) | 324 | }) |
| 320 | } | 325 | } |
| 321 | 326 | ||
| 322 | pub fn poll(&mut self, cx: &mut Context<Self>) { | 327 | pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context<Self>) { |
| 323 | if self.pending_poll.is_some() { | 328 | if self.pending_poll.is_some() { |
| 324 | return; | 329 | return; |
| 325 | } | 330 | } |
| @@ -331,8 +336,18 @@ impl AutoUpdater { · 5 unmodified lines | |||
| 331 | this.update(cx, |this, cx| { | 336 | this.update(cx, |this, cx| { |
| 332 | this.pending_poll = None; | 337 | this.pending_poll = None; |
| 333 | if let Err(error) = result { | 338 | if let Err(error) = result { |
| 334 | log::error!("auto-update failed: error:{:?}", error); | 339 | this.status = match check_type { |
| 335 | this.status = AutoUpdateStatus::Errored; | 340 | // Be quiet if the check was automated (e.g. when offline) |
| 341 | UpdateCheckType::Automatic => { | ||
| 342 | log::info!("auto-update check failed: error:{:?}", error); | ||
| 343 | AutoUpdateStatus::Idle | ||
| 344 | } | ||
| 345 | UpdateCheckType::Manual => { | ||
| 346 | log::error!("auto-update failed: error:{:?}", error); | ||
| 347 | AutoUpdateStatus::Errored | ||
| 348 | } | ||
| 349 | }; | ||
| 350 | |||
| 336 | cx.notify(); | 351 | cx.notify(); |
| 337 | } | 352 | } |
| 338 | }) | 353 | }) |
| @@ -452,6 +452,10 @@ impl Model { · 451 unmodified lines | |||
| 452 | | Model::Claude3_5SonnetV2 | 452 | | Model::Claude3_5SonnetV2 |
| 453 | | Model::Claude3_7Sonnet | 453 | | Model::Claude3_7Sonnet |
| 454 | | Model::Claude3_7SonnetThinking | 454 | | Model::Claude3_7SonnetThinking |
| 455 | | Model::ClaudeSonnet4 | ||
| 456 | | Model::ClaudeSonnet4Thinking | ||
| 457 | | Model::ClaudeOpus4 | ||
| 458 | | Model::ClaudeOpus4Thinking | ||
| 455 | | Model::Claude3Haiku | 459 | | Model::Claude3Haiku |
| 456 | | Model::Claude3Opus | 460 | | Model::Claude3Opus |
| 457 | | Model::Claude3Sonnet | 461 | | Model::Claude3Sonnet |
| @@ -111,7 +111,7 @@ pub struct ChannelMembership { · 110 unmodified lines | |||
| 111 | pub role: proto::ChannelRole, | 111 | pub role: proto::ChannelRole, |
| 112 | } | 112 | } |
| 113 | impl ChannelMembership { | 113 | impl ChannelMembership { |
| 114 | pub fn sort_key(&self) -> MembershipSortKey { | 114 | pub fn sort_key(&self) -> MembershipSortKey<'_> { |
| 115 | MembershipSortKey { | 115 | MembershipSortKey { |
| 116 | role_order: match self.role { | 116 | role_order: match self.role { |
| 117 | proto::ChannelRole::Admin => 0, | 117 | proto::ChannelRole::Admin => 0, |
| @@ -32,7 +32,7 @@ impl ChannelIndex { · 31 unmodified lines | |||
| 32 | .retain(|channel_id| !channels.contains(channel_id)); | 32 | .retain(|channel_id| !channels.contains(channel_id)); |
| 33 | } | 33 | } |
| 34 | 34 | ||
| 35 | pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard { | 35 | pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard<'_> { |
| 36 | ChannelPathsInsertGuard { | 36 | ChannelPathsInsertGuard { |
| 37 | channels_ordered: &mut self.channels_ordered, | 37 | channels_ordered: &mut self.channels_ordered, |
| 38 | channels_by_id: &mut self.channels_by_id, | 38 | channels_by_id: &mut self.channels_by_id, |
| @@ -269,7 +269,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) { · 268 unmodified lines | |||
| 269 | github_login: "nathansobo".into(), | 269 | github_login: "nathansobo".into(), |
| 270 | avatar_url: "http://avatar.com/nathansobo".into(), | 270 | avatar_url: "http://avatar.com/nathansobo".into(), |
| 271 | name: None, | 271 | name: None, |
| 272 | email: None, | ||
| 273 | }], | 272 | }], |
| 274 | }, | 273 | }, |
| 275 | ); | 274 | ); |
| @@ -323,7 +322,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) { · 47 unmodified lines | |||
| 323 | github_login: "maxbrunsfeld".into(), | 322 | github_login: "maxbrunsfeld".into(), |
| 324 | avatar_url: "http://avatar.com/maxbrunsfeld".into(), | 323 | avatar_url: "http://avatar.com/maxbrunsfeld".into(), |
| 325 | name: None, | 324 | name: None, |
| 326 | email: None, | ||
| 327 | }], | 325 | }], |
| 328 | }, | 326 | }, |
| 329 | ); | 327 | ); |
| @@ -368,7 +366,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) { · 38 unmodified lines | |||
| 368 | github_login: "as-cii".into(), | 366 | github_login: "as-cii".into(), |
| 369 | avatar_url: "http://avatar.com/as-cii".into(), | 367 | avatar_url: "http://avatar.com/as-cii".into(), |
| 370 | name: None, | 368 | name: None, |
| 371 | email: None, | ||
| 372 | }], | 369 | }], |
| 373 | }, | 370 | }, |
| 374 | ); | 371 | ); |
| @@ -127,6 +127,9 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> { · 126 unmodified lines | |||
| 127 | } | 127 | } |
| 128 | 128 | ||
| 129 | fn main() -> Result<()> { | 129 | fn main() -> Result<()> { |
| 130 | #[cfg(unix)] | ||
| 131 | util::prevent_root_execution(); | ||
| 132 | |||
| 130 | // Exit flatpak sandbox if needed | 133 | // Exit flatpak sandbox if needed |
| 131 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] | 134 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] |
| 132 | { | 135 | { |
| @@ -28,6 +28,9 @@ feature_flags.workspace = true · 27 unmodified lines | |||
| 28 | futures.workspace = true | 28 | futures.workspace = true |
| 29 | gpui.workspace = true | 29 | gpui.workspace = true |
| 30 | gpui_tokio.workspace = true | 30 | gpui_tokio.workspace = true |
| 31 | # Don't update `hickory-resolver`, it has a bug that causes it to not resolve DNS queries correctly. | ||
| 32 | # See https://github.com/hickory-dns/hickory-dns/issues/3048 | ||
| 33 | hickory-resolver = { version = "0.24", features = ["tokio-runtime"] } | ||
| 31 | http_client.workspace = true | 34 | http_client.workspace = true |
| 32 | http_client_tls.workspace = true | 35 | http_client_tls.workspace = true |
| 33 | httparse = "1.10" | 36 | httparse = "1.10" |
| @@ -36,6 +39,7 @@ paths.workspace = true · 2 unmodified lines | |||
| 36 | parking_lot.workspace = true | 39 | parking_lot.workspace = true |
| 37 | postage.workspace = true | 40 | postage.workspace = true |
| 38 | rand.workspace = true | 41 | rand.workspace = true |
| 42 | regex.workspace = true | ||
| 39 | release_channel.workspace = true | 43 | release_channel.workspace = true |
| 40 | rpc = { workspace = true, features = ["gpui"] } | 44 | rpc = { workspace = true, features = ["gpui"] } |
| 41 | schemars.workspace = true | 45 | schemars.workspace = true |
| @@ -48,7 +52,7 @@ telemetry_events.workspace = true · 6 unmodified lines | |||
| 48 | text.workspace = true | 52 | text.workspace = true |
| 49 | thiserror.workspace = true | 53 | thiserror.workspace = true |
| 50 | time.workspace = true | 54 | time.workspace = true |
| 51 | tiny_http = "0.8" | 55 | tiny_http.workspace = true |
| 52 | tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] } | 56 | tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] } |
| 53 | url.workspace = true | 57 | url.workspace = true |
| 54 | util.workspace = true | 58 | util.workspace = true |
| @@ -60,11 +64,12 @@ workspace-hack.workspace = true · 5 unmodified lines | |||
| 60 | [dev-dependencies] | 64 | [dev-dependencies] |
| 61 | clock = { workspace = true, features = ["test-support"] } | 65 | clock = { workspace = true, features = ["test-support"] } |
| 62 | collections = { workspace = true, features = ["test-support"] } | 66 | collections = { workspace = true, features = ["test-support"] } |
| 67 | fs.workspace = true | ||
| 63 | gpui = { workspace = true, features = ["test-support"] } | 68 | gpui = { workspace = true, features = ["test-support"] } |
| 69 | http_client = { workspace = true, features = ["test-support"] } | ||
| 64 | rpc = { workspace = true, features = ["test-support"] } | 70 | rpc = { workspace = true, features = ["test-support"] } |
| 65 | settings = { workspace = true, features = ["test-support"] } | 71 | settings = { workspace = true, features = ["test-support"] } |
| 66 | util = { workspace = true, features = ["test-support"] } | 72 | util = { workspace = true, features = ["test-support"] } |
| 67 | http_client = { workspace = true, features = ["test-support"] } | ||
| 68 | 73 | ||
| 69 | [target.'cfg(target_os = "windows")'.dependencies] | 74 | [target.'cfg(target_os = "windows")'.dependencies] |
| 70 | windows.workspace = true | 75 | windows.workspace = true |
| @@ -1887,8 +1887,16 @@ mod tests { · 1886 unmodified lines | |||
| 1887 | .set_entity(&entity3, &mut cx.to_async()); | 1887 | .set_entity(&entity3, &mut cx.to_async()); |
| 1888 | drop(subscription3); | 1888 | drop(subscription3); |
| 1889 | 1889 | ||
| 1890 | server.send(proto::JoinProject { project_id: 1 }); | 1890 | server.send(proto::JoinProject { |
| 1891 | server.send(proto::JoinProject { project_id: 2 }); | 1891 | project_id: 1, |
| 1892 | committer_name: None, | ||
| 1893 | committer_email: None, | ||
| 1894 | }); | ||
| 1895 | server.send(proto::JoinProject { | ||
| 1896 | project_id: 2, | ||
| 1897 | committer_name: None, | ||
| 1898 | committer_email: None, | ||
| 1899 | }); | ||
| 1892 | done_rx1.recv().await.unwrap(); | 1900 | done_rx1.recv().await.unwrap(); |
| 1893 | done_rx2.recv().await.unwrap(); | 1901 | done_rx2.recv().await.unwrap(); |
| 1894 | } | 1902 | } |
| @@ -3,20 +3,30 @@ · 2 unmodified lines | |||
| 3 | mod http_proxy; | 3 | mod http_proxy; |
| 4 | mod socks_proxy; | 4 | mod socks_proxy; |
| 5 | 5 | ||
| 6 | use std::sync::LazyLock; | ||
| 7 | |||
| 6 | use anyhow::{Context as _, Result}; | 8 | use anyhow::{Context as _, Result}; |
| 9 | use hickory_resolver::{ | ||
| 10 | AsyncResolver, TokioAsyncResolver, | ||
| 11 | config::LookupIpStrategy, | ||
| 12 | name_server::{GenericConnector, TokioRuntimeProvider}, | ||
| 13 | system_conf, | ||
| 14 | }; | ||
| 7 | use http_client::Url; | 15 | use http_client::Url; |
| 8 | use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy}; | 16 | use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy}; |
| 9 | use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy}; | 17 | use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy}; |
| 18 | use tokio_socks::{IntoTargetAddr, TargetAddr}; | ||
| 19 | use util::ResultExt; | ||
| 10 | 20 | ||
| 11 | pub(crate) async fn connect_proxy_stream( | 21 | pub(crate) async fn connect_proxy_stream( |
| 12 | proxy: &Url, | 22 | proxy: &Url, |
| 13 | rpc_host: (&str, u16), | 23 | rpc_host: (&str, u16), |
| 14 | ) -> Result<Box<dyn AsyncReadWrite>> { | 24 | ) -> Result<Box<dyn AsyncReadWrite>> { |
| 15 | let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy) else { | 25 | let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy).await else { |
| 16 | // If parsing the proxy URL fails, we must avoid falling back to an insecure connection. | 26 | // If parsing the proxy URL fails, we must avoid falling back to an insecure connection. |
| 17 | // SOCKS proxies are often used in contexts where security and privacy are critical, | 27 | // SOCKS proxies are often used in contexts where security and privacy are critical, |
| 18 | // so any fallback could expose users to significant risks. | 28 | // so any fallback could expose users to significant risks. |
| 19 | anyhow::bail!("Parsing proxy url failed"); | 29 | anyhow::bail!("Parsing proxy url type failed"); |
| 20 | }; | 30 | }; |
| 21 | 31 | ||
| 22 | // Connect to proxy and wrap protocol later | 32 | // Connect to proxy and wrap protocol later |
| @@ -39,10 +49,8 @@ enum ProxyType<'t> { · 16 unmodified lines | |||
| 39 | HttpProxy(HttpProxyType<'t>), | 49 | HttpProxy(HttpProxyType<'t>), |
| 40 | } | 50 | } |
| 41 | 51 | ||
| 42 | fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType)> { | 52 | async fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> { |
| 43 | let scheme = proxy.scheme(); | 53 | let scheme = proxy.scheme(); |
| 44 | let host = proxy.host()?.to_string(); | ||
| 45 | let port = proxy.port_or_known_default()?; | ||
| 46 | let proxy_type = match scheme { | 54 | let proxy_type = match scheme { |
| 47 | scheme if scheme.starts_with("socks") => { | 55 | scheme if scheme.starts_with("socks") => { |
| 48 | Some(ProxyType::SocksProxy(parse_socks_proxy(scheme, proxy))) | 56 | Some(ProxyType::SocksProxy(parse_socks_proxy(scheme, proxy))) |
| @@ -52,8 +60,38 @@ fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType)> { · 3 unmodified lines | |||
| 52 | } | 60 | } |
| 53 | _ => None, | 61 | _ => None, |
| 54 | }?; | 62 | }?; |
| 63 | let (ip, port) = { | ||
| 64 | let host = proxy.host()?.to_string(); | ||
| 65 | let port = proxy.port_or_known_default()?; | ||
| 66 | resolve_proxy_url_if_needed((host, port)).await.log_err()? | ||
| 67 | }; | ||
| 68 | |||
| 69 | Some(((ip, port), proxy_type)) | ||
| 70 | } | ||
| 71 | |||
| 72 | static SYSTEM_DNS_RESOLVER: LazyLock<AsyncResolver<GenericConnector<TokioRuntimeProvider>>> = | ||
| 73 | LazyLock::new(|| { | ||
| 74 | let (config, mut opts) = system_conf::read_system_conf().unwrap(); | ||
| 75 | opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; | ||
| 76 | TokioAsyncResolver::tokio(config, opts) | ||
| 77 | }); | ||
| 55 | 78 | ||
| 56 | Some(((host, port), proxy_type)) | 79 | async fn resolve_proxy_url_if_needed(proxy: (String, u16)) -> Result<(String, u16)> { |
| 80 | let proxy = proxy | ||
| 81 | .into_target_addr() | ||
| 82 | .context("Failed to parse proxy addr")?; | ||
| 83 | match proxy { | ||
| 84 | TargetAddr::Domain(domain, port) => { | ||
| 85 | let ip = SYSTEM_DNS_RESOLVER | ||
| 86 | .lookup_ip(domain.as_ref()) | ||
| 87 | .await? | ||
| 88 | .into_iter() | ||
| 89 | .next() | ||
| 90 | .ok_or_else(|| anyhow::anyhow!("No IP found for proxy domain {domain}"))?; | ||
| 91 | Ok((ip.to_string(), port)) | ||
| 92 | } | ||
| 93 | TargetAddr::Ip(ip_addr) => Ok((ip_addr.ip().to_string(), ip_addr.port())), | ||
| 94 | } | ||
| 57 | } | 95 | } |
| 58 | 96 | ||
| 59 | pub(crate) trait AsyncReadWrite: | 97 | pub(crate) trait AsyncReadWrite: |
| @@ -1,5 +1,7 @@ | |||
| 1 | //! socks proxy | 1 | //! socks proxy |
| 2 | 2 | ||
| 3 | use std::net::SocketAddr; | ||
| 4 | |||
| 3 | use anyhow::{Context as _, Result}; | 5 | use anyhow::{Context as _, Result}; |
| 4 | use http_client::Url; | 6 | use http_client::Url; |
| 5 | use tokio::net::TcpStream; | 7 | use tokio::net::TcpStream; |
| @@ -8,6 +10,8 @@ use tokio_socks::{ · 2 unmodified lines | |||
| 8 | tcp::{Socks4Stream, Socks5Stream}, | 10 | tcp::{Socks4Stream, Socks5Stream}, |
| 9 | }; | 11 | }; |
| 10 | 12 | ||
| 13 | use crate::proxy::SYSTEM_DNS_RESOLVER; | ||
| 14 | |||
| 11 | use super::AsyncReadWrite; | 15 | use super::AsyncReadWrite; |
| 12 | 16 | ||
| 13 | /// Identification to a Socks V4 Proxy | 17 | /// Identification to a Socks V4 Proxy |
| @@ -73,12 +77,14 @@ pub(super) async fn connect_socks_proxy_stream( · 59 unmodified lines | |||
| 73 | }; | 77 | }; |
| 74 | let rpc_host = match (rpc_host, local_dns) { | 78 | let rpc_host = match (rpc_host, local_dns) { |
| 75 | (TargetAddr::Domain(domain, port), true) => { | 79 | (TargetAddr::Domain(domain, port), true) => { |
| 76 | let ip_addr = tokio::net::lookup_host((domain.as_ref(), port)) | 80 | let ip_addr = SYSTEM_DNS_RESOLVER |
| 81 | .lookup_ip(domain.as_ref()) | ||
| 77 | .await | 82 | .await |
| 78 | .with_context(|| format!("Failed to lookup domain {}", domain))? | 83 | .with_context(|| format!("Failed to lookup domain {}", domain))? |
| 84 | .into_iter() | ||
| 79 | .next() | 85 | .next() |
| 80 | .ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?; | 86 | .ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?; |
| 81 | TargetAddr::Ip(ip_addr) | 87 | TargetAddr::Ip(SocketAddr::new(ip_addr, port)) |
| 82 | } | 88 | } |
| 83 | (rpc_host, _) => rpc_host, | 89 | (rpc_host, _) => rpc_host, |
| 84 | }; | 90 | }; |
| @@ -8,10 +8,11 @@ use futures::{Future, FutureExt, StreamExt}; · 7 unmodified lines | |||
| 8 | use gpui::{App, AppContext as _, BackgroundExecutor, Task}; | 8 | use gpui::{App, AppContext as _, BackgroundExecutor, Task}; |
| 9 | use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request}; | 9 | use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request}; |
| 10 | use parking_lot::Mutex; | 10 | use parking_lot::Mutex; |
| 11 | use regex::Regex; | ||
| 11 | use release_channel::ReleaseChannel; | 12 | use release_channel::ReleaseChannel; |
| 12 | use settings::{Settings, SettingsStore}; | 13 | use settings::{Settings, SettingsStore}; |
| 13 | use sha2::{Digest, Sha256}; | 14 | use sha2::{Digest, Sha256}; |
| 14 | use std::collections::{HashMap, HashSet}; | 15 | use std::collections::HashSet; |
| 15 | use std::fs::File; | 16 | use std::fs::File; |
| 16 | use std::io::Write; | 17 | use std::io::Write; |
| 17 | use std::sync::LazyLock; | 18 | use std::sync::LazyLock; |
| @@ -45,31 +46,13 @@ struct TelemetryState { · 27 unmodified lines | |||
| 45 | first_event_date_time: Option<Instant>, | 46 | first_event_date_time: Option<Instant>, |
| 46 | event_coalescer: EventCoalescer, | 47 | event_coalescer: EventCoalescer, |
| 47 | max_queue_size: usize, | 48 | max_queue_size: usize, |
| 48 | worktree_id_map: WorktreeIdMap, | 49 | worktrees_with_project_type_events_sent: HashSet<WorktreeId>, |
| 49 | 50 | ||
| 50 | os_name: String, | 51 | os_name: String, |
| 51 | app_version: String, | 52 | app_version: String, |
| 52 | os_version: Option<String>, | 53 | os_version: Option<String>, |
| 53 | } | 54 | } |
| 54 | 55 | ||
| 55 | #[derive(Debug)] | ||
| 56 | struct WorktreeIdMap(HashMap<String, ProjectCache>); | ||
| 57 | |||
| 58 | #[derive(Debug)] | ||
| 59 | struct ProjectCache { | ||
| 60 | name: String, | ||
| 61 | worktree_ids_reported: HashSet<WorktreeId>, | ||
| 62 | } | ||
| 63 | |||
| 64 | impl ProjectCache { | ||
| 65 | fn new(name: String) -> Self { | ||
| 66 | Self { | ||
| 67 | name, | ||
| 68 | worktree_ids_reported: HashSet::default(), | ||
| 69 | } | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | #[cfg(debug_assertions)] | 56 | #[cfg(debug_assertions)] |
| 74 | const MAX_QUEUE_LEN: usize = 5; | 57 | const MAX_QUEUE_LEN: usize = 5; |
| 75 | 58 | ||
| @@ -91,6 +74,10 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| { · 15 unmodified lines | |||
| 91 | }) | 74 | }) |
| 92 | }); | 75 | }); |
| 93 | 76 | ||
| 77 | static DOTNET_PROJECT_FILES_REGEX: LazyLock<Regex> = LazyLock::new(|| { | ||
| 78 | Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap() | ||
| 79 | }); | ||
| 80 | |||
| 94 | pub fn os_name() -> String { | 81 | pub fn os_name() -> String { |
| 95 | #[cfg(target_os = "macos")] | 82 | #[cfg(target_os = "macos")] |
| 96 | { | 83 | { |
| @@ -194,20 +181,7 @@ impl Telemetry { · 97 unmodified lines | |||
| 194 | first_event_date_time: None, | 181 | first_event_date_time: None, |
| 195 | event_coalescer: EventCoalescer::new(clock.clone()), | 182 | event_coalescer: EventCoalescer::new(clock.clone()), |
| 196 | max_queue_size: MAX_QUEUE_LEN, | 183 | max_queue_size: MAX_QUEUE_LEN, |
| 197 | worktree_id_map: WorktreeIdMap(HashMap::from_iter([ | 184 | worktrees_with_project_type_events_sent: HashSet::new(), |
| 198 | ( | ||
| 199 | "pnpm-lock.yaml".to_string(), | ||
| 200 | ProjectCache::new("pnpm".to_string()), | ||
| 201 | ), | ||
| 202 | ( | ||
| 203 | "yarn.lock".to_string(), | ||
| 204 | ProjectCache::new("yarn".to_string()), | ||
| 205 | ), | ||
| 206 | ( | ||
| 207 | "package.json".to_string(), | ||
| 208 | ProjectCache::new("node".to_string()), | ||
| 209 | ), | ||
| 210 | ])), | ||
| 211 | 185 | ||
| 212 | os_version: None, | 186 | os_version: None, |
| 213 | os_name: os_name(), | 187 | os_name: os_name(), |
| @@ -371,49 +345,58 @@ impl Telemetry { · 157 unmodified lines | |||
| 371 | } | 345 | } |
| 372 | } | 346 | } |
| 373 | 347 | ||
| 374 | pub fn report_discovered_project_events( | 348 | pub fn report_discovered_project_type_events( |
| 375 | self: &Arc<Self>, | 349 | self: &Arc<Self>, |
| 376 | worktree_id: WorktreeId, | 350 | worktree_id: WorktreeId, |
| 377 | updated_entries_set: &UpdatedEntriesSet, | 351 | updated_entries_set: &UpdatedEntriesSet, |
| 378 | ) { | 352 | ) { |
| 379 | let project_type_names: Vec<String> = { | 353 | let project_type_names = self.detect_project_types(worktree_id, updated_entries_set); |
| 380 | let mut state = self.state.lock(); | ||
| 381 | state | ||
| 382 | .worktree_id_map | ||
| 383 | .0 | ||
| 384 | .iter_mut() | ||
| 385 | .filter_map(|(project_file_name, project_type_telemetry)| { | ||
| 386 | if project_type_telemetry | ||
| 387 | .worktree_ids_reported | ||
| 388 | .contains(&worktree_id) | ||
| 389 | { | ||
| 390 | return None; | ||
| 391 | } | ||
| 392 | 354 | ||
| 393 | let project_file_found = updated_entries_set.iter().any(|(path, _, _)| { | 355 | for project_type_name in project_type_names { |
| 394 | path.as_ref() | 356 | telemetry::event!("Project Opened", project_type = project_type_name); |
| 395 | .file_name() | 357 | } |
| 396 | .and_then(|name| name.to_str()) | 358 | } |
| 397 | .map(|name_str| name_str == project_file_name) | ||
| 398 | .unwrap_or(false) | ||
| 399 | }); | ||
| 400 | 359 | ||
| 401 | if !project_file_found { | 360 | fn detect_project_types( |
| 402 | return None; | 361 | self: &Arc<Self>, |
| 403 | } | 362 | worktree_id: WorktreeId, |
| 363 | updated_entries_set: &UpdatedEntriesSet, | ||
| 364 | ) -> Vec<String> { | ||
| 365 | let mut state = self.state.lock(); | ||
| 366 | let mut project_names: HashSet<String> = HashSet::new(); | ||
| 404 | 367 | ||
| 405 | project_type_telemetry | 368 | if state |
| 406 | .worktree_ids_reported | 369 | .worktrees_with_project_type_events_sent |
| 407 | .insert(worktree_id); | 370 | .contains(&worktree_id) |
| 371 | { | ||
| 372 | return project_names.into_iter().collect(); | ||
| 373 | } | ||
| 408 | 374 | ||
| 409 | Some(project_type_telemetry.name.clone()) | 375 | for (path, _, _) in updated_entries_set.iter() { |
| 410 | }) | 376 | let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else { |
| 411 | .collect() | 377 | continue; |
| 412 | }; | 378 | }; |
| 413 | 379 | ||
| 414 | for project_type_name in project_type_names { | 380 | if file_name == "pnpm-lock.yaml" { |
| 415 | telemetry::event!("Project Opened", project_type = project_type_name); | 381 | project_names.insert("pnpm".to_string()); |
| 382 | } else if file_name == "yarn.lock" { | ||
| 383 | project_names.insert("yarn".to_string()); | ||
| 384 | } else if file_name == "package.json" { | ||
| 385 | project_names.insert("node".to_string()); | ||
| 386 | } else if DOTNET_PROJECT_FILES_REGEX.is_match(file_name) { | ||
| 387 | project_names.insert("dotnet".to_string()); | ||
| 388 | } | ||
| 416 | } | 389 | } |
| 390 | |||
| 391 | if !project_names.is_empty() { | ||
| 392 | state | ||
| 393 | .worktrees_with_project_type_events_sent | ||
| 394 | .insert(worktree_id); | ||
| 395 | } | ||
| 396 | |||
| 397 | let mut project_names_vec: Vec<String> = project_names.into_iter().collect(); | ||
| 398 | project_names_vec.sort(); | ||
| 399 | project_names_vec | ||
| 417 | } | 400 | } |
| 418 | 401 | ||
| 419 | fn report_event(self: &Arc<Self>, event: Event) { | 402 | fn report_event(self: &Arc<Self>, event: Event) { |
| @@ -578,7 +561,9 @@ mod tests { · 158 unmodified lines | |||
| 578 | use clock::FakeSystemClock; | 561 | use clock::FakeSystemClock; |
| 579 | use gpui::TestAppContext; | 562 | use gpui::TestAppContext; |
| 580 | use http_client::FakeHttpClient; | 563 | use http_client::FakeHttpClient; |
| 564 | use std::collections::HashMap; | ||
| 581 | use telemetry_events::FlexibleEvent; | 565 | use telemetry_events::FlexibleEvent; |
| 566 | use worktree::{PathChange, ProjectEntryId, WorktreeId}; | ||
| 582 | 567 | ||
| 583 | #[gpui::test] | 568 | #[gpui::test] |
| 584 | fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) { | 569 | fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) { |
| @@ -696,6 +681,97 @@ mod tests { · 111 unmodified lines | |||
| 696 | }); | 681 | }); |
| 697 | } | 682 | } |
| 698 | 683 | ||
| 684 | #[gpui::test] | ||
| 685 | fn test_project_discovery_does_not_double_report(cx: &mut gpui::TestAppContext) { | ||
| 686 | init_test(cx); | ||
| 687 | |||
| 688 | let clock = Arc::new(FakeSystemClock::new()); | ||
| 689 | let http = FakeHttpClient::with_200_response(); | ||
| 690 | let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx)); | ||
| 691 | let worktree_id = 1; | ||
| 692 | |||
| 693 | // First scan of worktree 1 returns project types | ||
| 694 | test_project_discovery_helper( | ||
| 695 | telemetry.clone(), | ||
| 696 | vec!["package.json"], | ||
| 697 | vec!["node"], | ||
| 698 | worktree_id, | ||
| 699 | ); | ||
| 700 | |||
| 701 | // Rescan of worktree 1 returns nothing as it has already been reported | ||
| 702 | test_project_discovery_helper(telemetry.clone(), vec!["package.json"], vec![], worktree_id); | ||
| 703 | } | ||
| 704 | |||
| 705 | #[gpui::test] | ||
| 706 | fn test_pnpm_project_discovery(cx: &mut gpui::TestAppContext) { | ||
| 707 | init_test(cx); | ||
| 708 | |||
| 709 | let clock = Arc::new(FakeSystemClock::new()); | ||
| 710 | let http = FakeHttpClient::with_200_response(); | ||
| 711 | let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx)); | ||
| 712 | |||
| 713 | test_project_discovery_helper( | ||
| 714 | telemetry.clone(), | ||
| 715 | vec!["package.json", "pnpm-lock.yaml"], | ||
| 716 | vec!["node", "pnpm"], | ||
| 717 | 1, | ||
| 718 | ); | ||
| 719 | } | ||
| 720 | |||
| 721 | #[gpui::test] | ||
| 722 | fn test_yarn_project_discovery(cx: &mut gpui::TestAppContext) { | ||
| 723 | init_test(cx); | ||
| 724 | |||
| 725 | let clock = Arc::new(FakeSystemClock::new()); | ||
| 726 | let http = FakeHttpClient::with_200_response(); | ||
| 727 | let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx)); | ||
| 728 | |||
| 729 | test_project_discovery_helper( | ||
| 730 | telemetry.clone(), | ||
| 731 | vec!["package.json", "yarn.lock"], | ||
| 732 | vec!["node", "yarn"], | ||
| 733 | 1, | ||
| 734 | ); | ||
| 735 | } | ||
| 736 | |||
| 737 | #[gpui::test] | ||
| 738 | fn test_dotnet_project_discovery(cx: &mut gpui::TestAppContext) { | ||
| 739 | init_test(cx); | ||
| 740 | |||
| 741 | let clock = Arc::new(FakeSystemClock::new()); | ||
| 742 | let http = FakeHttpClient::with_200_response(); | ||
| 743 | let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx)); | ||
| 744 | |||
| 745 | // Using different worktrees, as production code blocks from reporting a | ||
| 746 | // project type for the same worktree multiple times | ||
| 747 | |||
| 748 | test_project_discovery_helper( | ||
| 749 | telemetry.clone().clone(), | ||
| 750 | vec!["global.json"], | ||
| 751 | vec!["dotnet"], | ||
| 752 | 1, | ||
| 753 | ); | ||
| 754 | test_project_discovery_helper( | ||
| 755 | telemetry.clone(), | ||
| 756 | vec!["Directory.Build.props"], | ||
| 757 | vec!["dotnet"], | ||
| 758 | 2, | ||
| 759 | ); | ||
| 760 | test_project_discovery_helper(telemetry.clone(), vec!["file.csproj"], vec!["dotnet"], 3); | ||
| 761 | test_project_discovery_helper(telemetry.clone(), vec!["file.fsproj"], vec!["dotnet"], 4); | ||
| 762 | test_project_discovery_helper(telemetry.clone(), vec!["file.vbproj"], vec!["dotnet"], 5); | ||
| 763 | test_project_discovery_helper(telemetry.clone(), vec!["file.sln"], vec!["dotnet"], 6); | ||
| 764 | |||
| 765 | // Each worktree should only send a single project type event, even when | ||
| 766 | // encountering multiple files associated with that project type | ||
| 767 | test_project_discovery_helper( | ||
| 768 | telemetry, | ||
| 769 | vec!["global.json", "Directory.Build.props"], | ||
| 770 | vec!["dotnet"], | ||
| 771 | 7, | ||
| 772 | ); | ||
| 773 | } | ||
| 774 | |||
| 699 | // TODO: | 775 | // TODO: |
| 700 | // Test settings | 776 | // Test settings |
| 701 | // Update FakeHTTPClient to keep track of the number of requests and assert on it | 777 | // Update FakeHTTPClient to keep track of the number of requests and assert on it |
| @@ -712,4 +788,36 @@ mod tests { · 10 unmodified lines | |||
| 712 | && telemetry.state.lock().flush_events_task.is_none() | 788 | && telemetry.state.lock().flush_events_task.is_none() |
| 713 | && telemetry.state.lock().first_event_date_time.is_none() | 789 | && telemetry.state.lock().first_event_date_time.is_none() |
| 714 | } | 790 | } |
| 791 | |||
| 792 | fn test_project_discovery_helper( | ||
| 793 | telemetry: Arc<Telemetry>, | ||
| 794 | file_paths: Vec<&str>, | ||
| 795 | expected_project_types: Vec<&str>, | ||
| 796 | worktree_id_num: usize, | ||
| 797 | ) { | ||
| 798 | let worktree_id = WorktreeId::from_usize(worktree_id_num); | ||
| 799 | let entries: Vec<_> = file_paths | ||
| 800 | .into_iter() | ||
| 801 | .enumerate() | ||
| 802 | .map(|(i, path)| { | ||
| 803 | ( | ||
| 804 | Arc::from(std::path::Path::new(path)), | ||
| 805 | ProjectEntryId::from_proto(i as u64 + 1), | ||
| 806 | PathChange::Added, | ||
| 807 | ) | ||
| 808 | }) | ||
| 809 | .collect(); | ||
| 810 | let updated_entries: UpdatedEntriesSet = Arc::from(entries.as_slice()); | ||
| 811 | |||
| 812 | let mut detected_types = telemetry.detect_project_types(worktree_id, &updated_entries); | ||
| 813 | detected_types.sort(); | ||
| 814 | |||
| 815 | let mut expected_sorted = expected_project_types | ||
| 816 | .into_iter() | ||
| 817 | .map(String::from) | ||
| 818 | .collect::<Vec<_>>(); | ||
| 819 | expected_sorted.sort(); | ||
| 820 | |||
| 821 | assert_eq!(detected_types, expected_sorted); | ||
| 822 | } | ||
| 715 | } | 823 | } |
| @@ -49,7 +49,6 @@ pub struct User { · 48 unmodified lines | |||
| 49 | pub github_login: String, | 49 | pub github_login: String, |
| 50 | pub avatar_uri: SharedUri, | 50 | pub avatar_uri: SharedUri, |
| 51 | pub name: Option<String>, | 51 | pub name: Option<String>, |
| 52 | pub email: Option<String>, | ||
| 53 | } | 52 | } |
| 54 | 53 | ||
| 55 | #[derive(Clone, Debug, PartialEq, Eq)] | 54 | #[derive(Clone, Debug, PartialEq, Eq)] |
| @@ -58,6 +57,8 @@ pub struct Collaborator { · 2 unmodified lines | |||
| 58 | pub replica_id: ReplicaId, | 57 | pub replica_id: ReplicaId, |
| 59 | pub user_id: UserId, | 58 | pub user_id: UserId, |
| 60 | pub is_host: bool, | 59 | pub is_host: bool, |
| 60 | pub committer_name: Option<String>, | ||
| 61 | pub committer_email: Option<String>, | ||
| 61 | } | 62 | } |
| 62 | 63 | ||
| 63 | impl PartialOrd for User { | 64 | impl PartialOrd for User { |
| @@ -881,7 +882,6 @@ impl User { · 817 unmodified lines | |||
| 881 | github_login: message.github_login, | 882 | github_login: message.github_login, |
| 882 | avatar_uri: message.avatar_url.into(), | 883 | avatar_uri: message.avatar_url.into(), |
| 883 | name: message.name, | 884 | name: message.name, |
| 884 | email: message.email, | ||
| 885 | }) | 885 | }) |
| 886 | } | 886 | } |
| 887 | } | 887 | } |
| @@ -912,6 +912,8 @@ impl Collaborator { · 24 unmodified lines | |||
| 912 | replica_id: message.replica_id as ReplicaId, | 912 | replica_id: message.replica_id as ReplicaId, |
| 913 | user_id: message.user_id as UserId, | 913 | user_id: message.user_id as UserId, |
| 914 | is_host: message.is_host, | 914 | is_host: message.is_host, |
| 915 | committer_name: message.committer_name, | ||
| 916 | committer_email: message.committer_email, | ||
| 915 | }) | 917 | }) |
| 916 | } | 918 | } |
| 917 | } | 919 | } |
| @@ -185,7 +185,9 @@ CREATE TABLE "project_collaborators" ( · 184 unmodified lines | |||
| 185 | "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, | 185 | "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, |
| 186 | "user_id" INTEGER NOT NULL, | 186 | "user_id" INTEGER NOT NULL, |
| 187 | "replica_id" INTEGER NOT NULL, | 187 | "replica_id" INTEGER NOT NULL, |
| 188 | "is_host" BOOLEAN NOT NULL | 188 | "is_host" BOOLEAN NOT NULL, |
| 189 | "committer_name" VARCHAR, | ||
| 190 | "committer_email" VARCHAR | ||
| 189 | ); | 191 | ); |
| 190 | 192 | ||
| 191 | CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id"); | 193 | CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id"); |
| @@ -0,0 +1,4 @@ | |||
| 1 | alter table project_collaborators | ||
| 2 | add column committer_name varchar; | ||
| 3 | alter table project_collaborators | ||
| 4 | add column committer_email varchar; | ||
| @@ -97,7 +97,7 @@ impl std::fmt::Display for SystemIdHeader { · 96 unmodified lines | |||
| 97 | 97 | ||
| 98 | pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> { | 98 | pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> { |
| 99 | Router::new() | 99 | Router::new() |
| 100 | .route("/user", get(get_authenticated_user)) | 100 | .route("/user", get(update_or_create_authenticated_user)) |
| 101 | .route("/users/look_up", get(look_up_user)) | 101 | .route("/users/look_up", get(look_up_user)) |
| 102 | .route("/users/:id/access_tokens", post(create_access_token)) | 102 | .route("/users/:id/access_tokens", post(create_access_token)) |
| 103 | .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) | 103 | .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) |
| @@ -157,7 +157,7 @@ struct AuthenticatedUserResponse { · 53 unmodified lines | |||
| 157 | feature_flags: Vec<String>, | 157 | feature_flags: Vec<String>, |
| 158 | } | 158 | } |
| 159 | 159 | ||
| 160 | async fn get_authenticated_user( | 160 | async fn update_or_create_authenticated_user( |
| 161 | Query(params): Query<AuthenticatedUserParams>, | 161 | Query(params): Query<AuthenticatedUserParams>, |
| 162 | Extension(app): Extension<Arc<AppState>>, | 162 | Extension(app): Extension<Arc<AppState>>, |
| 163 | ) -> Result<Json<AuthenticatedUserResponse>> { | 163 | ) -> Result<Json<AuthenticatedUserResponse>> { |
| @@ -165,7 +165,7 @@ async fn get_authenticated_user( · 1 unmodified line | |||
| 165 | 165 | ||
| 166 | let user = app | 166 | let user = app |
| 167 | .db | 167 | .db |
| 168 | .get_or_create_user_by_github_account( | 168 | .update_or_create_user_by_github_account( |
| 169 | ¶ms.github_login, | 169 | ¶ms.github_login, |
| 170 | params.github_user_id, | 170 | params.github_user_id, |
| 171 | params.github_email.as_deref(), | 171 | params.github_email.as_deref(), |
| @@ -751,6 +751,8 @@ pub struct ProjectCollaborator { · 750 unmodified lines | |||
| 751 | pub user_id: UserId, | 751 | pub user_id: UserId, |
| 752 | pub replica_id: ReplicaId, | 752 | pub replica_id: ReplicaId, |
| 753 | pub is_host: bool, | 753 | pub is_host: bool, |
| 754 | pub committer_name: Option<String>, | ||
| 755 | pub committer_email: Option<String>, | ||
| 754 | } | 756 | } |
| 755 | 757 | ||
| 756 | impl ProjectCollaborator { | 758 | impl ProjectCollaborator { |
| @@ -760,6 +762,8 @@ impl ProjectCollaborator { · 3 unmodified lines | |||
| 760 | replica_id: self.replica_id.0 as u32, | 762 | replica_id: self.replica_id.0 as u32, |
| 761 | user_id: self.user_id.to_proto(), | 763 | user_id: self.user_id.to_proto(), |
| 762 | is_host: self.is_host, | 764 | is_host: self.is_host, |
| 765 | committer_name: self.committer_name.clone(), | ||
| 766 | committer_email: self.committer_email.clone(), | ||
| 763 | } | 767 | } |
| 764 | } | 768 | } |
| 765 | } | 769 | } |
| @@ -118,6 +118,8 @@ impl Database { · 117 unmodified lines | |||
| 118 | user_id: collaborator.user_id.to_proto(), | 118 | user_id: collaborator.user_id.to_proto(), |
| 119 | replica_id: collaborator.replica_id.0 as u32, | 119 | replica_id: collaborator.replica_id.0 as u32, |
| 120 | is_host: false, | 120 | is_host: false, |
| 121 | committer_name: None, | ||
| 122 | committer_email: None, | ||
| 121 | }) | 123 | }) |
| 122 | .collect(), | 124 | .collect(), |
| 123 | }) | 125 | }) |
| @@ -225,6 +227,8 @@ impl Database { · 101 unmodified lines | |||
| 225 | user_id: collaborator.user_id.to_proto(), | 227 | user_id: collaborator.user_id.to_proto(), |
| 226 | replica_id: collaborator.replica_id.0 as u32, | 228 | replica_id: collaborator.replica_id.0 as u32, |
| 227 | is_host: false, | 229 | is_host: false, |
| 230 | committer_name: None, | ||
| 231 | committer_email: None, | ||
| 228 | }) | 232 | }) |
| 229 | .collect(), | 233 | .collect(), |
| 230 | }, | 234 | }, |
| @@ -261,6 +265,8 @@ impl Database { · 30 unmodified lines | |||
| 261 | replica_id: db_collaborator.replica_id.0 as u32, | 265 | replica_id: db_collaborator.replica_id.0 as u32, |
| 262 | user_id: db_collaborator.user_id.to_proto(), | 266 | user_id: db_collaborator.user_id.to_proto(), |
| 263 | is_host: false, | 267 | is_host: false, |
| 268 | committer_name: None, | ||
| 269 | committer_email: None, | ||
| 264 | }) | 270 | }) |
| 265 | } else { | 271 | } else { |
| 266 | collaborator_ids_to_remove.push(db_collaborator.id); | 272 | collaborator_ids_to_remove.push(db_collaborator.id); |
| @@ -390,6 +396,8 @@ impl Database { · 123 unmodified lines | |||
| 390 | replica_id: row.replica_id.0 as u32, | 396 | replica_id: row.replica_id.0 as u32, |
| 391 | user_id: row.user_id.to_proto(), | 397 | user_id: row.user_id.to_proto(), |
| 392 | is_host: false, | 398 | is_host: false, |
| 399 | committer_name: None, | ||
| 400 | committer_email: None, | ||
| 393 | }); | 401 | }); |
| 394 | } | 402 | } |
| 395 | 403 | ||
| @@ -501,8 +501,10 @@ impl Database { · 500 unmodified lines | |||
| 501 | 501 | ||
| 502 | /// Returns all channels for the user with the given ID. | 502 | /// Returns all channels for the user with the given ID. |
| 503 | pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> { | 503 | pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> { |
| 504 | self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await }) | 504 | self.weak_transaction( |
| 505 | .await | 505 | |tx| async move { self.get_user_channels(user_id, None, true, &tx).await }, |
| 506 | ) | ||
| 507 | .await | ||
| 506 | } | 508 | } |
| 507 | 509 | ||
| 508 | /// Returns all channels for the user with the given ID that are descendants | 510 | /// Returns all channels for the user with the given ID that are descendants |
| @@ -737,7 +739,6 @@ impl Database { · 228 unmodified lines | |||
| 737 | ), | 739 | ), |
| 738 | github_login: user.github_login, | 740 | github_login: user.github_login, |
| 739 | name: user.name, | 741 | name: user.name, |
| 740 | email: user.email_address, | ||
| 741 | }) | 742 | }) |
| 742 | } | 743 | } |
| 743 | proto::ChannelMember { | 744 | proto::ChannelMember { |
| @@ -15,7 +15,7 @@ impl Database { · 14 unmodified lines | |||
| 15 | user_b_busy: bool, | 15 | user_b_busy: bool, |
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | self.transaction(|tx| async move { | 18 | self.weak_transaction(|tx| async move { |
| 19 | let user_a_participant = Alias::new("user_a_participant"); | 19 | let user_a_participant = Alias::new("user_a_participant"); |
| 20 | let user_b_participant = Alias::new("user_b_participant"); | 20 | let user_b_participant = Alias::new("user_b_participant"); |
| 21 | let mut db_contacts = contact::Entity::find() | 21 | let mut db_contacts = contact::Entity::find() |
| @@ -91,7 +91,7 @@ impl Database { · 69 unmodified lines | |||
| 91 | 91 | ||
| 92 | /// Returns whether the given user is a busy (on a call). | 92 | /// Returns whether the given user is a busy (on a call). |
| 93 | pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> { | 93 | pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> { |
| 94 | self.transaction(|tx| async move { | 94 | self.weak_transaction(|tx| async move { |
| 95 | let participant = room_participant::Entity::find() | 95 | let participant = room_participant::Entity::find() |
| 96 | .filter(room_participant::Column::UserId.eq(user_id)) | 96 | .filter(room_participant::Column::UserId.eq(user_id)) |
| 97 | .one(&*tx) | 97 | .one(&*tx) |
| @@ -71,7 +71,7 @@ impl Database { · 70 unmodified lines | |||
| 71 | ) -> Result<()> { | 71 | ) -> Result<()> { |
| 72 | self.weak_transaction(|tx| async move { | 72 | self.weak_transaction(|tx| async move { |
| 73 | let user = self | 73 | let user = self |
| 74 | .get_or_create_user_by_github_account_tx( | 74 | .update_or_create_user_by_github_account_tx( |
| 75 | github_login, | 75 | github_login, |
| 76 | github_user_id, | 76 | github_user_id, |
| 77 | github_email, | 77 | github_email, |
| @@ -98,7 +98,9 @@ impl Database { · 97 unmodified lines | |||
| 98 | user_id: ActiveValue::set(participant.user_id), | 98 | user_id: ActiveValue::set(participant.user_id), |
| 99 | replica_id: ActiveValue::set(ReplicaId(replica_id)), | 99 | replica_id: ActiveValue::set(ReplicaId(replica_id)), |
| 100 | is_host: ActiveValue::set(true), | 100 | is_host: ActiveValue::set(true), |
| 101 | ..Default::default() | 101 | id: ActiveValue::NotSet, |
| 102 | committer_name: ActiveValue::Set(None), | ||
| 103 | committer_email: ActiveValue::Set(None), | ||
| 102 | } | 104 | } |
| 103 | .insert(&*tx) | 105 | .insert(&*tx) |
| 104 | .await?; | 106 | .await?; |
| @@ -784,13 +786,27 @@ impl Database { · 679 unmodified lines | |||
| 784 | project_id: ProjectId, | 786 | project_id: ProjectId, |
| 785 | connection: ConnectionId, | 787 | connection: ConnectionId, |
| 786 | user_id: UserId, | 788 | user_id: UserId, |
| 789 | committer_name: Option<String>, | ||
| 790 | committer_email: Option<String>, | ||
| 787 | ) -> Result<TransactionGuard<(Project, ReplicaId)>> { | 791 | ) -> Result<TransactionGuard<(Project, ReplicaId)>> { |
| 788 | self.project_transaction(project_id, |tx| async move { | 792 | self.project_transaction(project_id, move |tx| { |
| 789 | let (project, role) = self | 793 | let committer_name = committer_name.clone(); |
| 790 | .access_project(project_id, connection, Capability::ReadOnly, &tx) | 794 | let committer_email = committer_email.clone(); |
| 791 | .await?; | 795 | async move { |
| 792 | self.join_project_internal(project, user_id, connection, role, &tx) | 796 | let (project, role) = self |
| 797 | .access_project(project_id, connection, Capability::ReadOnly, &tx) | ||
| 798 | .await?; | ||
| 799 | self.join_project_internal( | ||
| 800 | project, | ||
| 801 | user_id, | ||
| 802 | committer_name, | ||
| 803 | committer_email, | ||
| 804 | connection, | ||
| 805 | role, | ||
| 806 | &tx, | ||
| 807 | ) | ||
| 793 | .await | 808 | .await |
| 809 | } | ||
| 794 | }) | 810 | }) |
| 795 | .await | 811 | .await |
| 796 | } | 812 | } |
| @@ -799,6 +815,8 @@ impl Database { · 2 unmodified lines | |||
| 799 | &self, | 815 | &self, |
| 800 | project: project::Model, | 816 | project: project::Model, |
| 801 | user_id: UserId, | 817 | user_id: UserId, |
| 818 | committer_name: Option<String>, | ||
| 819 | committer_email: Option<String>, | ||
| 802 | connection: ConnectionId, | 820 | connection: ConnectionId, |
| 803 | role: ChannelRole, | 821 | role: ChannelRole, |
| 804 | tx: &DatabaseTransaction, | 822 | tx: &DatabaseTransaction, |
| @@ -822,7 +840,9 @@ impl Database { · 17 unmodified lines | |||
| 822 | user_id: ActiveValue::set(user_id), | 840 | user_id: ActiveValue::set(user_id), |
| 823 | replica_id: ActiveValue::set(replica_id), | 841 | replica_id: ActiveValue::set(replica_id), |
| 824 | is_host: ActiveValue::set(false), | 842 | is_host: ActiveValue::set(false), |
| 825 | ..Default::default() | 843 | id: ActiveValue::NotSet, |
| 844 | committer_name: ActiveValue::set(committer_name), | ||
| 845 | committer_email: ActiveValue::set(committer_email), | ||
| 826 | } | 846 | } |
| 827 | .insert(tx) | 847 | .insert(tx) |
| 828 | .await?; | 848 | .await?; |
| @@ -1026,6 +1046,8 @@ impl Database { · 197 unmodified lines | |||
| 1026 | user_id: collaborator.user_id, | 1046 | user_id: collaborator.user_id, |
| 1027 | replica_id: collaborator.replica_id, | 1047 | replica_id: collaborator.replica_id, |
| 1028 | is_host: collaborator.is_host, | 1048 | is_host: collaborator.is_host, |
| 1049 | committer_name: collaborator.committer_name, | ||
| 1050 | committer_email: collaborator.committer_email, | ||
| 1029 | }) | 1051 | }) |
| 1030 | .collect(), | 1052 | .collect(), |
| 1031 | worktrees, | 1053 | worktrees, |
| @@ -80,7 +80,7 @@ impl Database { · 79 unmodified lines | |||
| 80 | &self, | 80 | &self, |
| 81 | user_id: UserId, | 81 | user_id: UserId, |
| 82 | ) -> Result<Option<proto::IncomingCall>> { | 82 | ) -> Result<Option<proto::IncomingCall>> { |
| 83 | self.transaction(|tx| async move { | 83 | self.weak_transaction(|tx| async move { |
| 84 | let pending_participant = room_participant::Entity::find() | 84 | let pending_participant = room_participant::Entity::find() |
| 85 | .filter( | 85 | .filter( |
| 86 | room_participant::Column::UserId | 86 | room_participant::Column::UserId |
| @@ -553,6 +553,8 @@ impl Database { · 466 unmodified lines | |||
| 553 | user_id: collaborator.user_id, | 553 | user_id: collaborator.user_id, |
| 554 | replica_id: collaborator.replica_id, | 554 | replica_id: collaborator.replica_id, |
| 555 | is_host: collaborator.is_host, | 555 | is_host: collaborator.is_host, |
| 556 | committer_name: collaborator.committer_name.clone(), | ||
| 557 | committer_email: collaborator.committer_email.clone(), | ||
| 556 | }) | 558 | }) |
| 557 | .collect(), | 559 | .collect(), |
| 558 | worktrees: reshared_project.worktrees.clone(), | 560 | worktrees: reshared_project.worktrees.clone(), |
| @@ -857,6 +859,8 @@ impl Database { · 298 unmodified lines | |||
| 857 | user_id: collaborator.user_id, | 859 | user_id: collaborator.user_id, |
| 858 | replica_id: collaborator.replica_id, | 860 | replica_id: collaborator.replica_id, |
| 859 | is_host: collaborator.is_host, | 861 | is_host: collaborator.is_host, |
| 862 | committer_name: collaborator.committer_name, | ||
| 863 | committer_email: collaborator.committer_email, | ||
| 860 | }) | 864 | }) |
| 861 | .collect::<Vec<_>>(); | 865 | .collect::<Vec<_>>(); |
| 862 | 866 | ||
| @@ -111,7 +111,7 @@ impl Database { · 110 unmodified lines | |||
| 111 | .await | 111 | .await |
| 112 | } | 112 | } |
| 113 | 113 | ||
| 114 | pub async fn get_or_create_user_by_github_account( | 114 | pub async fn update_or_create_user_by_github_account( |
| 115 | &self, | 115 | &self, |
| 116 | github_login: &str, | 116 | github_login: &str, |
| 117 | github_user_id: i32, | 117 | github_user_id: i32, |
| @@ -121,7 +121,7 @@ impl Database { · 3 unmodified lines | |||
| 121 | initial_channel_id: Option<ChannelId>, | 121 | initial_channel_id: Option<ChannelId>, |
| 122 | ) -> Result<User> { | 122 | ) -> Result<User> { |
| 123 | self.transaction(|tx| async move { | 123 | self.transaction(|tx| async move { |
| 124 | self.get_or_create_user_by_github_account_tx( | 124 | self.update_or_create_user_by_github_account_tx( |
| 125 | github_login, | 125 | github_login, |
| 126 | github_user_id, | 126 | github_user_id, |
| 127 | github_email, | 127 | github_email, |
| @@ -135,7 +135,7 @@ impl Database { · 7 unmodified lines | |||
| 135 | .await | 135 | .await |
| 136 | } | 136 | } |
| 137 | 137 | ||
| 138 | pub async fn get_or_create_user_by_github_account_tx( | 138 | pub async fn update_or_create_user_by_github_account_tx( |
| 139 | &self, | 139 | &self, |
| 140 | github_login: &str, | 140 | github_login: &str, |
| 141 | github_user_id: i32, | 141 | github_user_id: i32, |
| @@ -13,6 +13,8 @@ pub struct Model { · 12 unmodified lines | |||
| 13 | pub user_id: UserId, | 13 | pub user_id: UserId, |
| 14 | pub replica_id: ReplicaId, | 14 | pub replica_id: ReplicaId, |
| 15 | pub is_host: bool, | 15 | pub is_host: bool, |
| 16 | pub committer_name: Option<String>, | ||
| 17 | pub committer_email: Option<String>, | ||
| 16 | } | 18 | } |
| 17 | 19 | ||
| 18 | impl Model { | 20 | impl Model { |
| @@ -126,12 +126,16 @@ async fn test_channel_buffers(db: &Arc<Database>) { · 125 unmodified lines | |||
| 126 | peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }), | 126 | peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }), |
| 127 | replica_id: 0, | 127 | replica_id: 0, |
| 128 | is_host: false, | 128 | is_host: false, |
| 129 | committer_name: None, | ||
| 130 | committer_email: None, | ||
| 129 | }, | 131 | }, |
| 130 | rpc::proto::Collaborator { | 132 | rpc::proto::Collaborator { |
| 131 | user_id: b_id.to_proto(), | 133 | user_id: b_id.to_proto(), |
| 132 | peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }), | 134 | peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }), |
| 133 | replica_id: 1, | 135 | replica_id: 1, |
| 134 | is_host: false, | 136 | is_host: false, |
| 137 | committer_name: None, | ||
| 138 | committer_email: None, | ||
| 135 | } | 139 | } |
| 136 | ] | 140 | ] |
| 137 | ); | 141 | ); |
| @@ -72,12 +72,12 @@ async fn test_get_users(db: &Arc<Database>) { · 71 unmodified lines | |||
| 72 | } | 72 | } |
| 73 | 73 | ||
| 74 | test_both_dbs!( | 74 | test_both_dbs!( |
| 75 | test_get_or_create_user_by_github_account, | 75 | test_update_or_create_user_by_github_account, |
| 76 | test_get_or_create_user_by_github_account_postgres, | 76 | test_update_or_create_user_by_github_account_postgres, |
| 77 | test_get_or_create_user_by_github_account_sqlite | 77 | test_update_or_create_user_by_github_account_sqlite |
| 78 | ); | 78 | ); |
| 79 | 79 | ||
| 80 | async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) { | 80 | async fn test_update_or_create_user_by_github_account(db: &Arc<Database>) { |
| 81 | db.create_user( | 81 | db.create_user( |
| 82 | "user1@example.com", | 82 | "user1@example.com", |
| 83 | None, | 83 | None, |
| @@ -104,7 +104,14 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) { · 20 unmodified lines | |||
| 104 | .user_id; | 104 | .user_id; |
| 105 | 105 | ||
| 106 | let user = db | 106 | let user = db |
| 107 | .get_or_create_user_by_github_account("the-new-login2", 102, None, None, Utc::now(), None) | 107 | .update_or_create_user_by_github_account( |
| 108 | "the-new-login2", | ||
| 109 | 102, | ||
| 110 | None, | ||
| 111 | None, | ||
| 112 | Utc::now(), | ||
| 113 | None, | ||
| 114 | ) | ||
| 108 | .await | 115 | .await |
| 109 | .unwrap(); | 116 | .unwrap(); |
| 110 | assert_eq!(user.id, user_id2); | 117 | assert_eq!(user.id, user_id2); |
| @@ -112,7 +119,7 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) { · 1 unmodified line | |||
| 112 | assert_eq!(user.github_user_id, 102); | 119 | assert_eq!(user.github_user_id, 102); |
| 113 | 120 | ||
| 114 | let user = db | 121 | let user = db |
| 115 | .get_or_create_user_by_github_account( | 122 | .update_or_create_user_by_github_account( |
| 116 | "login3", | 123 | "login3", |
| 117 | 103, | 124 | 103, |
| 118 | Some("user3@example.com"), | 125 | Some("user3@example.com"), |
| @@ -7,6 +7,12 @@ pub use token::*; · 6 unmodified lines | |||
| 7 | 7 | ||
| 8 | pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial"; | 8 | pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial"; |
| 9 | 9 | ||
| 10 | /// The name of the feature flag that bypasses the account age check. | ||
| 11 | pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-check"; | ||
| 12 | |||
| 13 | /// The minimum account age an account must have in order to use the LLM service. | ||
| 14 | pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30); | ||
| 15 | |||
| 10 | /// The default value to use for maximum spend per month if the user did not | 16 | /// The default value to use for maximum spend per month if the user did not |
| 11 | /// explicitly set a maximum spend. | 17 | /// explicitly set a maximum spend. |
| 12 | /// | 18 | /// |
| @@ -1,6 +1,6 @@ | |||
| 1 | use crate::db::billing_subscription::SubscriptionKind; | 1 | use crate::db::billing_subscription::SubscriptionKind; |
| 2 | use crate::db::{billing_customer, billing_subscription, user}; | 2 | use crate::db::{billing_customer, billing_subscription, user}; |
| 3 | use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG; | 3 | use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG}; |
| 4 | use crate::{Config, db::billing_preference}; | 4 | use crate::{Config, db::billing_preference}; |
| 5 | use anyhow::{Context as _, Result}; | 5 | use anyhow::{Context as _, Result}; |
| 6 | use chrono::{NaiveDateTime, Utc}; | 6 | use chrono::{NaiveDateTime, Utc}; |
| @@ -84,7 +84,7 @@ impl LlmTokenClaims { · 77 unmodified lines | |||
| 84 | .any(|flag| flag == "llm-closed-beta"), | 84 | .any(|flag| flag == "llm-closed-beta"), |
| 85 | bypass_account_age_check: feature_flags | 85 | bypass_account_age_check: feature_flags |
| 86 | .iter() | 86 | .iter() |
| 87 | .any(|flag| flag == "bypass-account-age-check"), | 87 | .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG), |
| 88 | can_use_web_search_tool: true, | 88 | can_use_web_search_tool: true, |
| 89 | use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"), | 89 | use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"), |
| 90 | plan, | 90 | plan, |
| @@ -4,14 +4,17 @@ use crate::api::billing::find_or_create_billing_customer; · 3 unmodified lines | |||
| 4 | use crate::api::{CloudflareIpCountryHeader, SystemIdHeader}; | 4 | use crate::api::{CloudflareIpCountryHeader, SystemIdHeader}; |
| 5 | use crate::db::billing_subscription::SubscriptionKind; | 5 | use crate::db::billing_subscription::SubscriptionKind; |
| 6 | use crate::llm::db::LlmDatabase; | 6 | use crate::llm::db::LlmDatabase; |
| 7 | use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, LlmTokenClaims}; | 7 | use crate::llm::{ |
| 8 | AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims, | ||
| 9 | MIN_ACCOUNT_AGE_FOR_LLM_USE, | ||
| 10 | }; | ||
| 8 | use crate::stripe_client::StripeCustomerId; | 11 | use crate::stripe_client::StripeCustomerId; |
| 9 | use crate::{ | 12 | use crate::{ |
| 10 | AppState, Error, Result, auth, | 13 | AppState, Error, Result, auth, |
| 11 | db::{ | 14 | db::{ |
| 12 | self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser, | 15 | self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser, |
| 13 | CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId, | 16 | CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId, |
| 14 | NotificationId, Project, ProjectId, RejoinedProject, RemoveChannelMemberResult, ReplicaId, | 17 | NotificationId, ProjectId, RejoinedProject, RemoveChannelMemberResult, |
| 15 | RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId, | 18 | RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId, |
| 16 | }, | 19 | }, |
| 17 | executor::Executor, | 20 | executor::Executor, |
| @@ -65,7 +68,7 @@ use std::{ · 47 unmodified lines | |||
| 65 | rc::Rc, | 68 | rc::Rc, |
| 66 | sync::{ | 69 | sync::{ |
| 67 | Arc, OnceLock, | 70 | Arc, OnceLock, |
| 68 | atomic::{AtomicBool, Ordering::SeqCst}, | 71 | atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, |
| 69 | }, | 72 | }, |
| 70 | time::{Duration, Instant}, | 73 | time::{Duration, Instant}, |
| 71 | }; | 74 | }; |
| @@ -86,10 +89,36 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15); · 14 unmodified lines | |||
| 86 | const MESSAGE_COUNT_PER_PAGE: usize = 100; | 89 | const MESSAGE_COUNT_PER_PAGE: usize = 100; |
| 87 | const MAX_MESSAGE_LEN: usize = 1024; | 90 | const MAX_MESSAGE_LEN: usize = 1024; |
| 88 | const NOTIFICATION_COUNT_PER_PAGE: usize = 50; | 91 | const NOTIFICATION_COUNT_PER_PAGE: usize = 50; |
| 92 | const MAX_CONCURRENT_CONNECTIONS: usize = 512; | ||
| 93 | |||
| 94 | static CONCURRENT_CONNECTIONS: AtomicUsize = AtomicUsize::new(0); | ||
| 89 | 95 | ||
| 90 | type MessageHandler = | 96 | type MessageHandler = |
| 91 | Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>; | 97 | Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>; |
| 92 | 98 | ||
| 99 | pub struct ConnectionGuard; | ||
| 100 | |||
| 101 | impl ConnectionGuard { | ||
| 102 | pub fn try_acquire() -> Result<Self, ()> { | ||
| 103 | let current_connections = CONCURRENT_CONNECTIONS.fetch_add(1, SeqCst); | ||
| 104 | if current_connections >= MAX_CONCURRENT_CONNECTIONS { | ||
| 105 | CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst); | ||
| 106 | tracing::error!( | ||
| 107 | "too many concurrent connections: {}", | ||
| 108 | current_connections + 1 | ||
| 109 | ); | ||
| 110 | return Err(()); | ||
| 111 | } | ||
| 112 | Ok(ConnectionGuard) | ||
| 113 | } | ||
| 114 | } | ||
| 115 | |||
| 116 | impl Drop for ConnectionGuard { | ||
| 117 | fn drop(&mut self) { | ||
| 118 | CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst); | ||
| 119 | } | ||
| 120 | } | ||
| 121 | |||
| 93 | struct Response<R> { | 122 | struct Response<R> { |
| 94 | peer: Arc<Peer>, | 123 | peer: Arc<Peer>, |
| 95 | receipt: Receipt<R>, | 124 | receipt: Receipt<R>, |
| @@ -722,6 +751,7 @@ impl Server { · 626 unmodified lines | |||
| 722 | system_id: Option<String>, | 751 | system_id: Option<String>, |
| 723 | send_connection_id: Option<oneshot::Sender<ConnectionId>>, | 752 | send_connection_id: Option<oneshot::Sender<ConnectionId>>, |
| 724 | executor: Executor, | 753 | executor: Executor, |
| 754 | connection_guard: Option<ConnectionGuard>, | ||
| 725 | ) -> impl Future<Output = ()> + use<> { | 755 | ) -> impl Future<Output = ()> + use<> { |
| 726 | let this = self.clone(); | 756 | let this = self.clone(); |
| 727 | let span = info_span!("handle connection", %address, | 757 | let span = info_span!("handle connection", %address, |
| @@ -742,6 +772,7 @@ impl Server { · 14 unmodified lines | |||
| 742 | tracing::error!("server is tearing down"); | 772 | tracing::error!("server is tearing down"); |
| 743 | return | 773 | return |
| 744 | } | 774 | } |
| 775 | |||
| 745 | let (connection_id, handle_io, mut incoming_rx) = this | 776 | let (connection_id, handle_io, mut incoming_rx) = this |
| 746 | .peer | 777 | .peer |
| 747 | .add_connection(connection, { | 778 | .add_connection(connection, { |
| @@ -783,6 +814,7 @@ impl Server { · 35 unmodified lines | |||
| 783 | tracing::error!(?error, "failed to send initial client update"); | 814 | tracing::error!(?error, "failed to send initial client update"); |
| 784 | return; | 815 | return; |
| 785 | } | 816 | } |
| 817 | drop(connection_guard); | ||
| 786 | 818 | ||
| 787 | let handle_io = handle_io.fuse(); | 819 | let handle_io = handle_io.fuse(); |
| 788 | futures::pin_mut!(handle_io); | 820 | futures::pin_mut!(handle_io); |
| @@ -1154,6 +1186,19 @@ pub async fn handle_websocket_request( · 365 unmodified lines | |||
| 1154 | } | 1186 | } |
| 1155 | 1187 | ||
| 1156 | let socket_address = socket_address.to_string(); | 1188 | let socket_address = socket_address.to_string(); |
| 1189 | |||
| 1190 | // Acquire connection guard before WebSocket upgrade | ||
| 1191 | let connection_guard = match ConnectionGuard::try_acquire() { | ||
| 1192 | Ok(guard) => guard, | ||
| 1193 | Err(()) => { | ||
| 1194 | return ( | ||
| 1195 | StatusCode::SERVICE_UNAVAILABLE, | ||
| 1196 | "Too many concurrent connections", | ||
| 1197 | ) | ||
| 1198 | .into_response(); | ||
| 1199 | } | ||
| 1200 | }; | ||
| 1201 | |||
| 1157 | ws.on_upgrade(move |socket| { | 1202 | ws.on_upgrade(move |socket| { |
| 1158 | let socket = socket | 1203 | let socket = socket |
| 1159 | .map_ok(to_tungstenite_message) | 1204 | .map_ok(to_tungstenite_message) |
| @@ -1171,6 +1216,7 @@ pub async fn handle_websocket_request( · 11 unmodified lines | |||
| 1171 | system_id_header.map(|header| header.to_string()), | 1216 | system_id_header.map(|header| header.to_string()), |
| 1172 | None, | 1217 | None, |
| 1173 | Executor::Production, | 1218 | Executor::Production, |
| 1219 | Some(connection_guard), | ||
| 1174 | ) | 1220 | ) |
| 1175 | .await; | 1221 | .await; |
| 1176 | } | 1222 | } |
| @@ -1844,28 +1890,16 @@ async fn join_project( · 667 unmodified lines | |||
| 1844 | 1890 | ||
| 1845 | let db = session.db().await; | 1891 | let db = session.db().await; |
| 1846 | let (project, replica_id) = &mut *db | 1892 | let (project, replica_id) = &mut *db |
| 1847 | .join_project(project_id, session.connection_id, session.user_id()) | 1893 | .join_project( |
| 1894 | project_id, | ||
| 1895 | session.connection_id, | ||
| 1896 | session.user_id(), | ||
| 1897 | request.committer_name.clone(), | ||
| 1898 | request.committer_email.clone(), | ||
| 1899 | ) | ||
| 1848 | .await?; | 1900 | .await?; |
| 1849 | drop(db); | 1901 | drop(db); |
| 1850 | tracing::info!(%project_id, "join remote project"); | 1902 | tracing::info!(%project_id, "join remote project"); |
| 1851 | join_project_internal(response, session, project, replica_id) | ||
| 1852 | } | ||
| 1853 | |||
| 1854 | trait JoinProjectInternalResponse { | ||
| 1855 | fn send(self, result: proto::JoinProjectResponse) -> Result<()>; | ||
| 1856 | } | ||
| 1857 | impl JoinProjectInternalResponse for Response<proto::JoinProject> { | ||
| 1858 | fn send(self, result: proto::JoinProjectResponse) -> Result<()> { | ||
| 1859 | Response::<proto::JoinProject>::send(self, result) | ||
| 1860 | } | ||
| 1861 | } | ||
| 1862 | |||
| 1863 | fn join_project_internal( | ||
| 1864 | response: impl JoinProjectInternalResponse, | ||
| 1865 | session: Session, | ||
| 1866 | project: &mut Project, | ||
| 1867 | replica_id: &ReplicaId, | ||
| 1868 | ) -> Result<()> { | ||
| 1869 | let collaborators = project | 1903 | let collaborators = project |
| 1870 | .collaborators | 1904 | .collaborators |
| 1871 | .iter() | 1905 | .iter() |
| @@ -1893,6 +1927,8 @@ fn join_project_internal( · 21 unmodified lines | |||
| 1893 | replica_id: replica_id.0 as u32, | 1927 | replica_id: replica_id.0 as u32, |
| 1894 | user_id: guest_user_id.to_proto(), | 1928 | user_id: guest_user_id.to_proto(), |
| 1895 | is_host: false, | 1929 | is_host: false, |
| 1930 | committer_name: request.committer_name.clone(), | ||
| 1931 | committer_email: request.committer_email.clone(), | ||
| 1896 | }), | 1932 | }), |
| 1897 | }; | 1933 | }; |
| 1898 | 1934 | ||
| @@ -2521,7 +2557,6 @@ async fn get_users( · 622 unmodified lines | |||
| 2521 | id: user.id.to_proto(), | 2557 | id: user.id.to_proto(), |
| 2522 | avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), | 2558 | avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), |
| 2523 | github_login: user.github_login, | 2559 | github_login: user.github_login, |
| 2524 | email: user.email_address, | ||
| 2525 | name: user.name, | 2560 | name: user.name, |
| 2526 | }) | 2561 | }) |
| 2527 | .collect(); | 2562 | .collect(); |
| @@ -2555,7 +2590,6 @@ async fn fuzzy_search_users( · 27 unmodified lines | |||
| 2555 | avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), | 2590 | avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), |
| 2556 | github_login: user.github_login, | 2591 | github_login: user.github_login, |
| 2557 | name: user.name, | 2592 | name: user.name, |
| 2558 | email: user.email_address, | ||
| 2559 | }) | 2593 | }) |
| 2560 | .collect(); | 2594 | .collect(); |
| 2561 | response.send(proto::UsersResponse { users })?; | 2595 | response.send(proto::UsersResponse { users })?; |
| @@ -2773,8 +2807,12 @@ async fn make_update_user_plan_message( · 211 unmodified lines | |||
| 2773 | (None, None) | 2807 | (None, None) |
| 2774 | }; | 2808 | }; |
| 2775 | 2809 | ||
| 2776 | let account_too_young = | 2810 | let bypass_account_age_check = feature_flags |
| 2777 | !matches!(plan, proto::Plan::ZedPro) && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE; | 2811 | .iter() |
| 2812 | .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG); | ||
| 2813 | let account_too_young = !matches!(plan, proto::Plan::ZedPro) | ||
| 2814 | && !bypass_account_age_check | ||
| 2815 | && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE; | ||
| 2778 | 2816 | ||
| 2779 | Ok(proto::UpdateUserPlan { | 2817 | Ok(proto::UpdateUserPlan { |
| 2780 | plan: plan.into(), | 2818 | plan: plan.into(), |
| @@ -4075,9 +4113,6 @@ async fn accept_terms_of_service( · 1294 unmodified lines | |||
| 4075 | Ok(()) | 4113 | Ok(()) |
| 4076 | } | 4114 | } |
| 4077 | 4115 | ||
| 4078 | /// The minimum account age an account must have in order to use the LLM service. | ||
| 4079 | pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30); | ||
| 4080 | |||
| 4081 | async fn get_llm_api_token( | 4116 | async fn get_llm_api_token( |
| 4082 | _request: proto::GetLlmToken, | 4117 | _request: proto::GetLlmToken, |
| 4083 | response: Response<proto::GetLlmToken>, | 4118 | response: Response<proto::GetLlmToken>, |
| @@ -127,7 +127,7 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result · 126 unmodified lines | |||
| 127 | log::info!("Seeding {:?} from GitHub", github_user.login); | 127 | log::info!("Seeding {:?} from GitHub", github_user.login); |
| 128 | 128 | ||
| 129 | let user = db | 129 | let user = db |
| 130 | .get_or_create_user_by_github_account( | 130 | .update_or_create_user_by_github_account( |
| 131 | &github_user.login, | 131 | &github_user.login, |
| 132 | github_user.id, | 132 | github_user.id, |
| 133 | github_user.email.as_deref(), | 133 | github_user.email.as_deref(), |
| @@ -180,7 +180,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes · 179 unmodified lines | |||
| 180 | server | 180 | server |
| 181 | .app_state | 181 | .app_state |
| 182 | .db | 182 | .db |
| 183 | .get_or_create_user_by_github_account("user_b", 100, None, None, Utc::now(), None) | 183 | .update_or_create_user_by_github_account("user_b", 100, None, None, Utc::now(), None) |
| 184 | .await | 184 | .await |
| 185 | .unwrap(); | 185 | .unwrap(); |
| 186 | 186 | ||
| @@ -1610,6 +1610,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut · 1609 unmodified lines | |||
| 1610 | .root(cx_a) | 1610 | .root(cx_a) |
| 1611 | .unwrap(); | 1611 | .unwrap(); |
| 1612 | 1612 | ||
| 1613 | executor.run_until_parked(); | ||
| 1614 | |||
| 1613 | workspace_a_project_b.update(cx_a2, |workspace, cx| { | 1615 | workspace_a_project_b.update(cx_a2, |workspace, cx| { |
| 1614 | assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); | 1616 | assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); |
| 1615 | assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); | 1617 | assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); |
| @@ -51,7 +51,7 @@ use std::{ · 50 unmodified lines | |||
| 51 | time::Duration, | 51 | time::Duration, |
| 52 | }; | 52 | }; |
| 53 | use unindent::Unindent as _; | 53 | use unindent::Unindent as _; |
| 54 | use util::{path, separator, uri}; | 54 | use util::{path, uri}; |
| 55 | use workspace::Pane; | 55 | use workspace::Pane; |
| 56 | 56 | ||
| 57 | #[ctor::ctor] | 57 | #[ctor::ctor] |
| @@ -1676,13 +1676,13 @@ async fn test_project_reconnect( · 1618 unmodified lines | |||
| 1676 | .map(|p| p.to_str().unwrap()) | 1676 | .map(|p| p.to_str().unwrap()) |
| 1677 | .collect::<Vec<_>>(), | 1677 | .collect::<Vec<_>>(), |
| 1678 | vec![ | 1678 | vec![ |
| 1679 | separator!("a.txt"), | 1679 | path!("a.txt"), |
| 1680 | separator!("b.txt"), | 1680 | path!("b.txt"), |
| 1681 | separator!("subdir2"), | 1681 | path!("subdir2"), |
| 1682 | separator!("subdir2/f.txt"), | 1682 | path!("subdir2/f.txt"), |
| 1683 | separator!("subdir2/g.txt"), | 1683 | path!("subdir2/g.txt"), |
| 1684 | separator!("subdir2/h.txt"), | 1684 | path!("subdir2/h.txt"), |
| 1685 | separator!("subdir2/i.txt") | 1685 | path!("subdir2/i.txt") |
| 1686 | ] | 1686 | ] |
| 1687 | ); | 1687 | ); |
| 1688 | assert!(worktree_a3.read(cx).has_update_observer()); | 1688 | assert!(worktree_a3.read(cx).has_update_observer()); |
| @@ -1709,13 +1709,13 @@ async fn test_project_reconnect( · 20 unmodified lines | |||
| 1709 | .map(|p| p.to_str().unwrap()) | 1709 | .map(|p| p.to_str().unwrap()) |
| 1710 | .collect::<Vec<_>>(), | 1710 | .collect::<Vec<_>>(), |
| 1711 | vec![ | 1711 | vec![ |
| 1712 | separator!("a.txt"), | 1712 | path!("a.txt"), |
| 1713 | separator!("b.txt"), | 1713 | path!("b.txt"), |
| 1714 | separator!("subdir2"), | 1714 | path!("subdir2"), |
| 1715 | separator!("subdir2/f.txt"), | 1715 | path!("subdir2/f.txt"), |
| 1716 | separator!("subdir2/g.txt"), | 1716 | path!("subdir2/g.txt"), |
| 1717 | separator!("subdir2/h.txt"), | 1717 | path!("subdir2/h.txt"), |
| 1718 | separator!("subdir2/i.txt") | 1718 | path!("subdir2/i.txt") |
| 1719 | ] | 1719 | ] |
| 1720 | ); | 1720 | ); |
| 1721 | assert!(project.worktree_for_id(worktree2_id, cx).is_none()); | 1721 | assert!(project.worktree_for_id(worktree2_id, cx).is_none()); |
| @@ -1806,13 +1806,13 @@ async fn test_project_reconnect( · 84 unmodified lines | |||
| 1806 | .map(|p| p.to_str().unwrap()) | 1806 | .map(|p| p.to_str().unwrap()) |
| 1807 | .collect::<Vec<_>>(), | 1807 | .collect::<Vec<_>>(), |
| 1808 | vec![ | 1808 | vec![ |
| 1809 | separator!("a.txt"), | 1809 | path!("a.txt"), |
| 1810 | separator!("b.txt"), | 1810 | path!("b.txt"), |
| 1811 | separator!("subdir2"), | 1811 | path!("subdir2"), |
| 1812 | separator!("subdir2/f.txt"), | 1812 | path!("subdir2/f.txt"), |
| 1813 | separator!("subdir2/g.txt"), | 1813 | path!("subdir2/g.txt"), |
| 1814 | separator!("subdir2/h.txt"), | 1814 | path!("subdir2/h.txt"), |
| 1815 | separator!("subdir2/j.txt") | 1815 | path!("subdir2/j.txt") |
| 1816 | ] | 1816 | ] |
| 1817 | ); | 1817 | ); |
| 1818 | assert!(project.worktree_for_id(worktree2_id, cx).is_none()); | 1818 | assert!(project.worktree_for_id(worktree2_id, cx).is_none()); |
| @@ -1876,7 +1876,6 @@ async fn test_active_call_events( · 57 unmodified lines | |||
| 1876 | github_login: "user_a".to_string(), | 1876 | github_login: "user_a".to_string(), |
| 1877 | avatar_uri: "avatar_a".into(), | 1877 | avatar_uri: "avatar_a".into(), |
| 1878 | name: None, | 1878 | name: None, |
| 1879 | email: None, | ||
| 1880 | }), | 1879 | }), |
| 1881 | project_id: project_a_id, | 1880 | project_id: project_a_id, |
| 1882 | worktree_root_names: vec!["a".to_string()], | 1881 | worktree_root_names: vec!["a".to_string()], |
| @@ -1896,7 +1895,6 @@ async fn test_active_call_events( · 13 unmodified lines | |||
| 1896 | github_login: "user_b".to_string(), | 1895 | github_login: "user_b".to_string(), |
| 1897 | avatar_uri: "avatar_b".into(), | 1896 | avatar_uri: "avatar_b".into(), |
| 1898 | name: None, | 1897 | name: None, |
| 1899 | email: None, | ||
| 1900 | }), | 1898 | }), |
| 1901 | project_id: project_b_id, | 1899 | project_id: project_b_id, |
| 1902 | worktree_root_names: vec!["b".to_string()] | 1900 | worktree_root_names: vec!["b".to_string()] |
| @@ -3317,13 +3315,13 @@ async fn test_fs_operations( · 1414 unmodified lines | |||
| 3317 | .map(|p| p.to_string_lossy()) | 3315 | .map(|p| p.to_string_lossy()) |
| 3318 | .collect::<Vec<_>>(), | 3316 | .collect::<Vec<_>>(), |
| 3319 | [ | 3317 | [ |
| 3320 | separator!("DIR"), | 3318 | path!("DIR"), |
| 3321 | separator!("DIR/SUBDIR"), | 3319 | path!("DIR/SUBDIR"), |
| 3322 | separator!("DIR/SUBDIR/f.txt"), | 3320 | path!("DIR/SUBDIR/f.txt"), |
| 3323 | separator!("DIR/e.txt"), | 3321 | path!("DIR/e.txt"), |
| 3324 | separator!("a.txt"), | 3322 | path!("a.txt"), |
| 3325 | separator!("b.txt"), | 3323 | path!("b.txt"), |
| 3326 | separator!("d.txt") | 3324 | path!("d.txt") |
| 3327 | ] | 3325 | ] |
| 3328 | ); | 3326 | ); |
| 3329 | }); | 3327 | }); |
| @@ -3335,13 +3333,13 @@ async fn test_fs_operations( · 5 unmodified lines | |||
| 3335 | .map(|p| p.to_string_lossy()) | 3333 | .map(|p| p.to_string_lossy()) |
| 3336 | .collect::<Vec<_>>(), | 3334 | .collect::<Vec<_>>(), |
| 3337 | [ | 3335 | [ |
| 3338 | separator!("DIR"), | 3336 | path!("DIR"), |
| 3339 | separator!("DIR/SUBDIR"), | 3337 | path!("DIR/SUBDIR"), |
| 3340 | separator!("DIR/SUBDIR/f.txt"), | 3338 | path!("DIR/SUBDIR/f.txt"), |
| 3341 | separator!("DIR/e.txt"), | 3339 | path!("DIR/e.txt"), |
| 3342 | separator!("a.txt"), | 3340 | path!("a.txt"), |
| 3343 | separator!("b.txt"), | 3341 | path!("b.txt"), |
| 3344 | separator!("d.txt") | 3342 | path!("d.txt") |
| 3345 | ] | 3343 | ] |
| 3346 | ); | 3344 | ); |
| 3347 | }); | 3345 | }); |
| @@ -3361,14 +3359,14 @@ async fn test_fs_operations( · 13 unmodified lines | |||
| 3361 | .map(|p| p.to_string_lossy()) | 3359 | .map(|p| p.to_string_lossy()) |
| 3362 | .collect::<Vec<_>>(), | 3360 | .collect::<Vec<_>>(), |
| 3363 | [ | 3361 | [ |
| 3364 | separator!("DIR"), | 3362 | path!("DIR"), |
| 3365 | separator!("DIR/SUBDIR"), | 3363 | path!("DIR/SUBDIR"), |
| 3366 | separator!("DIR/SUBDIR/f.txt"), | 3364 | path!("DIR/SUBDIR/f.txt"), |
| 3367 | separator!("DIR/e.txt"), | 3365 | path!("DIR/e.txt"), |
| 3368 | separator!("a.txt"), | 3366 | path!("a.txt"), |
| 3369 | separator!("b.txt"), | 3367 | path!("b.txt"), |
| 3370 | separator!("d.txt"), | 3368 | path!("d.txt"), |
| 3371 | separator!("f.txt") | 3369 | path!("f.txt") |
| 3372 | ] | 3370 | ] |
| 3373 | ); | 3371 | ); |
| 3374 | }); | 3372 | }); |
| @@ -3380,14 +3378,14 @@ async fn test_fs_operations( · 5 unmodified lines | |||
| 3380 | .map(|p| p.to_string_lossy()) | 3378 | .map(|p| p.to_string_lossy()) |
| 3381 | .collect::<Vec<_>>(), | 3379 | .collect::<Vec<_>>(), |
| 3382 | [ | 3380 | [ |
| 3383 | separator!("DIR"), | 3381 | path!("DIR"), |
| 3384 | separator!("DIR/SUBDIR"), | 3382 | path!("DIR/SUBDIR"), |
| 3385 | separator!("DIR/SUBDIR/f.txt"), | 3383 | path!("DIR/SUBDIR/f.txt"), |
| 3386 | separator!("DIR/e.txt"), | 3384 | path!("DIR/e.txt"), |
| 3387 | separator!("a.txt"), | 3385 | path!("a.txt"), |
| 3388 | separator!("b.txt"), | 3386 | path!("b.txt"), |
| 3389 | separator!("d.txt"), | 3387 | path!("d.txt"), |
| 3390 | separator!("f.txt") | 3388 | path!("f.txt") |
| 3391 | ] | 3389 | ] |
| 3392 | ); | 3390 | ); |
| 3393 | }); | 3391 | }); |
| @@ -30,7 +30,7 @@ use rpc::proto; · 29 unmodified lines | |||
| 30 | use serde_json::json; | 30 | use serde_json::json; |
| 31 | use settings::SettingsStore; | 31 | use settings::SettingsStore; |
| 32 | use std::{path::Path, sync::Arc}; | 32 | use std::{path::Path, sync::Arc}; |
| 33 | use util::{path, separator}; | 33 | use util::path; |
| 34 | 34 | ||
| 35 | #[gpui::test(iterations = 10)] | 35 | #[gpui::test(iterations = 10)] |
| 36 | async fn test_sharing_an_ssh_remote_project( | 36 | async fn test_sharing_an_ssh_remote_project( |
| @@ -198,7 +198,7 @@ async fn test_sharing_an_ssh_remote_project( · 161 unmodified lines | |||
| 198 | .path() | 198 | .path() |
| 199 | .to_string_lossy() | 199 | .to_string_lossy() |
| 200 | .to_string(), | 200 | .to_string(), |
| 201 | separator!("src/renamed.rs").to_string() | 201 | path!("src/renamed.rs").to_string() |
| 202 | ); | 202 | ); |
| 203 | }); | 203 | }); |
| 204 | } | 204 | } |
| @@ -671,7 +671,7 @@ async fn test_remote_server_debugger( · 466 unmodified lines | |||
| 671 | }); | 671 | }); |
| 672 | 672 | ||
| 673 | session.update(cx_a, |session, _| { | 673 | session.update(cx_a, |session, _| { |
| 674 | assert_eq!(session.binary().command, "ssh"); | 674 | assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh")); |
| 675 | }); | 675 | }); |
| 676 | 676 | ||
| 677 | let shutdown_session = workspace.update(cx_a, |workspace, cx| { | 677 | let shutdown_session = workspace.update(cx_a, |workspace, cx| { |
| @@ -258,6 +258,7 @@ impl TestServer { · 257 unmodified lines | |||
| 258 | None, | 258 | None, |
| 259 | Some(connection_id_tx), | 259 | Some(connection_id_tx), |
| 260 | Executor::Deterministic(cx.background_executor().clone()), | 260 | Executor::Deterministic(cx.background_executor().clone()), |
| 261 | None, | ||
| 261 | )) | 262 | )) |
| 262 | .detach(); | 263 | .detach(); |
| 263 | let connection_id = connection_id_rx.await.map_err(|e| { | 264 | let connection_id = connection_id_rx.await.map_err(|e| { |
| @@ -82,7 +82,7 @@ impl UserBackfiller { · 81 unmodified lines | |||
| 82 | { | 82 | { |
| 83 | Ok(github_user) => { | 83 | Ok(github_user) => { |
| 84 | self.db | 84 | self.db |
| 85 | .get_or_create_user_by_github_account( | 85 | .update_or_create_user_by_github_account( |
| 86 | &user.github_login, | 86 | &user.github_login, |
| 87 | github_user.id, | 87 | github_user.id, |
| 88 | user.email_address.as_deref(), | 88 | user.email_address.as_deref(), |
| @@ -90,7 +90,7 @@ impl ChatPanel { · 89 unmodified lines | |||
| 90 | languages.clone(), | 90 | languages.clone(), |
| 91 | user_store.clone(), | 91 | user_store.clone(), |
| 92 | None, | 92 | None, |
| 93 | cx.new(|cx| Editor::auto_height(4, window, cx)), | 93 | cx.new(|cx| Editor::auto_height(1, 4, window, cx)), |
| 94 | window, | 94 | window, |
| 95 | cx, | 95 | cx, |
| 96 | ) | 96 | ) |
| @@ -1218,7 +1218,6 @@ mod tests { · 1121 unmodified lines | |||
| 1218 | avatar_uri: "avatar_fgh".into(), | 1218 | avatar_uri: "avatar_fgh".into(), |
| 1219 | id: 103, | 1219 | id: 103, |
| 1220 | name: None, | 1220 | name: None, |
| 1221 | email: None, | ||
| 1222 | }), | 1221 | }), |
| 1223 | nonce: 5, | 1222 | nonce: 5, |
| 1224 | mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)], | 1223 | mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)], |
| @@ -1274,7 +1273,6 @@ mod tests { · 49 unmodified lines | |||
| 1274 | avatar_uri: "avatar_fgh".into(), | 1273 | avatar_uri: "avatar_fgh".into(), |
| 1275 | id: 103, | 1274 | id: 103, |
| 1276 | name: None, | 1275 | name: None, |
| 1277 | email: None, | ||
| 1278 | }), | 1276 | }), |
| 1279 | nonce: 5, | 1277 | nonce: 5, |
| 1280 | mentions: Vec::new(), | 1278 | mentions: Vec::new(), |
| @@ -1323,7 +1321,6 @@ mod tests { · 42 unmodified lines | |||
| 1323 | avatar_uri: "avatar_fgh".into(), | 1321 | avatar_uri: "avatar_fgh".into(), |
| 1324 | id: 103, | 1322 | id: 103, |
| 1325 | name: None, | 1323 | name: None, |
| 1326 | email: None, | ||
| 1327 | }), | 1324 | }), |
| 1328 | nonce: 5, | 1325 | nonce: 5, |
| 1329 | mentions: Vec::new(), | 1326 | mentions: Vec::new(), |
| @@ -8,7 +8,7 @@ · 7 unmodified lines | |||
| 8 | use anyhow::Result; | 8 | use anyhow::Result; |
| 9 | 9 | ||
| 10 | use crate::client::Client; | 10 | use crate::client::Client; |
| 11 | use crate::types::{self, Request}; | 11 | use crate::types::{self, Notification, Request}; |
| 12 | 12 | ||
| 13 | pub struct ModelContextProtocol { | 13 | pub struct ModelContextProtocol { |
| 14 | inner: Client, | 14 | inner: Client, |
| @@ -20,9 +20,10 @@ impl ModelContextProtocol { · 5 unmodified lines | |||
| 20 | } | 20 | } |
| 21 | 21 | ||
| 22 | fn supported_protocols() -> Vec<types::ProtocolVersion> { | 22 | fn supported_protocols() -> Vec<types::ProtocolVersion> { |
| 23 | vec![types::ProtocolVersion( | 23 | vec![ |
| 24 | types::LATEST_PROTOCOL_VERSION.to_string(), | 24 | types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()), |
| 25 | )] | 25 | types::ProtocolVersion(types::VERSION_2024_11_05.to_string()), |
| 26 | ] | ||
| 26 | } | 27 | } |
| 27 | 28 | ||
| 28 | pub async fn initialize( | 29 | pub async fn initialize( |
| @@ -42,7 +43,7 @@ impl ModelContextProtocol { · 13 unmodified lines | |||
| 42 | 43 | ||
| 43 | let response: types::InitializeResponse = self | 44 | let response: types::InitializeResponse = self |
| 44 | .inner | 45 | .inner |
| 45 | .request(types::request::Initialize::METHOD, params) | 46 | .request(types::requests::Initialize::METHOD, params) |
| 46 | .await?; | 47 | .await?; |
| 47 | 48 | ||
| 48 | anyhow::ensure!( | 49 | anyhow::ensure!( |
| @@ -53,16 +54,13 @@ impl ModelContextProtocol { · 4 unmodified lines | |||
| 53 | 54 | ||
| 54 | log::trace!("mcp server info {:?}", response.server_info); | 55 | log::trace!("mcp server info {:?}", response.server_info); |
| 55 | 56 | ||
| 56 | self.inner.notify( | ||
| 57 | types::NotificationType::Initialized.as_str(), | ||
| 58 | serde_json::json!({}), | ||
| 59 | )?; | ||
| 60 | |||
| 61 | let initialized_protocol = InitializedContextServerProtocol { | 57 | let initialized_protocol = InitializedContextServerProtocol { |
| 62 | inner: self.inner, | 58 | inner: self.inner, |
| 63 | initialize: response, | 59 | initialize: response, |
| 64 | }; | 60 | }; |
| 65 | 61 | ||
| 62 | initialized_protocol.notify::<types::notifications::Initialized>(())?; | ||
| 63 | |||
| 66 | Ok(initialized_protocol) | 64 | Ok(initialized_protocol) |
| 67 | } | 65 | } |
| 68 | } | 66 | } |
| @@ -96,4 +94,8 @@ impl InitializedContextServerProtocol { · 27 unmodified lines | |||
| 96 | pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> { | 94 | pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> { |
| 97 | self.inner.request(T::METHOD, params).await | 95 | self.inner.request(T::METHOD, params).await |
| 98 | } | 96 | } |
| 97 | |||
| 98 | pub fn notify<T: Notification>(&self, params: T::Params) -> Result<()> { | ||
| 99 | self.inner.notify(T::METHOD, params) | ||
| 100 | } | ||
| 99 | } | 101 | } |
| @@ -14,7 +14,7 @@ pub fn create_fake_transport( · 13 unmodified lines | |||
| 14 | executor: BackgroundExecutor, | 14 | executor: BackgroundExecutor, |
| 15 | ) -> FakeTransport { | 15 | ) -> FakeTransport { |
| 16 | let name = name.into(); | 16 | let name = name.into(); |
| 17 | FakeTransport::new(executor).on_request::<crate::types::request::Initialize>(move |_params| { | 17 | FakeTransport::new(executor).on_request::<crate::types::requests::Initialize>(move |_params| { |
| 18 | create_initialize_response(name.clone()) | 18 | create_initialize_response(name.clone()) |
| 19 | }) | 19 | }) |
| 20 | } | 20 | } |
| @@ -3,9 +3,10 @@ use serde::de::DeserializeOwned; · 2 unmodified lines | |||
| 3 | use serde::{Deserialize, Serialize}; | 3 | use serde::{Deserialize, Serialize}; |
| 4 | use url::Url; | 4 | use url::Url; |
| 5 | 5 | ||
| 6 | pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05"; | 6 | pub const LATEST_PROTOCOL_VERSION: &str = "2025-03-26"; |
| 7 | pub const VERSION_2024_11_05: &str = "2024-11-05"; | ||
| 7 | 8 | ||
| 8 | pub mod request { | 9 | pub mod requests { |
| 9 | use super::*; | 10 | use super::*; |
| 10 | 11 | ||
| 11 | macro_rules! request { | 12 | macro_rules! request { |
| @@ -82,6 +83,57 @@ pub trait Request { · 70 unmodified lines | |||
| 82 | const METHOD: &'static str; | 83 | const METHOD: &'static str; |
| 83 | } | 84 | } |
| 84 | 85 | ||
| 86 | pub mod notifications { | ||
| 87 | use super::*; | ||
| 88 | |||
| 89 | macro_rules! notification { | ||
| 90 | ($method:expr, $name:ident, $params:ty) => { | ||
| 91 | pub struct $name; | ||
| 92 | |||
| 93 | impl Notification for $name { | ||
| 94 | type Params = $params; | ||
| 95 | const METHOD: &'static str = $method; | ||
| 96 | } | ||
| 97 | }; | ||
| 98 | } | ||
| 99 | |||
| 100 | notification!("notifications/initialized", Initialized, ()); | ||
| 101 | notification!("notifications/progress", Progress, ProgressParams); | ||
| 102 | notification!("notifications/message", Message, MessageParams); | ||
| 103 | notification!( | ||
| 104 | "notifications/resources/updated", | ||
| 105 | ResourcesUpdated, | ||
| 106 | ResourcesUpdatedParams | ||
| 107 | ); | ||
| 108 | notification!( | ||
| 109 | "notifications/resources/list_changed", | ||
| 110 | ResourcesListChanged, | ||
| 111 | () | ||
| 112 | ); | ||
| 113 | notification!("notifications/tools/list_changed", ToolsListChanged, ()); | ||
| 114 | notification!("notifications/prompts/list_changed", PromptsListChanged, ()); | ||
| 115 | notification!("notifications/roots/list_changed", RootsListChanged, ()); | ||
| 116 | } | ||
| 117 | |||
| 118 | pub trait Notification { | ||
| 119 | type Params: DeserializeOwned + Serialize + Send + Sync + 'static; | ||
| 120 | const METHOD: &'static str; | ||
| 121 | } | ||
| 122 | |||
| 123 | #[derive(Debug, Serialize, Deserialize)] | ||
| 124 | #[serde(rename_all = "camelCase")] | ||
| 125 | pub struct MessageParams { | ||
| 126 | pub level: LoggingLevel, | ||
| 127 | pub logger: Option<String>, | ||
| 128 | pub data: serde_json::Value, | ||
| 129 | } | ||
| 130 | |||
| 131 | #[derive(Debug, Serialize, Deserialize)] | ||
| 132 | #[serde(rename_all = "camelCase")] | ||
| 133 | pub struct ResourcesUpdatedParams { | ||
| 134 | pub uri: String, | ||
| 135 | } | ||
| 136 | |||
| 85 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] | 137 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 86 | #[serde(transparent)] | 138 | #[serde(transparent)] |
| 87 | pub struct ProtocolVersion(pub String); | 139 | pub struct ProtocolVersion(pub String); |
| @@ -291,13 +343,20 @@ pub enum MessageContent { · 203 unmodified lines | |||
| 291 | #[serde(skip_serializing_if = "Option::is_none")] | 343 | #[serde(skip_serializing_if = "Option::is_none")] |
| 292 | annotations: Option<MessageAnnotations>, | 344 | annotations: Option<MessageAnnotations>, |
| 293 | }, | 345 | }, |
| 294 | #[serde(rename = "image")] | 346 | #[serde(rename = "image", rename_all = "camelCase")] |
| 295 | Image { | 347 | Image { |
| 296 | data: String, | 348 | data: String, |
| 297 | mime_type: String, | 349 | mime_type: String, |
| 298 | #[serde(skip_serializing_if = "Option::is_none")] | 350 | #[serde(skip_serializing_if = "Option::is_none")] |
| 299 | annotations: Option<MessageAnnotations>, | 351 | annotations: Option<MessageAnnotations>, |
| 300 | }, | 352 | }, |
| 353 | #[serde(rename = "audio", rename_all = "camelCase")] | ||
| 354 | Audio { | ||
| 355 | data: String, | ||
| 356 | mime_type: String, | ||
| 357 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 358 | annotations: Option<MessageAnnotations>, | ||
| 359 | }, | ||
| 301 | #[serde(rename = "resource")] | 360 | #[serde(rename = "resource")] |
| 302 | Resource { | 361 | Resource { |
| 303 | resource: ResourceContents, | 362 | resource: ResourceContents, |
| @@ -394,6 +453,8 @@ pub struct ServerCapabilities { · 90 unmodified lines | |||
| 394 | #[serde(skip_serializing_if = "Option::is_none")] | 453 | #[serde(skip_serializing_if = "Option::is_none")] |
| 395 | pub logging: Option<serde_json::Value>, | 454 | pub logging: Option<serde_json::Value>, |
| 396 | #[serde(skip_serializing_if = "Option::is_none")] | 455 | #[serde(skip_serializing_if = "Option::is_none")] |
| 456 | pub completions: Option<serde_json::Value>, | ||
| 457 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 397 | pub prompts: Option<PromptsCapabilities>, | 458 | pub prompts: Option<PromptsCapabilities>, |
| 398 | #[serde(skip_serializing_if = "Option::is_none")] | 459 | #[serde(skip_serializing_if = "Option::is_none")] |
| 399 | pub resources: Option<ResourcesCapabilities>, | 460 | pub resources: Option<ResourcesCapabilities>, |
| @@ -438,6 +499,28 @@ pub struct Tool { · 38 unmodified lines | |||
| 438 | #[serde(skip_serializing_if = "Option::is_none")] | 499 | #[serde(skip_serializing_if = "Option::is_none")] |
| 439 | pub description: Option<String>, | 500 | pub description: Option<String>, |
| 440 | pub input_schema: serde_json::Value, | 501 | pub input_schema: serde_json::Value, |
| 502 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 503 | pub annotations: Option<ToolAnnotations>, | ||
| 504 | } | ||
| 505 | |||
| 506 | #[derive(Debug, Serialize, Deserialize)] | ||
| 507 | #[serde(rename_all = "camelCase")] | ||
| 508 | pub struct ToolAnnotations { | ||
| 509 | /// A human-readable title for the tool. | ||
| 510 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 511 | pub title: Option<String>, | ||
| 512 | /// If true, the tool does not modify its environment. | ||
| 513 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 514 | pub read_only_hint: Option<bool>, | ||
| 515 | /// If true, the tool may perform destructive updates to its environment. | ||
| 516 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 517 | pub destructive_hint: Option<bool>, | ||
| 518 | /// If true, calling the tool repeatedly with the same arguments will have no additional effect on its environment. | ||
| 519 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 520 | pub idempotent_hint: Option<bool>, | ||
| 521 | /// If true, this tool may interact with an "open world" of external entities. | ||
| 522 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 523 | pub open_world_hint: Option<bool>, | ||
| 441 | } | 524 | } |
| 442 | 525 | ||
| 443 | #[derive(Debug, Serialize, Deserialize)] | 526 | #[derive(Debug, Serialize, Deserialize)] |
| @@ -528,34 +611,6 @@ pub struct ModelHint { · 84 unmodified lines | |||
| 528 | pub name: Option<String>, | 611 | pub name: Option<String>, |
| 529 | } | 612 | } |
| 530 | 613 | ||
| 531 | #[derive(Debug, Serialize, Deserialize)] | ||
| 532 | #[serde(rename_all = "camelCase")] | ||
| 533 | pub enum NotificationType { | ||
| 534 | Initialized, | ||
| 535 | Progress, | ||
| 536 | Message, | ||
| 537 | ResourcesUpdated, | ||
| 538 | ResourcesListChanged, | ||
| 539 | ToolsListChanged, | ||
| 540 | PromptsListChanged, | ||
| 541 | RootsListChanged, | ||
| 542 | } | ||
| 543 | |||
| 544 | impl NotificationType { | ||
| 545 | pub fn as_str(&self) -> &'static str { | ||
| 546 | match self { | ||
| 547 | NotificationType::Initialized => "notifications/initialized", | ||
| 548 | NotificationType::Progress => "notifications/progress", | ||
| 549 | NotificationType::Message => "notifications/message", | ||
| 550 | NotificationType::ResourcesUpdated => "notifications/resources/updated", | ||
| 551 | NotificationType::ResourcesListChanged => "notifications/resources/list_changed", | ||
| 552 | NotificationType::ToolsListChanged => "notifications/tools/list_changed", | ||
| 553 | NotificationType::PromptsListChanged => "notifications/prompts/list_changed", | ||
| 554 | NotificationType::RootsListChanged => "notifications/roots/list_changed", | ||
| 555 | } | ||
| 556 | } | ||
| 557 | } | ||
| 558 | |||
| 559 | #[derive(Debug, Serialize)] | 614 | #[derive(Debug, Serialize)] |
| 560 | #[serde(untagged)] | 615 | #[serde(untagged)] |
| 561 | pub enum ClientNotification { | 616 | pub enum ClientNotification { |
| @@ -576,12 +631,14 @@ pub enum ProgressToken { · 14 unmodified lines | |||
| 576 | Number(f64), | 631 | Number(f64), |
| 577 | } | 632 | } |
| 578 | 633 | ||
| 579 | #[derive(Debug, Serialize)] | 634 | #[derive(Debug, Serialize, Deserialize)] |
| 580 | #[serde(rename_all = "camelCase")] | 635 | #[serde(rename_all = "camelCase")] |
| 581 | pub struct ProgressParams { | 636 | pub struct ProgressParams { |
| 582 | pub progress_token: ProgressToken, | 637 | pub progress_token: ProgressToken, |
| 583 | pub progress: f64, | 638 | pub progress: f64, |
| 584 | #[serde(skip_serializing_if = "Option::is_none")] | 639 | #[serde(skip_serializing_if = "Option::is_none")] |
| 640 | pub message: Option<String>, | ||
| 641 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 585 | pub total: Option<f64>, | 642 | pub total: Option<f64>, |
| 586 | #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] | 643 | #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] |
| 587 | pub meta: Option<HashMap<String, serde_json::Value>>, | 644 | pub meta: Option<HashMap<String, serde_json::Value>>, |
| @@ -625,6 +682,8 @@ pub enum ToolResponseContent { · 37 unmodified lines | |||
| 625 | Text { text: String }, | 682 | Text { text: String }, |
| 626 | #[serde(rename = "image", rename_all = "camelCase")] | 683 | #[serde(rename = "image", rename_all = "camelCase")] |
| 627 | Image { data: String, mime_type: String }, | 684 | Image { data: String, mime_type: String }, |
| 685 | #[serde(rename = "audio", rename_all = "camelCase")] | ||
| 686 | Audio { data: String, mime_type: String }, | ||
| 628 | #[serde(rename = "resource")] | 687 | #[serde(rename = "resource")] |
| 629 | Resource { resource: ResourceContents }, | 688 | Resource { resource: ResourceContents }, |
| 630 | } | 689 | } |
| @@ -26,6 +26,7 @@ use parking_lot::Mutex; · 25 unmodified lines | |||
| 26 | use request::StatusNotification; | 26 | use request::StatusNotification; |
| 27 | use settings::SettingsStore; | 27 | use settings::SettingsStore; |
| 28 | use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace}; | 28 | use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace}; |
| 29 | use std::collections::hash_map::Entry; | ||
| 29 | use std::{ | 30 | use std::{ |
| 30 | any::TypeId, | 31 | any::TypeId, |
| 31 | env, | 32 | env, |
| @@ -731,42 +732,43 @@ impl Copilot { · 699 unmodified lines | |||
| 731 | return; | 732 | return; |
| 732 | } | 733 | } |
| 733 | 734 | ||
| 734 | registered_buffers | 735 | let entry = registered_buffers.entry(buffer.entity_id()); |
| 735 | .entry(buffer.entity_id()) | 736 | if let Entry::Vacant(e) = entry { |
| 736 | .or_insert_with(|| { | 737 | let Ok(uri) = uri_for_buffer(buffer, cx) else { |
| 737 | let uri: lsp::Url = uri_for_buffer(buffer, cx); | 738 | return; |
| 738 | let language_id = id_for_language(buffer.read(cx).language()); | 739 | }; |
| 739 | let snapshot = buffer.read(cx).snapshot(); | 740 | let language_id = id_for_language(buffer.read(cx).language()); |
| 740 | server | 741 | let snapshot = buffer.read(cx).snapshot(); |
| 741 | .notify::<lsp::notification::DidOpenTextDocument>( | 742 | server |
| 742 | &lsp::DidOpenTextDocumentParams { | 743 | .notify::<lsp::notification::DidOpenTextDocument>( |
| 743 | text_document: lsp::TextDocumentItem { | 744 | &lsp::DidOpenTextDocumentParams { |
| 744 | uri: uri.clone(), | 745 | text_document: lsp::TextDocumentItem { |
| 745 | language_id: language_id.clone(), | 746 | uri: uri.clone(), |
| 746 | version: 0, | 747 | language_id: language_id.clone(), |
| 747 | text: snapshot.text(), | 748 | version: 0, |
| 748 | }, | 749 | text: snapshot.text(), |
| 749 | }, | 750 | }, |
| 750 | ) | 751 | }, |
| 751 | .ok(); | 752 | ) |
| 753 | .ok(); | ||
| 752 | 754 | ||
| 753 | RegisteredBuffer { | 755 | e.insert(RegisteredBuffer { |
| 754 | uri, | 756 | uri, |
| 755 | language_id, | 757 | language_id, |
| 756 | snapshot, | 758 | snapshot, |
| 757 | snapshot_version: 0, | 759 | snapshot_version: 0, |
| 758 | pending_buffer_change: Task::ready(Some(())), | 760 | pending_buffer_change: Task::ready(Some(())), |
| 759 | _subscriptions: [ | 761 | _subscriptions: [ |
| 760 | cx.subscribe(buffer, |this, buffer, event, cx| { | 762 | cx.subscribe(buffer, |this, buffer, event, cx| { |
| 761 | this.handle_buffer_event(buffer, event, cx).log_err(); | 763 | this.handle_buffer_event(buffer, event, cx).log_err(); |
| 762 | }), | 764 | }), |
| 763 | cx.observe_release(buffer, move |this, _buffer, _cx| { | 765 | cx.observe_release(buffer, move |this, _buffer, _cx| { |
| 764 | this.buffers.remove(&weak_buffer); | 766 | this.buffers.remove(&weak_buffer); |
| 765 | this.unregister_buffer(&weak_buffer); | 767 | this.unregister_buffer(&weak_buffer); |
| 766 | }), | 768 | }), |
| 767 | ], | 769 | ], |
| 768 | } | ||
| 769 | }); | 770 | }); |
| 771 | } | ||
| 770 | } | 772 | } |
| 771 | } | 773 | } |
| 772 | 774 | ||
| @@ -798,7 +800,9 @@ impl Copilot { · 25 unmodified lines | |||
| 798 | language::BufferEvent::FileHandleChanged | 800 | language::BufferEvent::FileHandleChanged |
| 799 | | language::BufferEvent::LanguageChanged => { | 801 | | language::BufferEvent::LanguageChanged => { |
| 800 | let new_language_id = id_for_language(buffer.read(cx).language()); | 802 | let new_language_id = id_for_language(buffer.read(cx).language()); |
| 801 | let new_uri = uri_for_buffer(&buffer, cx); | 803 | let Ok(new_uri) = uri_for_buffer(&buffer, cx) else { |
| 804 | return Ok(()); | ||
| 805 | }; | ||
| 802 | if new_uri != registered_buffer.uri | 806 | if new_uri != registered_buffer.uri |
| 803 | || new_language_id != registered_buffer.language_id | 807 | || new_language_id != registered_buffer.language_id |
| 804 | { | 808 | { |
| @@ -1068,11 +1072,13 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String { · 263 unmodified lines | |||
| 1068 | .unwrap_or_else(|| "plaintext".to_string()) | 1072 | .unwrap_or_else(|| "plaintext".to_string()) |
| 1069 | } | 1073 | } |
| 1070 | 1074 | ||
| 1071 | fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> lsp::Url { | 1075 | fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> { |
| 1072 | if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { | 1076 | if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { |
| 1073 | lsp::Url::from_file_path(file.abs_path(cx)).unwrap() | 1077 | lsp::Url::from_file_path(file.abs_path(cx)) |
| 1074 | } else { | 1078 | } else { |
| 1075 | format!("buffer://{}", buffer.entity_id()).parse().unwrap() | 1079 | format!("buffer://{}", buffer.entity_id()) |
| 1080 | .parse() | ||
| 1081 | .map_err(|_| ()) | ||
| 1076 | } | 1082 | } |
| 1077 | } | 1083 | } |
| 1078 | 1084 | ||
| @@ -51,6 +51,9 @@ telemetry.workspace = true · 50 unmodified lines | |||
| 51 | util.workspace = true | 51 | util.workspace = true |
| 52 | workspace-hack.workspace = true | 52 | workspace-hack.workspace = true |
| 53 | 53 | ||
| 54 | [target.'cfg(not(windows))'.dependencies] | ||
| 55 | libc.workspace = true | ||
| 56 | |||
| 54 | [dev-dependencies] | 57 | [dev-dependencies] |
| 55 | async-pipe.workspace = true | 58 | async-pipe.workspace = true |
| 56 | gpui = { workspace = true, features = ["test-support"] } | 59 | gpui = { workspace = true, features = ["test-support"] } |
| @@ -93,7 +93,7 @@ impl<'a> From<&'a str> for DebugAdapterName { · 92 unmodified lines | |||
| 93 | } | 93 | } |
| 94 | } | 94 | } |
| 95 | 95 | ||
| 96 | #[derive(Debug, Clone, PartialEq)] | 96 | #[derive(Debug, Clone, PartialEq, Serialize)] |
| 97 | pub struct TcpArguments { | 97 | pub struct TcpArguments { |
| 98 | pub host: Ipv4Addr, | 98 | pub host: Ipv4Addr, |
| 99 | pub port: u16, | 99 | pub port: u16, |
| @@ -179,9 +179,9 @@ impl DebugTaskDefinition { · 79 unmodified lines | |||
| 179 | } | 179 | } |
| 180 | 180 | ||
| 181 | /// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session. | 181 | /// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session. |
| 182 | #[derive(Debug, Clone, PartialEq)] | 182 | #[derive(Debug, Clone, PartialEq, Serialize)] |
| 183 | pub struct DebugAdapterBinary { | 183 | pub struct DebugAdapterBinary { |
| 184 | pub command: String, | 184 | pub command: Option<String>, |
| 185 | pub arguments: Vec<String>, | 185 | pub arguments: Vec<String>, |
| 186 | pub envs: HashMap<String, String>, | 186 | pub envs: HashMap<String, String>, |
| 187 | pub cwd: Option<PathBuf>, | 187 | pub cwd: Option<PathBuf>, |
| @@ -369,6 +369,10 @@ pub trait DebugAdapter: 'static + Send + Sync { · 181 unmodified lines | |||
| 369 | } | 369 | } |
| 370 | 370 | ||
| 371 | async fn dap_schema(&self) -> serde_json::Value; | 371 | async fn dap_schema(&self) -> serde_json::Value; |
| 372 | |||
| 373 | fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> { | ||
| 374 | None | ||
| 375 | } | ||
| 372 | } | 376 | } |
| 373 | 377 | ||
| 374 | #[cfg(any(test, feature = "test-support"))] | 378 | #[cfg(any(test, feature = "test-support"))] |
| @@ -433,7 +437,7 @@ impl DebugAdapter for FakeAdapter { · 58 unmodified lines | |||
| 433 | _: &mut AsyncApp, | 437 | _: &mut AsyncApp, |
| 434 | ) -> Result<DebugAdapterBinary> { | 438 | ) -> Result<DebugAdapterBinary> { |
| 435 | Ok(DebugAdapterBinary { | 439 | Ok(DebugAdapterBinary { |
| 436 | command: "command".into(), | 440 | command: Some("command".into()), |
| 437 | arguments: vec![], | 441 | arguments: vec![], |
| 438 | connection: None, | 442 | connection: None, |
| 439 | envs: HashMap::default(), | 443 | envs: HashMap::default(), |
| @@ -218,7 +218,7 @@ impl DebugAdapterClient { · 217 unmodified lines | |||
| 218 | 218 | ||
| 219 | pub fn add_log_handler<F>(&self, f: F, kind: LogKind) | 219 | pub fn add_log_handler<F>(&self, f: F, kind: LogKind) |
| 220 | where | 220 | where |
| 221 | F: 'static + Send + FnMut(IoKind, &str), | 221 | F: 'static + Send + FnMut(IoKind, Option<&str>, &str), |
| 222 | { | 222 | { |
| 223 | self.transport_delegate.add_log_handler(f, kind); | 223 | self.transport_delegate.add_log_handler(f, kind); |
| 224 | } | 224 | } |
| @@ -297,7 +297,7 @@ mod tests { · 72 unmodified lines | |||
| 297 | let client = DebugAdapterClient::start( | 297 | let client = DebugAdapterClient::start( |
| 298 | crate::client::SessionId(1), | 298 | crate::client::SessionId(1), |
| 299 | DebugAdapterBinary { | 299 | DebugAdapterBinary { |
| 300 | command: "command".into(), | 300 | command: Some("command".into()), |
| 301 | arguments: Default::default(), | 301 | arguments: Default::default(), |
| 302 | envs: Default::default(), | 302 | envs: Default::default(), |
| 303 | connection: None, | 303 | connection: None, |
| @@ -367,7 +367,7 @@ mod tests { · 63 unmodified lines | |||
| 367 | let client = DebugAdapterClient::start( | 367 | let client = DebugAdapterClient::start( |
| 368 | crate::client::SessionId(1), | 368 | crate::client::SessionId(1), |
| 369 | DebugAdapterBinary { | 369 | DebugAdapterBinary { |
| 370 | command: "command".into(), | 370 | command: Some("command".into()), |
| 371 | arguments: Default::default(), | 371 | arguments: Default::default(), |
| 372 | envs: Default::default(), | 372 | envs: Default::default(), |
| 373 | connection: None, | 373 | connection: None, |
| @@ -420,7 +420,7 @@ mod tests { · 46 unmodified lines | |||
| 420 | let client = DebugAdapterClient::start( | 420 | let client = DebugAdapterClient::start( |
| 421 | crate::client::SessionId(1), | 421 | crate::client::SessionId(1), |
| 422 | DebugAdapterBinary { | 422 | DebugAdapterBinary { |
| 423 | command: "command".into(), | 423 | command: Some("command".into()), |
| 424 | arguments: Default::default(), | 424 | arguments: Default::default(), |
| 425 | envs: Default::default(), | 425 | envs: Default::default(), |
| 426 | connection: None, | 426 | connection: None, |
| @@ -19,11 +19,11 @@ use std::{collections::BTreeMap, sync::Arc}; · 18 unmodified lines | |||
| 19 | pub trait DapLocator: Send + Sync { | 19 | pub trait DapLocator: Send + Sync { |
| 20 | fn name(&self) -> SharedString; | 20 | fn name(&self) -> SharedString; |
| 21 | /// Determines whether this locator can generate debug target for given task. | 21 | /// Determines whether this locator can generate debug target for given task. |
| 22 | fn create_scenario( | 22 | async fn create_scenario( |
| 23 | &self, | 23 | &self, |
| 24 | build_config: &TaskTemplate, | 24 | build_config: &TaskTemplate, |
| 25 | resolved_label: &str, | 25 | resolved_label: &str, |
| 26 | adapter: DebugAdapterName, | 26 | adapter: &DebugAdapterName, |
| 27 | ) -> Option<DebugScenario>; | 27 | ) -> Option<DebugScenario>; |
| 28 | 28 | ||
| 29 | async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest>; | 29 | async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest>; |
| @@ -1,4 +1,4 @@ | |||
| 1 | use anyhow::{Context as _, Result, bail}; | 1 | use anyhow::{Context as _, Result, anyhow, bail}; |
| 2 | use dap_types::{ | 2 | use dap_types::{ |
| 3 | ErrorResponse, | 3 | ErrorResponse, |
| 4 | messages::{Message, Response}, | 4 | messages::{Message, Response}, |
| @@ -12,7 +12,6 @@ use smol::{ · 7 unmodified lines | |||
| 12 | io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader}, | 12 | io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader}, |
| 13 | lock::Mutex, | 13 | lock::Mutex, |
| 14 | net::{TcpListener, TcpStream}, | 14 | net::{TcpListener, TcpStream}, |
| 15 | process::Child, | ||
| 16 | }; | 15 | }; |
| 17 | use std::{ | 16 | use std::{ |
| 18 | collections::HashMap, | 17 | collections::HashMap, |
| @@ -22,11 +21,13 @@ use std::{ · 3 unmodified lines | |||
| 22 | time::Duration, | 21 | time::Duration, |
| 23 | }; | 22 | }; |
| 24 | use task::TcpArgumentsTemplate; | 23 | use task::TcpArgumentsTemplate; |
| 25 | use util::{ConnectionResult, ResultExt as _}; | 24 | use util::ConnectionResult; |
| 26 | 25 | ||
| 27 | use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings}; | 26 | use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings}; |
| 28 | 27 | ||
| 29 | pub type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>; | 28 | pub(crate) type IoMessage = str; |
| 29 | pub(crate) type Command = str; | ||
| 30 | pub type IoHandler = Box<dyn Send + FnMut(IoKind, Option<&Command>, &IoMessage)>; | ||
| 30 | 31 | ||
| 31 | #[derive(PartialEq, Eq, Clone, Copy)] | 32 | #[derive(PartialEq, Eq, Clone, Copy)] |
| 32 | pub enum LogKind { | 33 | pub enum LogKind { |
| @@ -86,10 +87,12 @@ impl Transport { · 53 unmodified lines | |||
| 86 | TcpTransport::start(binary, cx) | 87 | TcpTransport::start(binary, cx) |
| 87 | .await | 88 | .await |
| 88 | .map(|(transports, tcp)| (transports, Self::Tcp(tcp))) | 89 | .map(|(transports, tcp)| (transports, Self::Tcp(tcp))) |
| 90 | .context("Tried to connect to a debug adapter via TCP transport layer") | ||
| 89 | } else { | 91 | } else { |
| 90 | StdioTransport::start(binary, cx) | 92 | StdioTransport::start(binary, cx) |
| 91 | .await | 93 | .await |
| 92 | .map(|(transports, stdio)| (transports, Self::Stdio(stdio))) | 94 | .map(|(transports, stdio)| (transports, Self::Stdio(stdio))) |
| 95 | .context("Tried to connect to a debug adapter via stdin/stdout transport layer") | ||
| 93 | } | 96 | } |
| 94 | } | 97 | } |
| 95 | 98 | ||
| @@ -102,7 +105,7 @@ impl Transport { · 6 unmodified lines | |||
| 102 | } | 105 | } |
| 103 | } | 106 | } |
| 104 | 107 | ||
| 105 | async fn kill(&self) -> Result<()> { | 108 | async fn kill(&self) { |
| 106 | match self { | 109 | match self { |
| 107 | Transport::Stdio(stdio_transport) => stdio_transport.kill().await, | 110 | Transport::Stdio(stdio_transport) => stdio_transport.kill().await, |
| 108 | Transport::Tcp(tcp_transport) => tcp_transport.kill().await, | 111 | Transport::Tcp(tcp_transport) => tcp_transport.kill().await, |
| @@ -191,7 +194,7 @@ impl TransportDelegate { · 82 unmodified lines | |||
| 191 | match Self::handle_output( | 194 | match Self::handle_output( |
| 192 | params.output, | 195 | params.output, |
| 193 | client_tx, | 196 | client_tx, |
| 194 | pending_requests, | 197 | pending_requests.clone(), |
| 195 | output_log_handler, | 198 | output_log_handler, |
| 196 | ) | 199 | ) |
| 197 | .await | 200 | .await |
| @@ -199,6 +202,12 @@ impl TransportDelegate { · 1 unmodified line | |||
| 199 | Ok(()) => {} | 202 | Ok(()) => {} |
| 200 | Err(e) => log::error!("Error handling debugger output: {e}"), | 203 | Err(e) => log::error!("Error handling debugger output: {e}"), |
| 201 | } | 204 | } |
| 205 | let mut pending_requests = pending_requests.lock().await; | ||
| 206 | pending_requests.drain().for_each(|(_, request)| { | ||
| 207 | request | ||
| 208 | .send(Err(anyhow!("debugger shutdown unexpectedly"))) | ||
| 209 | .ok(); | ||
| 210 | }); | ||
| 202 | })); | 211 | })); |
| 203 | 212 | ||
| 204 | if let Some(stderr) = params.stderr.take() { | 213 | if let Some(stderr) = params.stderr.take() { |
| @@ -289,7 +298,7 @@ impl TransportDelegate { · 84 unmodified lines | |||
| 289 | if let Some(log_handlers) = log_handlers.as_ref() { | 298 | if let Some(log_handlers) = log_handlers.as_ref() { |
| 290 | for (kind, handler) in log_handlers.lock().iter_mut() { | 299 | for (kind, handler) in log_handlers.lock().iter_mut() { |
| 291 | if matches!(kind, LogKind::Adapter) { | 300 | if matches!(kind, LogKind::Adapter) { |
| 292 | handler(IoKind::StdOut, line.as_str()); | 301 | handler(IoKind::StdOut, None, line.as_str()); |
| 293 | } | 302 | } |
| 294 | } | 303 | } |
| 295 | } | 304 | } |
| @@ -323,6 +332,12 @@ impl TransportDelegate { · 27 unmodified lines | |||
| 323 | } | 332 | } |
| 324 | } | 333 | } |
| 325 | 334 | ||
| 335 | let command = match &message { | ||
| 336 | Message::Request(request) => Some(request.command.as_str()), | ||
| 337 | Message::Response(response) => Some(response.command.as_str()), | ||
| 338 | _ => None, | ||
| 339 | }; | ||
| 340 | |||
| 326 | let message = match serde_json::to_string(&message) { | 341 | let message = match serde_json::to_string(&message) { |
| 327 | Ok(message) => message, | 342 | Ok(message) => message, |
| 328 | Err(e) => break Err(e.into()), | 343 | Err(e) => break Err(e.into()), |
| @@ -331,7 +346,7 @@ impl TransportDelegate { · 2 unmodified lines | |||
| 331 | if let Some(log_handlers) = log_handlers.as_ref() { | 346 | if let Some(log_handlers) = log_handlers.as_ref() { |
| 332 | for (kind, log_handler) in log_handlers.lock().iter_mut() { | 347 | for (kind, log_handler) in log_handlers.lock().iter_mut() { |
| 333 | if matches!(kind, LogKind::Rpc) { | 348 | if matches!(kind, LogKind::Rpc) { |
| 334 | log_handler(IoKind::StdIn, &message); | 349 | log_handler(IoKind::StdIn, command, &message); |
| 335 | } | 350 | } |
| 336 | } | 351 | } |
| 337 | } | 352 | } |
| @@ -416,7 +431,7 @@ impl TransportDelegate { · 78 unmodified lines | |||
| 416 | Ok(_) => { | 431 | Ok(_) => { |
| 417 | for (kind, log_handler) in log_handlers.lock().iter_mut() { | 432 | for (kind, log_handler) in log_handlers.lock().iter_mut() { |
| 418 | if matches!(kind, LogKind::Adapter) { | 433 | if matches!(kind, LogKind::Adapter) { |
| 419 | log_handler(IoKind::StdErr, buffer.as_str()); | 434 | log_handler(IoKind::StdErr, None, buffer.as_str()); |
| 420 | } | 435 | } |
| 421 | } | 436 | } |
| 422 | 437 | ||
| @@ -505,17 +520,24 @@ impl TransportDelegate { · 82 unmodified lines | |||
| 505 | Err(e) => return ConnectionResult::Result(Err(e)), | 520 | Err(e) => return ConnectionResult::Result(Err(e)), |
| 506 | }; | 521 | }; |
| 507 | 522 | ||
| 523 | let message = | ||
| 524 | serde_json::from_str::<Message>(message_str).context("deserializing server message"); | ||
| 525 | |||
| 508 | if let Some(log_handlers) = log_handlers { | 526 | if let Some(log_handlers) = log_handlers { |
| 527 | let command = match &message { | ||
| 528 | Ok(Message::Request(request)) => Some(request.command.as_str()), | ||
| 529 | Ok(Message::Response(response)) => Some(response.command.as_str()), | ||
| 530 | _ => None, | ||
| 531 | }; | ||
| 532 | |||
| 509 | for (kind, log_handler) in log_handlers.lock().iter_mut() { | 533 | for (kind, log_handler) in log_handlers.lock().iter_mut() { |
| 510 | if matches!(kind, LogKind::Rpc) { | 534 | if matches!(kind, LogKind::Rpc) { |
| 511 | log_handler(IoKind::StdOut, message_str); | 535 | log_handler(IoKind::StdOut, command, message_str); |
| 512 | } | 536 | } |
| 513 | } | 537 | } |
| 514 | } | 538 | } |
| 515 | 539 | ||
| 516 | ConnectionResult::Result( | 540 | ConnectionResult::Result(message) |
| 517 | serde_json::from_str::<Message>(message_str).context("deserializing server message"), | ||
| 518 | ) | ||
| 519 | } | 541 | } |
| 520 | 542 | ||
| 521 | pub async fn shutdown(&self) -> Result<()> { | 543 | pub async fn shutdown(&self) -> Result<()> { |
| @@ -531,7 +553,7 @@ impl TransportDelegate { · 9 unmodified lines | |||
| 531 | current_requests.clear(); | 553 | current_requests.clear(); |
| 532 | pending_requests.clear(); | 554 | pending_requests.clear(); |
| 533 | 555 | ||
| 534 | let _ = self.transport.kill().await.log_err(); | 556 | self.transport.kill().await; |
| 535 | 557 | ||
| 536 | drop(current_requests); | 558 | drop(current_requests); |
| 537 | drop(pending_requests); | 559 | drop(pending_requests); |
| @@ -551,7 +573,7 @@ impl TransportDelegate { · 13 unmodified lines | |||
| 551 | 573 | ||
| 552 | pub fn add_log_handler<F>(&self, f: F, kind: LogKind) | 574 | pub fn add_log_handler<F>(&self, f: F, kind: LogKind) |
| 553 | where | 575 | where |
| 554 | F: 'static + Send + FnMut(IoKind, &str), | 576 | F: 'static + Send + FnMut(IoKind, Option<&Command>, &IoMessage), |
| 555 | { | 577 | { |
| 556 | let mut log_handlers = self.log_handlers.lock(); | 578 | let mut log_handlers = self.log_handlers.lock(); |
| 557 | log_handlers.push((kind, Box::new(f))); | 579 | log_handlers.push((kind, Box::new(f))); |
| @@ -562,7 +584,7 @@ pub struct TcpTransport { · 4 unmodified lines | |||
| 562 | pub port: u16, | 584 | pub port: u16, |
| 563 | pub host: Ipv4Addr, | 585 | pub host: Ipv4Addr, |
| 564 | pub timeout: u64, | 586 | pub timeout: u64, |
| 565 | process: Mutex<Child>, | 587 | process: Option<Mutex<Child>>, |
| 566 | } | 588 | } |
| 567 | 589 | ||
| 568 | impl TcpTransport { | 590 | impl TcpTransport { |
| @@ -591,26 +613,23 @@ impl TcpTransport { · 22 unmodified lines | |||
| 591 | let host = connection_args.host; | 613 | let host = connection_args.host; |
| 592 | let port = connection_args.port; | 614 | let port = connection_args.port; |
| 593 | 615 | ||
| 594 | let mut command = util::command::new_std_command(&binary.command); | 616 | let mut process = if let Some(command) = &binary.command { |
| 595 | util::set_pre_exec_to_start_new_session(&mut command); | 617 | let mut command = util::command::new_std_command(&command); |
| 596 | let mut command = smol::process::Command::from(command); | ||
| 597 | 618 | ||
| 598 | if let Some(cwd) = &binary.cwd { | 619 | if let Some(cwd) = &binary.cwd { |
| 599 | command.current_dir(cwd); | 620 | command.current_dir(cwd); |
| 600 | } | 621 | } |
| 601 | |||
| 602 | command.args(&binary.arguments); | ||
| 603 | command.envs(&binary.envs); | ||
| 604 | 622 | ||
| 605 | command | 623 | command.args(&binary.arguments); |
| 606 | .stdin(Stdio::null()) | 624 | command.envs(&binary.envs); |
| 607 | .stdout(Stdio::piped()) | ||
| 608 | .stderr(Stdio::piped()) | ||
| 609 | .kill_on_drop(true); | ||
| 610 | 625 | ||
| 611 | let mut process = command | 626 | Some( |
| 612 | .spawn() | 627 | Child::spawn(command, Stdio::null()) |
| 613 | .with_context(|| "failed to start debug adapter.")?; | 628 | .with_context(|| "failed to start debug adapter.")?, |
| 629 | ) | ||
| 630 | } else { | ||
| 631 | None | ||
| 632 | }; | ||
| 614 | 633 | ||
| 615 | let address = SocketAddrV4::new(host, port); | 634 | let address = SocketAddrV4::new(host, port); |
| 616 | 635 | ||
| @@ -628,15 +647,18 @@ impl TcpTransport { · 11 unmodified lines | |||
| 628 | match TcpStream::connect(address).await { | 647 | match TcpStream::connect(address).await { |
| 629 | Ok(stream) => return Ok((process, stream.split())), | 648 | Ok(stream) => return Ok((process, stream.split())), |
| 630 | Err(_) => { | 649 | Err(_) => { |
| 631 | if let Ok(Some(_)) = process.try_status() { | 650 | if let Some(p) = &mut process { |
| 632 | let output = process.output().await?; | 651 | if let Ok(Some(_)) = p.try_status() { |
| 633 | let output = if output.stderr.is_empty() { | 652 | let output = process.take().unwrap().into_inner().output().await?; |
| 634 | String::from_utf8_lossy(&output.stdout).to_string() | 653 | let output = if output.stderr.is_empty() { |
| 635 | } else { | 654 | String::from_utf8_lossy(&output.stdout).to_string() |
| 636 | String::from_utf8_lossy(&output.stderr).to_string() | 655 | } else { |
| 637 | }; | 656 | String::from_utf8_lossy(&output.stderr).to_string() |
| 638 | anyhow::bail!("{output}\nerror: process exited before debugger attached."); | 657 | }; |
| 658 | anyhow::bail!("{output}\nerror: process exited before debugger attached."); | ||
| 659 | } | ||
| 639 | } | 660 | } |
| 661 | |||
| 640 | cx.background_executor().timer(Duration::from_millis(100)).await; | 662 | cx.background_executor().timer(Duration::from_millis(100)).await; |
| 641 | } | 663 | } |
| 642 | } | 664 | } |
| @@ -649,13 +671,13 @@ impl TcpTransport { · 6 unmodified lines | |||
| 649 | host, | 671 | host, |
| 650 | port | 672 | port |
| 651 | ); | 673 | ); |
| 652 | let stdout = process.stdout.take(); | 674 | let stdout = process.as_mut().and_then(|p| p.stdout.take()); |
| 653 | let stderr = process.stderr.take(); | 675 | let stderr = process.as_mut().and_then(|p| p.stderr.take()); |
| 654 | 676 | ||
| 655 | let this = Self { | 677 | let this = Self { |
| 656 | port, | 678 | port, |
| 657 | host, | 679 | host, |
| 658 | process: Mutex::new(process), | 680 | process: process.map(Mutex::new), |
| 659 | timeout, | 681 | timeout, |
| 660 | }; | 682 | }; |
| 661 | 683 | ||
| @@ -673,10 +695,19 @@ impl TcpTransport { · 11 unmodified lines | |||
| 673 | true | 695 | true |
| 674 | } | 696 | } |
| 675 | 697 | ||
| 676 | async fn kill(&self) -> Result<()> { | 698 | async fn kill(&self) { |
| 677 | self.process.lock().await.kill()?; | 699 | if let Some(process) = &self.process { |
| 700 | let mut process = process.lock().await; | ||
| 701 | Child::kill(&mut process); | ||
| 702 | } | ||
| 703 | } | ||
| 704 | } | ||
| 678 | 705 | ||
| 679 | Ok(()) | 706 | impl Drop for TcpTransport { |
| 707 | fn drop(&mut self) { | ||
| 708 | if let Some(mut p) = self.process.take() { | ||
| 709 | p.get_mut().kill(); | ||
| 710 | } | ||
| 680 | } | 711 | } |
| 681 | } | 712 | } |
| 682 | 713 | ||
| @@ -687,9 +718,12 @@ pub struct StdioTransport { · 4 unmodified lines | |||
| 687 | impl StdioTransport { | 718 | impl StdioTransport { |
| 688 | #[allow(dead_code, reason = "This is used in non test builds of Zed")] | 719 | #[allow(dead_code, reason = "This is used in non test builds of Zed")] |
| 689 | async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> { | 720 | async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> { |
| 690 | let mut command = util::command::new_std_command(&binary.command); | 721 | let Some(binary_command) = &binary.command else { |
| 691 | util::set_pre_exec_to_start_new_session(&mut command); | 722 | bail!( |
| 692 | let mut command = smol::process::Command::from(command); | 723 | "When using the `stdio` transport, the path to a debug adapter binary must be set by Zed." |
| 724 | ); | ||
| 725 | }; | ||
| 726 | let mut command = util::command::new_std_command(&binary_command); | ||
| 693 | 727 | ||
| 694 | if let Some(cwd) = &binary.cwd { | 728 | if let Some(cwd) = &binary.cwd { |
| 695 | command.current_dir(cwd); | 729 | command.current_dir(cwd); |
| @@ -698,16 +732,10 @@ impl StdioTransport { · 2 unmodified lines | |||
| 698 | command.args(&binary.arguments); | 732 | command.args(&binary.arguments); |
| 699 | command.envs(&binary.envs); | 733 | command.envs(&binary.envs); |
| 700 | 734 | ||
| 701 | command | 735 | let mut process = Child::spawn(command, Stdio::piped()).with_context(|| { |
| 702 | .stdin(Stdio::piped()) | ||
| 703 | .stdout(Stdio::piped()) | ||
| 704 | .stderr(Stdio::piped()) | ||
| 705 | .kill_on_drop(true); | ||
| 706 | |||
| 707 | let mut process = command.spawn().with_context(|| { | ||
| 708 | format!( | 736 | format!( |
| 709 | "failed to spawn command `{} {}`.", | 737 | "failed to spawn command `{} {}`.", |
| 710 | binary.command, | 738 | binary_command, |
| 711 | binary.arguments.join(" ") | 739 | binary.arguments.join(" ") |
| 712 | ) | 740 | ) |
| 713 | })?; | 741 | })?; |
| @@ -722,7 +750,7 @@ impl StdioTransport { · 8 unmodified lines | |||
| 722 | if stderr.is_none() { | 750 | if stderr.is_none() { |
| 723 | bail!( | 751 | bail!( |
| 724 | "Failed to connect to stderr for debug adapter command {}", | 752 | "Failed to connect to stderr for debug adapter command {}", |
| 725 | &binary.command | 753 | &binary_command |
| 726 | ); | 754 | ); |
| 727 | } | 755 | } |
| 728 | 756 | ||
| @@ -745,9 +773,15 @@ impl StdioTransport { · 16 unmodified lines | |||
| 745 | false | 773 | false |
| 746 | } | 774 | } |
| 747 | 775 | ||
| 748 | async fn kill(&self) -> Result<()> { | 776 | async fn kill(&self) { |
| 749 | self.process.lock().await.kill()?; | 777 | let mut process = self.process.lock().await; |
| 750 | Ok(()) | 778 | Child::kill(&mut process); |
| 779 | } | ||
| 780 | } | ||
| 781 | |||
| 782 | impl Drop for StdioTransport { | ||
| 783 | fn drop(&mut self) { | ||
| 784 | self.process.get_mut().kill(); | ||
| 751 | } | 785 | } |
| 752 | } | 786 | } |
| 753 | 787 | ||
| @@ -921,7 +955,66 @@ impl FakeTransport { · 167 unmodified lines | |||
| 921 | false | 955 | false |
| 922 | } | 956 | } |
| 923 | 957 | ||
| 924 | async fn kill(&self) -> Result<()> { | 958 | async fn kill(&self) {} |
| 925 | Ok(()) | 959 | } |
| 960 | |||
| 961 | struct Child { | ||
| 962 | process: smol::process::Child, | ||
| 963 | } | ||
| 964 | |||
| 965 | impl std::ops::Deref for Child { | ||
| 966 | type Target = smol::process::Child; | ||
| 967 | |||
| 968 | fn deref(&self) -> &Self::Target { | ||
| 969 | &self.process | ||
| 970 | } | ||
| 971 | } | ||
| 972 | |||
| 973 | impl std::ops::DerefMut for Child { | ||
| 974 | fn deref_mut(&mut self) -> &mut Self::Target { | ||
| 975 | &mut self.process | ||
| 976 | } | ||
| 977 | } | ||
| 978 | |||
| 979 | impl Child { | ||
| 980 | fn into_inner(self) -> smol::process::Child { | ||
| 981 | self.process | ||
| 982 | } | ||
| 983 | |||
| 984 | #[cfg(not(windows))] | ||
| 985 | fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> { | ||
| 986 | util::set_pre_exec_to_start_new_session(&mut command); | ||
| 987 | let process = smol::process::Command::from(command) | ||
| 988 | .stdin(stdin) | ||
| 989 | .stdout(Stdio::piped()) | ||
| 990 | .stderr(Stdio::piped()) | ||
| 991 | .spawn()?; | ||
| 992 | Ok(Self { process }) | ||
| 993 | } | ||
| 994 | |||
| 995 | #[cfg(windows)] | ||
| 996 | fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> { | ||
| 997 | // TODO(windows): create a job object and add the child process handle to it, | ||
| 998 | // see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects | ||
| 999 | let process = smol::process::Command::from(command) | ||
| 1000 | .stdin(stdin) | ||
| 1001 | .stdout(Stdio::piped()) | ||
| 1002 | .stderr(Stdio::piped()) | ||
| 1003 | .spawn()?; | ||
| 1004 | Ok(Self { process }) | ||
| 1005 | } | ||
| 1006 | |||
| 1007 | #[cfg(not(windows))] | ||
| 1008 | fn kill(&mut self) { | ||
| 1009 | let pid = self.process.id(); | ||
| 1010 | unsafe { | ||
| 1011 | libc::killpg(pid as i32, libc::SIGKILL); | ||
| 1012 | } | ||
| 1013 | } | ||
| 1014 | |||
| 1015 | #[cfg(windows)] | ||
| 1016 | fn kill(&mut self) { | ||
| 1017 | // TODO(windows): terminate the job object in kill | ||
| 1018 | let _ = self.process.kill(); | ||
| 926 | } | 1019 | } |
| 927 | } | 1020 | } |
| @@ -23,6 +23,7 @@ doctest = false · 22 unmodified lines | |||
| 23 | [dependencies] | 23 | [dependencies] |
| 24 | anyhow.workspace = true | 24 | anyhow.workspace = true |
| 25 | async-trait.workspace = true | 25 | async-trait.workspace = true |
| 26 | collections.workspace = true | ||
| 26 | dap.workspace = true | 27 | dap.workspace = true |
| 27 | futures.workspace = true | 28 | futures.workspace = true |
| 28 | gpui.workspace = true | 29 | gpui.workspace = true |
| @@ -21,18 +21,21 @@ impl CodeLldbDebugAdapter { · 20 unmodified lines | |||
| 21 | 21 | ||
| 22 | fn request_args( | 22 | fn request_args( |
| 23 | &self, | 23 | &self, |
| 24 | delegate: &Arc<dyn DapDelegate>, | ||
| 24 | task_definition: &DebugTaskDefinition, | 25 | task_definition: &DebugTaskDefinition, |
| 25 | ) -> Result<dap::StartDebuggingRequestArguments> { | 26 | ) -> Result<dap::StartDebuggingRequestArguments> { |
| 26 | // CodeLLDB uses `name` for a terminal label. | 27 | // CodeLLDB uses `name` for a terminal label. |
| 27 | let mut configuration = task_definition.config.clone(); | 28 | let mut configuration = task_definition.config.clone(); |
| 28 | 29 | ||
| 29 | configuration | 30 | let obj = configuration |
| 30 | .as_object_mut() | 31 | .as_object_mut() |
| 31 | .context("CodeLLDB is not a valid json object")? | 32 | .context("CodeLLDB is not a valid json object")?; |
| 32 | .insert( | 33 | |
| 33 | "name".into(), | 34 | obj.entry("name") |
| 34 | Value::String(String::from(task_definition.label.as_ref())), | 35 | .or_insert(Value::String(String::from(task_definition.label.as_ref()))); |
| 35 | ); | 36 | |
| 37 | obj.entry("cwd") | ||
| 38 | .or_insert(delegate.worktree_root_path().to_string_lossy().into()); | ||
| 36 | 39 | ||
| 37 | let request = self.request_kind(&configuration)?; | 40 | let request = self.request_kind(&configuration)?; |
| 38 | 41 | ||
| @@ -359,13 +362,13 @@ impl DebugAdapter for CodeLldbDebugAdapter { · 320 unmodified lines | |||
| 359 | }; | 362 | }; |
| 360 | 363 | ||
| 361 | Ok(DebugAdapterBinary { | 364 | Ok(DebugAdapterBinary { |
| 362 | command: command.unwrap(), | 365 | command: Some(command.unwrap()), |
| 363 | cwd: Some(delegate.worktree_root_path().to_path_buf()), | 366 | cwd: Some(delegate.worktree_root_path().to_path_buf()), |
| 364 | arguments: vec![ | 367 | arguments: vec![ |
| 365 | "--settings".into(), | 368 | "--settings".into(), |
| 366 | json!({"sourceLanguages": ["cpp", "rust"]}).to_string(), | 369 | json!({"sourceLanguages": ["cpp", "rust"]}).to_string(), |
| 367 | ], | 370 | ], |
| 368 | request_args: self.request_args(&config)?, | 371 | request_args: self.request_args(delegate, &config)?, |
| 369 | envs: HashMap::default(), | 372 | envs: HashMap::default(), |
| 370 | connection: None, | 373 | connection: None, |
| 371 | }) | 374 | }) |
| @@ -177,18 +177,23 @@ impl DebugAdapter for GdbDebugAdapter { · 176 unmodified lines | |||
| 177 | 177 | ||
| 178 | let gdb_path = user_setting_path.unwrap_or(gdb_path?); | 178 | let gdb_path = user_setting_path.unwrap_or(gdb_path?); |
| 179 | 179 | ||
| 180 | let request_args = StartDebuggingRequestArguments { | 180 | let mut configuration = config.config.clone(); |
| 181 | request: self.request_kind(&config.config)?, | 181 | if let Some(configuration) = configuration.as_object_mut() { |
| 182 | configuration: config.config.clone(), | 182 | configuration |
| 183 | }; | 183 | .entry("cwd") |
| 184 | .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into()); | ||
| 185 | } | ||
| 184 | 186 | ||
| 185 | Ok(DebugAdapterBinary { | 187 | Ok(DebugAdapterBinary { |
| 186 | command: gdb_path, | 188 | command: Some(gdb_path), |
| 187 | arguments: vec!["-i=dap".into()], | 189 | arguments: vec!["-i=dap".into()], |
| 188 | envs: HashMap::default(), | 190 | envs: HashMap::default(), |
| 189 | cwd: Some(delegate.worktree_root_path().to_path_buf()), | 191 | cwd: Some(delegate.worktree_root_path().to_path_buf()), |
| 190 | connection: None, | 192 | connection: None, |
| 191 | request_args, | 193 | request_args: StartDebuggingRequestArguments { |
| 194 | request: self.request_kind(&config.config)?, | ||
| 195 | configuration, | ||
| 196 | }, | ||
| 192 | }) | 197 | }) |
| 193 | } | 198 | } |
| 194 | } | 199 | } |
| @@ -2,7 +2,7 @@ use anyhow::{Context as _, bail}; · 1 unmodified line | |||
| 2 | use dap::{ | 2 | use dap::{ |
| 3 | StartDebuggingRequestArguments, | 3 | StartDebuggingRequestArguments, |
| 4 | adapters::{ | 4 | adapters::{ |
| 5 | DebugTaskDefinition, DownloadedFileType, download_adapter_from_github, | 5 | DebugTaskDefinition, DownloadedFileType, TcpArguments, download_adapter_from_github, |
| 6 | latest_github_release, | 6 | latest_github_release, |
| 7 | }, | 7 | }, |
| 8 | }; | 8 | }; |
| @@ -10,6 +10,7 @@ use dap::{ · 1 unmodified line | |||
| 10 | use gpui::{AsyncApp, SharedString}; | 10 | use gpui::{AsyncApp, SharedString}; |
| 11 | use language::LanguageName; | 11 | use language::LanguageName; |
| 12 | use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock}; | 12 | use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock}; |
| 13 | use task::TcpArgumentsTemplate; | ||
| 13 | use util; | 14 | use util; |
| 14 | 15 | ||
| 15 | use crate::*; | 16 | use crate::*; |
| @@ -433,10 +434,6 @@ impl DebugAdapter for GoDebugAdapter { · 417 unmodified lines | |||
| 433 | 434 | ||
| 434 | adapter_path.join("dlv").to_string_lossy().to_string() | 435 | adapter_path.join("dlv").to_string_lossy().to_string() |
| 435 | }; | 436 | }; |
| 436 | let minidelve_path = self.install_shim(delegate).await?; | ||
| 437 | let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); | ||
| 438 | |||
| 439 | let (host, port, _) = crate::configure_tcp_connection(tcp_connection).await?; | ||
| 440 | 437 | ||
| 441 | let cwd = task_definition | 438 | let cwd = task_definition |
| 442 | .config | 439 | .config |
| @@ -445,31 +442,58 @@ impl DebugAdapter for GoDebugAdapter { · 2 unmodified lines | |||
| 445 | .map(PathBuf::from) | 442 | .map(PathBuf::from) |
| 446 | .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()); | 443 | .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()); |
| 447 | 444 | ||
| 448 | let arguments = if cfg!(windows) { | 445 | let arguments; |
| 449 | vec![ | 446 | let command; |
| 450 | delve_path, | 447 | let connection; |
| 451 | "dap".into(), | ||
| 452 | "--listen".into(), | ||
| 453 | format!("{}:{}", host, port), | ||
| 454 | "--headless".into(), | ||
| 455 | ] | ||
| 456 | } else { | ||
| 457 | vec![ | ||
| 458 | delve_path, | ||
| 459 | "dap".into(), | ||
| 460 | "--listen".into(), | ||
| 461 | format!("{}:{}", host, port), | ||
| 462 | ] | ||
| 463 | }; | ||
| 464 | 448 | ||
| 449 | let mut configuration = task_definition.config.clone(); | ||
| 450 | if let Some(configuration) = configuration.as_object_mut() { | ||
| 451 | configuration | ||
| 452 | .entry("cwd") | ||
| 453 | .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into()); | ||
| 454 | } | ||
| 455 | |||
| 456 | if let Some(connection_options) = &task_definition.tcp_connection { | ||
| 457 | command = None; | ||
| 458 | arguments = vec![]; | ||
| 459 | let (host, port, timeout) = | ||
| 460 | crate::configure_tcp_connection(connection_options.clone()).await?; | ||
| 461 | connection = Some(TcpArguments { | ||
| 462 | host, | ||
| 463 | port, | ||
| 464 | timeout, | ||
| 465 | }); | ||
| 466 | } else { | ||
| 467 | let minidelve_path = self.install_shim(delegate).await?; | ||
| 468 | let (host, port, _) = | ||
| 469 | crate::configure_tcp_connection(TcpArgumentsTemplate::default()).await?; | ||
| 470 | command = Some(minidelve_path.to_string_lossy().into_owned()); | ||
| 471 | connection = None; | ||
| 472 | arguments = if cfg!(windows) { | ||
| 473 | vec![ | ||
| 474 | delve_path, | ||
| 475 | "dap".into(), | ||
| 476 | "--listen".into(), | ||
| 477 | format!("{}:{}", host, port), | ||
| 478 | "--headless".into(), | ||
| 479 | ] | ||
| 480 | } else { | ||
| 481 | vec![ | ||
| 482 | delve_path, | ||
| 483 | "dap".into(), | ||
| 484 | "--listen".into(), | ||
| 485 | format!("{}:{}", host, port), | ||
| 486 | ] | ||
| 487 | }; | ||
| 488 | } | ||
| 465 | Ok(DebugAdapterBinary { | 489 | Ok(DebugAdapterBinary { |
| 466 | command: minidelve_path.to_string_lossy().into_owned(), | 490 | command, |
| 467 | arguments, | 491 | arguments, |
| 468 | cwd: Some(cwd), | 492 | cwd: Some(cwd), |
| 469 | envs: HashMap::default(), | 493 | envs: HashMap::default(), |
| 470 | connection: None, | 494 | connection, |
| 471 | request_args: StartDebuggingRequestArguments { | 495 | request_args: StartDebuggingRequestArguments { |
| 472 | configuration: task_definition.config.clone(), | 496 | configuration, |
| 473 | request: self.request_kind(&task_definition.config)?, | 497 | request: self.request_kind(&task_definition.config)?, |
| 474 | }, | 498 | }, |
| 475 | }) | 499 | }) |
| @@ -2,6 +2,7 @@ use adapters::latest_github_release; · 1 unmodified line | |||
| 2 | use anyhow::Context as _; | 2 | use anyhow::Context as _; |
| 3 | use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; | 3 | use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; |
| 4 | use gpui::AsyncApp; | 4 | use gpui::AsyncApp; |
| 5 | use serde_json::Value; | ||
| 5 | use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; | 6 | use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; |
| 6 | use task::DebugRequest; | 7 | use task::DebugRequest; |
| 7 | use util::ResultExt; | 8 | use util::ResultExt; |
| @@ -68,13 +69,44 @@ impl JsDebugAdapter { · 60 unmodified lines | |||
| 68 | let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); | 69 | let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); |
| 69 | let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; | 70 | let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; |
| 70 | 71 | ||
| 72 | let mut configuration = task_definition.config.clone(); | ||
| 73 | if let Some(configuration) = configuration.as_object_mut() { | ||
| 74 | if let Some(program) = configuration | ||
| 75 | .get("program") | ||
| 76 | .cloned() | ||
| 77 | .and_then(|value| value.as_str().map(str::to_owned)) | ||
| 78 | { | ||
| 79 | match program.as_str() { | ||
| 80 | "npm" | "pnpm" | "yarn" | "bun" | ||
| 81 | if !configuration.contains_key("runtimeExecutable") | ||
| 82 | && !configuration.contains_key("runtimeArgs") => | ||
| 83 | { | ||
| 84 | configuration.remove("program"); | ||
| 85 | configuration.insert("runtimeExecutable".to_owned(), program.into()); | ||
| 86 | if let Some(args) = configuration.remove("args") { | ||
| 87 | configuration.insert("runtimeArgs".to_owned(), args); | ||
| 88 | } | ||
| 89 | } | ||
| 90 | _ => {} | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | configuration | ||
| 95 | .entry("cwd") | ||
| 96 | .or_insert(delegate.worktree_root_path().to_string_lossy().into()); | ||
| 97 | |||
| 98 | configuration.entry("type").and_modify(normalize_task_type); | ||
| 99 | } | ||
| 100 | |||
| 71 | Ok(DebugAdapterBinary { | 101 | Ok(DebugAdapterBinary { |
| 72 | command: delegate | 102 | command: Some( |
| 73 | .node_runtime() | 103 | delegate |
| 74 | .binary_path() | 104 | .node_runtime() |
| 75 | .await? | 105 | .binary_path() |
| 76 | .to_string_lossy() | 106 | .await? |
| 77 | .into_owned(), | 107 | .to_string_lossy() |
| 108 | .into_owned(), | ||
| 109 | ), | ||
| 78 | arguments: vec![ | 110 | arguments: vec![ |
| 79 | adapter_path | 111 | adapter_path |
| 80 | .join(Self::ADAPTER_PATH) | 112 | .join(Self::ADAPTER_PATH) |
| @@ -91,7 +123,7 @@ impl JsDebugAdapter { · 10 unmodified lines | |||
| 91 | timeout, | 123 | timeout, |
| 92 | }), | 124 | }), |
| 93 | request_args: StartDebuggingRequestArguments { | 125 | request_args: StartDebuggingRequestArguments { |
| 94 | configuration: task_definition.config.clone(), | 126 | configuration, |
| 95 | request: self.request_kind(&task_definition.config)?, | 127 | request: self.request_kind(&task_definition.config)?, |
| 96 | }, | 128 | }, |
| 97 | }) | 129 | }) |
| @@ -171,7 +203,7 @@ impl DebugAdapter for JsDebugAdapter { · 73 unmodified lines | |||
| 171 | "properties": { | 203 | "properties": { |
| 172 | "type": { | 204 | "type": { |
| 173 | "type": "string", | 205 | "type": "string", |
| 174 | "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"], | 206 | "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"], |
| 175 | "description": "The type of debug session", | 207 | "description": "The type of debug session", |
| 176 | "default": "pwa-node" | 208 | "default": "pwa-node" |
| 177 | }, | 209 | }, |
| @@ -431,4 +463,25 @@ impl DebugAdapter for JsDebugAdapter { · 253 unmodified lines | |||
| 431 | self.get_installed_binary(delegate, &config, user_installed_path, cx) | 463 | self.get_installed_binary(delegate, &config, user_installed_path, cx) |
| 432 | .await | 464 | .await |
| 433 | } | 465 | } |
| 466 | |||
| 467 | fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> { | ||
| 468 | let label = args.configuration.get("name")?.as_str()?; | ||
| 469 | Some(label.to_owned()) | ||
| 470 | } | ||
| 471 | } | ||
| 472 | |||
| 473 | fn normalize_task_type(task_type: &mut Value) { | ||
| 474 | let Some(task_type_str) = task_type.as_str() else { | ||
| 475 | return; | ||
| 476 | }; | ||
| 477 | |||
| 478 | let new_name = match task_type_str { | ||
| 479 | "node" | "pwa-node" => "pwa-node", | ||
| 480 | "chrome" | "pwa-chrome" => "pwa-chrome", | ||
| 481 | "edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge", | ||
| 482 | _ => task_type_str, | ||
| 483 | } | ||
| 484 | .to_owned(); | ||
| 485 | |||
| 486 | *task_type = Value::String(new_name); | ||
| 434 | } | 487 | } |
| @@ -71,13 +71,21 @@ impl PhpDebugAdapter { · 70 unmodified lines | |||
| 71 | let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); | 71 | let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); |
| 72 | let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; | 72 | let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; |
| 73 | 73 | ||
| 74 | let mut configuration = task_definition.config.clone(); | ||
| 75 | if let Some(obj) = configuration.as_object_mut() { | ||
| 76 | obj.entry("cwd") | ||
| 77 | .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into()); | ||
| 78 | } | ||
| 79 | |||
| 74 | Ok(DebugAdapterBinary { | 80 | Ok(DebugAdapterBinary { |
| 75 | command: delegate | 81 | command: Some( |
| 76 | .node_runtime() | 82 | delegate |
| 77 | .binary_path() | 83 | .node_runtime() |
| 78 | .await? | 84 | .binary_path() |
| 79 | .to_string_lossy() | 85 | .await? |
| 80 | .into_owned(), | 86 | .to_string_lossy() |
| 87 | .into_owned(), | ||
| 88 | ), | ||
| 81 | arguments: vec![ | 89 | arguments: vec![ |
| 82 | adapter_path | 90 | adapter_path |
| 83 | .join(Self::ADAPTER_PATH) | 91 | .join(Self::ADAPTER_PATH) |
| @@ -93,7 +101,7 @@ impl PhpDebugAdapter { · 9 unmodified lines | |||
| 93 | cwd: Some(delegate.worktree_root_path().to_path_buf()), | 101 | cwd: Some(delegate.worktree_root_path().to_path_buf()), |
| 94 | envs: HashMap::default(), | 102 | envs: HashMap::default(), |
| 95 | request_args: StartDebuggingRequestArguments { | 103 | request_args: StartDebuggingRequestArguments { |
| 96 | configuration: task_definition.config.clone(), | 104 | configuration, |
| 97 | request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?, | 105 | request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?, |
| 98 | }, | 106 | }, |
| 99 | }) | 107 | }) |
| @@ -83,6 +83,7 @@ impl PythonDebugAdapter { · 82 unmodified lines | |||
| 83 | 83 | ||
| 84 | fn request_args( | 84 | fn request_args( |
| 85 | &self, | 85 | &self, |
| 86 | delegate: &Arc<dyn DapDelegate>, | ||
| 86 | task_definition: &DebugTaskDefinition, | 87 | task_definition: &DebugTaskDefinition, |
| 87 | ) -> Result<StartDebuggingRequestArguments> { | 88 | ) -> Result<StartDebuggingRequestArguments> { |
| 88 | let request = self.request_kind(&task_definition.config)?; | 89 | let request = self.request_kind(&task_definition.config)?; |
| @@ -95,6 +96,11 @@ impl PythonDebugAdapter { · 6 unmodified lines | |||
| 95 | } | 96 | } |
| 96 | } | 97 | } |
| 97 | 98 | ||
| 99 | if let Some(obj) = configuration.as_object_mut() { | ||
| 100 | obj.entry("cwd") | ||
| 101 | .or_insert(delegate.worktree_root_path().to_string_lossy().into()); | ||
| 102 | } | ||
| 103 | |||
| 98 | Ok(StartDebuggingRequestArguments { | 104 | Ok(StartDebuggingRequestArguments { |
| 99 | configuration, | 105 | configuration, |
| 100 | request, | 106 | request, |
| @@ -187,7 +193,7 @@ impl PythonDebugAdapter { · 86 unmodified lines | |||
| 187 | ); | 193 | ); |
| 188 | 194 | ||
| 189 | Ok(DebugAdapterBinary { | 195 | Ok(DebugAdapterBinary { |
| 190 | command: python_command, | 196 | command: Some(python_command), |
| 191 | arguments, | 197 | arguments, |
| 192 | connection: Some(adapters::TcpArguments { | 198 | connection: Some(adapters::TcpArguments { |
| 193 | host, | 199 | host, |
| @@ -196,7 +202,7 @@ impl PythonDebugAdapter { · 2 unmodified lines | |||
| 196 | }), | 202 | }), |
| 197 | cwd: Some(delegate.worktree_root_path().to_path_buf()), | 203 | cwd: Some(delegate.worktree_root_path().to_path_buf()), |
| 198 | envs: HashMap::default(), | 204 | envs: HashMap::default(), |
| 199 | request_args: self.request_args(config)?, | 205 | request_args: self.request_args(delegate, config)?, |
| 200 | }) | 206 | }) |
| 201 | } | 207 | } |
| 202 | } | 208 | } |
| @@ -1,16 +1,18 @@ | |||
| 1 | use anyhow::Result; | 1 | use anyhow::{Result, bail}; |
| 2 | use async_trait::async_trait; | 2 | use async_trait::async_trait; |
| 3 | use collections::FxHashMap; | ||
| 3 | use dap::{ | 4 | use dap::{ |
| 4 | DebugRequest, StartDebuggingRequestArguments, | 5 | DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, |
| 5 | adapters::{ | 6 | adapters::{ |
| 6 | DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, | 7 | DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, |
| 7 | }, | 8 | }, |
| 8 | }; | 9 | }; |
| 9 | use gpui::{AsyncApp, SharedString}; | 10 | use gpui::{AsyncApp, SharedString}; |
| 10 | use language::LanguageName; | 11 | use language::LanguageName; |
| 12 | use serde::{Deserialize, Serialize}; | ||
| 11 | use serde_json::json; | 13 | use serde_json::json; |
| 12 | use std::path::PathBuf; | 14 | use std::path::PathBuf; |
| 13 | use std::sync::Arc; | 15 | use std::{ffi::OsStr, sync::Arc}; |
| 14 | use task::{DebugScenario, ZedDebugConfig}; | 16 | use task::{DebugScenario, ZedDebugConfig}; |
| 15 | use util::command::new_smol_command; | 17 | use util::command::new_smol_command; |
| 16 | 18 | ||
| @@ -21,6 +23,18 @@ impl RubyDebugAdapter { · 4 unmodified lines | |||
| 21 | const ADAPTER_NAME: &'static str = "Ruby"; | 23 | const ADAPTER_NAME: &'static str = "Ruby"; |
| 22 | } | 24 | } |
| 23 | 25 | ||
| 26 | #[derive(Serialize, Deserialize)] | ||
| 27 | struct RubyDebugConfig { | ||
| 28 | script_or_command: Option<String>, | ||
| 29 | script: Option<String>, | ||
| 30 | command: Option<String>, | ||
| 31 | #[serde(default)] | ||
| 32 | args: Vec<String>, | ||
| 33 | #[serde(default)] | ||
| 34 | env: FxHashMap<String, String>, | ||
| 35 | cwd: Option<PathBuf>, | ||
| 36 | } | ||
| 37 | |||
| 24 | #[async_trait(?Send)] | 38 | #[async_trait(?Send)] |
| 25 | impl DebugAdapter for RubyDebugAdapter { | 39 | impl DebugAdapter for RubyDebugAdapter { |
| 26 | fn name(&self) -> DebugAdapterName { | 40 | fn name(&self) -> DebugAdapterName { |
| @@ -31,185 +45,70 @@ impl DebugAdapter for RubyDebugAdapter { · 4 unmodified lines | |||
| 31 | Some(SharedString::new_static("Ruby").into()) | 45 | Some(SharedString::new_static("Ruby").into()) |
| 32 | } | 46 | } |
| 33 | 47 | ||
| 48 | fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> { | ||
| 49 | Ok(StartDebuggingRequestArgumentsRequest::Launch) | ||
| 50 | } | ||
| 51 | |||
| 34 | async fn dap_schema(&self) -> serde_json::Value { | 52 | async fn dap_schema(&self) -> serde_json::Value { |
| 35 | json!({ | 53 | json!({ |
| 36 | "oneOf": [ | 54 | "type": "object", |
| 37 | { | 55 | "properties": { |
| 38 | "allOf": [ | 56 | "command": { |
| 39 | { | 57 | "type": "string", |
| 40 | "type": "object", | 58 | "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)", |
| 41 | "required": ["request"], | ||
| 42 | "properties": { | ||
| 43 | "request": { | ||
| 44 | "type": "string", | ||
| 45 | "enum": ["launch"], | ||
| 46 | "description": "Request to launch a new process" | ||
| 47 | } | ||
| 48 | } | ||
| 49 | }, | ||
| 50 | { | ||
| 51 | "type": "object", | ||
| 52 | "required": ["script"], | ||
| 53 | "properties": { | ||
| 54 | "command": { | ||
| 55 | "type": "string", | ||
| 56 | "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)", | ||
| 57 | "default": "ruby" | ||
| 58 | }, | ||
| 59 | "script": { | ||
| 60 | "type": "string", | ||
| 61 | "description": "Absolute path to a Ruby file." | ||
| 62 | }, | ||
| 63 | "cwd": { | ||
| 64 | "type": "string", | ||
| 65 | "description": "Directory to execute the program in", | ||
| 66 | "default": "${ZED_WORKTREE_ROOT}" | ||
| 67 | }, | ||
| 68 | "args": { | ||
| 69 | "type": "array", | ||
| 70 | "description": "Command line arguments passed to the program", | ||
| 71 | "items": { | ||
| 72 | "type": "string" | ||
| 73 | }, | ||
| 74 | "default": [] | ||
| 75 | }, | ||
| 76 | "env": { | ||
| 77 | "type": "object", | ||
| 78 | "description": "Additional environment variables to pass to the debugging (and debugged) process", | ||
| 79 | "default": {} | ||
| 80 | }, | ||
| 81 | "showProtocolLog": { | ||
| 82 | "type": "boolean", | ||
| 83 | "description": "Show a log of DAP requests, events, and responses", | ||
| 84 | "default": false | ||
| 85 | }, | ||
| 86 | "useBundler": { | ||
| 87 | "type": "boolean", | ||
| 88 | "description": "Execute Ruby programs with `bundle exec` instead of directly", | ||
| 89 | "default": false | ||
| 90 | }, | ||
| 91 | "bundlePath": { | ||
| 92 | "type": "string", | ||
| 93 | "description": "Location of the bundle executable" | ||
| 94 | }, | ||
| 95 | "rdbgPath": { | ||
| 96 | "type": "string", | ||
| 97 | "description": "Location of the rdbg executable" | ||
| 98 | }, | ||
| 99 | "askParameters": { | ||
| 100 | "type": "boolean", | ||
| 101 | "description": "Ask parameters at first." | ||
| 102 | }, | ||
| 103 | "debugPort": { | ||
| 104 | "type": "string", | ||
| 105 | "description": "UNIX domain socket name or TPC/IP host:port" | ||
| 106 | }, | ||
| 107 | "waitLaunchTime": { | ||
| 108 | "type": "number", | ||
| 109 | "description": "Wait time before connection in milliseconds" | ||
| 110 | }, | ||
| 111 | "localfs": { | ||
| 112 | "type": "boolean", | ||
| 113 | "description": "true if the VSCode and debugger run on a same machine", | ||
| 114 | "default": false | ||
| 115 | }, | ||
| 116 | "useTerminal": { | ||
| 117 | "type": "boolean", | ||
| 118 | "description": "Create a new terminal and then execute commands there", | ||
| 119 | "default": false | ||
| 120 | } | ||
| 121 | } | ||
| 122 | } | ||
| 123 | ] | ||
| 124 | }, | 59 | }, |
| 125 | { | 60 | "script": { |
| 126 | "allOf": [ | 61 | "type": "string", |
| 127 | { | 62 | "description": "Absolute path to a Ruby file." |
| 128 | "type": "object", | 63 | }, |
| 129 | "required": ["request"], | 64 | "cwd": { |
| 130 | "properties": { | 65 | "type": "string", |
| 131 | "request": { | 66 | "description": "Directory to execute the program in", |
| 132 | "type": "string", | 67 | "default": "${ZED_WORKTREE_ROOT}" |
| 133 | "enum": ["attach"], | 68 | }, |
| 134 | "description": "Request to attach to an existing process" | 69 | "args": { |
| 135 | } | 70 | "type": "array", |
| 136 | } | 71 | "description": "Command line arguments passed to the program", |
| 137 | }, | 72 | "items": { |
| 138 | { | 73 | "type": "string" |
| 139 | "type": "object", | 74 | }, |
| 140 | "properties": { | 75 | "default": [] |
| 141 | "rdbgPath": { | 76 | }, |
| 142 | "type": "string", | 77 | "env": { |
| 143 | "description": "Location of the rdbg executable" | 78 | "type": "object", |
| 144 | }, | 79 | "description": "Additional environment variables to pass to the debugging (and debugged) process", |
| 145 | "debugPort": { | 80 | "default": {} |
| 146 | "type": "string", | 81 | }, |
| 147 | "description": "UNIX domain socket name or TPC/IP host:port" | 82 | } |
| 148 | }, | ||
| 149 | "showProtocolLog": { | ||
| 150 | "type": "boolean", | ||
| 151 | "description": "Show a log of DAP requests, events, and responses", | ||
| 152 | "default": false | ||
| 153 | }, | ||
| 154 | "localfs": { | ||
| 155 | "type": "boolean", | ||
| 156 | "description": "true if the VSCode and debugger run on a same machine", | ||
| 157 | "default": false | ||
| 158 | }, | ||
| 159 | "localfsMap": { | ||
| 160 | "type": "string", | ||
| 161 | "description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`." | ||
| 162 | }, | ||
| 163 | "env": { | ||
| 164 | "type": "object", | ||
| 165 | "description": "Additional environment variables to pass to the rdbg process", | ||
| 166 | "default": {} | ||
| 167 | } | ||
| 168 | } | ||
| 169 | } | ||
| 170 | ] | ||
| 171 | } | ||
| 172 | ] | ||
| 173 | }) | 83 | }) |
| 174 | } | 84 | } |
| 175 | 85 | ||
| 176 | fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> { | 86 | fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> { |
| 177 | let mut config = serde_json::Map::new(); | 87 | match zed_scenario.request { |
| 178 | |||
| 179 | match &zed_scenario.request { | ||
| 180 | DebugRequest::Launch(launch) => { | 88 | DebugRequest::Launch(launch) => { |
| 181 | config.insert("request".to_string(), json!("launch")); | 89 | let config = RubyDebugConfig { |
| 182 | config.insert("script".to_string(), json!(launch.program)); | 90 | script_or_command: Some(launch.program), |
| 183 | config.insert("command".to_string(), json!("ruby")); | 91 | script: None, |
| 184 | 92 | command: None, | |
| 185 | if !launch.args.is_empty() { | 93 | args: launch.args, |
| 186 | config.insert("args".to_string(), json!(launch.args)); | 94 | env: launch.env, |
| 187 | } | 95 | cwd: launch.cwd.clone(), |
| 188 | 96 | }; | |
| 189 | if !launch.env.is_empty() { | 97 | |
| 190 | config.insert("env".to_string(), json!(launch.env)); | 98 | let config = serde_json::to_value(config)?; |
| 191 | } | 99 | |
| 192 | 100 | Ok(DebugScenario { | |
| 193 | if let Some(cwd) = &launch.cwd { | 101 | adapter: zed_scenario.adapter, |
| 194 | config.insert("cwd".to_string(), json!(cwd)); | 102 | label: zed_scenario.label, |
| 195 | } | 103 | config, |
| 196 | 104 | tcp_connection: None, | |
| 197 | // Ruby stops on entry so there's no need to handle that case | 105 | build: None, |
| 106 | }) | ||
| 198 | } | 107 | } |
| 199 | DebugRequest::Attach(attach) => { | 108 | DebugRequest::Attach(_) => { |
| 200 | config.insert("request".to_string(), json!("attach")); | 109 | anyhow::bail!("Attach requests are unsupported"); |
| 201 | |||
| 202 | config.insert("processId".to_string(), json!(attach.process_id)); | ||
| 203 | } | 110 | } |
| 204 | } | 111 | } |
| 205 | |||
| 206 | Ok(DebugScenario { | ||
| 207 | adapter: zed_scenario.adapter, | ||
| 208 | label: zed_scenario.label, | ||
| 209 | config: serde_json::Value::Object(config), | ||
| 210 | tcp_connection: None, | ||
| 211 | build: None, | ||
| 212 | }) | ||
| 213 | } | 112 | } |
| 214 | 113 | ||
| 215 | async fn get_binary( | 114 | async fn get_binary( |
| @@ -247,26 +146,58 @@ impl DebugAdapter for RubyDebugAdapter { · 31 unmodified lines | |||
| 247 | 146 | ||
| 248 | let tcp_connection = definition.tcp_connection.clone().unwrap_or_default(); | 147 | let tcp_connection = definition.tcp_connection.clone().unwrap_or_default(); |
| 249 | let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; | 148 | let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; |
| 149 | let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?; | ||
| 250 | 150 | ||
| 251 | let arguments = vec![ | 151 | let mut arguments = vec![ |
| 252 | "--open".to_string(), | 152 | "--open".to_string(), |
| 253 | format!("--port={}", port), | 153 | format!("--port={}", port), |
| 254 | format!("--host={}", host), | 154 | format!("--host={}", host), |
| 255 | ]; | 155 | ]; |
| 256 | 156 | ||
| 157 | if let Some(script) = &ruby_config.script { | ||
| 158 | arguments.push(script.clone()); | ||
| 159 | } else if let Some(command) = &ruby_config.command { | ||
| 160 | arguments.push("--command".to_string()); | ||
| 161 | arguments.push(command.clone()); | ||
| 162 | } else if let Some(command_or_script) = &ruby_config.script_or_command { | ||
| 163 | if delegate | ||
| 164 | .which(OsStr::new(&command_or_script)) | ||
| 165 | .await | ||
| 166 | .is_some() | ||
| 167 | { | ||
| 168 | arguments.push("--command".to_string()); | ||
| 169 | } | ||
| 170 | arguments.push(command_or_script.clone()); | ||
| 171 | } else { | ||
| 172 | bail!("Ruby debug config must have 'script' or 'command' args"); | ||
| 173 | } | ||
| 174 | |||
| 175 | arguments.extend(ruby_config.args); | ||
| 176 | |||
| 177 | let mut configuration = definition.config.clone(); | ||
| 178 | if let Some(configuration) = configuration.as_object_mut() { | ||
| 179 | configuration | ||
| 180 | .entry("cwd") | ||
| 181 | .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into()); | ||
| 182 | } | ||
| 183 | |||
| 257 | Ok(DebugAdapterBinary { | 184 | Ok(DebugAdapterBinary { |
| 258 | command: rdbg_path.to_string_lossy().to_string(), | 185 | command: Some(rdbg_path.to_string_lossy().to_string()), |
| 259 | arguments, | 186 | arguments, |
| 260 | connection: Some(dap::adapters::TcpArguments { | 187 | connection: Some(dap::adapters::TcpArguments { |
| 261 | host, | 188 | host, |
| 262 | port, | 189 | port, |
| 263 | timeout, | 190 | timeout, |
| 264 | }), | 191 | }), |
| 265 | cwd: None, | 192 | cwd: Some( |
| 266 | envs: std::collections::HashMap::default(), | 193 | ruby_config |
| 194 | .cwd | ||
| 195 | .unwrap_or(delegate.worktree_root_path().to_owned()), | ||
| 196 | ), | ||
| 197 | envs: ruby_config.env.into_iter().collect(), | ||
| 267 | request_args: StartDebuggingRequestArguments { | 198 | request_args: StartDebuggingRequestArguments { |
| 268 | request: self.request_kind(&definition.config)?, | 199 | request: self.request_kind(&definition.config)?, |
| 269 | configuration: definition.config.clone(), | 200 | configuration, |
| 270 | }, | 201 | }, |
| 271 | }) | 202 | }) |
| 272 | } | 203 | } |
| @@ -74,7 +74,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> Threa · 73 unmodified lines | |||
| 74 | } | 74 | } |
| 75 | 75 | ||
| 76 | async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> { | 76 | async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> { |
| 77 | log::info!("Opening main db"); | 77 | log::info!("Opening database {}", db_path.display()); |
| 78 | ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true) | 78 | ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true) |
| 79 | .with_db_initialization_query(DB_INITIALIZE_QUERY) | 79 | .with_db_initialization_query(DB_INITIALIZE_QUERY) |
| 80 | .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY) | 80 | .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY) |
| @@ -84,7 +84,7 @@ async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnectio · 3 unmodified lines | |||
| 84 | } | 84 | } |
| 85 | 85 | ||
| 86 | async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection { | 86 | async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection { |
| 87 | log::info!("Opening fallback db"); | 87 | log::warn!("Opening fallback in-memory database"); |
| 88 | ThreadSafeConnection::builder::<M>(FALLBACK_DB_NAME, false) | 88 | ThreadSafeConnection::builder::<M>(FALLBACK_DB_NAME, false) |
| 89 | .with_db_initialization_query(DB_INITIALIZE_QUERY) | 89 | .with_db_initialization_query(DB_INITIALIZE_QUERY) |
| 90 | .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY) | 90 | .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY) |
| @@ -1,4 +1,5 @@ | |||
| 1 | use dap::{ | 1 | use dap::{ |
| 2 | adapters::DebugAdapterName, | ||
| 2 | client::SessionId, | 3 | client::SessionId, |
| 3 | debugger_settings::DebuggerSettings, | 4 | debugger_settings::DebuggerSettings, |
| 4 | transport::{IoKind, LogKind}, | 5 | transport::{IoKind, LogKind}, |
| @@ -31,6 +32,13 @@ use workspace::{ · 26 unmodified lines | |||
| 31 | ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex}, | 32 | ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex}, |
| 32 | }; | 33 | }; |
| 33 | 34 | ||
| 35 | // TODO: | ||
| 36 | // - [x] stop sorting by session ID | ||
| 37 | // - [x] pick the most recent session by default (logs if available, RPC messages otherwise) | ||
| 38 | // - [ ] dump the launch/attach request somewhere (logs?) | ||
| 39 | |||
| 40 | const MAX_SESSIONS: usize = 10; | ||
| 41 | |||
| 34 | struct DapLogView { | 42 | struct DapLogView { |
| 35 | editor: Entity<Editor>, | 43 | editor: Entity<Editor>, |
| 36 | focus_handle: FocusHandle, | 44 | focus_handle: FocusHandle, |
| @@ -43,9 +51,9 @@ struct DapLogView { · 6 unmodified lines | |||
| 43 | 51 | ||
| 44 | pub struct LogStore { | 52 | pub struct LogStore { |
| 45 | projects: HashMap<WeakEntity<Project>, ProjectState>, | 53 | projects: HashMap<WeakEntity<Project>, ProjectState>, |
| 46 | debug_clients: HashMap<SessionId, DebugAdapterState>, | 54 | debug_sessions: VecDeque<DebugAdapterState>, |
| 47 | rpc_tx: UnboundedSender<(SessionId, IoKind, String)>, | 55 | rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>, |
| 48 | adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>, | 56 | adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>, |
| 49 | } | 57 | } |
| 50 | 58 | ||
| 51 | struct ProjectState { | 59 | struct ProjectState { |
| @@ -53,13 +61,19 @@ struct ProjectState { · 1 unmodified line | |||
| 53 | } | 61 | } |
| 54 | 62 | ||
| 55 | struct DebugAdapterState { | 63 | struct DebugAdapterState { |
| 56 | log_messages: VecDeque<String>, | 64 | id: SessionId, |
| 65 | log_messages: VecDeque<SharedString>, | ||
| 57 | rpc_messages: RpcMessages, | 66 | rpc_messages: RpcMessages, |
| 67 | adapter_name: DebugAdapterName, | ||
| 68 | has_adapter_logs: bool, | ||
| 69 | is_terminated: bool, | ||
| 58 | } | 70 | } |
| 59 | 71 | ||
| 60 | struct RpcMessages { | 72 | struct RpcMessages { |
| 61 | messages: VecDeque<String>, | 73 | messages: VecDeque<SharedString>, |
| 62 | last_message_kind: Option<MessageKind>, | 74 | last_message_kind: Option<MessageKind>, |
| 75 | initialization_sequence: Vec<SharedString>, | ||
| 76 | last_init_message_kind: Option<MessageKind>, | ||
| 63 | } | 77 | } |
| 64 | 78 | ||
| 65 | impl RpcMessages { | 79 | impl RpcMessages { |
| @@ -68,7 +82,9 @@ impl RpcMessages { · 2 unmodified lines | |||
| 68 | fn new() -> Self { | 82 | fn new() -> Self { |
| 69 | Self { | 83 | Self { |
| 70 | last_message_kind: None, | 84 | last_message_kind: None, |
| 85 | last_init_message_kind: None, | ||
| 71 | messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT), | 86 | messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT), |
| 87 | initialization_sequence: Vec::new(), | ||
| 72 | } | 88 | } |
| 73 | } | 89 | } |
| 74 | } | 90 | } |
| @@ -92,22 +108,27 @@ impl MessageKind { · 17 unmodified lines | |||
| 92 | } | 108 | } |
| 93 | 109 | ||
| 94 | impl DebugAdapterState { | 110 | impl DebugAdapterState { |
| 95 | fn new() -> Self { | 111 | fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self { |
| 96 | Self { | 112 | Self { |
| 113 | id, | ||
| 97 | log_messages: VecDeque::new(), | 114 | log_messages: VecDeque::new(), |
| 98 | rpc_messages: RpcMessages::new(), | 115 | rpc_messages: RpcMessages::new(), |
| 116 | adapter_name, | ||
| 117 | has_adapter_logs, | ||
| 118 | is_terminated: false, | ||
| 99 | } | 119 | } |
| 100 | } | 120 | } |
| 101 | } | 121 | } |
| 102 | 122 | ||
| 103 | impl LogStore { | 123 | impl LogStore { |
| 104 | pub fn new(cx: &Context<Self>) -> Self { | 124 | pub fn new(cx: &Context<Self>) -> Self { |
| 105 | let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>(); | 125 | let (rpc_tx, mut rpc_rx) = |
| 126 | unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>(); | ||
| 106 | cx.spawn(async move |this, cx| { | 127 | cx.spawn(async move |this, cx| { |
| 107 | while let Some((client_id, io_kind, message)) = rpc_rx.next().await { | 128 | while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await { |
| 108 | if let Some(this) = this.upgrade() { | 129 | if let Some(this) = this.upgrade() { |
| 109 | this.update(cx, |this, cx| { | 130 | this.update(cx, |this, cx| { |
| 110 | this.on_rpc_log(client_id, io_kind, &message, cx); | 131 | this.add_debug_adapter_message(session_id, io_kind, command, message, cx); |
| 111 | })?; | 132 | })?; |
| 112 | } | 133 | } |
| 113 | 134 | ||
| @@ -117,12 +138,13 @@ impl LogStore { · 3 unmodified lines | |||
| 117 | }) | 138 | }) |
| 118 | .detach_and_log_err(cx); | 139 | .detach_and_log_err(cx); |
| 119 | 140 | ||
| 120 | let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, String)>(); | 141 | let (adapter_log_tx, mut adapter_log_rx) = |
| 142 | unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>(); | ||
| 121 | cx.spawn(async move |this, cx| { | 143 | cx.spawn(async move |this, cx| { |
| 122 | while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await { | 144 | while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await { |
| 123 | if let Some(this) = this.upgrade() { | 145 | if let Some(this) = this.upgrade() { |
| 124 | this.update(cx, |this, cx| { | 146 | this.update(cx, |this, cx| { |
| 125 | this.on_adapter_log(client_id, io_kind, &message, cx); | 147 | this.add_debug_adapter_log(session_id, io_kind, message, cx); |
| 126 | })?; | 148 | })?; |
| 127 | } | 149 | } |
| 128 | 150 | ||
| @@ -135,30 +157,10 @@ impl LogStore { · 6 unmodified lines | |||
| 135 | rpc_tx, | 157 | rpc_tx, |
| 136 | adapter_log_tx, | 158 | adapter_log_tx, |
| 137 | projects: HashMap::new(), | 159 | projects: HashMap::new(), |
| 138 | debug_clients: HashMap::new(), | 160 | debug_sessions: Default::default(), |
| 139 | } | 161 | } |
| 140 | } | 162 | } |
| 141 | 163 | ||
| 142 | fn on_rpc_log( | ||
| 143 | &mut self, | ||
| 144 | client_id: SessionId, | ||
| 145 | io_kind: IoKind, | ||
| 146 | message: &str, | ||
| 147 | cx: &mut Context<Self>, | ||
| 148 | ) { | ||
| 149 | self.add_debug_client_message(client_id, io_kind, message.to_string(), cx); | ||
| 150 | } | ||
| 151 | |||
| 152 | fn on_adapter_log( | ||
| 153 | &mut self, | ||
| 154 | client_id: SessionId, | ||
| 155 | io_kind: IoKind, | ||
| 156 | message: &str, | ||
| 157 | cx: &mut Context<Self>, | ||
| 158 | ) { | ||
| 159 | self.add_debug_client_log(client_id, io_kind, message.to_string(), cx); | ||
| 160 | } | ||
| 161 | |||
| 162 | pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) { | 164 | pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) { |
| 163 | let weak_project = project.downgrade(); | 165 | let weak_project = project.downgrade(); |
| 164 | self.projects.insert( | 166 | self.projects.insert( |
| @@ -174,13 +176,15 @@ impl LogStore { · 9 unmodified lines | |||
| 174 | dap_store::DapStoreEvent::DebugClientStarted(session_id) => { | 176 | dap_store::DapStoreEvent::DebugClientStarted(session_id) => { |
| 175 | let session = dap_store.read(cx).session_by_id(session_id); | 177 | let session = dap_store.read(cx).session_by_id(session_id); |
| 176 | if let Some(session) = session { | 178 | if let Some(session) = session { |
| 177 | this.add_debug_client(*session_id, session, cx); | 179 | this.add_debug_session(*session_id, session, cx); |
| 178 | } | 180 | } |
| 179 | } | 181 | } |
| 180 | dap_store::DapStoreEvent::DebugClientShutdown(session_id) => { | 182 | dap_store::DapStoreEvent::DebugClientShutdown(session_id) => { |
| 181 | this.remove_debug_client(*session_id, cx); | 183 | this.get_debug_adapter_state(*session_id) |
| 184 | .iter_mut() | ||
| 185 | .for_each(|state| state.is_terminated = true); | ||
| 186 | this.clean_sessions(cx); | ||
| 182 | } | 187 | } |
| 183 | |||
| 184 | _ => {} | 188 | _ => {} |
| 185 | }, | 189 | }, |
| 186 | ), | 190 | ), |
| @@ -190,63 +194,88 @@ impl LogStore { · 3 unmodified lines | |||
| 190 | } | 194 | } |
| 191 | 195 | ||
| 192 | fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> { | 196 | fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> { |
| 193 | self.debug_clients.get_mut(&id) | 197 | self.debug_sessions |
| 198 | .iter_mut() | ||
| 199 | .find(|adapter_state| adapter_state.id == id) | ||
| 194 | } | 200 | } |
| 195 | 201 | ||
| 196 | fn add_debug_client_message( | 202 | fn add_debug_adapter_message( |
| 197 | &mut self, | 203 | &mut self, |
| 198 | id: SessionId, | 204 | id: SessionId, |
| 199 | io_kind: IoKind, | 205 | io_kind: IoKind, |
| 200 | message: String, | 206 | command: Option<SharedString>, |
| 207 | message: SharedString, | ||
| 201 | cx: &mut Context<Self>, | 208 | cx: &mut Context<Self>, |
| 202 | ) { | 209 | ) { |
| 203 | let Some(debug_client_state) = self.get_debug_adapter_state(id) else { | 210 | let Some(debug_client_state) = self.get_debug_adapter_state(id) else { |
| 204 | return; | 211 | return; |
| 205 | }; | 212 | }; |
| 206 | 213 | ||
| 214 | let is_init_seq = command.as_ref().is_some_and(|command| { | ||
| 215 | matches!( | ||
| 216 | command.as_ref(), | ||
| 217 | "attach" | "launch" | "initialize" | "configurationDone" | ||
| 218 | ) | ||
| 219 | }); | ||
| 220 | |||
| 207 | let kind = match io_kind { | 221 | let kind = match io_kind { |
| 208 | IoKind::StdOut | IoKind::StdErr => MessageKind::Receive, | 222 | IoKind::StdOut | IoKind::StdErr => MessageKind::Receive, |
| 209 | IoKind::StdIn => MessageKind::Send, | 223 | IoKind::StdIn => MessageKind::Send, |
| 210 | }; | 224 | }; |
| 211 | 225 | ||
| 212 | let rpc_messages = &mut debug_client_state.rpc_messages; | 226 | let rpc_messages = &mut debug_client_state.rpc_messages; |
| 227 | |||
| 228 | // Push a separator if the kind has changed | ||
| 213 | if rpc_messages.last_message_kind != Some(kind) { | 229 | if rpc_messages.last_message_kind != Some(kind) { |
| 214 | Self::add_debug_client_entry( | 230 | Self::get_debug_adapter_entry( |
| 215 | &mut rpc_messages.messages, | 231 | &mut rpc_messages.messages, |
| 216 | id, | 232 | id, |
| 217 | kind.label().to_string(), | 233 | kind.label().into(), |
| 218 | LogKind::Rpc, | 234 | LogKind::Rpc, |
| 219 | cx, | 235 | cx, |
| 220 | ); | 236 | ); |
| 221 | rpc_messages.last_message_kind = Some(kind); | 237 | rpc_messages.last_message_kind = Some(kind); |
| 222 | } | 238 | } |
| 223 | Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx); | 239 | |
| 240 | let entry = Self::get_debug_adapter_entry( | ||
| 241 | &mut rpc_messages.messages, | ||
| 242 | id, | ||
| 243 | message, | ||
| 244 | LogKind::Rpc, | ||
| 245 | cx, | ||
| 246 | ); | ||
| 247 | |||
| 248 | if is_init_seq { | ||
| 249 | if rpc_messages.last_init_message_kind != Some(kind) { | ||
| 250 | rpc_messages | ||
| 251 | .initialization_sequence | ||
| 252 | .push(SharedString::from(kind.label())); | ||
| 253 | rpc_messages.last_init_message_kind = Some(kind); | ||
| 254 | } | ||
| 255 | rpc_messages.initialization_sequence.push(entry); | ||
| 256 | } | ||
| 224 | 257 | ||
| 225 | cx.notify(); | 258 | cx.notify(); |
| 226 | } | 259 | } |
| 227 | 260 | ||
| 228 | fn add_debug_client_log( | 261 | fn add_debug_adapter_log( |
| 229 | &mut self, | 262 | &mut self, |
| 230 | id: SessionId, | 263 | id: SessionId, |
| 231 | io_kind: IoKind, | 264 | io_kind: IoKind, |
| 232 | message: String, | 265 | message: SharedString, |
| 233 | cx: &mut Context<Self>, | 266 | cx: &mut Context<Self>, |
| 234 | ) { | 267 | ) { |
| 235 | let Some(debug_client_state) = self.get_debug_adapter_state(id) else { | 268 | let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else { |
| 236 | return; | 269 | return; |
| 237 | }; | 270 | }; |
| 238 | 271 | ||
| 239 | let message = match io_kind { | 272 | let message = match io_kind { |
| 240 | IoKind::StdErr => { | 273 | IoKind::StdErr => format!("stderr: {message}").into(), |
| 241 | let mut message = message.clone(); | ||
| 242 | message.insert_str(0, "stderr: "); | ||
| 243 | message | ||
| 244 | } | ||
| 245 | _ => message, | 274 | _ => message, |
| 246 | }; | 275 | }; |
| 247 | 276 | ||
| 248 | Self::add_debug_client_entry( | 277 | Self::get_debug_adapter_entry( |
| 249 | &mut debug_client_state.log_messages, | 278 | &mut debug_adapter_state.log_messages, |
| 250 | id, | 279 | id, |
| 251 | message, | 280 | message, |
| 252 | LogKind::Adapter, | 281 | LogKind::Adapter, |
| @@ -255,13 +284,13 @@ impl LogStore { · 2 unmodified lines | |||
| 255 | cx.notify(); | 284 | cx.notify(); |
| 256 | } | 285 | } |
| 257 | 286 | ||
| 258 | fn add_debug_client_entry( | 287 | fn get_debug_adapter_entry( |
| 259 | log_lines: &mut VecDeque<String>, | 288 | log_lines: &mut VecDeque<SharedString>, |
| 260 | id: SessionId, | 289 | id: SessionId, |
| 261 | message: String, | 290 | message: SharedString, |
| 262 | kind: LogKind, | 291 | kind: LogKind, |
| 263 | cx: &mut Context<Self>, | 292 | cx: &mut Context<Self>, |
| 264 | ) { | 293 | ) -> SharedString { |
| 265 | while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { | 294 | while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { |
| 266 | log_lines.pop_front(); | 295 | log_lines.pop_front(); |
| 267 | } | 296 | } |
| @@ -275,33 +304,69 @@ impl LogStore { · 7 unmodified lines | |||
| 275 | ) | 304 | ) |
| 276 | .ok() | 305 | .ok() |
| 277 | }) | 306 | }) |
| 307 | .map(SharedString::from) | ||
| 278 | .unwrap_or(message) | 308 | .unwrap_or(message) |
| 279 | } else { | 309 | } else { |
| 280 | message | 310 | message |
| 281 | }; | 311 | }; |
| 282 | log_lines.push_back(entry.clone()); | 312 | log_lines.push_back(entry.clone()); |
| 283 | 313 | ||
| 284 | cx.emit(Event::NewLogEntry { id, entry, kind }); | 314 | cx.emit(Event::NewLogEntry { |
| 315 | id, | ||
| 316 | entry: entry.clone(), | ||
| 317 | kind, | ||
| 318 | }); | ||
| 319 | |||
| 320 | entry | ||
| 285 | } | 321 | } |
| 286 | 322 | ||
| 287 | fn add_debug_client( | 323 | fn add_debug_session( |
| 288 | &mut self, | 324 | &mut self, |
| 289 | client_id: SessionId, | 325 | session_id: SessionId, |
| 290 | client: Entity<Session>, | 326 | session: Entity<Session>, |
| 291 | cx: &App, | 327 | cx: &mut Context<Self>, |
| 292 | ) -> Option<&mut DebugAdapterState> { | 328 | ) { |
| 293 | let client_state = self | 329 | if self |
| 294 | .debug_clients | 330 | .debug_sessions |
| 295 | .entry(client_id) | 331 | .iter_mut() |
| 296 | .or_insert_with(DebugAdapterState::new); | 332 | .any(|adapter_state| adapter_state.id == session_id) |
| 333 | { | ||
| 334 | return; | ||
| 335 | } | ||
| 336 | |||
| 337 | let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| { | ||
| 338 | ( | ||
| 339 | session.adapter(), | ||
| 340 | session | ||
| 341 | .adapter_client() | ||
| 342 | .map(|client| client.has_adapter_logs()) | ||
| 343 | .unwrap_or(false), | ||
| 344 | ) | ||
| 345 | }); | ||
| 346 | |||
| 347 | self.debug_sessions.push_back(DebugAdapterState::new( | ||
| 348 | session_id, | ||
| 349 | adapter_name, | ||
| 350 | has_adapter_logs, | ||
| 351 | )); | ||
| 352 | |||
| 353 | self.clean_sessions(cx); | ||
| 297 | 354 | ||
| 298 | let io_tx = self.rpc_tx.clone(); | 355 | let io_tx = self.rpc_tx.clone(); |
| 299 | 356 | ||
| 300 | let client = client.read(cx).adapter_client()?; | 357 | let Some(client) = session.read(cx).adapter_client() else { |
| 358 | return; | ||
| 359 | }; | ||
| 360 | |||
| 301 | client.add_log_handler( | 361 | client.add_log_handler( |
| 302 | move |io_kind, message| { | 362 | move |io_kind, command, message| { |
| 303 | io_tx | 363 | io_tx |
| 304 | .unbounded_send((client_id, io_kind, message.to_string())) | 364 | .unbounded_send(( |
| 365 | session_id, | ||
| 366 | io_kind, | ||
| 367 | command.map(|command| command.to_owned().into()), | ||
| 368 | message.to_owned().into(), | ||
| 369 | )) | ||
| 305 | .ok(); | 370 | .ok(); |
| 306 | }, | 371 | }, |
| 307 | LogKind::Rpc, | 372 | LogKind::Rpc, |
| @@ -309,34 +374,66 @@ impl LogStore { · 1 unmodified line | |||
| 309 | 374 | ||
| 310 | let log_io_tx = self.adapter_log_tx.clone(); | 375 | let log_io_tx = self.adapter_log_tx.clone(); |
| 311 | client.add_log_handler( | 376 | client.add_log_handler( |
| 312 | move |io_kind, message| { | 377 | move |io_kind, command, message| { |
| 313 | log_io_tx | 378 | log_io_tx |
| 314 | .unbounded_send((client_id, io_kind, message.to_string())) | 379 | .unbounded_send(( |
| 380 | session_id, | ||
| 381 | io_kind, | ||
| 382 | command.map(|command| command.to_owned().into()), | ||
| 383 | message.to_owned().into(), | ||
| 384 | )) | ||
| 315 | .ok(); | 385 | .ok(); |
| 316 | }, | 386 | }, |
| 317 | LogKind::Adapter, | 387 | LogKind::Adapter, |
| 318 | ); | 388 | ); |
| 319 | |||
| 320 | Some(client_state) | ||
| 321 | } | 389 | } |
| 322 | 390 | ||
| 323 | fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context<Self>) { | 391 | fn clean_sessions(&mut self, cx: &mut Context<Self>) { |
| 324 | self.debug_clients.remove(&client_id); | 392 | let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS); |
| 393 | self.debug_sessions.retain(|session| { | ||
| 394 | if to_remove > 0 && session.is_terminated { | ||
| 395 | to_remove -= 1; | ||
| 396 | return false; | ||
| 397 | } | ||
| 398 | true | ||
| 399 | }); | ||
| 325 | cx.notify(); | 400 | cx.notify(); |
| 326 | } | 401 | } |
| 327 | 402 | ||
| 328 | fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> { | 403 | fn log_messages_for_session( |
| 329 | Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages) | 404 | &mut self, |
| 405 | session_id: SessionId, | ||
| 406 | ) -> Option<&mut VecDeque<SharedString>> { | ||
| 407 | self.debug_sessions | ||
| 408 | .iter_mut() | ||
| 409 | .find(|session| session.id == session_id) | ||
| 410 | .map(|state| &mut state.log_messages) | ||
| 411 | } | ||
| 412 | |||
| 413 | fn rpc_messages_for_session( | ||
| 414 | &mut self, | ||
| 415 | session_id: SessionId, | ||
| 416 | ) -> Option<&mut VecDeque<SharedString>> { | ||
| 417 | self.debug_sessions.iter_mut().find_map(|state| { | ||
| 418 | if state.id == session_id { | ||
| 419 | Some(&mut state.rpc_messages.messages) | ||
| 420 | } else { | ||
| 421 | None | ||
| 422 | } | ||
| 423 | }) | ||
| 330 | } | 424 | } |
| 331 | 425 | ||
| 332 | fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> { | 426 | fn initialization_sequence_for_session( |
| 333 | Some( | 427 | &mut self, |
| 334 | &mut self | 428 | session_id: SessionId, |
| 335 | .debug_clients | 429 | ) -> Option<&mut Vec<SharedString>> { |
| 336 | .get_mut(&client_id)? | 430 | self.debug_sessions.iter_mut().find_map(|state| { |
| 337 | .rpc_messages | 431 | if state.id == session_id { |
| 338 | .messages, | 432 | Some(&mut state.rpc_messages.initialization_sequence) |
| 339 | ) | 433 | } else { |
| 434 | None | ||
| 435 | } | ||
| 436 | }) | ||
| 340 | } | 437 | } |
| 341 | } | 438 | } |
| 342 | 439 | ||
| @@ -356,18 +453,15 @@ impl Render for DapLogToolbarItemView { · 13 unmodified lines | |||
| 356 | return Empty.into_any_element(); | 453 | return Empty.into_any_element(); |
| 357 | }; | 454 | }; |
| 358 | 455 | ||
| 359 | let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| { | 456 | let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| { |
| 360 | ( | 457 | ( |
| 361 | log_view.menu_items(cx).unwrap_or_default(), | 458 | log_view.menu_items(cx), |
| 362 | log_view.current_view.map(|(client_id, _)| client_id), | 459 | log_view.current_view.map(|(session_id, _)| session_id), |
| 363 | ) | 460 | ) |
| 364 | }); | 461 | }); |
| 365 | 462 | ||
| 366 | let current_client = current_client_id.and_then(|current_client_id| { | 463 | let current_client = current_session_id |
| 367 | menu_rows | 464 | .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id)); |
| 368 | .iter() | ||
| 369 | .find(|row| row.client_id == current_client_id) | ||
| 370 | }); | ||
| 371 | 465 | ||
| 372 | let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") | 466 | let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") |
| 373 | .anchor(gpui::Corner::TopLeft) | 467 | .anchor(gpui::Corner::TopLeft) |
| @@ -377,8 +471,8 @@ impl Render for DapLogToolbarItemView { · 3 unmodified lines | |||
| 377 | .map(|sub_item| { | 471 | .map(|sub_item| { |
| 378 | Cow::Owned(format!( | 472 | Cow::Owned(format!( |
| 379 | "{} ({}) - {}", | 473 | "{} ({}) - {}", |
| 380 | sub_item.client_name, | 474 | sub_item.adapter_name, |
| 381 | sub_item.client_id.0, | 475 | sub_item.session_id.0, |
| 382 | match sub_item.selected_entry { | 476 | match sub_item.selected_entry { |
| 383 | LogKind::Adapter => ADAPTER_LOGS, | 477 | LogKind::Adapter => ADAPTER_LOGS, |
| 384 | LogKind::Rpc => RPC_MESSAGES, | 478 | LogKind::Rpc => RPC_MESSAGES, |
| @@ -397,9 +491,10 @@ impl Render for DapLogToolbarItemView { · 12 unmodified lines | |||
| 397 | .w_full() | 491 | .w_full() |
| 398 | .pl_2() | 492 | .pl_2() |
| 399 | .child( | 493 | .child( |
| 400 | Label::new( | 494 | Label::new(format!( |
| 401 | format!("{}. {}", row.client_id.0, row.client_name,), | 495 | "{}. {}", |
| 402 | ) | 496 | row.session_id.0, row.adapter_name, |
| 497 | )) | ||
| 403 | .color(workspace::ui::Color::Muted), | 498 | .color(workspace::ui::Color::Muted), |
| 404 | ) | 499 | ) |
| 405 | .into_any_element() | 500 | .into_any_element() |
| @@ -415,23 +510,40 @@ impl Render for DapLogToolbarItemView { · 9 unmodified lines | |||
| 415 | .into_any_element() | 510 | .into_any_element() |
| 416 | }, | 511 | }, |
| 417 | window.handler_for(&log_view, move |view, window, cx| { | 512 | window.handler_for(&log_view, move |view, window, cx| { |
| 418 | view.show_log_messages_for_adapter(row.client_id, window, cx); | 513 | view.show_log_messages_for_adapter(row.session_id, window, cx); |
| 419 | }), | 514 | }), |
| 420 | ); | 515 | ); |
| 421 | } | 516 | } |
| 422 | 517 | ||
| 423 | menu = menu.custom_entry( | 518 | menu = menu |
| 424 | move |_window, _cx| { | 519 | .custom_entry( |
| 425 | div() | 520 | move |_window, _cx| { |
| 426 | .w_full() | 521 | div() |
| 427 | .pl_4() | 522 | .w_full() |
| 428 | .child(Label::new(RPC_MESSAGES)) | 523 | .pl_4() |
| 429 | .into_any_element() | 524 | .child(Label::new(RPC_MESSAGES)) |
| 430 | }, | 525 | .into_any_element() |
| 431 | window.handler_for(&log_view, move |view, window, cx| { | 526 | }, |
| 432 | view.show_rpc_trace_for_server(row.client_id, window, cx); | 527 | window.handler_for(&log_view, move |view, window, cx| { |
| 433 | }), | 528 | view.show_rpc_trace_for_server(row.session_id, window, cx); |
| 434 | ); | 529 | }), |
| 530 | ) | ||
| 531 | .custom_entry( | ||
| 532 | move |_window, _cx| { | ||
| 533 | div() | ||
| 534 | .w_full() | ||
| 535 | .pl_4() | ||
| 536 | .child(Label::new(INITIALIZATION_SEQUENCE)) | ||
| 537 | .into_any_element() | ||
| 538 | }, | ||
| 539 | window.handler_for(&log_view, move |view, window, cx| { | ||
| 540 | view.show_initialization_sequence_for_server( | ||
| 541 | row.session_id, | ||
| 542 | window, | ||
| 543 | cx, | ||
| 544 | ); | ||
| 545 | }), | ||
| 546 | ); | ||
| 435 | } | 547 | } |
| 436 | 548 | ||
| 437 | menu | 549 | menu |
| @@ -518,7 +630,13 @@ impl DapLogView { · 80 unmodified lines | |||
| 518 | } | 630 | } |
| 519 | }); | 631 | }); |
| 520 | 632 | ||
| 521 | Self { | 633 | let state_info = log_store |
| 634 | .read(cx) | ||
| 635 | .debug_sessions | ||
| 636 | .back() | ||
| 637 | .map(|session| (session.id, session.has_adapter_logs)); | ||
| 638 | |||
| 639 | let mut this = Self { | ||
| 522 | editor, | 640 | editor, |
| 523 | focus_handle, | 641 | focus_handle, |
| 524 | project, | 642 | project, |
| @@ -526,7 +644,17 @@ impl DapLogView { · 1 unmodified line | |||
| 526 | editor_subscriptions, | 644 | editor_subscriptions, |
| 527 | current_view: None, | 645 | current_view: None, |
| 528 | _subscriptions: vec![events_subscriptions], | 646 | _subscriptions: vec![events_subscriptions], |
| 647 | }; | ||
| 648 | |||
| 649 | if let Some((session_id, have_adapter_logs)) = state_info { | ||
| 650 | if have_adapter_logs { | ||
| 651 | this.show_log_messages_for_adapter(session_id, window, cx); | ||
| 652 | } else { | ||
| 653 | this.show_rpc_trace_for_server(session_id, window, cx); | ||
| 654 | } | ||
| 529 | } | 655 | } |
| 656 | |||
| 657 | this | ||
| 530 | } | 658 | } |
| 531 | 659 | ||
| 532 | fn editor_for_logs( | 660 | fn editor_for_logs( |
| @@ -559,42 +687,34 @@ impl DapLogView { · 26 unmodified lines | |||
| 559 | (editor, vec![editor_subscription, search_subscription]) | 687 | (editor, vec![editor_subscription, search_subscription]) |
| 560 | } | 688 | } |
| 561 | 689 | ||
| 562 | fn menu_items(&self, cx: &App) -> Option<Vec<DapMenuItem>> { | 690 | fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> { |
| 563 | let mut menu_items = self | 691 | self.log_store |
| 564 | .project | ||
| 565 | .read(cx) | 692 | .read(cx) |
| 566 | .dap_store() | 693 | .debug_sessions |
| 567 | .read(cx) | 694 | .iter() |
| 568 | .sessions() | 695 | .rev() |
| 569 | .filter_map(|session| { | 696 | .map(|state| DapMenuItem { |
| 570 | let session = session.read(cx); | 697 | session_id: state.id, |
| 571 | session.adapter(); | 698 | adapter_name: state.adapter_name.clone(), |
| 572 | let client = session.adapter_client()?; | 699 | has_adapter_logs: state.has_adapter_logs, |
| 573 | Some(DapMenuItem { | 700 | selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), |
| 574 | client_id: client.id(), | ||
| 575 | client_name: session.adapter().to_string(), | ||
| 576 | has_adapter_logs: client.has_adapter_logs(), | ||
| 577 | selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), | ||
| 578 | }) | ||
| 579 | }) | 701 | }) |
| 580 | .collect::<Vec<_>>(); | 702 | .collect::<Vec<_>>() |
| 581 | menu_items.sort_by_key(|item| item.client_id.0); | ||
| 582 | Some(menu_items) | ||
| 583 | } | 703 | } |
| 584 | 704 | ||
| 585 | fn show_rpc_trace_for_server( | 705 | fn show_rpc_trace_for_server( |
| 586 | &mut self, | 706 | &mut self, |
| 587 | client_id: SessionId, | 707 | session_id: SessionId, |
| 588 | window: &mut Window, | 708 | window: &mut Window, |
| 589 | cx: &mut Context<Self>, | 709 | cx: &mut Context<Self>, |
| 590 | ) { | 710 | ) { |
| 591 | let rpc_log = self.log_store.update(cx, |log_store, _| { | 711 | let rpc_log = self.log_store.update(cx, |log_store, _| { |
| 592 | log_store | 712 | log_store |
| 593 | .rpc_messages_for_client(client_id) | 713 | .rpc_messages_for_session(session_id) |
| 594 | .map(|state| log_contents(&state)) | 714 | .map(|state| log_contents(state.iter().cloned())) |
| 595 | }); | 715 | }); |
| 596 | if let Some(rpc_log) = rpc_log { | 716 | if let Some(rpc_log) = rpc_log { |
| 597 | self.current_view = Some((client_id, LogKind::Rpc)); | 717 | self.current_view = Some((session_id, LogKind::Rpc)); |
| 598 | let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); | 718 | let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); |
| 599 | let language = self.project.read(cx).languages().language_for_name("JSON"); | 719 | let language = self.project.read(cx).languages().language_for_name("JSON"); |
| 600 | editor | 720 | editor |
| @@ -626,17 +746,17 @@ impl DapLogView { · 25 unmodified lines | |||
| 626 | 746 | ||
| 627 | fn show_log_messages_for_adapter( | 747 | fn show_log_messages_for_adapter( |
| 628 | &mut self, | 748 | &mut self, |
| 629 | client_id: SessionId, | 749 | session_id: SessionId, |
| 630 | window: &mut Window, | 750 | window: &mut Window, |
| 631 | cx: &mut Context<Self>, | 751 | cx: &mut Context<Self>, |
| 632 | ) { | 752 | ) { |
| 633 | let message_log = self.log_store.update(cx, |log_store, _| { | 753 | let message_log = self.log_store.update(cx, |log_store, _| { |
| 634 | log_store | 754 | log_store |
| 635 | .log_messages_for_client(client_id) | 755 | .log_messages_for_session(session_id) |
| 636 | .map(|state| log_contents(&state)) | 756 | .map(|state| log_contents(state.iter().cloned())) |
| 637 | }); | 757 | }); |
| 638 | if let Some(message_log) = message_log { | 758 | if let Some(message_log) = message_log { |
| 639 | self.current_view = Some((client_id, LogKind::Adapter)); | 759 | self.current_view = Some((session_id, LogKind::Adapter)); |
| 640 | let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx); | 760 | let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx); |
| 641 | editor | 761 | editor |
| 642 | .read(cx) | 762 | .read(cx) |
| @@ -652,14 +772,53 @@ impl DapLogView { · 9 unmodified lines | |||
| 652 | 772 | ||
| 653 | cx.focus_self(window); | 773 | cx.focus_self(window); |
| 654 | } | 774 | } |
| 775 | |||
| 776 | fn show_initialization_sequence_for_server( | ||
| 777 | &mut self, | ||
| 778 | session_id: SessionId, | ||
| 779 | window: &mut Window, | ||
| 780 | cx: &mut Context<Self>, | ||
| 781 | ) { | ||
| 782 | let rpc_log = self.log_store.update(cx, |log_store, _| { | ||
| 783 | log_store | ||
| 784 | .initialization_sequence_for_session(session_id) | ||
| 785 | .map(|state| log_contents(state.iter().cloned())) | ||
| 786 | }); | ||
| 787 | if let Some(rpc_log) = rpc_log { | ||
| 788 | self.current_view = Some((session_id, LogKind::Rpc)); | ||
| 789 | let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); | ||
| 790 | let language = self.project.read(cx).languages().language_for_name("JSON"); | ||
| 791 | editor | ||
| 792 | .read(cx) | ||
| 793 | .buffer() | ||
| 794 | .read(cx) | ||
| 795 | .as_singleton() | ||
| 796 | .expect("log buffer should be a singleton") | ||
| 797 | .update(cx, |_, cx| { | ||
| 798 | cx.spawn({ | ||
| 799 | let buffer = cx.entity(); | ||
| 800 | async move |_, cx| { | ||
| 801 | let language = language.await.ok(); | ||
| 802 | buffer.update(cx, |buffer, cx| { | ||
| 803 | buffer.set_language(language, cx); | ||
| 804 | }) | ||
| 805 | } | ||
| 806 | }) | ||
| 807 | .detach_and_log_err(cx); | ||
| 808 | }); | ||
| 809 | |||
| 810 | self.editor = editor; | ||
| 811 | self.editor_subscriptions = editor_subscriptions; | ||
| 812 | cx.notify(); | ||
| 813 | } | ||
| 814 | |||
| 815 | cx.focus_self(window); | ||
| 816 | } | ||
| 655 | } | 817 | } |
| 656 | 818 | ||
| 657 | fn log_contents(lines: &VecDeque<String>) -> String { | 819 | fn log_contents(lines: impl Iterator<Item = SharedString>) -> String { |
| 658 | let (a, b) = lines.as_slices(); | 820 | lines.fold(String::new(), |mut acc, el| { |
| 659 | let a = a.iter().map(move |v| v.as_ref()); | 821 | acc.push_str(&el); |
| 660 | let b = b.iter().map(move |v| v.as_ref()); | ||
| 661 | a.chain(b).fold(String::new(), |mut acc, el| { | ||
| 662 | acc.push_str(el); | ||
| 663 | acc.push('\n'); | 822 | acc.push('\n'); |
| 664 | acc | 823 | acc |
| 665 | }) | 824 | }) |
| @@ -667,14 +826,15 @@ fn log_contents(lines: &VecDeque<String>) -> String { · 1 unmodified line | |||
| 667 | 826 | ||
| 668 | #[derive(Clone, PartialEq)] | 827 | #[derive(Clone, PartialEq)] |
| 669 | pub(crate) struct DapMenuItem { | 828 | pub(crate) struct DapMenuItem { |
| 670 | pub client_id: SessionId, | 829 | pub session_id: SessionId, |
| 671 | pub client_name: String, | 830 | pub adapter_name: DebugAdapterName, |
| 672 | pub has_adapter_logs: bool, | 831 | pub has_adapter_logs: bool, |
| 673 | pub selected_entry: LogKind, | 832 | pub selected_entry: LogKind, |
| 674 | } | 833 | } |
| 675 | 834 | ||
| 676 | const ADAPTER_LOGS: &str = "Adapter Logs"; | 835 | const ADAPTER_LOGS: &str = "Adapter Logs"; |
| 677 | const RPC_MESSAGES: &str = "RPC Messages"; | 836 | const RPC_MESSAGES: &str = "RPC Messages"; |
| 837 | const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence"; | ||
| 678 | 838 | ||
| 679 | impl Render for DapLogView { | 839 | impl Render for DapLogView { |
| 680 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | 840 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| @@ -836,7 +996,7 @@ impl Focusable for DapLogView { · 155 unmodified lines | |||
| 836 | pub enum Event { | 996 | pub enum Event { |
| 837 | NewLogEntry { | 997 | NewLogEntry { |
| 838 | id: SessionId, | 998 | id: SessionId, |
| 839 | entry: String, | 999 | entry: SharedString, |
| 840 | kind: LogKind, | 1000 | kind: LogKind, |
| 841 | }, | 1001 | }, |
| 842 | } | 1002 | } |
| @@ -849,12 +1009,16 @@ impl EventEmitter<SearchEvent> for DapLogView {} · 6 unmodified lines | |||
| 849 | #[cfg(any(test, feature = "test-support"))] | 1009 | #[cfg(any(test, feature = "test-support"))] |
| 850 | impl LogStore { | 1010 | impl LogStore { |
| 851 | pub fn contained_session_ids(&self) -> Vec<SessionId> { | 1011 | pub fn contained_session_ids(&self) -> Vec<SessionId> { |
| 852 | self.debug_clients.keys().cloned().collect() | 1012 | self.debug_sessions |
| 1013 | .iter() | ||
| 1014 | .map(|session| session.id) | ||
| 1015 | .collect() | ||
| 853 | } | 1016 | } |
| 854 | 1017 | ||
| 855 | pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> { | 1018 | pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> { |
| 856 | self.debug_clients | 1019 | self.debug_sessions |
| 857 | .get(&session_id) | 1020 | .iter() |
| 1021 | .find(|adapter_state| adapter_state.id == session_id) | ||
| 858 | .expect("This session should exist if a test is calling") | 1022 | .expect("This session should exist if a test is calling") |
| 859 | .rpc_messages | 1023 | .rpc_messages |
| 860 | .messages | 1024 | .messages |
| @@ -862,9 +1026,10 @@ impl LogStore { · 1 unmodified line | |||
| 862 | .into() | 1026 | .into() |
| 863 | } | 1027 | } |
| 864 | 1028 | ||
| 865 | pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> { | 1029 | pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> { |
| 866 | self.debug_clients | 1030 | self.debug_sessions |
| 867 | .get(&session_id) | 1031 | .iter() |
| 1032 | .find(|adapter_state| adapter_state.id == session_id) | ||
| 868 | .expect("This session should exist if a test is calling") | 1033 | .expect("This session should exist if a test is calling") |
| 869 | .log_messages | 1034 | .log_messages |
| 870 | .clone() | 1035 | .clone() |
| @@ -2,25 +2,23 @@ use crate::persistence::DebuggerPaneItem; · 1 unmodified line | |||
| 2 | use crate::session::DebugSession; | 2 | use crate::session::DebugSession; |
| 3 | use crate::session::running::RunningState; | 3 | use crate::session::running::RunningState; |
| 4 | use crate::{ | 4 | use crate::{ |
| 5 | ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames, | 5 | ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList, |
| 6 | FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal, | 6 | FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, |
| 7 | NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, | 7 | NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop, |
| 8 | ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker, | 8 | ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal, |
| 9 | persistence, spawn_task_or_modal, | ||
| 10 | }; | 9 | }; |
| 11 | use anyhow::Result; | 10 | use anyhow::Result; |
| 12 | use command_palette_hooks::CommandPaletteFilter; | ||
| 13 | use dap::StartDebuggingRequestArguments; | ||
| 14 | use dap::adapters::DebugAdapterName; | 11 | use dap::adapters::DebugAdapterName; |
| 15 | use dap::debugger_settings::DebugPanelDockPosition; | 12 | use dap::debugger_settings::DebugPanelDockPosition; |
| 16 | use dap::{ | 13 | use dap::{ |
| 17 | ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, | 14 | ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, |
| 18 | client::SessionId, debugger_settings::DebuggerSettings, | 15 | client::SessionId, debugger_settings::DebuggerSettings, |
| 19 | }; | 16 | }; |
| 17 | use dap::{DapRegistry, StartDebuggingRequestArguments}; | ||
| 20 | use gpui::{ | 18 | use gpui::{ |
| 21 | Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter, | 19 | Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId, |
| 22 | FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity, | 20 | EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, |
| 23 | actions, anchored, deferred, | 21 | WeakEntity, actions, anchored, deferred, |
| 24 | }; | 22 | }; |
| 25 | 23 | ||
| 26 | use language::Buffer; | 24 | use language::Buffer; |
| @@ -29,10 +27,10 @@ use project::{Fs, WorktreeId}; · 2 unmodified lines | |||
| 29 | use project::{Project, debugger::session::ThreadStatus}; | 27 | use project::{Project, debugger::session::ThreadStatus}; |
| 30 | use rpc::proto::{self}; | 28 | use rpc::proto::{self}; |
| 31 | use settings::Settings; | 29 | use settings::Settings; |
| 32 | use std::any::TypeId; | ||
| 33 | use std::sync::Arc; | 30 | use std::sync::Arc; |
| 34 | use task::{DebugScenario, TaskContext}; | 31 | use task::{DebugScenario, TaskContext}; |
| 35 | use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*}; | 32 | use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*}; |
| 33 | use util::maybe; | ||
| 36 | use workspace::SplitDirection; | 34 | use workspace::SplitDirection; |
| 37 | use workspace::{ | 35 | use workspace::{ |
| 38 | Pane, Workspace, | 36 | Pane, Workspace, |
| @@ -140,82 +138,6 @@ impl DebugPanel { · 101 unmodified lines | |||
| 140 | .map(|session| session.read(cx).running_state().clone()) | 138 | .map(|session| session.read(cx).running_state().clone()) |
| 141 | } | 139 | } |
| 142 | 140 | ||
| 143 | pub(crate) fn filter_action_types(&self, cx: &mut App) { | ||
| 144 | let (has_active_session, supports_restart, support_step_back, status) = self | ||
| 145 | .active_session() | ||
| 146 | .map(|item| { | ||
| 147 | let running = item.read(cx).running_state().clone(); | ||
| 148 | let caps = running.read(cx).capabilities(cx); | ||
| 149 | ( | ||
| 150 | !running.read(cx).session().read(cx).is_terminated(), | ||
| 151 | caps.supports_restart_request.unwrap_or_default(), | ||
| 152 | caps.supports_step_back.unwrap_or_default(), | ||
| 153 | running.read(cx).thread_status(cx), | ||
| 154 | ) | ||
| 155 | }) | ||
| 156 | .unwrap_or((false, false, false, None)); | ||
| 157 | |||
| 158 | let filter = CommandPaletteFilter::global_mut(cx); | ||
| 159 | let debugger_action_types = [ | ||
| 160 | TypeId::of::<Detach>(), | ||
| 161 | TypeId::of::<Stop>(), | ||
| 162 | TypeId::of::<ToggleIgnoreBreakpoints>(), | ||
| 163 | ]; | ||
| 164 | |||
| 165 | let running_action_types = [TypeId::of::<Pause>()]; | ||
| 166 | |||
| 167 | let stopped_action_type = [ | ||
| 168 | TypeId::of::<Continue>(), | ||
| 169 | TypeId::of::<StepOver>(), | ||
| 170 | TypeId::of::<StepInto>(), | ||
| 171 | TypeId::of::<StepOut>(), | ||
| 172 | TypeId::of::<ShowStackTrace>(), | ||
| 173 | TypeId::of::<editor::actions::DebuggerRunToCursor>(), | ||
| 174 | TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(), | ||
| 175 | ]; | ||
| 176 | |||
| 177 | let step_back_action_type = [TypeId::of::<StepBack>()]; | ||
| 178 | let restart_action_type = [TypeId::of::<Restart>()]; | ||
| 179 | |||
| 180 | if has_active_session { | ||
| 181 | filter.show_action_types(debugger_action_types.iter()); | ||
| 182 | |||
| 183 | if supports_restart { | ||
| 184 | filter.show_action_types(restart_action_type.iter()); | ||
| 185 | } else { | ||
| 186 | filter.hide_action_types(&restart_action_type); | ||
| 187 | } | ||
| 188 | |||
| 189 | if support_step_back { | ||
| 190 | filter.show_action_types(step_back_action_type.iter()); | ||
| 191 | } else { | ||
| 192 | filter.hide_action_types(&step_back_action_type); | ||
| 193 | } | ||
| 194 | |||
| 195 | match status { | ||
| 196 | Some(ThreadStatus::Running) => { | ||
| 197 | filter.show_action_types(running_action_types.iter()); | ||
| 198 | filter.hide_action_types(&stopped_action_type); | ||
| 199 | } | ||
| 200 | Some(ThreadStatus::Stopped) => { | ||
| 201 | filter.show_action_types(stopped_action_type.iter()); | ||
| 202 | filter.hide_action_types(&running_action_types); | ||
| 203 | } | ||
| 204 | _ => { | ||
| 205 | filter.hide_action_types(&running_action_types); | ||
| 206 | filter.hide_action_types(&stopped_action_type); | ||
| 207 | } | ||
| 208 | } | ||
| 209 | } else { | ||
| 210 | // show only the `debug: start` | ||
| 211 | filter.hide_action_types(&debugger_action_types); | ||
| 212 | filter.hide_action_types(&step_back_action_type); | ||
| 213 | filter.hide_action_types(&restart_action_type); | ||
| 214 | filter.hide_action_types(&running_action_types); | ||
| 215 | filter.hide_action_types(&stopped_action_type); | ||
| 216 | } | ||
| 217 | } | ||
| 218 | |||
| 219 | pub fn load( | 141 | pub fn load( |
| 220 | workspace: WeakEntity<Workspace>, | 142 | workspace: WeakEntity<Workspace>, |
| 221 | cx: &mut AsyncWindowContext, | 143 | cx: &mut AsyncWindowContext, |
| @@ -233,17 +155,6 @@ impl DebugPanel { · 11 unmodified lines | |||
| 233 | ) | 155 | ) |
| 234 | }); | 156 | }); |
| 235 | 157 | ||
| 236 | cx.observe_new::<DebugPanel>(|debug_panel, _, cx| { | ||
| 237 | Self::filter_action_types(debug_panel, cx); | ||
| 238 | }) | ||
| 239 | .detach(); | ||
| 240 | |||
| 241 | cx.observe(&debug_panel, |_, debug_panel, cx| { | ||
| 242 | debug_panel.update(cx, |debug_panel, cx| { | ||
| 243 | Self::filter_action_types(debug_panel, cx); | ||
| 244 | }); | ||
| 245 | }) | ||
| 246 | .detach(); | ||
| 247 | workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone())); | 158 | workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone())); |
| 248 | 159 | ||
| 249 | debug_panel | 160 | debug_panel |
| @@ -269,6 +180,19 @@ impl DebugPanel { · 19 unmodified lines | |||
| 269 | cx, | 180 | cx, |
| 270 | ) | 181 | ) |
| 271 | }); | 182 | }); |
| 183 | let worktree = worktree_id.or_else(|| { | ||
| 184 | active_buffer | ||
| 185 | .as_ref() | ||
| 186 | .and_then(|buffer| buffer.read(cx).file()) | ||
| 187 | .map(|f| f.worktree_id(cx)) | ||
| 188 | }); | ||
| 189 | let Some(worktree) = worktree | ||
| 190 | .and_then(|id| self.project.read(cx).worktree_for_id(id, cx)) | ||
| 191 | .or_else(|| self.project.read(cx).visible_worktrees(cx).next()) | ||
| 192 | else { | ||
| 193 | log::debug!("Could not find a worktree to spawn the debug session in"); | ||
| 194 | return; | ||
| 195 | }; | ||
| 272 | self.debug_scenario_scheduled_last = true; | 196 | self.debug_scenario_scheduled_last = true; |
| 273 | if let Some(inventory) = self | 197 | if let Some(inventory) = self |
| 274 | .project | 198 | .project |
| @@ -303,7 +227,7 @@ impl DebugPanel { · 28 unmodified lines | |||
| 303 | .await?; | 227 | .await?; |
| 304 | dap_store | 228 | dap_store |
| 305 | .update(cx, |dap_store, cx| { | 229 | .update(cx, |dap_store, cx| { |
| 306 | dap_store.boot_session(session.clone(), definition, cx) | 230 | dap_store.boot_session(session.clone(), definition, worktree, cx) |
| 307 | })? | 231 | })? |
| 308 | .await | 232 | .await |
| 309 | } | 233 | } |
| @@ -412,7 +336,7 @@ impl DebugPanel { · 102 unmodified lines | |||
| 412 | let dap_store_handle = self.project.read(cx).dap_store().clone(); | 336 | let dap_store_handle = self.project.read(cx).dap_store().clone(); |
| 413 | let label = curr_session.read(cx).label().clone(); | 337 | let label = curr_session.read(cx).label().clone(); |
| 414 | let adapter = curr_session.read(cx).adapter().clone(); | 338 | let adapter = curr_session.read(cx).adapter().clone(); |
| 415 | let binary = curr_session.read(cx).binary().clone(); | 339 | let binary = curr_session.read(cx).binary().cloned().unwrap(); |
| 416 | let task = curr_session.update(cx, |session, cx| session.shutdown(cx)); | 340 | let task = curr_session.update(cx, |session, cx| session.shutdown(cx)); |
| 417 | 341 | ||
| 418 | cx.spawn_in(window, async move |this, cx| { | 342 | cx.spawn_in(window, async move |this, cx| { |
| @@ -426,8 +350,26 @@ impl DebugPanel { · 7 unmodified lines | |||
| 426 | }); | 350 | }); |
| 427 | (session, task) | 351 | (session, task) |
| 428 | })?; | 352 | })?; |
| 429 | Self::register_session(this.clone(), session, true, cx).await?; | 353 | Self::register_session(this.clone(), session.clone(), true, cx).await?; |
| 430 | task.await | 354 | |
| 355 | if let Err(error) = task.await { | ||
| 356 | session | ||
| 357 | .update(cx, |session, cx| { | ||
| 358 | session | ||
| 359 | .console_output(cx) | ||
| 360 | .unbounded_send(format!( | ||
| 361 | "Session failed to restart with error: {}", | ||
| 362 | error | ||
| 363 | )) | ||
| 364 | .ok(); | ||
| 365 | session.shutdown(cx) | ||
| 366 | })? | ||
| 367 | .await; | ||
| 368 | |||
| 369 | return Err(error); | ||
| 370 | }; | ||
| 371 | |||
| 372 | Ok(()) | ||
| 431 | }) | 373 | }) |
| 432 | .detach_and_log_err(cx); | 374 | .detach_and_log_err(cx); |
| 433 | } | 375 | } |
| @@ -445,12 +387,12 @@ impl DebugPanel { · 11 unmodified lines | |||
| 445 | }; | 387 | }; |
| 446 | 388 | ||
| 447 | let dap_store_handle = self.project.read(cx).dap_store().clone(); | 389 | let dap_store_handle = self.project.read(cx).dap_store().clone(); |
| 448 | let mut label = parent_session.read(cx).label().clone(); | 390 | let label = self.label_for_child_session(&parent_session, request, cx); |
| 449 | if !label.ends_with("(child)") { | ||
| 450 | label = format!("{label} (child)").into(); | ||
| 451 | } | ||
| 452 | let adapter = parent_session.read(cx).adapter().clone(); | 391 | let adapter = parent_session.read(cx).adapter().clone(); |
| 453 | let mut binary = parent_session.read(cx).binary().clone(); | 392 | let Some(mut binary) = parent_session.read(cx).binary().cloned() else { |
| 393 | log::error!("Attempted to start a child-session without a binary"); | ||
| 394 | return; | ||
| 395 | }; | ||
| 454 | binary.request_args = request.clone(); | 396 | binary.request_args = request.clone(); |
| 455 | cx.spawn_in(window, async move |this, cx| { | 397 | cx.spawn_in(window, async move |this, cx| { |
| 456 | let (session, task) = dap_store_handle.update(cx, |dap_store, cx| { | 398 | let (session, task) = dap_store_handle.update(cx, |dap_store, cx| { |
| @@ -462,7 +404,11 @@ impl DebugPanel { · 5 unmodified lines | |||
| 462 | }); | 404 | }); |
| 463 | (session, task) | 405 | (session, task) |
| 464 | })?; | 406 | })?; |
| 465 | Self::register_session(this, session, false, cx).await?; | 407 | // Focus child sessions if the parent has never emitted a stopped event; |
| 408 | // this improves our JavaScript experience, as it always spawns a "main" session that then spawns subsessions. | ||
| 409 | let parent_ever_stopped = | ||
| 410 | parent_session.update(cx, |this, _| this.has_ever_stopped())?; | ||
| 411 | Self::register_session(this, session, !parent_ever_stopped, cx).await?; | ||
| 466 | task.await | 412 | task.await |
| 467 | }) | 413 | }) |
| 468 | .detach_and_log_err(cx); | 414 | .detach_and_log_err(cx); |
| @@ -579,6 +525,26 @@ impl DebugPanel { · 110 unmodified lines | |||
| 579 | } | 525 | } |
| 580 | } | 526 | } |
| 581 | 527 | ||
| 528 | fn copy_debug_adapter_arguments( | ||
| 529 | &mut self, | ||
| 530 | _: &CopyDebugAdapterArguments, | ||
| 531 | _window: &mut Window, | ||
| 532 | cx: &mut Context<Self>, | ||
| 533 | ) { | ||
| 534 | let content = maybe!({ | ||
| 535 | let mut session = self.active_session()?.read(cx).session(cx); | ||
| 536 | while let Some(parent) = session.read(cx).parent_session().cloned() { | ||
| 537 | session = parent; | ||
| 538 | } | ||
| 539 | let binary = session.read(cx).binary()?; | ||
| 540 | let content = serde_json::to_string_pretty(&binary).ok()?; | ||
| 541 | Some(content) | ||
| 542 | }); | ||
| 543 | if let Some(content) = content { | ||
| 544 | cx.write_to_clipboard(ClipboardItem::new_string(content)); | ||
| 545 | } | ||
| 546 | } | ||
| 547 | |||
| 582 | pub(crate) fn top_controls_strip( | 548 | pub(crate) fn top_controls_strip( |
| 583 | &mut self, | 549 | &mut self, |
| 584 | window: &mut Window, | 550 | window: &mut Window, |
| @@ -608,6 +574,12 @@ impl DebugPanel { · 23 unmodified lines | |||
| 608 | } | 574 | } |
| 609 | }) | 575 | }) |
| 610 | }; | 576 | }; |
| 577 | let documentation_button = || { | ||
| 578 | IconButton::new("debug-open-documentation", IconName::CircleHelp) | ||
| 579 | .icon_size(IconSize::Small) | ||
| 580 | .on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger")) | ||
| 581 | .tooltip(Tooltip::text("Open Documentation")) | ||
| 582 | }; | ||
| 611 | 583 | ||
| 612 | Some( | 584 | Some( |
| 613 | div.border_b_1() | 585 | div.border_b_1() |
| @@ -629,6 +601,8 @@ impl DebugPanel { · 15 unmodified lines | |||
| 629 | project::debugger::session::ThreadStatus::Exited, | 601 | project::debugger::session::ThreadStatus::Exited, |
| 630 | ); | 602 | ); |
| 631 | let capabilities = running_state.read(cx).capabilities(cx); | 603 | let capabilities = running_state.read(cx).capabilities(cx); |
| 604 | let supports_detach = | ||
| 605 | running_state.read(cx).session().read(cx).is_attached(); | ||
| 632 | this.map(|this| { | 606 | this.map(|this| { |
| 633 | if thread_status == ThreadStatus::Running { | 607 | if thread_status == ThreadStatus::Running { |
| 634 | this.child( | 608 | this.child( |
| @@ -817,33 +791,48 @@ impl DebugPanel { · 182 unmodified lines | |||
| 817 | } | 791 | } |
| 818 | }), | 792 | }), |
| 819 | ) | 793 | ) |
| 820 | .child( | 794 | .when( |
| 821 | IconButton::new("debug-disconnect", IconName::DebugDetach) | 795 | supports_detach, |
| 822 | .icon_size(IconSize::XSmall) | 796 | |div| { |
| 823 | .on_click(window.listener_for( | 797 | div.child( |
| 824 | &running_state, | 798 | IconButton::new( |
| 825 | |this, _, _, cx| { | 799 | "debug-disconnect", |
| 826 | this.detach_client(cx); | 800 | IconName::DebugDetach, |
| 827 | }, | 801 | ) |
| 828 | )) | 802 | .disabled( |
| 829 | .tooltip({ | 803 | thread_status != ThreadStatus::Stopped |
| 830 | let focus_handle = focus_handle.clone(); | 804 | && thread_status != ThreadStatus::Running, |
| 831 | move |window, cx| { | 805 | ) |
| 832 | Tooltip::for_action_in( | 806 | .icon_size(IconSize::XSmall) |
| 833 | "Detach", | 807 | .on_click(window.listener_for( |
| 834 | &Detach, | 808 | &running_state, |
| 835 | &focus_handle, | 809 | |this, _, _, cx| { |
| 836 | window, | 810 | this.detach_client(cx); |
| 837 | cx, | 811 | }, |
| 838 | ) | 812 | )) |
| 839 | } | 813 | .tooltip({ |
| 840 | }), | 814 | let focus_handle = focus_handle.clone(); |
| 815 | move |window, cx| { | ||
| 816 | Tooltip::for_action_in( | ||
| 817 | "Detach", | ||
| 818 | &Detach, | ||
| 819 | &focus_handle, | ||
| 820 | window, | ||
| 821 | cx, | ||
| 822 | ) | ||
| 823 | } | ||
| 824 | }), | ||
| 825 | ) | ||
| 826 | }, | ||
| 841 | ) | 827 | ) |
| 842 | }, | 828 | }, |
| 843 | ), | 829 | ), |
| 844 | ) | 830 | ) |
| 845 | .justify_around() | 831 | .justify_around() |
| 846 | .when(is_side, |this| this.child(new_session_button())), | 832 | .when(is_side, |this| { |
| 833 | this.child(new_session_button()) | ||
| 834 | .child(documentation_button()) | ||
| 835 | }), | ||
| 847 | ) | 836 | ) |
| 848 | .child( | 837 | .child( |
| 849 | h_flex() | 838 | h_flex() |
| @@ -884,7 +873,10 @@ impl DebugPanel { · 34 unmodified lines | |||
| 884 | window, | 873 | window, |
| 885 | cx, | 874 | cx, |
| 886 | )) | 875 | )) |
| 887 | .when(!is_side, |this| this.child(new_session_button())), | 876 | .when(!is_side, |this| { |
| 877 | this.child(new_session_button()) | ||
| 878 | .child(documentation_button()) | ||
| 879 | }), | ||
| 888 | ), | 880 | ), |
| 889 | ), | 881 | ), |
| 890 | ) | 882 | ) |
| @@ -1042,6 +1034,25 @@ impl DebugPanel { · 151 unmodified lines | |||
| 1042 | cx.emit(PanelEvent::ZoomIn); | 1034 | cx.emit(PanelEvent::ZoomIn); |
| 1043 | } | 1035 | } |
| 1044 | } | 1036 | } |
| 1037 | |||
| 1038 | fn label_for_child_session( | ||
| 1039 | &self, | ||
| 1040 | parent_session: &Entity<Session>, | ||
| 1041 | request: &StartDebuggingRequestArguments, | ||
| 1042 | cx: &mut Context<'_, Self>, | ||
| 1043 | ) -> SharedString { | ||
| 1044 | let adapter = parent_session.read(cx).adapter(); | ||
| 1045 | if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) { | ||
| 1046 | if let Some(label) = adapter.label_for_child_session(request) { | ||
| 1047 | return label.into(); | ||
| 1048 | } | ||
| 1049 | } | ||
| 1050 | let mut label = parent_session.read(cx).label().clone(); | ||
| 1051 | if !label.ends_with("(child)") { | ||
| 1052 | label = format!("{label} (child)").into(); | ||
| 1053 | } | ||
| 1054 | label | ||
| 1055 | } | ||
| 1045 | } | 1056 | } |
| 1046 | 1057 | ||
| 1047 | async fn register_session_inner( | 1058 | async fn register_session_inner( |
| @@ -1069,6 +1080,11 @@ async fn register_session_inner( · 21 unmodified lines | |||
| 1069 | .ok(); | 1080 | .ok(); |
| 1070 | let serialized_layout = persistence::get_serialized_layout(adapter_name).await; | 1081 | let serialized_layout = persistence::get_serialized_layout(adapter_name).await; |
| 1071 | let debug_session = this.update_in(cx, |this, window, cx| { | 1082 | let debug_session = this.update_in(cx, |this, window, cx| { |
| 1083 | let parent_session = this | ||
| 1084 | .sessions | ||
| 1085 | .iter() | ||
| 1086 | .find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx)) | ||
| 1087 | .cloned(); | ||
| 1072 | this.sessions.retain(|session| { | 1088 | this.sessions.retain(|session| { |
| 1073 | !session | 1089 | !session |
| 1074 | .read(cx) | 1090 | .read(cx) |
| @@ -1082,8 +1098,8 @@ async fn register_session_inner( · 7 unmodified lines | |||
| 1082 | let debug_session = DebugSession::running( | 1098 | let debug_session = DebugSession::running( |
| 1083 | this.project.clone(), | 1099 | this.project.clone(), |
| 1084 | this.workspace.clone(), | 1100 | this.workspace.clone(), |
| 1101 | parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()), | ||
| 1085 | session, | 1102 | session, |
| 1086 | cx.weak_entity(), | ||
| 1087 | serialized_layout, | 1103 | serialized_layout, |
| 1088 | this.position(window, cx).axis(), | 1104 | this.position(window, cx).axis(), |
| 1089 | window, | 1105 | window, |
| @@ -1363,6 +1379,7 @@ impl Render for DebugPanel { · 273 unmodified lines | |||
| 1363 | }); | 1379 | }); |
| 1364 | cx.notify(); | 1380 | cx.notify(); |
| 1365 | })) | 1381 | })) |
| 1382 | .on_action(cx.listener(Self::copy_debug_adapter_arguments)) | ||
| 1366 | .when(self.active_session.is_some(), |this| { | 1383 | .when(self.active_session.is_some(), |this| { |
| 1367 | this.on_mouse_down( | 1384 | this.on_mouse_down( |
| 1368 | MouseButton::Right, | 1385 | MouseButton::Right, |
| @@ -1,14 +1,17 @@ | |||
| 1 | use std::any::TypeId; | ||
| 2 | |||
| 1 | use dap::debugger_settings::DebuggerSettings; | 3 | use dap::debugger_settings::DebuggerSettings; |
| 2 | use debugger_panel::{DebugPanel, ToggleFocus}; | 4 | use debugger_panel::{DebugPanel, ToggleFocus}; |
| 3 | use editor::Editor; | 5 | use editor::Editor; |
| 4 | use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt}; | 6 | use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt}; |
| 5 | use gpui::{App, EntityInputHandler, actions}; | 7 | use gpui::{App, DispatchPhase, EntityInputHandler, actions}; |
| 6 | use new_process_modal::{NewProcessModal, NewProcessMode}; | 8 | use new_process_modal::{NewProcessModal, NewProcessMode}; |
| 7 | use project::debugger::{self, breakpoint_store::SourceBreakpoint}; | 9 | use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus}; |
| 8 | use session::DebugSession; | 10 | use session::DebugSession; |
| 9 | use settings::Settings; | 11 | use settings::Settings; |
| 10 | use stack_trace_view::StackTraceView; | 12 | use stack_trace_view::StackTraceView; |
| 11 | use tasks_ui::{Spawn, TaskOverrides}; | 13 | use tasks_ui::{Spawn, TaskOverrides}; |
| 14 | use ui::{FluentBuilder, InteractiveElement}; | ||
| 12 | use util::maybe; | 15 | use util::maybe; |
| 13 | use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace}; | 16 | use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace}; |
| 14 | 17 | ||
| @@ -53,6 +56,8 @@ actions!( · 38 unmodified lines | |||
| 53 | ] | 56 | ] |
| 54 | ); | 57 | ); |
| 55 | 58 | ||
| 59 | actions!(dev, [CopyDebugAdapterArguments]); | ||
| 60 | |||
| 56 | pub fn init(cx: &mut App) { | 61 | pub fn init(cx: &mut App) { |
| 57 | DebuggerSettings::register(cx); | 62 | DebuggerSettings::register(cx); |
| 58 | workspace::FollowableViewRegistry::register::<DebugSession>(cx); | 63 | workspace::FollowableViewRegistry::register::<DebugSession>(cx); |
| @@ -68,107 +73,20 @@ pub fn init(cx: &mut App) { · 9 unmodified lines | |||
| 68 | .register_action(|workspace, _: &ToggleFocus, window, cx| { | 73 | .register_action(|workspace, _: &ToggleFocus, window, cx| { |
| 69 | workspace.toggle_panel_focus::<DebugPanel>(window, cx); | 74 | workspace.toggle_panel_focus::<DebugPanel>(window, cx); |
| 70 | }) | 75 | }) |
| 71 | .register_action(|workspace, _: &Pause, _, cx| { | 76 | .register_action(|workspace: &mut Workspace, _: &Start, window, cx| { |
| 72 | if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) { | 77 | NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); |
| 73 | if let Some(active_item) = debug_panel | ||
| 74 | .read(cx) | ||
| 75 | .active_session() | ||
| 76 | .map(|session| session.read(cx).running_state().clone()) | ||
| 77 | { | ||
| 78 | active_item.update(cx, |item, cx| item.pause_thread(cx)) | ||
| 79 | } | ||
| 80 | } | ||
| 81 | }) | ||
| 82 | .register_action(|workspace, _: &Restart, _, cx| { | ||
| 83 | if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) { | ||
| 84 | if let Some(active_item) = debug_panel | ||
| 85 | .read(cx) | ||
| 86 | .active_session() | ||
| 87 | .map(|session| session.read(cx).running_state().clone()) | ||
| 88 | { | ||
| 89 | active_item.update(cx, |item, cx| item.restart_session(cx)) | ||
| 90 | } | ||
| 91 | } | ||
| 92 | }) | ||
| 93 | .register_action(|workspace, _: &Continue, _, cx| { | ||
| 94 | if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) { | ||
| 95 | if let Some(active_item) = debug_panel | ||
| 96 | .read(cx) | ||
| 97 | .active_session() | ||
| 98 | .map(|session| session.read(cx).running_state().clone()) | ||
| 99 | { | ||
| 100 | active_item.update(cx, |item, cx| item.continue_thread(cx)) | ||
| 101 | } | ||
| 102 | } | ||
| 103 | }) | ||
| 104 | .register_action(|workspace, _: &StepInto, _, cx| { | ||
| 105 | if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) { | ||
| 106 | if let Some(active_item) = debug_panel | ||
| 107 | .read(cx) | ||
| 108 | .active_session() | ||
| 109 | .map(|session| session.read(cx).running_state().clone()) | ||
| 110 | { | ||
| 111 | active_item.update(cx, |item, cx| item.step_in(cx)) | ||
| 112 | } | ||
| 113 | } | ||
| 114 | }) | ||
| 115 | .register_action(|workspace, _: &StepOver, _, cx| { | ||
| 116 | if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) { | ||
| 117 | if let Some(active_item) = debug_panel | ||
| 118 | .read(cx) | ||
| 119 | .active_session() | ||
| 120 | .map(|session| session.read(cx).running_state().clone()) | ||
| 121 | { | ||
| 122 | active_item.update(cx, |item, cx| item.step_over(cx)) | ||
| 123 | } | ||
| 124 | } | ||
| 125 | }) | ||
| 126 | .register_action(|workspace, _: &StepOut, _, cx| { | ||
| 127 | if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) { | ||
| 128 | if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { | ||
| 129 | panel | ||
| 130 | .active_session() | ||
| 131 | .map(|session| session.read(cx).running_state().clone()) | ||
| 132 | }) { | ||
| 133 | active_item.update(cx, |item, cx| item.step_out(cx)) | ||
| 134 | } | ||
| 135 | } | ||
| 136 | }) | ||
| 137 | .register_action(|workspace, _: &StepBack, _, cx| { | ||
| 138 | if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) { | ||
| 139 | if let Some(active_item) = debug_panel | ||
| 140 | .read(cx) | ||
| 141 | .active_session() | ||
| 142 | .map(|session| session.read(cx).running_state().clone()) | ||
| 143 | { | ||
| 144 | active_item.update(cx, |item, cx| item.step_back(cx)) | ||
| 145 | } | ||
| 146 | } | ||
| 147 | }) | ||
| 148 | .register_action(|workspace, _: &Stop, _, cx| { | ||
| 149 | if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) { | ||
| 150 | if let Some(active_item) = debug_panel | ||
| 151 | .read(cx) | ||
| 152 | .active_session() | ||
| 153 | .map(|session| session.read(cx).running_state().clone()) | ||
| 154 | { | ||
| 155 | cx.defer(move |cx| { | ||
| 156 | active_item.update(cx, |item, cx| item.stop_thread(cx)) | ||
| 157 | }) | ||
| 158 | } | ||
| 159 | } | ||
| 160 | }) | ||
| 161 | .register_action(|workspace, _: &ToggleIgnoreBreakpoints, _, cx| { | ||
| 162 | if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) { | ||
| 163 | if let Some(active_item) = debug_panel | ||
| 164 | .read(cx) | ||
| 165 | .active_session() | ||
| 166 | .map(|session| session.read(cx).running_state().clone()) | ||
| 167 | { | ||
| 168 | active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) | ||
| 169 | } | ||
| 170 | } | ||
| 171 | }) | 78 | }) |
| 79 | .register_action( | ||
| 80 | |workspace: &mut Workspace, _: &RerunLastSession, window, cx| { | ||
| 81 | let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else { | ||
| 82 | return; | ||
| 83 | }; | ||
| 84 | |||
| 85 | debug_panel.update(cx, |debug_panel, cx| { | ||
| 86 | debug_panel.rerun_last_session(workspace, window, cx); | ||
| 87 | }) | ||
| 88 | }, | ||
| 89 | ) | ||
| 172 | .register_action( | 90 | .register_action( |
| 173 | |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| { | 91 | |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| { |
| 174 | workspace.project().update(cx, |project, cx| { | 92 | workspace.project().update(cx, |project, cx| { |
| @@ -178,135 +96,244 @@ pub fn init(cx: &mut App) { · 3 unmodified lines | |||
| 178 | }) | 96 | }) |
| 179 | }, | 97 | }, |
| 180 | ) | 98 | ) |
| 181 | .register_action( | 99 | .register_action_renderer(|div, workspace, _, cx| { |
| 182 | |workspace: &mut Workspace, _: &ShowStackTrace, window, cx| { | 100 | let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else { |
| 183 | let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else { | 101 | return div; |
| 184 | return; | 102 | }; |
| 185 | }; | 103 | let Some(active_item) = debug_panel |
| 104 | .read(cx) | ||
| 105 | .active_session() | ||
| 106 | .map(|session| session.read(cx).running_state().clone()) | ||
| 107 | else { | ||
| 108 | return div; | ||
| 109 | }; | ||
| 110 | let running_state = active_item.read(cx); | ||
| 111 | if running_state.session().read(cx).is_terminated() { | ||
| 112 | return div; | ||
| 113 | } | ||
| 186 | 114 | ||
| 187 | if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) { | 115 | let caps = running_state.capabilities(cx); |
| 188 | let is_active = workspace | 116 | let supports_step_back = caps.supports_step_back.unwrap_or_default(); |
| 189 | .active_item(cx) | 117 | let supports_detach = running_state.session().read(cx).is_attached(); |
| 190 | .is_some_and(|item| item.item_id() == existing.item_id()); | 118 | let status = running_state.thread_status(cx); |
| 191 | workspace.activate_item(&existing, true, !is_active, window, cx); | ||
| 192 | } else { | ||
| 193 | let Some(active_session) = debug_panel.read(cx).active_session() else { | ||
| 194 | return; | ||
| 195 | }; | ||
| 196 | 119 | ||
| 197 | let project = workspace.project(); | 120 | let active_item = active_item.downgrade(); |
| 121 | div.when(status == Some(ThreadStatus::Running), |div| { | ||
| 122 | let active_item = active_item.clone(); | ||
| 123 | div.on_action(move |_: &Pause, _, cx| { | ||
| 124 | active_item | ||
| 125 | .update(cx, |item, cx| item.pause_thread(cx)) | ||
| 126 | .ok(); | ||
| 127 | }) | ||
| 128 | }) | ||
| 129 | .when(status == Some(ThreadStatus::Stopped), |div| { | ||
| 130 | div.on_action({ | ||
| 131 | let active_item = active_item.clone(); | ||
| 132 | move |_: &StepInto, _, cx| { | ||
| 133 | active_item.update(cx, |item, cx| item.step_in(cx)).ok(); | ||
| 134 | } | ||
| 135 | }) | ||
| 136 | .on_action({ | ||
| 137 | let active_item = active_item.clone(); | ||
| 138 | move |_: &StepOver, _, cx| { | ||
| 139 | active_item.update(cx, |item, cx| item.step_over(cx)).ok(); | ||
| 140 | } | ||
| 141 | }) | ||
| 142 | .on_action({ | ||
| 143 | let active_item = active_item.clone(); | ||
| 144 | move |_: &StepOut, _, cx| { | ||
| 145 | active_item.update(cx, |item, cx| item.step_out(cx)).ok(); | ||
| 146 | } | ||
| 147 | }) | ||
| 148 | .when(supports_step_back, |div| { | ||
| 149 | let active_item = active_item.clone(); | ||
| 150 | div.on_action(move |_: &StepBack, _, cx| { | ||
| 151 | active_item.update(cx, |item, cx| item.step_back(cx)).ok(); | ||
| 152 | }) | ||
| 153 | }) | ||
| 154 | .on_action({ | ||
| 155 | let active_item = active_item.clone(); | ||
| 156 | move |_: &Continue, _, cx| { | ||
| 157 | active_item | ||
| 158 | .update(cx, |item, cx| item.continue_thread(cx)) | ||
| 159 | .ok(); | ||
| 160 | } | ||
| 161 | }) | ||
| 162 | .on_action(cx.listener( | ||
| 163 | |workspace, _: &ShowStackTrace, window, cx| { | ||
| 164 | let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else { | ||
| 165 | return; | ||
| 166 | }; | ||
| 198 | 167 | ||
| 199 | let stack_trace_view = active_session.update(cx, |session, cx| { | 168 | if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) |
| 200 | session.stack_trace_view(project, window, cx).clone() | 169 | { |
| 201 | }); | 170 | let is_active = workspace |
| 171 | .active_item(cx) | ||
| 172 | .is_some_and(|item| item.item_id() == existing.item_id()); | ||
| 173 | workspace | ||
| 174 | .activate_item(&existing, true, !is_active, window, cx); | ||
| 175 | } else { | ||
| 176 | let Some(active_session) = | ||
| 177 | debug_panel.read(cx).active_session() | ||
| 178 | else { | ||
| 179 | return; | ||
| 180 | }; | ||
| 202 | 181 | ||
| 203 | workspace.add_item_to_active_pane( | 182 | let project = workspace.project(); |
| 204 | Box::new(stack_trace_view), | ||
| 205 | None, | ||
| 206 | true, | ||
| 207 | window, | ||
| 208 | cx, | ||
| 209 | ); | ||
| 210 | } | ||
| 211 | }, | ||
| 212 | ) | ||
| 213 | .register_action(|workspace: &mut Workspace, _: &Start, window, cx| { | ||
| 214 | NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); | ||
| 215 | }) | ||
| 216 | .register_action( | ||
| 217 | |workspace: &mut Workspace, _: &RerunLastSession, window, cx| { | ||
| 218 | let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else { | ||
| 219 | return; | ||
| 220 | }; | ||
| 221 | 183 | ||
| 222 | debug_panel.update(cx, |debug_panel, cx| { | 184 | let stack_trace_view = |
| 223 | debug_panel.rerun_last_session(workspace, window, cx); | 185 | active_session.update(cx, |session, cx| { |
| 186 | session.stack_trace_view(project, window, cx).clone() | ||
| 187 | }); | ||
| 188 | |||
| 189 | workspace.add_item_to_active_pane( | ||
| 190 | Box::new(stack_trace_view), | ||
| 191 | None, | ||
| 192 | true, | ||
| 193 | window, | ||
| 194 | cx, | ||
| 195 | ); | ||
| 196 | } | ||
| 197 | }, | ||
| 198 | )) | ||
| 199 | }) | ||
| 200 | .when(supports_detach, |div| { | ||
| 201 | let active_item = active_item.clone(); | ||
| 202 | div.on_action(move |_: &Detach, _, cx| { | ||
| 203 | active_item | ||
| 204 | .update(cx, |item, cx| item.detach_client(cx)) | ||
| 205 | .ok(); | ||
| 224 | }) | 206 | }) |
| 225 | }, | 207 | }) |
| 226 | ); | 208 | .on_action({ |
| 209 | let active_item = active_item.clone(); | ||
| 210 | move |_: &Restart, _, cx| { | ||
| 211 | active_item | ||
| 212 | .update(cx, |item, cx| item.restart_session(cx)) | ||
| 213 | .ok(); | ||
| 214 | } | ||
| 215 | }) | ||
| 216 | .on_action({ | ||
| 217 | let active_item = active_item.clone(); | ||
| 218 | move |_: &Stop, _, cx| { | ||
| 219 | active_item.update(cx, |item, cx| item.stop_thread(cx)).ok(); | ||
| 220 | } | ||
| 221 | }) | ||
| 222 | .on_action({ | ||
| 223 | let active_item = active_item.clone(); | ||
| 224 | move |_: &ToggleIgnoreBreakpoints, _, cx| { | ||
| 225 | active_item | ||
| 226 | .update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) | ||
| 227 | .ok(); | ||
| 228 | } | ||
| 229 | }) | ||
| 230 | }); | ||
| 227 | }) | 231 | }) |
| 228 | }) | 232 | }) |
| 229 | .detach(); | 233 | .detach(); |
| 230 | 234 | ||
| 231 | cx.observe_new({ | 235 | cx.observe_new({ |
| 232 | move |editor: &mut Editor, _, cx| { | 236 | move |editor: &mut Editor, _, _| { |
| 233 | editor | 237 | editor |
| 234 | .register_action(cx.listener( | 238 | .register_action_renderer(move |editor, window, cx| { |
| 235 | move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| { | 239 | let Some(workspace) = editor.workspace() else { |
| 236 | maybe!({ | 240 | return; |
| 237 | let debug_panel = | 241 | }; |
| 238 | editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?; | 242 | let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else { |
| 239 | let cursor_point: language::Point = editor.selections.newest(cx).head(); | 243 | return; |
| 240 | let active_session = debug_panel.read(cx).active_session()?; | 244 | }; |
| 245 | let Some(active_session) = debug_panel | ||
| 246 | .clone() | ||
| 247 | .update(cx, |panel, _| panel.active_session()) | ||
| 248 | else { | ||
| 249 | return; | ||
| 250 | }; | ||
| 251 | let editor = cx.entity().downgrade(); | ||
| 252 | window.on_action(TypeId::of::<editor::actions::RunToCursor>(), { | ||
| 253 | let editor = editor.clone(); | ||
| 254 | let active_session = active_session.clone(); | ||
| 255 | move |_, phase, _, cx| { | ||
| 256 | if phase != DispatchPhase::Bubble { | ||
| 257 | return; | ||
| 258 | } | ||
| 259 | maybe!({ | ||
| 260 | let (buffer, position, _) = editor | ||
| 261 | .update(cx, |editor, cx| { | ||
| 262 | let cursor_point: language::Point = | ||
| 263 | editor.selections.newest(cx).head(); | ||
| 241 | 264 | ||
| 242 | let (buffer, position, _) = editor | 265 | editor |
| 243 | .buffer() | 266 | .buffer() |
| 244 | .read(cx) | 267 | .read(cx) |
| 245 | .point_to_buffer_point(cursor_point, cx)?; | 268 | .point_to_buffer_point(cursor_point, cx) |
| 269 | }) | ||
| 270 | .ok()??; | ||
| 246 | 271 | ||
| 247 | let path = | 272 | let path = |
| 248 | debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer( | 273 | debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer( |
| 249 | &buffer, cx, | 274 | &buffer, cx, |
| 250 | )?; | 275 | )?; |
| 251 | 276 | ||
| 252 | let source_breakpoint = SourceBreakpoint { | 277 | let source_breakpoint = SourceBreakpoint { |
| 253 | row: position.row, | 278 | row: position.row, |
| 254 | path, | 279 | path, |
| 255 | message: None, | 280 | message: None, |
| 256 | condition: None, | 281 | condition: None, |
| 257 | hit_condition: None, | 282 | hit_condition: None, |
| 258 | state: debugger::breakpoint_store::BreakpointState::Enabled, | 283 | state: debugger::breakpoint_store::BreakpointState::Enabled, |
| 259 | }; | 284 | }; |
| 260 | 285 | ||
| 261 | active_session.update(cx, |session, cx| { | 286 | active_session.update(cx, |session, cx| { |
| 262 | session.running_state().update(cx, |state, cx| { | 287 | session.running_state().update(cx, |state, cx| { |
| 263 | if let Some(thread_id) = state.selected_thread_id() { | 288 | if let Some(thread_id) = state.selected_thread_id() { |
| 264 | state.session().update(cx, |session, cx| { | 289 | state.session().update(cx, |session, cx| { |
| 265 | session.run_to_position( | 290 | session.run_to_position( |
| 266 | source_breakpoint, | 291 | source_breakpoint, |
| 267 | thread_id, | 292 | thread_id, |
| 268 | cx, | 293 | cx, |
| 269 | ); | 294 | ); |
| 270 | }) | 295 | }) |
| 271 | } | 296 | } |
| 297 | }); | ||
| 272 | }); | 298 | }); |
| 273 | }); | ||
| 274 | 299 | ||
| 275 | Some(()) | 300 | Some(()) |
| 276 | }); | 301 | }); |
| 277 | }, | 302 | } |
| 278 | )) | 303 | }); |
| 279 | .detach(); | ||
| 280 | |||
| 281 | editor | ||
| 282 | .register_action(cx.listener( | ||
| 283 | move |editor, _: &editor::actions::DebuggerEvaluateSelectedText, window, cx| { | ||
| 284 | maybe!({ | ||
| 285 | let debug_panel = | ||
| 286 | editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?; | ||
| 287 | let active_session = debug_panel.read(cx).active_session()?; | ||
| 288 | 304 | ||
| 289 | let text = editor.text_for_range( | 305 | window.on_action( |
| 290 | editor.selections.newest(cx).range(), | 306 | TypeId::of::<editor::actions::EvaluateSelectedText>(), |
| 291 | &mut None, | 307 | move |_, _, window, cx| { |
| 292 | window, | 308 | maybe!({ |
| 293 | cx, | 309 | let text = editor |
| 294 | )?; | 310 | .update(cx, |editor, cx| { |
| 311 | editor.text_for_range( | ||
| 312 | editor.selections.newest(cx).range(), | ||
| 313 | &mut None, | ||
| 314 | window, | ||
| 315 | cx, | ||
| 316 | ) | ||
| 317 | }) | ||
| 318 | .ok()??; | ||
| 295 | 319 | ||
| 296 | active_session.update(cx, |session, cx| { | 320 | active_session.update(cx, |session, cx| { |
| 297 | session.running_state().update(cx, |state, cx| { | 321 | session.running_state().update(cx, |state, cx| { |
| 298 | let stack_id = state.selected_stack_frame_id(cx); | 322 | let stack_id = state.selected_stack_frame_id(cx); |
| 299 | 323 | ||
| 300 | state.session().update(cx, |session, cx| { | 324 | state.session().update(cx, |session, cx| { |
| 301 | session.evaluate(text, None, stack_id, None, cx).detach(); | 325 | session |
| 326 | .evaluate(text, None, stack_id, None, cx) | ||
| 327 | .detach(); | ||
| 328 | }); | ||
| 302 | }); | 329 | }); |
| 303 | }); | 330 | }); |
| 304 | }); | ||
| 305 | 331 | ||
| 306 | Some(()) | 332 | Some(()) |
| 307 | }); | 333 | }); |
| 308 | }, | 334 | }, |
| 309 | )) | 335 | ); |
| 336 | }) | ||
| 310 | .detach(); | 337 | .detach(); |
| 311 | } | 338 | } |
| 312 | }) | 339 | }) |
| @@ -1,4 +1,4 @@ | |||
| 1 | use collections::FxHashMap; | 1 | use collections::{FxHashMap, HashMap}; |
| 2 | use language::LanguageRegistry; | 2 | use language::LanguageRegistry; |
| 3 | use paths::local_debug_file_relative_path; | 3 | use paths::local_debug_file_relative_path; |
| 4 | use std::{ | 4 | use std::{ |
| @@ -15,9 +15,9 @@ use dap::{ · 10 unmodified lines | |||
| 15 | use editor::{Editor, EditorElement, EditorStyle}; | 15 | use editor::{Editor, EditorElement, EditorStyle}; |
| 16 | use fuzzy::{StringMatch, StringMatchCandidate}; | 16 | use fuzzy::{StringMatch, StringMatchCandidate}; |
| 17 | use gpui::{ | 17 | use gpui::{ |
| 18 | App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, | 18 | Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, |
| 19 | InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription, | 19 | HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, |
| 20 | TextStyle, UnderlineStyle, WeakEntity, | 20 | Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, |
| 21 | }; | 21 | }; |
| 22 | use itertools::Itertools as _; | 22 | use itertools::Itertools as _; |
| 23 | use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; | 23 | use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; |
| @@ -28,10 +28,10 @@ use theme::ThemeSettings; · 4 unmodified lines | |||
| 28 | use ui::{ | 28 | use ui::{ |
| 29 | ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, | 29 | ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, |
| 30 | ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize, | 30 | ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize, |
| 31 | IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _, | 31 | IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label, |
| 32 | ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt, | 32 | LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce, |
| 33 | StyledTypography, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, px, | 33 | SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable, |
| 34 | relative, rems, v_flex, | 34 | Tooltip, Window, div, h_flex, px, relative, rems, v_flex, |
| 35 | }; | 35 | }; |
| 36 | use util::ResultExt; | 36 | use util::ResultExt; |
| 37 | use workspace::{ModalView, Workspace, pane}; | 37 | use workspace::{ModalView, Workspace, pane}; |
| @@ -50,7 +50,7 @@ pub(super) struct NewProcessModal { · 12 unmodified lines | |||
| 50 | mode: NewProcessMode, | 50 | mode: NewProcessMode, |
| 51 | debug_picker: Entity<Picker<DebugDelegate>>, | 51 | debug_picker: Entity<Picker<DebugDelegate>>, |
| 52 | attach_mode: Entity<AttachMode>, | 52 | attach_mode: Entity<AttachMode>, |
| 53 | launch_mode: Entity<ConfigureMode>, | 53 | configure_mode: Entity<ConfigureMode>, |
| 54 | task_mode: TaskMode, | 54 | task_mode: TaskMode, |
| 55 | debugger: Option<DebugAdapterName>, | 55 | debugger: Option<DebugAdapterName>, |
| 56 | // save_scenario_state: Option<SaveScenarioState>, | 56 | // save_scenario_state: Option<SaveScenarioState>, |
| @@ -194,26 +194,31 @@ impl NewProcessModal { · 137 unmodified lines | |||
| 194 | return Ok(()); | 194 | return Ok(()); |
| 195 | }; | 195 | }; |
| 196 | 196 | ||
| 197 | let (used_tasks, current_resolved_tasks) = | 197 | let (used_tasks, current_resolved_tasks) = task_inventory |
| 198 | task_inventory.update(cx, |task_inventory, cx| { | 198 | .update(cx, |task_inventory, cx| { |
| 199 | task_inventory | 199 | task_inventory |
| 200 | .used_and_current_resolved_tasks(&task_contexts, cx) | 200 | .used_and_current_resolved_tasks(task_contexts.clone(), cx) |
| 201 | })?; | 201 | })? |
| 202 | 202 | .await; | |
| 203 | debug_picker | 203 | |
| 204 | .update_in(cx, |picker, window, cx| { | 204 | if let Ok(task) = debug_picker.update(cx, |picker, cx| { |
| 205 | picker.delegate.tasks_loaded( | 205 | picker.delegate.tasks_loaded( |
| 206 | task_contexts.clone(), | 206 | task_contexts.clone(), |
| 207 | languages, | 207 | languages, |
| 208 | lsp_tasks.clone(), | 208 | lsp_tasks.clone(), |
| 209 | current_resolved_tasks.clone(), | 209 | current_resolved_tasks.clone(), |
| 210 | add_current_language_tasks, | 210 | add_current_language_tasks, |
| 211 | cx, | 211 | cx, |
| 212 | ); | 212 | ) |
| 213 | picker.refresh(window, cx); | 213 | }) { |
| 214 | cx.notify(); | 214 | task.await; |
| 215 | }) | 215 | debug_picker |
| 216 | .ok(); | 216 | .update_in(cx, |picker, window, cx| { |
| 217 | picker.refresh(window, cx); | ||
| 218 | cx.notify(); | ||
| 219 | }) | ||
| 220 | .ok(); | ||
| 221 | } | ||
| 217 | 222 | ||
| 218 | if let Some(active_cwd) = task_contexts | 223 | if let Some(active_cwd) = task_contexts |
| 219 | .active_context() | 224 | .active_context() |
| @@ -253,7 +258,7 @@ impl NewProcessModal { · 33 unmodified lines | |||
| 253 | Self { | 258 | Self { |
| 254 | debug_picker, | 259 | debug_picker, |
| 255 | attach_mode, | 260 | attach_mode, |
| 256 | launch_mode: configure_mode, | 261 | configure_mode, |
| 257 | task_mode, | 262 | task_mode, |
| 258 | debugger: None, | 263 | debugger: None, |
| 259 | mode, | 264 | mode, |
| @@ -283,7 +288,7 @@ impl NewProcessModal { · 23 unmodified lines | |||
| 283 | NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| { | 288 | NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| { |
| 284 | this.clone().render(window, cx).into_any_element() | 289 | this.clone().render(window, cx).into_any_element() |
| 285 | }), | 290 | }), |
| 286 | NewProcessMode::Launch => self.launch_mode.update(cx, |this, cx| { | 291 | NewProcessMode::Launch => self.configure_mode.update(cx, |this, cx| { |
| 287 | this.clone().render(dap_menu, window, cx).into_any_element() | 292 | this.clone().render(dap_menu, window, cx).into_any_element() |
| 288 | }), | 293 | }), |
| 289 | NewProcessMode::Debug => v_flex() | 294 | NewProcessMode::Debug => v_flex() |
| @@ -297,7 +302,7 @@ impl NewProcessModal { · 7 unmodified lines | |||
| 297 | match self.mode { | 302 | match self.mode { |
| 298 | NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx), | 303 | NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx), |
| 299 | NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx), | 304 | NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx), |
| 300 | NewProcessMode::Launch => self.launch_mode.read(cx).program.focus_handle(cx), | 305 | NewProcessMode::Launch => self.configure_mode.read(cx).program.focus_handle(cx), |
| 301 | NewProcessMode::Debug => self.debug_picker.focus_handle(cx), | 306 | NewProcessMode::Debug => self.debug_picker.focus_handle(cx), |
| 302 | } | 307 | } |
| 303 | } | 308 | } |
| @@ -305,7 +310,7 @@ impl NewProcessModal { · 1 unmodified line | |||
| 305 | fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> { | 310 | fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> { |
| 306 | let request = match self.mode { | 311 | let request = match self.mode { |
| 307 | NewProcessMode::Launch => Some(DebugRequest::Launch( | 312 | NewProcessMode::Launch => Some(DebugRequest::Launch( |
| 308 | self.launch_mode.read(cx).debug_request(cx), | 313 | self.configure_mode.read(cx).debug_request(cx), |
| 309 | )), | 314 | )), |
| 310 | NewProcessMode::Attach => Some(DebugRequest::Attach( | 315 | NewProcessMode::Attach => Some(DebugRequest::Attach( |
| 311 | self.attach_mode.read(cx).debug_request(), | 316 | self.attach_mode.read(cx).debug_request(), |
| @@ -315,7 +320,7 @@ impl NewProcessModal { · 3 unmodified lines | |||
| 315 | let label = suggested_label(&request, debugger); | 320 | let label = suggested_label(&request, debugger); |
| 316 | 321 | ||
| 317 | let stop_on_entry = if let NewProcessMode::Launch = &self.mode { | 322 | let stop_on_entry = if let NewProcessMode::Launch = &self.mode { |
| 318 | Some(self.launch_mode.read(cx).stop_on_entry.selected()) | 323 | Some(self.configure_mode.read(cx).stop_on_entry.selected()) |
| 319 | } else { | 324 | } else { |
| 320 | None | 325 | None |
| 321 | }; | 326 | }; |
| @@ -476,10 +481,9 @@ impl NewProcessModal { · 154 unmodified lines | |||
| 476 | .get_index_of(adapter.0.as_ref()) | 481 | .get_index_of(adapter.0.as_ref()) |
| 477 | .unwrap_or(usize::MAX) | 482 | .unwrap_or(usize::MAX) |
| 478 | }); | 483 | }); |
| 479 | } | 484 | if self.debugger.is_none() { |
| 480 | 485 | self.debugger = available_adapters.first().cloned(); | |
| 481 | if self.debugger.is_none() { | 486 | } |
| 482 | self.debugger = available_adapters.first().cloned(); | ||
| 483 | } | 487 | } |
| 484 | 488 | ||
| 485 | let label = self | 489 | let label = self |
| @@ -831,7 +835,7 @@ impl Render for NewProcessModal { · 345 unmodified lines | |||
| 831 | .disabled( | 835 | .disabled( |
| 832 | self.debugger.is_none() | 836 | self.debugger.is_none() |
| 833 | || self | 837 | || self |
| 834 | .launch_mode | 838 | .configure_mode |
| 835 | .read(cx) | 839 | .read(cx) |
| 836 | .program | 840 | .program |
| 837 | .read(cx) | 841 | .read(cx) |
| @@ -1143,41 +1147,67 @@ impl DebugDelegate { · 305 unmodified lines | |||
| 1143 | current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>, | 1147 | current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>, |
| 1144 | add_current_language_tasks: bool, | 1148 | add_current_language_tasks: bool, |
| 1145 | cx: &mut Context<Picker<Self>>, | 1149 | cx: &mut Context<Picker<Self>>, |
| 1146 | ) { | 1150 | ) -> Task<()> { |
| 1147 | self.task_contexts = Some(task_contexts.clone()); | 1151 | self.task_contexts = Some(task_contexts.clone()); |
| 1148 | 1152 | let task = self.task_store.update(cx, |task_store, cx| { | |
| 1149 | let (recent, scenarios) = self | 1153 | task_store.task_inventory().map(|inventory| { |
| 1150 | .task_store | 1154 | inventory.update(cx, |inventory, cx| { |
| 1151 | .update(cx, |task_store, cx| { | 1155 | inventory.list_debug_scenarios( |
| 1152 | task_store.task_inventory().map(|inventory| { | 1156 | &task_contexts, |
| 1153 | inventory.update(cx, |inventory, cx| { | 1157 | lsp_tasks, |
| 1154 | inventory.list_debug_scenarios( | 1158 | current_resolved_tasks, |
| 1155 | &task_contexts, | 1159 | add_current_language_tasks, |
| 1156 | lsp_tasks, | 1160 | cx, |
| 1157 | current_resolved_tasks, | 1161 | ) |
| 1158 | add_current_language_tasks, | ||
| 1159 | cx, | ||
| 1160 | ) | ||
| 1161 | }) | ||
| 1162 | }) | 1162 | }) |
| 1163 | }) | 1163 | }) |
| 1164 | .unwrap_or_default(); | 1164 | }); |
| 1165 | cx.spawn(async move |this, cx| { | ||
| 1166 | let (recent, scenarios) = if let Some(task) = task { | ||
| 1167 | task.await | ||
| 1168 | } else { | ||
| 1169 | (Vec::new(), Vec::new()) | ||
| 1170 | }; | ||
| 1165 | 1171 | ||
| 1166 | if !recent.is_empty() { | 1172 | this.update(cx, |this, cx| { |
| 1167 | self.last_used_candidate_index = Some(recent.len() - 1); | 1173 | if !recent.is_empty() { |
| 1168 | } | 1174 | this.delegate.last_used_candidate_index = Some(recent.len() - 1); |
| 1175 | } | ||
| 1169 | 1176 | ||
| 1170 | let dap_registry = cx.global::<DapRegistry>(); | 1177 | let dap_registry = cx.global::<DapRegistry>(); |
| 1178 | let hide_vscode = scenarios.iter().any(|(kind, _)| match kind { | ||
| 1179 | TaskSourceKind::Worktree { | ||
| 1180 | id: _, | ||
| 1181 | directory_in_worktree: dir, | ||
| 1182 | id_base: _, | ||
| 1183 | } => dir.ends_with(".zed"), | ||
| 1184 | _ => false, | ||
| 1185 | }); | ||
| 1171 | 1186 | ||
| 1172 | self.candidates = recent | 1187 | this.delegate.candidates = recent |
| 1173 | .into_iter() | 1188 | .into_iter() |
| 1174 | .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario)) | 1189 | .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario)) |
| 1175 | .chain(scenarios.into_iter().map(|(kind, scenario)| { | 1190 | .chain( |
| 1176 | let (language, scenario) = | 1191 | scenarios |
| 1177 | Self::get_scenario_kind(&languages, &dap_registry, scenario); | 1192 | .into_iter() |
| 1178 | (language.or(Some(kind)), scenario) | 1193 | .filter(|(kind, _)| match kind { |
| 1179 | })) | 1194 | TaskSourceKind::Worktree { |
| 1180 | .collect(); | 1195 | id: _, |
| 1196 | directory_in_worktree: dir, | ||
| 1197 | id_base: _, | ||
| 1198 | } => !(hide_vscode && dir.ends_with(".vscode")), | ||
| 1199 | _ => true, | ||
| 1200 | }) | ||
| 1201 | .map(|(kind, scenario)| { | ||
| 1202 | let (language, scenario) = | ||
| 1203 | Self::get_scenario_kind(&languages, &dap_registry, scenario); | ||
| 1204 | (language.or(Some(kind)), scenario) | ||
| 1205 | }), | ||
| 1206 | ) | ||
| 1207 | .collect(); | ||
| 1208 | }) | ||
| 1209 | .ok(); | ||
| 1210 | }) | ||
| 1181 | } | 1211 | } |
| 1182 | } | 1212 | } |
| 1183 | 1213 | ||
| @@ -1202,7 +1232,7 @@ impl PickerDelegate for DebugDelegate { · 18 unmodified lines | |||
| 1202 | } | 1232 | } |
| 1203 | 1233 | ||
| 1204 | fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> { | 1234 | fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> { |
| 1205 | "".into() | 1235 | "Find a debug task, or debug a command.".into() |
| 1206 | } | 1236 | } |
| 1207 | 1237 | ||
| 1208 | fn update_matches( | 1238 | fn update_matches( |
| @@ -1265,6 +1295,116 @@ impl PickerDelegate for DebugDelegate { · 56 unmodified lines | |||
| 1265 | } | 1295 | } |
| 1266 | } | 1296 | } |
| 1267 | 1297 | ||
| 1298 | fn confirm_input( | ||
| 1299 | &mut self, | ||
| 1300 | _secondary: bool, | ||
| 1301 | window: &mut Window, | ||
| 1302 | cx: &mut Context<Picker<Self>>, | ||
| 1303 | ) { | ||
| 1304 | let text = self.prompt.clone(); | ||
| 1305 | let (task_context, worktree_id) = self | ||
| 1306 | .task_contexts | ||
| 1307 | .as_ref() | ||
| 1308 | .and_then(|task_contexts| { | ||
| 1309 | Some(( | ||
| 1310 | task_contexts.active_context().cloned()?, | ||
| 1311 | task_contexts.worktree(), | ||
| 1312 | )) | ||
| 1313 | }) | ||
| 1314 | .unwrap_or_default(); | ||
| 1315 | |||
| 1316 | let mut args = shlex::split(&text).into_iter().flatten().peekable(); | ||
| 1317 | let mut env = HashMap::default(); | ||
| 1318 | while args.peek().is_some_and(|arg| arg.contains('=')) { | ||
| 1319 | let arg = args.next().unwrap(); | ||
| 1320 | let (lhs, rhs) = arg.split_once('=').unwrap(); | ||
| 1321 | env.insert(lhs.to_string(), rhs.to_string()); | ||
| 1322 | } | ||
| 1323 | |||
| 1324 | let program = if let Some(program) = args.next() { | ||
| 1325 | program | ||
| 1326 | } else { | ||
| 1327 | env = HashMap::default(); | ||
| 1328 | text | ||
| 1329 | }; | ||
| 1330 | |||
| 1331 | let args = args.collect::<Vec<_>>(); | ||
| 1332 | let task = task::TaskTemplate { | ||
| 1333 | label: "one-off".to_owned(), | ||
| 1334 | env, | ||
| 1335 | command: program, | ||
| 1336 | args, | ||
| 1337 | ..Default::default() | ||
| 1338 | }; | ||
| 1339 | |||
| 1340 | let Some(location) = self | ||
| 1341 | .task_contexts | ||
| 1342 | .as_ref() | ||
| 1343 | .and_then(|cx| cx.location().cloned()) | ||
| 1344 | else { | ||
| 1345 | return; | ||
| 1346 | }; | ||
| 1347 | let file = location.buffer.read(cx).file(); | ||
| 1348 | let language = location.buffer.read(cx).language(); | ||
| 1349 | let language_name = language.as_ref().map(|l| l.name()); | ||
| 1350 | let Some(adapter): Option<DebugAdapterName> = | ||
| 1351 | language::language_settings::language_settings(language_name, file, cx) | ||
| 1352 | .debuggers | ||
| 1353 | .first() | ||
| 1354 | .map(SharedString::from) | ||
| 1355 | .map(Into::into) | ||
| 1356 | .or_else(|| { | ||
| 1357 | language.and_then(|l| { | ||
| 1358 | l.config() | ||
| 1359 | .debuggers | ||
| 1360 | .first() | ||
| 1361 | .map(SharedString::from) | ||
| 1362 | .map(Into::into) | ||
| 1363 | }) | ||
| 1364 | }) | ||
| 1365 | else { | ||
| 1366 | return; | ||
| 1367 | }; | ||
| 1368 | let locators = cx.global::<DapRegistry>().locators(); | ||
| 1369 | cx.spawn_in(window, async move |this, cx| { | ||
| 1370 | let Some(debug_scenario) = cx | ||
| 1371 | .background_spawn(async move { | ||
| 1372 | for locator in locators { | ||
| 1373 | if let Some(scenario) = | ||
| 1374 | locator.1.create_scenario(&task, "one-off", &adapter).await | ||
| 1375 | { | ||
| 1376 | return Some(scenario); | ||
| 1377 | } | ||
| 1378 | } | ||
| 1379 | None | ||
| 1380 | }) | ||
| 1381 | .await | ||
| 1382 | else { | ||
| 1383 | return; | ||
| 1384 | }; | ||
| 1385 | |||
| 1386 | this.update_in(cx, |this, window, cx| { | ||
| 1387 | send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx); | ||
| 1388 | this.delegate | ||
| 1389 | .debug_panel | ||
| 1390 | .update(cx, |panel, cx| { | ||
| 1391 | panel.start_session( | ||
| 1392 | debug_scenario, | ||
| 1393 | task_context, | ||
| 1394 | None, | ||
| 1395 | worktree_id, | ||
| 1396 | window, | ||
| 1397 | cx, | ||
| 1398 | ); | ||
| 1399 | }) | ||
| 1400 | .ok(); | ||
| 1401 | cx.emit(DismissEvent); | ||
| 1402 | }) | ||
| 1403 | .ok(); | ||
| 1404 | }) | ||
| 1405 | .detach(); | ||
| 1406 | } | ||
| 1407 | |||
| 1268 | fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) { | 1408 | fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) { |
| 1269 | let debug_scenario = self | 1409 | let debug_scenario = self |
| 1270 | .matches | 1410 | .matches |
| @@ -1300,6 +1440,60 @@ impl PickerDelegate for DebugDelegate { · 29 unmodified lines | |||
| 1300 | cx.emit(DismissEvent); | 1440 | cx.emit(DismissEvent); |
| 1301 | } | 1441 | } |
| 1302 | 1442 | ||
| 1443 | fn render_footer( | ||
| 1444 | &self, | ||
| 1445 | window: &mut Window, | ||
| 1446 | cx: &mut Context<Picker<Self>>, | ||
| 1447 | ) -> Option<ui::AnyElement> { | ||
| 1448 | let current_modifiers = window.modifiers(); | ||
| 1449 | let footer = h_flex() | ||
| 1450 | .w_full() | ||
| 1451 | .h_8() | ||
| 1452 | .p_2() | ||
| 1453 | .justify_between() | ||
| 1454 | .rounded_b_sm() | ||
| 1455 | .bg(cx.theme().colors().ghost_element_selected) | ||
| 1456 | .border_t_1() | ||
| 1457 | .border_color(cx.theme().colors().border_variant) | ||
| 1458 | .child( | ||
| 1459 | // TODO: add button to open selected task in debug.json | ||
| 1460 | h_flex().into_any_element(), | ||
| 1461 | ) | ||
| 1462 | .map(|this| { | ||
| 1463 | if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() { | ||
| 1464 | let action = picker::ConfirmInput { | ||
| 1465 | secondary: current_modifiers.secondary(), | ||
| 1466 | } | ||
| 1467 | .boxed_clone(); | ||
| 1468 | this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| { | ||
| 1469 | Button::new("launch-custom", "Launch Custom") | ||
| 1470 | .label_size(LabelSize::Small) | ||
| 1471 | .key_binding(keybind) | ||
| 1472 | .on_click(move |_, window, cx| { | ||
| 1473 | window.dispatch_action(action.boxed_clone(), cx) | ||
| 1474 | }) | ||
| 1475 | })) | ||
| 1476 | } else { | ||
| 1477 | this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map( | ||
| 1478 | |keybind| { | ||
| 1479 | let is_recent_selected = | ||
| 1480 | self.divider_index >= Some(self.selected_index); | ||
| 1481 | let run_entry_label = | ||
| 1482 | if is_recent_selected { "Rerun" } else { "Spawn" }; | ||
| 1483 | |||
| 1484 | Button::new("spawn", run_entry_label) | ||
| 1485 | .label_size(LabelSize::Small) | ||
| 1486 | .key_binding(keybind) | ||
| 1487 | .on_click(|_, window, cx| { | ||
| 1488 | window.dispatch_action(menu::Confirm.boxed_clone(), cx); | ||
| 1489 | }) | ||
| 1490 | }, | ||
| 1491 | )) | ||
| 1492 | } | ||
| 1493 | }); | ||
| 1494 | Some(footer.into_any_element()) | ||
| 1495 | } | ||
| 1496 | |||
| 1303 | fn render_match( | 1497 | fn render_match( |
| 1304 | &self, | 1498 | &self, |
| 1305 | ix: usize, | 1499 | ix: usize, |
| @@ -1369,40 +1563,3 @@ pub(crate) fn resolve_path(path: &mut String) { · 63 unmodified lines | |||
| 1369 | ); | 1563 | ); |
| 1370 | }; | 1564 | }; |
| 1371 | } | 1565 | } |
| 1372 | |||
| 1373 | #[cfg(test)] | ||
| 1374 | impl NewProcessModal { | ||
| 1375 | // #[cfg(test)] | ||
| 1376 | // pub(crate) fn set_configure( | ||
| 1377 | // &mut self, | ||
| 1378 | // program: impl AsRef<str>, | ||
| 1379 | // cwd: impl AsRef<str>, | ||
| 1380 | // stop_on_entry: bool, | ||
| 1381 | // window: &mut Window, | ||
| 1382 | // cx: &mut Context<Self>, | ||
| 1383 | // ) { | ||
| 1384 | // self.mode = NewProcessMode::Launch; | ||
| 1385 | // self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into())); | ||
| 1386 | |||
| 1387 | // self.launch_mode.update(cx, |configure, cx| { | ||
| 1388 | // configure.program.update(cx, |editor, cx| { | ||
| 1389 | // editor.clear(window, cx); | ||
| 1390 | // editor.set_text(program.as_ref(), window, cx); | ||
| 1391 | // }); | ||
| 1392 | |||
| 1393 | // configure.cwd.update(cx, |editor, cx| { | ||
| 1394 | // editor.clear(window, cx); | ||
| 1395 | // editor.set_text(cwd.as_ref(), window, cx); | ||
| 1396 | // }); | ||
| 1397 | |||
| 1398 | // configure.stop_on_entry = match stop_on_entry { | ||
| 1399 | // true => ToggleState::Selected, | ||
| 1400 | // _ => ToggleState::Unselected, | ||
| 1401 | // } | ||
| 1402 | // }) | ||
| 1403 | // } | ||
| 1404 | |||
| 1405 | // pub(crate) fn save_scenario(&mut self, _window: &mut Window, _cx: &mut Context<Self>) { | ||
| 1406 | // self.save_debug_scenario(window, cx); | ||
| 1407 | // } | ||
| 1408 | } | ||
| @@ -1,6 +1,6 @@ | |||
| 1 | pub mod running; | 1 | pub mod running; |
| 2 | 2 | ||
| 3 | use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout}; | 3 | use crate::{StackTraceView, persistence::SerializedLayout, session::running::DebugTerminal}; |
| 4 | use dap::client::SessionId; | 4 | use dap::client::SessionId; |
| 5 | use gpui::{ | 5 | use gpui::{ |
| 6 | App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, | 6 | App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, |
| @@ -22,7 +22,6 @@ pub struct DebugSession { · 15 unmodified lines | |||
| 22 | running_state: Entity<RunningState>, | 22 | running_state: Entity<RunningState>, |
| 23 | label: OnceLock<SharedString>, | 23 | label: OnceLock<SharedString>, |
| 24 | stack_trace_view: OnceCell<Entity<StackTraceView>>, | 24 | stack_trace_view: OnceCell<Entity<StackTraceView>>, |
| 25 | _debug_panel: WeakEntity<DebugPanel>, | ||
| 26 | _worktree_store: WeakEntity<WorktreeStore>, | 25 | _worktree_store: WeakEntity<WorktreeStore>, |
| 27 | workspace: WeakEntity<Workspace>, | 26 | workspace: WeakEntity<Workspace>, |
| 28 | _subscriptions: [Subscription; 1], | 27 | _subscriptions: [Subscription; 1], |
| @@ -38,8 +37,8 @@ impl DebugSession { · 9 unmodified lines | |||
| 38 | pub(crate) fn running( | 37 | pub(crate) fn running( |
| 39 | project: Entity<Project>, | 38 | project: Entity<Project>, |
| 40 | workspace: WeakEntity<Workspace>, | 39 | workspace: WeakEntity<Workspace>, |
| 40 | parent_terminal: Option<Entity<DebugTerminal>>, | ||
| 41 | session: Entity<Session>, | 41 | session: Entity<Session>, |
| 42 | _debug_panel: WeakEntity<DebugPanel>, | ||
| 43 | serialized_layout: Option<SerializedLayout>, | 42 | serialized_layout: Option<SerializedLayout>, |
| 44 | dock_axis: Axis, | 43 | dock_axis: Axis, |
| 45 | window: &mut Window, | 44 | window: &mut Window, |
| @@ -50,6 +49,7 @@ impl DebugSession { · 4 unmodified lines | |||
| 50 | session.clone(), | 49 | session.clone(), |
| 51 | project.clone(), | 50 | project.clone(), |
| 52 | workspace.clone(), | 51 | workspace.clone(), |
| 52 | parent_terminal, | ||
| 53 | serialized_layout, | 53 | serialized_layout, |
| 54 | dock_axis, | 54 | dock_axis, |
| 55 | window, | 55 | window, |
| @@ -64,7 +64,6 @@ impl DebugSession { · 8 unmodified lines | |||
| 64 | remote_id: None, | 64 | remote_id: None, |
| 65 | running_state, | 65 | running_state, |
| 66 | label: OnceLock::new(), | 66 | label: OnceLock::new(), |
| 67 | _debug_panel, | ||
| 68 | stack_trace_view: OnceCell::new(), | 67 | stack_trace_view: OnceCell::new(), |
| 69 | _worktree_store: project.read(cx).worktree_store().downgrade(), | 68 | _worktree_store: project.read(cx).worktree_store().downgrade(), |
| 70 | workspace, | 69 | workspace, |
| @@ -605,6 +605,7 @@ impl RunningState { · 604 unmodified lines | |||
| 605 | session: Entity<Session>, | 605 | session: Entity<Session>, |
| 606 | project: Entity<Project>, | 606 | project: Entity<Project>, |
| 607 | workspace: WeakEntity<Workspace>, | 607 | workspace: WeakEntity<Workspace>, |
| 608 | parent_terminal: Option<Entity<DebugTerminal>>, | ||
| 608 | serialized_pane_layout: Option<SerializedLayout>, | 609 | serialized_pane_layout: Option<SerializedLayout>, |
| 609 | dock_axis: Axis, | 610 | dock_axis: Axis, |
| 610 | window: &mut Window, | 611 | window: &mut Window, |
| @@ -617,7 +618,8 @@ impl RunningState { · 6 unmodified lines | |||
| 617 | StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx) | 618 | StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx) |
| 618 | }); | 619 | }); |
| 619 | 620 | ||
| 620 | let debug_terminal = cx.new(|cx| DebugTerminal::empty(window, cx)); | 621 | let debug_terminal = |
| 622 | parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx))); | ||
| 621 | 623 | ||
| 622 | let variable_list = | 624 | let variable_list = |
| 623 | cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx)); | 625 | cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx)); |
| @@ -816,20 +818,20 @@ impl RunningState { · 192 unmodified lines | |||
| 816 | 818 | ||
| 817 | let request_type = dap_registry | 819 | let request_type = dap_registry |
| 818 | .adapter(&adapter) | 820 | .adapter(&adapter) |
| 819 | .ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter)) | 821 | .with_context(|| format!("{}: is not a valid adapter name", &adapter)) |
| 820 | .and_then(|adapter| adapter.request_kind(&config)); | 822 | .and_then(|adapter| adapter.request_kind(&config)); |
| 821 | 823 | ||
| 822 | let config_is_valid = request_type.is_ok(); | 824 | let config_is_valid = request_type.is_ok(); |
| 823 | 825 | ||
| 824 | let build_output = if let Some(build) = build { | 826 | let build_output = if let Some(build) = build { |
| 825 | let (task, locator_name) = match build { | 827 | let (task_template, locator_name) = match build { |
| 826 | BuildTaskDefinition::Template { | 828 | BuildTaskDefinition::Template { |
| 827 | task_template, | 829 | task_template, |
| 828 | locator_name, | 830 | locator_name, |
| 829 | } => (task_template, locator_name), | 831 | } => (task_template, locator_name), |
| 830 | BuildTaskDefinition::ByName(ref label) => { | 832 | BuildTaskDefinition::ByName(ref label) => { |
| 831 | let Some(task) = task_store.update(cx, |this, cx| { | 833 | let task = task_store.update(cx, |this, cx| { |
| 832 | this.task_inventory().and_then(|inventory| { | 834 | this.task_inventory().map(|inventory| { |
| 833 | inventory.read(cx).task_template_by_label( | 835 | inventory.read(cx).task_template_by_label( |
| 834 | buffer, | 836 | buffer, |
| 835 | worktree_id, | 837 | worktree_id, |
| @@ -837,14 +839,15 @@ impl RunningState { · 1 unmodified line | |||
| 837 | cx, | 839 | cx, |
| 838 | ) | 840 | ) |
| 839 | }) | 841 | }) |
| 840 | })? | 842 | })?; |
| 841 | else { | 843 | let task = match task { |
| 842 | anyhow::bail!("Couldn't find task template for {:?}", build) | 844 | Some(task) => task.await, |
| 843 | }; | 845 | None => None, |
| 846 | }.with_context(|| format!("Couldn't find task template for {build:?}"))?; | ||
| 844 | (task, None) | 847 | (task, None) |
| 845 | } | 848 | } |
| 846 | }; | 849 | }; |
| 847 | let Some(task) = task.resolve_task("debug-build-task", &task_context) else { | 850 | let Some(task) = task_template.resolve_task("debug-build-task", &task_context) else { |
| 848 | anyhow::bail!("Could not resolve task variables within a debug scenario"); | 851 | anyhow::bail!("Could not resolve task variables within a debug scenario"); |
| 849 | }; | 852 | }; |
| 850 | 853 | ||
| @@ -852,7 +855,7 @@ impl RunningState { · 1 unmodified line | |||
| 852 | debug_assert!(!config_is_valid); | 855 | debug_assert!(!config_is_valid); |
| 853 | Some(locator_name) | 856 | Some(locator_name) |
| 854 | } else if !config_is_valid { | 857 | } else if !config_is_valid { |
| 855 | dap_store | 858 | let task = dap_store |
| 856 | .update(cx, |this, cx| { | 859 | .update(cx, |this, cx| { |
| 857 | this.debug_scenario_for_build_task( | 860 | this.debug_scenario_for_build_task( |
| 858 | task.original_task().clone(), | 861 | task.original_task().clone(), |
| @@ -860,17 +863,21 @@ impl RunningState { · 1 unmodified line | |||
| 860 | task.display_label().to_owned().into(), | 863 | task.display_label().to_owned().into(), |
| 861 | cx, | 864 | cx, |
| 862 | ) | 865 | ) |
| 863 | .and_then(|scenario| { | 866 | |
| 864 | match scenario.build { | 867 | }); |
| 865 | Some(BuildTaskDefinition::Template { | 868 | if let Ok(t) = task { |
| 866 | locator_name, .. | 869 | t.await.and_then(|scenario| { |
| 867 | }) => locator_name, | 870 | match scenario.build { |
| 868 | _ => None, | 871 | Some(BuildTaskDefinition::Template { |
| 869 | } | 872 | locator_name, .. |
| 870 | }) | 873 | }) => locator_name, |
| 874 | _ => None, | ||
| 875 | } | ||
| 871 | }) | 876 | }) |
| 872 | .ok() | 877 | } else { |
| 873 | .flatten() | 878 | None |
| 879 | } | ||
| 880 | |||
| 874 | } else { | 881 | } else { |
| 875 | None | 882 | None |
| 876 | }; | 883 | }; |
| @@ -929,15 +936,13 @@ impl RunningState { · 52 unmodified lines | |||
| 929 | }; | 936 | }; |
| 930 | 937 | ||
| 931 | if config_is_valid { | 938 | if config_is_valid { |
| 932 | // Ok(DebugTaskDefinition { | ||
| 933 | // label, | ||
| 934 | // adapter: DebugAdapterName(adapter), | ||
| 935 | // config, | ||
| 936 | // tcp_connection, | ||
| 937 | // }) | ||
| 938 | } else if let Some((task, locator_name)) = build_output { | 939 | } else if let Some((task, locator_name)) = build_output { |
| 939 | let locator_name = | 940 | let locator_name = |
| 940 | locator_name.context("Could not find a valid locator for a build task")?; | 941 | locator_name.with_context(|| { |
| 942 | format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err() | ||
| 943 | .map(|err| err.to_string()) | ||
| 944 | .unwrap_or_default()) | ||
| 945 | })?; | ||
| 941 | let request = dap_store | 946 | let request = dap_store |
| 942 | .update(cx, |this, cx| { | 947 | .update(cx, |this, cx| { |
| 943 | this.run_debug_locator(&locator_name, task, cx) | 948 | this.run_debug_locator(&locator_name, task, cx) |
| @@ -953,7 +958,7 @@ impl RunningState { · 9 unmodified lines | |||
| 953 | 958 | ||
| 954 | let scenario = dap_registry | 959 | let scenario = dap_registry |
| 955 | .adapter(&adapter) | 960 | .adapter(&adapter) |
| 956 | .ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter)) | 961 | .with_context(|| anyhow!("{}: is not a valid adapter name", &adapter)) |
| 957 | .map(|adapter| adapter.config_from_zed_format(zed_config))??; | 962 | .map(|adapter| adapter.config_from_zed_format(zed_config))??; |
| 958 | config = scenario.config; | 963 | config = scenario.config; |
| 959 | Self::substitute_variables_in_config(&mut config, &task_context); | 964 | Self::substitute_variables_in_config(&mut config, &task_context); |
| @@ -992,7 +997,7 @@ impl RunningState { · 32 unmodified lines | |||
| 992 | let cwd = Some(&request.cwd) | 997 | let cwd = Some(&request.cwd) |
| 993 | .filter(|cwd| cwd.len() > 0) | 998 | .filter(|cwd| cwd.len() > 0) |
| 994 | .map(PathBuf::from) | 999 | .map(PathBuf::from) |
| 995 | .or_else(|| session.binary().cwd.clone()); | 1000 | .or_else(|| session.binary().unwrap().cwd.clone()); |
| 996 | 1001 | ||
| 997 | let mut args = request.args.clone(); | 1002 | let mut args = request.args.clone(); |
| 998 | 1003 | ||
| @@ -1,4 +1,5 @@ | |||
| 1 | use std::{ | 1 | use std::{ |
| 2 | ops::Range, | ||
| 2 | path::{Path, PathBuf}, | 3 | path::{Path, PathBuf}, |
| 3 | sync::Arc, | 4 | sync::Arc, |
| 4 | time::Duration, | 5 | time::Duration, |
| @@ -277,10 +278,9 @@ impl BreakpointList { · 272 unmodified lines | |||
| 277 | let selected_ix = self.selected_ix; | 278 | let selected_ix = self.selected_ix; |
| 278 | let focus_handle = self.focus_handle.clone(); | 279 | let focus_handle = self.focus_handle.clone(); |
| 279 | uniform_list( | 280 | uniform_list( |
| 280 | cx.entity(), | ||
| 281 | "breakpoint-list", | 281 | "breakpoint-list", |
| 282 | self.breakpoints.len(), | 282 | self.breakpoints.len(), |
| 283 | move |this, range, window, cx| { | 283 | cx.processor(move |this, range: Range<usize>, window, cx| { |
| 284 | range | 284 | range |
| 285 | .clone() | 285 | .clone() |
| 286 | .zip(&mut this.breakpoints[range]) | 286 | .zip(&mut this.breakpoints[range]) |
| @@ -291,7 +291,7 @@ impl BreakpointList { · 4 unmodified lines | |||
| 291 | .into_any_element() | 291 | .into_any_element() |
| 292 | }) | 292 | }) |
| 293 | .collect() | 293 | .collect() |
| 294 | }, | 294 | }), |
| 295 | ) | 295 | ) |
| 296 | .track_scroll(self.scroll_handle.clone()) | 296 | .track_scroll(self.scroll_handle.clone()) |
| 297 | .flex_grow() | 297 | .flex_grow() |
| @@ -8,7 +8,7 @@ use project::{ · 7 unmodified lines | |||
| 8 | ProjectItem as _, ProjectPath, | 8 | ProjectItem as _, ProjectPath, |
| 9 | debugger::session::{Session, SessionEvent}, | 9 | debugger::session::{Session, SessionEvent}, |
| 10 | }; | 10 | }; |
| 11 | use std::{path::Path, sync::Arc}; | 11 | use std::{ops::Range, path::Path, sync::Arc}; |
| 12 | use ui::{Scrollbar, ScrollbarState, prelude::*}; | 12 | use ui::{Scrollbar, ScrollbarState, prelude::*}; |
| 13 | use workspace::Workspace; | 13 | use workspace::Workspace; |
| 14 | 14 | ||
| @@ -20,7 +20,7 @@ pub struct ModuleList { · 5 unmodified lines | |||
| 20 | focus_handle: FocusHandle, | 20 | focus_handle: FocusHandle, |
| 21 | scrollbar_state: ScrollbarState, | 21 | scrollbar_state: ScrollbarState, |
| 22 | entries: Vec<Module>, | 22 | entries: Vec<Module>, |
| 23 | _rebuild_task: Task<()>, | 23 | _rebuild_task: Option<Task<()>>, |
| 24 | _subscription: Subscription, | 24 | _subscription: Subscription, |
| 25 | } | 25 | } |
| 26 | 26 | ||
| @@ -34,14 +34,16 @@ impl ModuleList { · 7 unmodified lines | |||
| 34 | 34 | ||
| 35 | let _subscription = cx.subscribe(&session, |this, _, event, cx| match event { | 35 | let _subscription = cx.subscribe(&session, |this, _, event, cx| match event { |
| 36 | SessionEvent::Stopped(_) | SessionEvent::Modules => { | 36 | SessionEvent::Stopped(_) | SessionEvent::Modules => { |
| 37 | this.schedule_rebuild(cx); | 37 | if this._rebuild_task.is_some() { |
| 38 | this.schedule_rebuild(cx); | ||
| 39 | } | ||
| 38 | } | 40 | } |
| 39 | _ => {} | 41 | _ => {} |
| 40 | }); | 42 | }); |
| 41 | 43 | ||
| 42 | let scroll_handle = UniformListScrollHandle::new(); | 44 | let scroll_handle = UniformListScrollHandle::new(); |
| 43 | 45 | ||
| 44 | let mut this = Self { | 46 | Self { |
| 45 | scrollbar_state: ScrollbarState::new(scroll_handle.clone()), | 47 | scrollbar_state: ScrollbarState::new(scroll_handle.clone()), |
| 46 | scroll_handle, | 48 | scroll_handle, |
| 47 | session, | 49 | session, |
| @@ -50,14 +52,12 @@ impl ModuleList { · 2 unmodified lines | |||
| 50 | entries: Vec::new(), | 52 | entries: Vec::new(), |
| 51 | selected_ix: None, | 53 | selected_ix: None, |
| 52 | _subscription, | 54 | _subscription, |
| 53 | _rebuild_task: Task::ready(()), | 55 | _rebuild_task: None, |
| 54 | }; | 56 | } |
| 55 | this.schedule_rebuild(cx); | ||
| 56 | this | ||
| 57 | } | 57 | } |
| 58 | 58 | ||
| 59 | fn schedule_rebuild(&mut self, cx: &mut Context<Self>) { | 59 | fn schedule_rebuild(&mut self, cx: &mut Context<Self>) { |
| 60 | self._rebuild_task = cx.spawn(async move |this, cx| { | 60 | self._rebuild_task = Some(cx.spawn(async move |this, cx| { |
| 61 | this.update(cx, |this, cx| { | 61 | this.update(cx, |this, cx| { |
| 62 | let modules = this | 62 | let modules = this |
| 63 | .session | 63 | .session |
| @@ -66,7 +66,7 @@ impl ModuleList { · 2 unmodified lines | |||
| 66 | cx.notify(); | 66 | cx.notify(); |
| 67 | }) | 67 | }) |
| 68 | .ok(); | 68 | .ok(); |
| 69 | }); | 69 | })); |
| 70 | } | 70 | } |
| 71 | 71 | ||
| 72 | fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) { | 72 | fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) { |
| @@ -281,10 +281,11 @@ impl ModuleList { · 208 unmodified lines | |||
| 281 | 281 | ||
| 282 | fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | 282 | fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| 283 | uniform_list( | 283 | uniform_list( |
| 284 | cx.entity(), | ||
| 285 | "module-list", | 284 | "module-list", |
| 286 | self.entries.len(), | 285 | self.entries.len(), |
| 287 | |this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(), | 286 | cx.processor(|this, range: Range<usize>, _window, cx| { |
| 287 | range.map(|ix| this.render_entry(ix, cx)).collect() | ||
| 288 | }), | ||
| 288 | ) | 289 | ) |
| 289 | .track_scroll(self.scroll_handle.clone()) | 290 | .track_scroll(self.scroll_handle.clone()) |
| 290 | .size_full() | 291 | .size_full() |
| @@ -299,6 +300,9 @@ impl Focusable for ModuleList { · 8 unmodified lines | |||
| 299 | 300 | ||
| 300 | impl Render for ModuleList { | 301 | impl Render for ModuleList { |
| 301 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | 302 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| 303 | if self._rebuild_task.is_none() { | ||
| 304 | self.schedule_rebuild(cx); | ||
| 305 | } | ||
| 302 | div() | 306 | div() |
| 303 | .track_focus(&self.focus_handle) | 307 | .track_focus(&self.focus_handle) |
| 304 | .on_action(cx.listener(Self::select_last)) | 308 | .on_action(cx.listener(Self::select_last)) |
| @@ -5,9 +5,10 @@ use std::time::Duration; · 4 unmodified lines | |||
| 5 | use anyhow::{Context as _, Result, anyhow}; | 5 | use anyhow::{Context as _, Result, anyhow}; |
| 6 | use dap::StackFrameId; | 6 | use dap::StackFrameId; |
| 7 | use gpui::{ | 7 | use gpui::{ |
| 8 | AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy, | 8 | AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton, |
| 9 | Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list, | 9 | Stateful, Subscription, Task, WeakEntity, list, |
| 10 | }; | 10 | }; |
| 11 | use util::debug_panic; | ||
| 11 | 12 | ||
| 12 | use crate::StackTraceView; | 13 | use crate::StackTraceView; |
| 13 | use language::PointUtf16; | 14 | use language::PointUtf16; |
| @@ -35,13 +36,16 @@ pub struct StackFrameList { · 21 unmodified lines | |||
| 35 | selected_ix: Option<usize>, | 36 | selected_ix: Option<usize>, |
| 36 | opened_stack_frame_id: Option<StackFrameId>, | 37 | opened_stack_frame_id: Option<StackFrameId>, |
| 37 | scrollbar_state: ScrollbarState, | 38 | scrollbar_state: ScrollbarState, |
| 38 | scroll_handle: UniformListScrollHandle, | 39 | list_state: ListState, |
| 40 | error: Option<SharedString>, | ||
| 39 | _refresh_task: Task<()>, | 41 | _refresh_task: Task<()>, |
| 40 | } | 42 | } |
| 41 | 43 | ||
| 42 | #[derive(Debug, PartialEq, Eq)] | 44 | #[derive(Debug, PartialEq, Eq)] |
| 43 | pub enum StackFrameEntry { | 45 | pub enum StackFrameEntry { |
| 44 | Normal(dap::StackFrame), | 46 | Normal(dap::StackFrame), |
| 47 | /// Used to indicate that the frame is artificial and is a visual label or separator | ||
| 48 | Label(dap::StackFrame), | ||
| 45 | Collapsed(Vec<dap::StackFrame>), | 49 | Collapsed(Vec<dap::StackFrame>), |
| 46 | } | 50 | } |
| 47 | 51 | ||
| @@ -54,7 +58,6 @@ impl StackFrameList { · 6 unmodified lines | |||
| 54 | cx: &mut Context<Self>, | 58 | cx: &mut Context<Self>, |
| 55 | ) -> Self { | 59 | ) -> Self { |
| 56 | let focus_handle = cx.focus_handle(); | 60 | let focus_handle = cx.focus_handle(); |
| 57 | let scroll_handle = UniformListScrollHandle::new(); | ||
| 58 | 61 | ||
| 59 | let _subscription = | 62 | let _subscription = |
| 60 | cx.subscribe_in(&session, window, |this, _, event, window, cx| match event { | 63 | cx.subscribe_in(&session, window, |this, _, event, window, cx| match event { |
| @@ -67,17 +70,27 @@ impl StackFrameList { · 6 unmodified lines | |||
| 67 | _ => {} | 70 | _ => {} |
| 68 | }); | 71 | }); |
| 69 | 72 | ||
| 73 | let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), { | ||
| 74 | let this = cx.weak_entity(); | ||
| 75 | move |ix, _window, cx| { | ||
| 76 | this.update(cx, |this, cx| this.render_entry(ix, cx)) | ||
| 77 | .unwrap_or(div().into_any()) | ||
| 78 | } | ||
| 79 | }); | ||
| 80 | let scrollbar_state = ScrollbarState::new(list_state.clone()); | ||
| 81 | |||
| 70 | let mut this = Self { | 82 | let mut this = Self { |
| 71 | scrollbar_state: ScrollbarState::new(scroll_handle.clone()), | ||
| 72 | session, | 83 | session, |
| 73 | workspace, | 84 | workspace, |
| 74 | focus_handle, | 85 | focus_handle, |
| 75 | state, | 86 | state, |
| 76 | _subscription, | 87 | _subscription, |
| 77 | entries: Default::default(), | 88 | entries: Default::default(), |
| 89 | error: None, | ||
| 78 | selected_ix: None, | 90 | selected_ix: None, |
| 79 | opened_stack_frame_id: None, | 91 | opened_stack_frame_id: None, |
| 80 | scroll_handle, | 92 | list_state, |
| 93 | scrollbar_state, | ||
| 81 | _refresh_task: Task::ready(()), | 94 | _refresh_task: Task::ready(()), |
| 82 | }; | 95 | }; |
| 83 | this.schedule_refresh(true, window, cx); | 96 | this.schedule_refresh(true, window, cx); |
| @@ -89,37 +102,35 @@ impl StackFrameList { · 5 unmodified lines | |||
| 89 | &self.entries | 102 | &self.entries |
| 90 | } | 103 | } |
| 91 | 104 | ||
| 92 | pub(crate) fn flatten_entries(&self, show_collapsed: bool) -> Vec<dap::StackFrame> { | 105 | pub(crate) fn flatten_entries( |
| 106 | &self, | ||
| 107 | show_collapsed: bool, | ||
| 108 | show_labels: bool, | ||
| 109 | ) -> Vec<dap::StackFrame> { | ||
| 93 | self.entries | 110 | self.entries |
| 94 | .iter() | 111 | .iter() |
| 95 | .flat_map(|frame| match frame { | 112 | .flat_map(|frame| match frame { |
| 96 | StackFrameEntry::Normal(frame) => vec![frame.clone()], | 113 | StackFrameEntry::Normal(frame) => vec![frame.clone()], |
| 97 | StackFrameEntry::Collapsed(frames) => { | 114 | StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()], |
| 98 | if show_collapsed { | 115 | StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(), |
| 99 | frames.clone() | 116 | _ => vec![], |
| 100 | } else { | ||
| 101 | vec![] | ||
| 102 | } | ||
| 103 | } | ||
| 104 | }) | 117 | }) |
| 105 | .collect::<Vec<_>>() | 118 | .collect::<Vec<_>>() |
| 106 | } | 119 | } |
| 107 | 120 | ||
| 108 | fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> { | 121 | fn stack_frames(&self, cx: &mut App) -> Result<Vec<StackFrame>> { |
| 109 | self.state | 122 | if let Ok(Some(thread_id)) = self.state.read_with(cx, |state, _| state.thread_id) { |
| 110 | .read_with(cx, |state, _| state.thread_id) | 123 | self.session |
| 111 | .ok() | 124 | .update(cx, |this, cx| this.stack_frames(thread_id, cx)) |
| 112 | .flatten() | 125 | } else { |
| 113 | .map(|thread_id| { | 126 | Ok(Vec::default()) |
| 114 | self.session | 127 | } |
| 115 | .update(cx, |this, cx| this.stack_frames(thread_id, cx)) | ||
| 116 | }) | ||
| 117 | .unwrap_or_default() | ||
| 118 | } | 128 | } |
| 119 | 129 | ||
| 120 | #[cfg(test)] | 130 | #[cfg(test)] |
| 121 | pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> { | 131 | pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> { |
| 122 | self.stack_frames(cx) | 132 | self.stack_frames(cx) |
| 133 | .unwrap_or_default() | ||
| 123 | .into_iter() | 134 | .into_iter() |
| 124 | .map(|stack_frame| stack_frame.dap.clone()) | 135 | .map(|stack_frame| stack_frame.dap.clone()) |
| 125 | .collect() | 136 | .collect() |
| @@ -141,7 +152,7 @@ impl StackFrameList { · 15 unmodified lines | |||
| 141 | let debounce = this | 152 | let debounce = this |
| 142 | .update(cx, |this, cx| { | 153 | .update(cx, |this, cx| { |
| 143 | let new_stack_frames = this.stack_frames(cx); | 154 | let new_stack_frames = this.stack_frames(cx); |
| 144 | new_stack_frames.is_empty() && !this.entries.is_empty() | 155 | new_stack_frames.unwrap_or_default().is_empty() && !this.entries.is_empty() |
| 145 | }) | 156 | }) |
| 146 | .ok() | 157 | .ok() |
| 147 | .unwrap_or_default(); | 158 | .unwrap_or_default(); |
| @@ -168,20 +179,34 @@ impl StackFrameList { · 20 unmodified lines | |||
| 168 | .and_then(|ix| self.entries.get(ix)) | 179 | .and_then(|ix| self.entries.get(ix)) |
| 169 | .and_then(|entry| match entry { | 180 | .and_then(|entry| match entry { |
| 170 | StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id), | 181 | StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id), |
| 171 | StackFrameEntry::Collapsed(stack_frames) => { | 182 | StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => None, |
| 172 | stack_frames.first().map(|stack_frame| stack_frame.id) | ||
| 173 | } | ||
| 174 | }); | 183 | }); |
| 175 | let mut entries = Vec::new(); | 184 | let mut entries = Vec::new(); |
| 176 | let mut collapsed_entries = Vec::new(); | 185 | let mut collapsed_entries = Vec::new(); |
| 177 | let mut first_stack_frame = None; | 186 | let mut first_stack_frame = None; |
| 178 | 187 | let mut first_stack_frame_with_path = None; | |
| 179 | let stack_frames = self.stack_frames(cx); | 188 | |
| 189 | let stack_frames = match self.stack_frames(cx) { | ||
| 190 | Ok(stack_frames) => stack_frames, | ||
| 191 | Err(e) => { | ||
| 192 | self.error = Some(format!("{}", e).into()); | ||
| 193 | self.entries.clear(); | ||
| 194 | self.selected_ix = None; | ||
| 195 | self.list_state.reset(0); | ||
| 196 | cx.emit(StackFrameListEvent::BuiltEntries); | ||
| 197 | cx.notify(); | ||
| 198 | return; | ||
| 199 | } | ||
| 200 | }; | ||
| 180 | for stack_frame in &stack_frames { | 201 | for stack_frame in &stack_frames { |
| 181 | match stack_frame.dap.presentation_hint { | 202 | match stack_frame.dap.presentation_hint { |
| 182 | Some(dap::StackFramePresentationHint::Deemphasize) => { | 203 | Some(dap::StackFramePresentationHint::Deemphasize) |
| 204 | | Some(dap::StackFramePresentationHint::Subtle) => { | ||
| 183 | collapsed_entries.push(stack_frame.dap.clone()); | 205 | collapsed_entries.push(stack_frame.dap.clone()); |
| 184 | } | 206 | } |
| 207 | Some(dap::StackFramePresentationHint::Label) => { | ||
| 208 | entries.push(StackFrameEntry::Label(stack_frame.dap.clone())); | ||
| 209 | } | ||
| 185 | _ => { | 210 | _ => { |
| 186 | let collapsed_entries = std::mem::take(&mut collapsed_entries); | 211 | let collapsed_entries = std::mem::take(&mut collapsed_entries); |
| 187 | if !collapsed_entries.is_empty() { | 212 | if !collapsed_entries.is_empty() { |
| @@ -189,6 +214,15 @@ impl StackFrameList { · 1 unmodified line | |||
| 189 | } | 214 | } |
| 190 | 215 | ||
| 191 | first_stack_frame.get_or_insert(entries.len()); | 216 | first_stack_frame.get_or_insert(entries.len()); |
| 217 | |||
| 218 | if stack_frame | ||
| 219 | .dap | ||
| 220 | .source | ||
| 221 | .as_ref() | ||
| 222 | .is_some_and(|source| source.path.is_some()) | ||
| 223 | { | ||
| 224 | first_stack_frame_with_path.get_or_insert(entries.len()); | ||
| 225 | } | ||
| 192 | entries.push(StackFrameEntry::Normal(stack_frame.dap.clone())); | 226 | entries.push(StackFrameEntry::Normal(stack_frame.dap.clone())); |
| 193 | } | 227 | } |
| 194 | } | 228 | } |
| @@ -198,22 +232,23 @@ impl StackFrameList { · 3 unmodified lines | |||
| 198 | if !collapsed_entries.is_empty() { | 232 | if !collapsed_entries.is_empty() { |
| 199 | entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); | 233 | entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); |
| 200 | } | 234 | } |
| 235 | self.entries = entries; | ||
| 201 | 236 | ||
| 202 | std::mem::swap(&mut self.entries, &mut entries); | 237 | if let Some(ix) = first_stack_frame_with_path |
| 203 | 238 | .or(first_stack_frame) | |
| 204 | if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) { | 239 | .filter(|_| open_first_stack_frame) |
| 240 | { | ||
| 205 | self.select_ix(Some(ix), cx); | 241 | self.select_ix(Some(ix), cx); |
| 206 | self.activate_selected_entry(window, cx); | 242 | self.activate_selected_entry(window, cx); |
| 207 | } else if let Some(old_selected_frame_id) = old_selected_frame_id { | 243 | } else if let Some(old_selected_frame_id) = old_selected_frame_id { |
| 208 | let ix = self.entries.iter().position(|entry| match entry { | 244 | let ix = self.entries.iter().position(|entry| match entry { |
| 209 | StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id, | 245 | StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id, |
| 210 | StackFrameEntry::Collapsed(frames) => { | 246 | StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => false, |
| 211 | frames.iter().any(|frame| frame.id == old_selected_frame_id) | ||
| 212 | } | ||
| 213 | }); | 247 | }); |
| 214 | self.selected_ix = ix; | 248 | self.selected_ix = ix; |
| 215 | } | 249 | } |
| 216 | 250 | ||
| 251 | self.list_state.reset(self.entries.len()); | ||
| 217 | cx.emit(StackFrameListEvent::BuiltEntries); | 252 | cx.emit(StackFrameListEvent::BuiltEntries); |
| 218 | cx.notify(); | 253 | cx.notify(); |
| 219 | } | 254 | } |
| @@ -228,6 +263,7 @@ impl StackFrameList { · 8 unmodified lines | |||
| 228 | .entries | 263 | .entries |
| 229 | .iter() | 264 | .iter() |
| 230 | .flat_map(|entry| match entry { | 265 | .flat_map(|entry| match entry { |
| 266 | StackFrameEntry::Label(stack_frame) => std::slice::from_ref(stack_frame), | ||
| 231 | StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame), | 267 | StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame), |
| 232 | StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(), | 268 | StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(), |
| 233 | }) | 269 | }) |
| @@ -340,6 +376,33 @@ impl StackFrameList { · 106 unmodified lines | |||
| 340 | }); | 376 | }); |
| 341 | } | 377 | } |
| 342 | 378 | ||
| 379 | fn render_label_entry( | ||
| 380 | &self, | ||
| 381 | stack_frame: &dap::StackFrame, | ||
| 382 | _cx: &mut Context<Self>, | ||
| 383 | ) -> AnyElement { | ||
| 384 | h_flex() | ||
| 385 | .rounded_md() | ||
| 386 | .justify_between() | ||
| 387 | .w_full() | ||
| 388 | .group("") | ||
| 389 | .id(("label-stack-frame", stack_frame.id)) | ||
| 390 | .p_1() | ||
| 391 | .on_any_mouse_down(|_, _, cx| { | ||
| 392 | cx.stop_propagation(); | ||
| 393 | }) | ||
| 394 | .child( | ||
| 395 | v_flex().justify_center().gap_0p5().child( | ||
| 396 | Label::new(stack_frame.name.clone()) | ||
| 397 | .size(LabelSize::Small) | ||
| 398 | .weight(FontWeight::BOLD) | ||
| 399 | .truncate() | ||
| 400 | .color(Color::Info), | ||
| 401 | ), | ||
| 402 | ) | ||
| 403 | .into_any() | ||
| 404 | } | ||
| 405 | |||
| 343 | fn render_normal_entry( | 406 | fn render_normal_entry( |
| 344 | &self, | 407 | &self, |
| 345 | ix: usize, | 408 | ix: usize, |
| @@ -443,7 +506,7 @@ impl StackFrameList { · 97 unmodified lines | |||
| 443 | .into_any() | 506 | .into_any() |
| 444 | } | 507 | } |
| 445 | 508 | ||
| 446 | pub(crate) fn expand_collapsed_entry(&mut self, ix: usize) { | 509 | pub(crate) fn expand_collapsed_entry(&mut self, ix: usize, cx: &mut Context<Self>) { |
| 447 | let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else { | 510 | let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else { |
| 448 | return; | 511 | return; |
| 449 | }; | 512 | }; |
| @@ -452,6 +515,9 @@ impl StackFrameList { · 2 unmodified lines | |||
| 452 | .map(StackFrameEntry::Normal); | 515 | .map(StackFrameEntry::Normal); |
| 453 | self.entries.splice(ix..ix + 1, entries); | 516 | self.entries.splice(ix..ix + 1, entries); |
| 454 | self.selected_ix = Some(ix); | 517 | self.selected_ix = Some(ix); |
| 518 | self.list_state.reset(self.entries.len()); | ||
| 519 | cx.emit(StackFrameListEvent::BuiltEntries); | ||
| 520 | cx.notify(); | ||
| 455 | } | 521 | } |
| 456 | 522 | ||
| 457 | fn render_collapsed_entry( | 523 | fn render_collapsed_entry( |
| @@ -501,6 +567,7 @@ impl StackFrameList { · 43 unmodified lines | |||
| 501 | 567 | ||
| 502 | fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement { | 568 | fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement { |
| 503 | match &self.entries[ix] { | 569 | match &self.entries[ix] { |
| 570 | StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx), | ||
| 504 | StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx), | 571 | StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx), |
| 505 | StackFrameEntry::Collapsed(stack_frames) => { | 572 | StackFrameEntry::Collapsed(stack_frames) => { |
| 506 | self.render_collapsed_entry(ix, stack_frames, cx) | 573 | self.render_collapsed_entry(ix, stack_frames, cx) |
| @@ -543,10 +610,6 @@ impl StackFrameList { · 36 unmodified lines | |||
| 543 | 610 | ||
| 544 | fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) { | 611 | fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) { |
| 545 | self.selected_ix = ix; | 612 | self.selected_ix = ix; |
| 546 | if let Some(ix) = self.selected_ix { | ||
| 547 | self.scroll_handle | ||
| 548 | .scroll_to_item(ix, ScrollStrategy::Center); | ||
| 549 | } | ||
| 550 | cx.notify(); | 613 | cx.notify(); |
| 551 | } | 614 | } |
| 552 | 615 | ||
| @@ -621,7 +684,10 @@ impl StackFrameList { · 68 unmodified lines | |||
| 621 | self.go_to_stack_frame_inner(stack_frame, window, cx) | 684 | self.go_to_stack_frame_inner(stack_frame, window, cx) |
| 622 | .detach_and_log_err(cx) | 685 | .detach_and_log_err(cx) |
| 623 | } | 686 | } |
| 624 | StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix), | 687 | StackFrameEntry::Label(_) => { |
| 688 | debug_panic!("You should not be able to select a label stack frame") | ||
| 689 | } | ||
| 690 | StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix, cx), | ||
| 625 | } | 691 | } |
| 626 | cx.notify(); | 692 | cx.notify(); |
| 627 | } | 693 | } |
| @@ -630,15 +696,11 @@ impl StackFrameList { · 2 unmodified lines | |||
| 630 | self.activate_selected_entry(window, cx); | 696 | self.activate_selected_entry(window, cx); |
| 631 | } | 697 | } |
| 632 | 698 | ||
| 633 | fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | 699 | fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { |
| 634 | uniform_list( | 700 | div() |
| 635 | cx.entity(), | 701 | .p_1() |
| 636 | "stack-frame-list", | 702 | .size_full() |
| 637 | self.entries.len(), | 703 | .child(list(self.list_state.clone()).size_full()) |
| 638 | |this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(), | ||
| 639 | ) | ||
| 640 | .track_scroll(self.scroll_handle.clone()) | ||
| 641 | .size_full() | ||
| 642 | } | 704 | } |
| 643 | } | 705 | } |
| 644 | 706 | ||
| @@ -647,12 +709,27 @@ impl Render for StackFrameList { · 2 unmodified lines | |||
| 647 | div() | 709 | div() |
| 648 | .track_focus(&self.focus_handle) | 710 | .track_focus(&self.focus_handle) |
| 649 | .size_full() | 711 | .size_full() |
| 650 | .p_1() | ||
| 651 | .on_action(cx.listener(Self::select_next)) | 712 | .on_action(cx.listener(Self::select_next)) |
| 652 | .on_action(cx.listener(Self::select_previous)) | 713 | .on_action(cx.listener(Self::select_previous)) |
| 653 | .on_action(cx.listener(Self::select_first)) | 714 | .on_action(cx.listener(Self::select_first)) |
| 654 | .on_action(cx.listener(Self::select_last)) | 715 | .on_action(cx.listener(Self::select_last)) |
| 655 | .on_action(cx.listener(Self::confirm)) | 716 | .on_action(cx.listener(Self::confirm)) |
| 717 | .when_some(self.error.clone(), |el, error| { | ||
| 718 | el.child( | ||
| 719 | h_flex() | ||
| 720 | .bg(cx.theme().status().warning_background) | ||
| 721 | .border_b_1() | ||
| 722 | .border_color(cx.theme().status().warning_border) | ||
| 723 | .pl_1() | ||
| 724 | .child(Icon::new(IconName::Warning).color(Color::Warning)) | ||
| 725 | .gap_2() | ||
| 726 | .child( | ||
| 727 | Label::new(error) | ||
| 728 | .size(LabelSize::Small) | ||
| 729 | .color(Color::Warning), | ||
| 730 | ), | ||
| 731 | ) | ||
| 732 | }) | ||
| 656 | .child(self.render_list(window, cx)) | 733 | .child(self.render_list(window, cx)) |
| 657 | .child(self.render_vertical_scrollbar(cx)) | 734 | .child(self.render_vertical_scrollbar(cx)) |
| 658 | } | 735 | } |
| @@ -980,10 +980,11 @@ impl Render for VariableList { · 979 unmodified lines | |||
| 980 | .on_action(cx.listener(Self::edit_variable)) | 980 | .on_action(cx.listener(Self::edit_variable)) |
| 981 | .child( | 981 | .child( |
| 982 | uniform_list( | 982 | uniform_list( |
| 983 | cx.entity().clone(), | ||
| 984 | "variable-list", | 983 | "variable-list", |
| 985 | self.entries.len(), | 984 | self.entries.len(), |
| 986 | move |this, range, window, cx| this.render_entries(range, window, cx), | 985 | cx.processor(move |this, range: Range<usize>, window, cx| { |
| 986 | this.render_entries(range, window, cx) | ||
| 987 | }), | ||
| 987 | ) | 988 | ) |
| 988 | .track_scroll(self.list_handle.clone()) | 989 | .track_scroll(self.list_handle.clone()) |
| 989 | .gap_1_5() | 990 | .gap_1_5() |
| @@ -16,7 +16,7 @@ use ui::{ActiveTheme as _, Context, ParentElement as _, Styled as _, div}; · 15 unmodified lines | |||
| 16 | use util::ResultExt as _; | 16 | use util::ResultExt as _; |
| 17 | use workspace::{ | 17 | use workspace::{ |
| 18 | Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, | 18 | Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, |
| 19 | item::{BreadcrumbText, ItemEvent}, | 19 | item::{BreadcrumbText, ItemEvent, SaveOptions}, |
| 20 | searchable::SearchableItemHandle, | 20 | searchable::SearchableItemHandle, |
| 21 | }; | 21 | }; |
| 22 | 22 | ||
| @@ -148,7 +148,7 @@ impl StackTraceView { · 125 unmodified lines | |||
| 148 | 148 | ||
| 149 | let stack_frames = self | 149 | let stack_frames = self |
| 150 | .stack_frame_list | 150 | .stack_frame_list |
| 151 | .read_with(cx, |list, _| list.flatten_entries(false)); | 151 | .read_with(cx, |list, _| list.flatten_entries(false, false)); |
| 152 | 152 | ||
| 153 | let frames_to_open: Vec<_> = stack_frames | 153 | let frames_to_open: Vec<_> = stack_frames |
| 154 | .into_iter() | 154 | .into_iter() |
| @@ -237,7 +237,7 @@ impl StackTraceView { · 82 unmodified lines | |||
| 237 | 237 | ||
| 238 | let stack_frames = self | 238 | let stack_frames = self |
| 239 | .stack_frame_list | 239 | .stack_frame_list |
| 240 | .read_with(cx, |session, _| session.flatten_entries(false)); | 240 | .read_with(cx, |session, _| session.flatten_entries(false, false)); |
| 241 | 241 | ||
| 242 | let active_idx = self | 242 | let active_idx = self |
| 243 | .selected_stack_frame_id | 243 | .selected_stack_frame_id |
| @@ -386,12 +386,12 @@ impl Item for StackTraceView { · 142 unmodified lines | |||
| 386 | 386 | ||
| 387 | fn save( | 387 | fn save( |
| 388 | &mut self, | 388 | &mut self, |
| 389 | format: bool, | 389 | options: SaveOptions, |
| 390 | project: Entity<Project>, | 390 | project: Entity<Project>, |
| 391 | window: &mut Window, | 391 | window: &mut Window, |
| 392 | cx: &mut Context<Self>, | 392 | cx: &mut Context<Self>, |
| 393 | ) -> Task<Result<()>> { | 393 | ) -> Task<Result<()>> { |
| 394 | self.editor.save(format, project, window, cx) | 394 | self.editor.save(options, project, window, cx) |
| 395 | } | 395 | } |
| 396 | 396 | ||
| 397 | fn save_as( | 397 | fn save_as( |
| @@ -33,6 +33,7 @@ use std::{ · 32 unmodified lines | |||
| 33 | use terminal_view::terminal_panel::TerminalPanel; | 33 | use terminal_view::terminal_panel::TerminalPanel; |
| 34 | use tests::{active_debug_session_panel, init_test, init_test_workspace}; | 34 | use tests::{active_debug_session_panel, init_test, init_test_workspace}; |
| 35 | use util::path; | 35 | use util::path; |
| 36 | use workspace::item::SaveOptions; | ||
| 36 | use workspace::{Item, dock::Panel}; | 37 | use workspace::{Item, dock::Panel}; |
| 37 | 38 | ||
| 38 | #[gpui::test] | 39 | #[gpui::test] |
| @@ -443,18 +444,20 @@ async fn test_handle_start_debugging_request( · 404 unmodified lines | |||
| 443 | .update(cx, |workspace, _window, cx| { | 444 | .update(cx, |workspace, _window, cx| { |
| 444 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | 445 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); |
| 445 | 446 | ||
| 446 | // Active session does not change on spawn. | 447 | // Active session changes on spawn, as the parent has never stopped. |
| 447 | let active_session = debug_panel | 448 | let active_session = debug_panel |
| 448 | .read(cx) | 449 | .read(cx) |
| 449 | .active_session() | 450 | .active_session() |
| 450 | .unwrap() | 451 | .unwrap() |
| 451 | .read(cx) | 452 | .read(cx) |
| 452 | .session(cx); | 453 | .session(cx); |
| 453 | |||
| 454 | assert_eq!(active_session, sessions[0].read(cx).session(cx)); | ||
| 455 | assert!(active_session.read(cx).parent_session().is_none()); | ||
| 456 | |||
| 457 | let current_sessions = debug_panel.read(cx).sessions(); | 454 | let current_sessions = debug_panel.read(cx).sessions(); |
| 455 | assert_eq!(active_session, current_sessions[1].read(cx).session(cx)); | ||
| 456 | assert_eq!( | ||
| 457 | active_session.read(cx).parent_session(), | ||
| 458 | Some(¤t_sessions[0].read(cx).session(cx)) | ||
| 459 | ); | ||
| 460 | |||
| 458 | assert_eq!(current_sessions.len(), 2); | 461 | assert_eq!(current_sessions.len(), 2); |
| 459 | assert_eq!(current_sessions[0], sessions[0]); | 462 | assert_eq!(current_sessions[0], sessions[0]); |
| 460 | 463 | ||
| @@ -468,14 +471,19 @@ async fn test_handle_start_debugging_request( · 7 unmodified lines | |||
| 468 | 471 | ||
| 469 | // We should preserve the original binary (params to spawn process etc.) except for launch params | 472 | // We should preserve the original binary (params to spawn process etc.) except for launch params |
| 470 | // (as they come from reverse spawn request). | 473 | // (as they come from reverse spawn request). |
| 471 | let mut original_binary = parent_session.read(cx).binary().clone(); | 474 | let mut original_binary = parent_session.read(cx).binary().cloned().unwrap(); |
| 472 | original_binary.request_args = StartDebuggingRequestArguments { | 475 | original_binary.request_args = StartDebuggingRequestArguments { |
| 473 | request: StartDebuggingRequestArgumentsRequest::Launch, | 476 | request: StartDebuggingRequestArgumentsRequest::Launch, |
| 474 | configuration: fake_config.clone(), | 477 | configuration: fake_config.clone(), |
| 475 | }; | 478 | }; |
| 476 | 479 | ||
| 477 | assert_eq!( | 480 | assert_eq!( |
| 478 | current_sessions[1].read(cx).session(cx).read(cx).binary(), | 481 | current_sessions[1] |
| 482 | .read(cx) | ||
| 483 | .session(cx) | ||
| 484 | .read(cx) | ||
| 485 | .binary() | ||
| 486 | .unwrap(), | ||
| 479 | &original_binary | 487 | &original_binary |
| 480 | ); | 488 | ); |
| 481 | }) | 489 | }) |
| @@ -1213,7 +1221,15 @@ async fn test_send_breakpoints_when_editor_has_been_saved( · 731 unmodified lines | |||
| 1213 | 1221 | ||
| 1214 | editor | 1222 | editor |
| 1215 | .update_in(cx, |editor, window, cx| { | 1223 | .update_in(cx, |editor, window, cx| { |
| 1216 | editor.save(true, project.clone(), window, cx) | 1224 | editor.save( |
| 1225 | SaveOptions { | ||
| 1226 | format: true, | ||
| 1227 | autosave: false, | ||
| 1228 | }, | ||
| 1229 | project.clone(), | ||
| 1230 | window, | ||
| 1231 | cx, | ||
| 1232 | ) | ||
| 1217 | }) | 1233 | }) |
| 1218 | .await | 1234 | .await |
| 1219 | .unwrap(); | 1235 | .unwrap(); |
| @@ -718,7 +718,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo · 717 unmodified lines | |||
| 718 | stack_frame_list.entries() | 718 | stack_frame_list.entries() |
| 719 | ); | 719 | ); |
| 720 | 720 | ||
| 721 | stack_frame_list.expand_collapsed_entry(1); | 721 | stack_frame_list.expand_collapsed_entry(1, cx); |
| 722 | 722 | ||
| 723 | assert_eq!( | 723 | assert_eq!( |
| 724 | &vec![ | 724 | &vec![ |
| @@ -735,7 +735,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo · 10 unmodified lines | |||
| 735 | stack_frame_list.entries() | 735 | stack_frame_list.entries() |
| 736 | ); | 736 | ); |
| 737 | 737 | ||
| 738 | stack_frame_list.expand_collapsed_entry(4); | 738 | stack_frame_list.expand_collapsed_entry(4, cx); |
| 739 | 739 | ||
| 740 | assert_eq!( | 740 | assert_eq!( |
| 741 | &vec![ | 741 | &vec![ |
| @@ -191,7 +191,10 @@ async fn test_basic_fetch_initial_scope_and_variables( · 190 unmodified lines | |||
| 191 | running_state.update(cx, |running_state, cx| { | 191 | running_state.update(cx, |running_state, cx| { |
| 192 | let (stack_frame_list, stack_frame_id) = | 192 | let (stack_frame_list, stack_frame_id) = |
| 193 | running_state.stack_frame_list().update(cx, |list, _| { | 193 | running_state.stack_frame_list().update(cx, |list, _| { |
| 194 | (list.flatten_entries(true), list.opened_stack_frame_id()) | 194 | ( |
| 195 | list.flatten_entries(true, true), | ||
| 196 | list.opened_stack_frame_id(), | ||
| 197 | ) | ||
| 195 | }); | 198 | }); |
| 196 | 199 | ||
| 197 | assert_eq!(stack_frames, stack_frame_list); | 200 | assert_eq!(stack_frames, stack_frame_list); |
| @@ -432,7 +435,10 @@ async fn test_fetch_variables_for_multiple_scopes( · 234 unmodified lines | |||
| 432 | running_state.update(cx, |running_state, cx| { | 435 | running_state.update(cx, |running_state, cx| { |
| 433 | let (stack_frame_list, stack_frame_id) = | 436 | let (stack_frame_list, stack_frame_id) = |
| 434 | running_state.stack_frame_list().update(cx, |list, _| { | 437 | running_state.stack_frame_list().update(cx, |list, _| { |
| 435 | (list.flatten_entries(true), list.opened_stack_frame_id()) | 438 | ( |
| 439 | list.flatten_entries(true, true), | ||
| 440 | list.opened_stack_frame_id(), | ||
| 441 | ) | ||
| 436 | }); | 442 | }); |
| 437 | 443 | ||
| 438 | assert_eq!(Some(1), stack_frame_id); | 444 | assert_eq!(Some(1), stack_frame_id); |
| @@ -1459,7 +1465,10 @@ async fn test_variable_list_only_sends_requests_when_rendering( · 1020 unmodified lines | |||
| 1459 | running_state.update(cx, |running_state, cx| { | 1465 | running_state.update(cx, |running_state, cx| { |
| 1460 | let (stack_frame_list, stack_frame_id) = | 1466 | let (stack_frame_list, stack_frame_id) = |
| 1461 | running_state.stack_frame_list().update(cx, |list, _| { | 1467 | running_state.stack_frame_list().update(cx, |list, _| { |
| 1462 | (list.flatten_entries(true), list.opened_stack_frame_id()) | 1468 | ( |
| 1469 | list.flatten_entries(true, true), | ||
| 1470 | list.opened_stack_frame_id(), | ||
| 1471 | ) | ||
| 1463 | }); | 1472 | }); |
| 1464 | 1473 | ||
| 1465 | assert_eq!(Some(1), stack_frame_id); | 1474 | assert_eq!(Some(1), stack_frame_id); |
| @@ -1741,7 +1750,10 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( · 275 unmodified lines | |||
| 1741 | running_state.update(cx, |running_state, cx| { | 1750 | running_state.update(cx, |running_state, cx| { |
| 1742 | let (stack_frame_list, stack_frame_id) = | 1751 | let (stack_frame_list, stack_frame_id) = |
| 1743 | running_state.stack_frame_list().update(cx, |list, _| { | 1752 | running_state.stack_frame_list().update(cx, |list, _| { |
| 1744 | (list.flatten_entries(true), list.opened_stack_frame_id()) | 1753 | ( |
| 1754 | list.flatten_entries(true, true), | ||
| 1755 | list.opened_stack_frame_id(), | ||
| 1756 | ) | ||
| 1745 | }); | 1757 | }); |
| 1746 | 1758 | ||
| 1747 | let variable_list = running_state.variable_list().read(cx); | 1759 | let variable_list = running_state.variable_list().read(cx); |
| @@ -1796,7 +1808,10 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( · 48 unmodified lines | |||
| 1796 | running_state.update(cx, |running_state, cx| { | 1808 | running_state.update(cx, |running_state, cx| { |
| 1797 | let (stack_frame_list, stack_frame_id) = | 1809 | let (stack_frame_list, stack_frame_id) = |
| 1798 | running_state.stack_frame_list().update(cx, |list, _| { | 1810 | running_state.stack_frame_list().update(cx, |list, _| { |
| 1799 | (list.flatten_entries(true), list.opened_stack_frame_id()) | 1811 | ( |
| 1812 | list.flatten_entries(true, true), | ||
| 1813 | list.opened_stack_frame_id(), | ||
| 1814 | ) | ||
| 1800 | }); | 1815 | }); |
| 1801 | 1816 | ||
| 1802 | let variable_list = running_state.variable_list().read(cx); | 1817 | let variable_list = running_state.variable_list().read(cx); |
| @@ -43,7 +43,7 @@ use ui::{Icon, IconName, Label, h_flex, prelude::*}; · 42 unmodified lines | |||
| 43 | use util::ResultExt; | 43 | use util::ResultExt; |
| 44 | use workspace::{ | 44 | use workspace::{ |
| 45 | ItemNavHistory, ToolbarItemLocation, Workspace, | 45 | ItemNavHistory, ToolbarItemLocation, Workspace, |
| 46 | item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, | 46 | item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, |
| 47 | searchable::SearchableItemHandle, | 47 | searchable::SearchableItemHandle, |
| 48 | }; | 48 | }; |
| 49 | 49 | ||
| @@ -849,12 +849,12 @@ impl Item for ProjectDiagnosticsEditor { · 799 unmodified lines | |||
| 849 | 849 | ||
| 850 | fn save( | 850 | fn save( |
| 851 | &mut self, | 851 | &mut self, |
| 852 | format: bool, | 852 | options: SaveOptions, |
| 853 | project: Entity<Project>, | 853 | project: Entity<Project>, |
| 854 | window: &mut Window, | 854 | window: &mut Window, |
| 855 | cx: &mut Context<Self>, | 855 | cx: &mut Context<Self>, |
| 856 | ) -> Task<Result<()>> { | 856 | ) -> Task<Result<()>> { |
| 857 | self.editor.save(format, project, window, cx) | 857 | self.editor.save(options, project, window, cx) |
| 858 | } | 858 | } |
| 859 | 859 | ||
| 860 | fn save_as( | 860 | fn save_as( |
| @@ -35,7 +35,6 @@ assets.workspace = true · 34 unmodified lines | |||
| 35 | client.workspace = true | 35 | client.workspace = true |
| 36 | clock.workspace = true | 36 | clock.workspace = true |
| 37 | collections.workspace = true | 37 | collections.workspace = true |
| 38 | command_palette_hooks.workspace = true | ||
| 39 | convert_case.workspace = true | 38 | convert_case.workspace = true |
| 40 | dap.workspace = true | 39 | dap.workspace = true |
| 41 | db.workspace = true | 40 | db.workspace = true |
| @@ -87,6 +87,7 @@ pub struct ToggleCodeActions { · 86 unmodified lines | |||
| 87 | #[derive(PartialEq, Clone, Debug)] | 87 | #[derive(PartialEq, Clone, Debug)] |
| 88 | pub enum CodeActionSource { | 88 | pub enum CodeActionSource { |
| 89 | Indicator(DisplayRow), | 89 | Indicator(DisplayRow), |
| 90 | RunMenu(DisplayRow), | ||
| 90 | QuickActionBar, | 91 | QuickActionBar, |
| 91 | } | 92 | } |
| 92 | 93 | ||
| @@ -243,6 +244,8 @@ impl_actions!( · 150 unmodified lines | |||
| 243 | ] | 244 | ] |
| 244 | ); | 245 | ); |
| 245 | 246 | ||
| 247 | actions!(debugger, [RunToCursor, EvaluateSelectedText]); | ||
| 248 | |||
| 246 | actions!( | 249 | actions!( |
| 247 | editor, | 250 | editor, |
| 248 | [ | 251 | [ |
| @@ -426,8 +429,6 @@ actions!( · 177 unmodified lines | |||
| 426 | DisableBreakpoint, | 429 | DisableBreakpoint, |
| 427 | EnableBreakpoint, | 430 | EnableBreakpoint, |
| 428 | EditLogBreakpoint, | 431 | EditLogBreakpoint, |
| 429 | DebuggerRunToCursor, | ||
| 430 | DebuggerEvaluateSelectedText, | ||
| 431 | ToggleAutoSignatureHelp, | 432 | ToggleAutoSignatureHelp, |
| 432 | ToggleGitBlameInline, | 433 | ToggleGitBlameInline, |
| 433 | OpenGitBlameCommit, | 434 | OpenGitBlameCommit, |
| @@ -1,2382 +1,495 @@ | |||
| 1 | use crate::{ | 1 | use crate::{code_context_menus::CompletionsMenu, editor_settings::SnippetSortOrder}; |
| 2 | code_context_menus::{CompletionsMenu, SortableMatch}, | 2 | use fuzzy::StringMatchCandidate; |
| 3 | editor_settings::SnippetSortOrder, | ||
| 4 | }; | ||
| 5 | use fuzzy::StringMatch; | ||
| 6 | use gpui::TestAppContext; | 3 | use gpui::TestAppContext; |
| 4 | use language::CodeLabel; | ||
| 5 | use lsp::{CompletionItem, CompletionItemKind, LanguageServerId}; | ||
| 6 | use project::{Completion, CompletionSource}; | ||
| 7 | use std::sync::Arc; | ||
| 8 | use std::sync::atomic::AtomicBool; | ||
| 9 | use text::Anchor; | ||
| 7 | 10 | ||
| 8 | #[gpui::test] | 11 | #[gpui::test] |
| 9 | fn test_sort_matches_local_variable_over_global_variable(_cx: &mut TestAppContext) { | 12 | async fn test_sort_matches_local_variable_over_global_variable(cx: &mut TestAppContext) { |
| 10 | // Case 1: "foo" | 13 | // Case 1: "foo" |
| 11 | let query: Option<&str> = Some("foo"); | 14 | let completions = vec![ |
| 12 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 15 | CompletionBuilder::constant("foo_bar_baz", "7fffffff"), |
| 13 | SortableMatch { | 16 | CompletionBuilder::variable("foo_bar_qux", "7ffffffe"), |
| 14 | string_match: StringMatch { | 17 | CompletionBuilder::constant("floorf64", "80000000"), |
| 15 | candidate_id: 0, | 18 | CompletionBuilder::constant("floorf32", "80000000"), |
| 16 | score: 0.2727272727272727, | 19 | CompletionBuilder::constant("floorf16", "80000000"), |
| 17 | positions: vec![], | 20 | CompletionBuilder::constant("floorf128", "80000000"), |
| 18 | string: "foo_bar_baz".to_string(), | ||
| 19 | }, | ||
| 20 | is_snippet: false, | ||
| 21 | sort_text: Some("7fffffff"), | ||
| 22 | sort_kind: 2, | ||
| 23 | sort_label: "foo_bar_baz", | ||
| 24 | }, | ||
| 25 | SortableMatch { | ||
| 26 | string_match: StringMatch { | ||
| 27 | candidate_id: 0, | ||
| 28 | score: 0.2727272727272727, | ||
| 29 | positions: vec![], | ||
| 30 | string: "foo_bar_qux".to_string(), | ||
| 31 | }, | ||
| 32 | is_snippet: false, | ||
| 33 | sort_text: Some("7ffffffe"), | ||
| 34 | sort_kind: 1, | ||
| 35 | sort_label: "foo_bar_qux", | ||
| 36 | }, | ||
| 37 | SortableMatch { | ||
| 38 | string_match: StringMatch { | ||
| 39 | candidate_id: 0, | ||
| 40 | score: 0.22499999999999998, | ||
| 41 | positions: vec![], | ||
| 42 | string: "floorf64".to_string(), | ||
| 43 | }, | ||
| 44 | is_snippet: false, | ||
| 45 | sort_text: Some("80000000"), | ||
| 46 | sort_kind: 2, | ||
| 47 | sort_label: "floorf64", | ||
| 48 | }, | ||
| 49 | SortableMatch { | ||
| 50 | string_match: StringMatch { | ||
| 51 | candidate_id: 0, | ||
| 52 | score: 0.22499999999999998, | ||
| 53 | positions: vec![], | ||
| 54 | string: "floorf32".to_string(), | ||
| 55 | }, | ||
| 56 | is_snippet: false, | ||
| 57 | sort_text: Some("80000000"), | ||
| 58 | sort_kind: 2, | ||
| 59 | sort_label: "floorf32", | ||
| 60 | }, | ||
| 61 | SortableMatch { | ||
| 62 | string_match: StringMatch { | ||
| 63 | candidate_id: 0, | ||
| 64 | score: 0.22499999999999998, | ||
| 65 | positions: vec![], | ||
| 66 | string: "floorf16".to_string(), | ||
| 67 | }, | ||
| 68 | is_snippet: false, | ||
| 69 | sort_text: Some("80000000"), | ||
| 70 | sort_kind: 2, | ||
| 71 | sort_label: "floorf16", | ||
| 72 | }, | ||
| 73 | SortableMatch { | ||
| 74 | string_match: StringMatch { | ||
| 75 | candidate_id: 0, | ||
| 76 | score: 0.2, | ||
| 77 | positions: vec![], | ||
| 78 | string: "floorf128".to_string(), | ||
| 79 | }, | ||
| 80 | is_snippet: false, | ||
| 81 | sort_text: Some("80000000"), | ||
| 82 | sort_kind: 2, | ||
| 83 | sort_label: "floorf128", | ||
| 84 | }, | ||
| 85 | ]; | 21 | ]; |
| 86 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 22 | let matches = sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await; |
| 87 | assert_eq!( | 23 | assert_eq!(matches[0], "foo_bar_qux"); |
| 88 | matches[0].string_match.string.as_str(), | 24 | assert_eq!(matches[1], "foo_bar_baz"); |
| 89 | "foo_bar_qux", | 25 | assert_eq!(matches[2], "floorf16"); |
| 90 | "Match order not expected" | 26 | assert_eq!(matches[3], "floorf32"); |
| 91 | ); | ||
| 92 | assert_eq!( | ||
| 93 | matches[1].string_match.string.as_str(), | ||
| 94 | "foo_bar_baz", | ||
| 95 | "Match order not expected" | ||
| 96 | ); | ||
| 97 | assert_eq!( | ||
| 98 | matches[2].string_match.string.as_str(), | ||
| 99 | "floorf16", | ||
| 100 | "Match order not expected" | ||
| 101 | ); | ||
| 102 | assert_eq!( | ||
| 103 | matches[3].string_match.string.as_str(), | ||
| 104 | "floorf32", | ||
| 105 | "Match order not expected" | ||
| 106 | ); | ||
| 107 | 27 | ||
| 108 | // Case 2: "foobar" | 28 | // Case 2: "foobar" |
| 109 | let query: Option<&str> = Some("foobar"); | 29 | let completions = vec![ |
| 110 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 30 | CompletionBuilder::constant("foo_bar_baz", "7fffffff"), |
| 111 | SortableMatch { | 31 | CompletionBuilder::variable("foo_bar_qux", "7ffffffe"), |
| 112 | string_match: StringMatch { | ||
| 113 | candidate_id: 0, | ||
| 114 | score: 0.4363636363636364, | ||
| 115 | positions: vec![], | ||
| 116 | string: "foo_bar_baz".to_string(), | ||
| 117 | }, | ||
| 118 | is_snippet: false, | ||
| 119 | sort_text: Some("7fffffff"), | ||
| 120 | sort_kind: 2, | ||
| 121 | sort_label: "foo_bar_baz", | ||
| 122 | }, | ||
| 123 | SortableMatch { | ||
| 124 | string_match: StringMatch { | ||
| 125 | candidate_id: 0, | ||
| 126 | score: 0.4363636363636364, | ||
| 127 | positions: vec![], | ||
| 128 | string: "foo_bar_qux".to_string(), | ||
| 129 | }, | ||
| 130 | is_snippet: false, | ||
| 131 | sort_text: Some("7ffffffe"), | ||
| 132 | sort_kind: 1, | ||
| 133 | sort_label: "foo_bar_qux", | ||
| 134 | }, | ||
| 135 | ]; | 32 | ]; |
| 136 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 33 | let matches = sort_matches("foobar", &completions, SnippetSortOrder::default(), cx).await; |
| 137 | assert_eq!( | 34 | assert_eq!(matches[0], "foo_bar_qux"); |
| 138 | matches[0].string_match.string.as_str(), | 35 | assert_eq!(matches[1], "foo_bar_baz"); |
| 139 | "foo_bar_qux", | ||
| 140 | "Match order not expected" | ||
| 141 | ); | ||
| 142 | assert_eq!( | ||
| 143 | matches[1].string_match.string.as_str(), | ||
| 144 | "foo_bar_baz", | ||
| 145 | "Match order not expected" | ||
| 146 | ); | ||
| 147 | } | 36 | } |
| 148 | 37 | ||
| 149 | #[gpui::test] | 38 | #[gpui::test] |
| 150 | fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) { | 39 | async fn test_sort_matches_local_variable_over_global_enum(cx: &mut TestAppContext) { |
| 151 | // Case 1: "ele" | 40 | // Case 1: "ele" |
| 152 | let query: Option<&str> = Some("ele"); | 41 | let completions = vec![ |
| 153 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 42 | CompletionBuilder::constant("ElementType", "7fffffff"), |
| 154 | SortableMatch { | 43 | CompletionBuilder::variable("element_type", "7ffffffe"), |
| 155 | string_match: StringMatch { | 44 | CompletionBuilder::constant("simd_select", "80000000"), |
| 156 | candidate_id: 0, | 45 | CompletionBuilder::keyword("while let", "7fffffff"), |
| 157 | score: 0.2727272727272727, | ||
| 158 | positions: vec![], | ||
| 159 | string: "ElementType".to_string(), | ||
| 160 | }, | ||
| 161 | is_snippet: false, | ||
| 162 | sort_text: Some("7fffffff"), | ||
| 163 | sort_kind: 2, | ||
| 164 | sort_label: "ElementType", | ||
| 165 | }, | ||
| 166 | SortableMatch { | ||
| 167 | string_match: StringMatch { | ||
| 168 | candidate_id: 0, | ||
| 169 | score: 0.25, | ||
| 170 | positions: vec![], | ||
| 171 | string: "element_type".to_string(), | ||
| 172 | }, | ||
| 173 | is_snippet: false, | ||
| 174 | sort_text: Some("7ffffffe"), | ||
| 175 | sort_kind: 1, | ||
| 176 | sort_label: "element_type", | ||
| 177 | }, | ||
| 178 | SortableMatch { | ||
| 179 | string_match: StringMatch { | ||
| 180 | candidate_id: 0, | ||
| 181 | score: 0.16363636363636364, | ||
| 182 | positions: vec![], | ||
| 183 | string: "simd_select".to_string(), | ||
| 184 | }, | ||
| 185 | is_snippet: false, | ||
| 186 | sort_text: Some("80000000"), | ||
| 187 | sort_kind: 2, | ||
| 188 | sort_label: "simd_select", | ||
| 189 | }, | ||
| 190 | SortableMatch { | ||
| 191 | string_match: StringMatch { | ||
| 192 | candidate_id: 0, | ||
| 193 | score: 0.16, | ||
| 194 | positions: vec![], | ||
| 195 | string: "while let".to_string(), | ||
| 196 | }, | ||
| 197 | is_snippet: false, | ||
| 198 | sort_text: Some("7fffffff"), | ||
| 199 | sort_kind: 0, | ||
| 200 | sort_label: "while let", | ||
| 201 | }, | ||
| 202 | ]; | 46 | ]; |
| 203 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 47 | let matches = sort_matches("ele", &completions, SnippetSortOrder::default(), cx).await; |
| 204 | assert_eq!( | 48 | assert_eq!(matches[0], "element_type"); |
| 205 | matches[0].string_match.string.as_str(), | 49 | assert_eq!(matches[1], "ElementType"); |
| 206 | "element_type", | ||
| 207 | "Match order not expected" | ||
| 208 | ); | ||
| 209 | assert_eq!( | ||
| 210 | matches[1].string_match.string.as_str(), | ||
| 211 | "ElementType", | ||
| 212 | "Match order not expected" | ||
| 213 | ); | ||
| 214 | 50 | ||
| 215 | // Case 2: "eleme" | 51 | // Case 2: "eleme" |
| 216 | let query: Option<&str> = Some("eleme"); | 52 | let completions = vec![ |
| 217 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 53 | CompletionBuilder::constant("ElementType", "7fffffff"), |
| 218 | SortableMatch { | 54 | CompletionBuilder::variable("element_type", "7ffffffe"), |
| 219 | string_match: StringMatch { | 55 | CompletionBuilder::constant("REPLACEMENT_CHARACTER", "80000000"), |
| 220 | candidate_id: 0, | ||
| 221 | score: 0.4545454545454546, | ||
| 222 | positions: vec![], | ||
| 223 | string: "ElementType".to_string(), | ||
| 224 | }, | ||
| 225 | is_snippet: false, | ||
| 226 | sort_text: Some("7fffffff"), | ||
| 227 | sort_kind: 2, | ||
| 228 | sort_label: "ElementType", | ||
| 229 | }, | ||
| 230 | SortableMatch { | ||
| 231 | string_match: StringMatch { | ||
| 232 | candidate_id: 0, | ||
| 233 | score: 0.41666666666666663, | ||
| 234 | positions: vec![], | ||
| 235 | string: "element_type".to_string(), | ||
| 236 | }, | ||
| 237 | is_snippet: false, | ||
| 238 | sort_text: Some("7ffffffe"), | ||
| 239 | sort_kind: 1, | ||
| 240 | sort_label: "element_type", | ||
| 241 | }, | ||
| 242 | SortableMatch { | ||
| 243 | string_match: StringMatch { | ||
| 244 | candidate_id: 0, | ||
| 245 | score: 0.04714285714285713, | ||
| 246 | positions: vec![], | ||
| 247 | string: "REPLACEMENT_CHARACTER".to_string(), | ||
| 248 | }, | ||
| 249 | is_snippet: false, | ||
| 250 | sort_text: Some("80000000"), | ||
| 251 | sort_kind: 2, | ||
| 252 | sort_label: "REPLACEMENT_CHARACTER", | ||
| 253 | }, | ||
| 254 | ]; | 56 | ]; |
| 255 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 57 | let matches = sort_matches("eleme", &completions, SnippetSortOrder::default(), cx).await; |
| 256 | assert_eq!( | 58 | assert_eq!(matches[0], "element_type"); |
| 257 | matches[0].string_match.string.as_str(), | 59 | assert_eq!(matches[1], "ElementType"); |
| 258 | "element_type", | 60 | } |
| 259 | "Match order not expected" | ||
| 260 | ); | ||
| 261 | assert_eq!( | ||
| 262 | matches[1].string_match.string.as_str(), | ||
| 263 | "ElementType", | ||
| 264 | "Match order not expected" | ||
| 265 | ); | ||
| 266 | 61 | ||
| 267 | // Case 3: "Elem" | 62 | #[gpui::test] |
| 268 | let query: Option<&str> = Some("Elem"); | 63 | async fn test_sort_matches_capitalization(cx: &mut TestAppContext) { |
| 269 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 64 | // Case 1: "Elem" |
| 270 | SortableMatch { | 65 | let completions = vec![ |
| 271 | string_match: StringMatch { | 66 | CompletionBuilder::constant("ElementType", "7fffffff"), |
| 272 | candidate_id: 0, | 67 | CompletionBuilder::variable("element_type", "7ffffffe"), |
| 273 | score: 0.36363636363636365, | ||
| 274 | positions: vec![], | ||
| 275 | string: "ElementType".to_string(), | ||
| 276 | }, | ||
| 277 | is_snippet: false, | ||
| 278 | sort_text: Some("7fffffff"), | ||
| 279 | sort_kind: 2, | ||
| 280 | sort_label: "ElementType", | ||
| 281 | }, | ||
| 282 | SortableMatch { | ||
| 283 | string_match: StringMatch { | ||
| 284 | candidate_id: 0, | ||
| 285 | score: 0.0003333333333333333, | ||
| 286 | positions: vec![], | ||
| 287 | string: "element_type".to_string(), | ||
| 288 | }, | ||
| 289 | is_snippet: false, | ||
| 290 | sort_text: Some("7ffffffe"), | ||
| 291 | sort_kind: 1, | ||
| 292 | sort_label: "element_type", | ||
| 293 | }, | ||
| 294 | ]; | 68 | ]; |
| 295 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 69 | let matches = sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await; |
| 296 | assert_eq!( | 70 | assert_eq!(matches[0], "ElementType"); |
| 297 | matches[0].string_match.string.as_str(), | 71 | assert_eq!(matches[1], "element_type"); |
| 298 | "ElementType", | ||
| 299 | "Match order not expected" | ||
| 300 | ); | ||
| 301 | assert_eq!( | ||
| 302 | matches[1].string_match.string.as_str(), | ||
| 303 | "element_type", | ||
| 304 | "Match order not expected" | ||
| 305 | ); | ||
| 306 | } | 72 | } |
| 307 | 73 | ||
| 308 | #[gpui::test] | 74 | #[gpui::test] |
| 309 | fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) { | 75 | async fn test_sort_matches_for_unreachable(cx: &mut TestAppContext) { |
| 310 | // Case 1: "unre" | 76 | // Case 1: "unre" |
| 311 | let query: Option<&str> = Some("unre"); | 77 | let completions = vec![ |
| 312 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 78 | CompletionBuilder::function("unreachable", "80000000"), |
| 313 | SortableMatch { | 79 | CompletionBuilder::function("unreachable!(…)", "7fffffff"), |
| 314 | string_match: StringMatch { | 80 | CompletionBuilder::function("unchecked_rem", "80000000"), |
| 315 | candidate_id: 0, | 81 | CompletionBuilder::function("unreachable_unchecked", "80000000"), |
| 316 | score: 0.36363636363636365, | ||
| 317 | positions: vec![], | ||
| 318 | string: "unreachable".to_string(), | ||
| 319 | }, | ||
| 320 | is_snippet: false, | ||
| 321 | sort_text: Some("80000000"), | ||
| 322 | sort_kind: 2, | ||
| 323 | sort_label: "unreachable", | ||
| 324 | }, | ||
| 325 | SortableMatch { | ||
| 326 | string_match: StringMatch { | ||
| 327 | candidate_id: 0, | ||
| 328 | score: 0.26666666666666666, | ||
| 329 | positions: vec![], | ||
| 330 | string: "unreachable!(…)".to_string(), | ||
| 331 | }, | ||
| 332 | is_snippet: true, | ||
| 333 | sort_text: Some("7fffffff"), | ||
| 334 | sort_kind: 2, | ||
| 335 | sort_label: "unreachable!(…)", | ||
| 336 | }, | ||
| 337 | SortableMatch { | ||
| 338 | string_match: StringMatch { | ||
| 339 | candidate_id: 0, | ||
| 340 | score: 0.24615384615384617, | ||
| 341 | positions: vec![], | ||
| 342 | string: "unchecked_rem".to_string(), | ||
| 343 | }, | ||
| 344 | is_snippet: false, | ||
| 345 | sort_text: Some("80000000"), | ||
| 346 | sort_kind: 2, | ||
| 347 | sort_label: "unchecked_rem", | ||
| 348 | }, | ||
| 349 | SortableMatch { | ||
| 350 | string_match: StringMatch { | ||
| 351 | candidate_id: 0, | ||
| 352 | score: 0.19047619047619047, | ||
| 353 | positions: vec![], | ||
| 354 | string: "unreachable_unchecked".to_string(), | ||
| 355 | }, | ||
| 356 | is_snippet: false, | ||
| 357 | sort_text: Some("80000000"), | ||
| 358 | sort_kind: 2, | ||
| 359 | sort_label: "unreachable_unchecked", | ||
| 360 | }, | ||
| 361 | ]; | 82 | ]; |
| 362 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 83 | let matches = sort_matches("unre", &completions, SnippetSortOrder::default(), cx).await; |
| 363 | assert_eq!( | 84 | assert_eq!(matches[0], "unreachable!(…)"); |
| 364 | matches[0].string_match.string.as_str(), | ||
| 365 | "unreachable!(…)", | ||
| 366 | "Match order not expected" | ||
| 367 | ); | ||
| 368 | 85 | ||
| 369 | // Case 2: "unrea" | 86 | // Case 2: "unrea" |
| 370 | let query: Option<&str> = Some("unrea"); | 87 | let completions = vec![ |
| 371 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 88 | CompletionBuilder::function("unreachable", "80000000"), |
| 372 | SortableMatch { | 89 | CompletionBuilder::function("unreachable!(…)", "7fffffff"), |
| 373 | string_match: StringMatch { | 90 | CompletionBuilder::function("unreachable_unchecked", "80000000"), |
| 374 | candidate_id: 0, | ||
| 375 | score: 0.4545454545454546, | ||
| 376 | positions: vec![], | ||
| 377 | string: "unreachable".to_string(), | ||
| 378 | }, | ||
| 379 | is_snippet: true, | ||
| 380 | sort_text: Some("80000000"), | ||
| 381 | sort_kind: 3, | ||
| 382 | sort_label: "unreachable", | ||
| 383 | }, | ||
| 384 | SortableMatch { | ||
| 385 | string_match: StringMatch { | ||
| 386 | candidate_id: 0, | ||
| 387 | score: 0.3333333333333333, | ||
| 388 | positions: vec![], | ||
| 389 | string: "unreachable!(…)".to_string(), | ||
| 390 | }, | ||
| 391 | is_snippet: true, | ||
| 392 | sort_text: Some("7fffffff"), | ||
| 393 | sort_kind: 3, | ||
| 394 | sort_label: "unreachable!(…)", | ||
| 395 | }, | ||
| 396 | SortableMatch { | ||
| 397 | string_match: StringMatch { | ||
| 398 | candidate_id: 0, | ||
| 399 | score: 0.23809523809523808, | ||
| 400 | positions: vec![], | ||
| 401 | string: "unreachable_unchecked".to_string(), | ||
| 402 | }, | ||
| 403 | is_snippet: true, | ||
| 404 | sort_text: Some("80000000"), | ||
| 405 | sort_kind: 3, | ||
| 406 | sort_label: "unreachable_unchecked", | ||
| 407 | }, | ||
| 408 | ]; | 91 | ]; |
| 409 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 92 | let matches = sort_matches("unrea", &completions, SnippetSortOrder::default(), cx).await; |
| 410 | assert_eq!( | 93 | assert_eq!(matches[0], "unreachable!(…)"); |
| 411 | matches[0].string_match.string.as_str(), | ||
| 412 | "unreachable!(…)", | ||
| 413 | "Match order not expected" | ||
| 414 | ); | ||
| 415 | 94 | ||
| 416 | // Case 3: "unreach" | 95 | // Case 3: "unreach" |
| 417 | let query: Option<&str> = Some("unreach"); | 96 | let completions = vec![ |
| 418 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 97 | CompletionBuilder::function("unreachable", "80000000"), |
| 419 | SortableMatch { | 98 | CompletionBuilder::function("unreachable!(…)", "7fffffff"), |
| 420 | string_match: StringMatch { | 99 | CompletionBuilder::function("unreachable_unchecked", "80000000"), |
| 421 | candidate_id: 0, | ||
| 422 | score: 0.6363636363636364, | ||
| 423 | positions: vec![], | ||
| 424 | string: "unreachable".to_string(), | ||
| 425 | }, | ||
| 426 | is_snippet: false, | ||
| 427 | sort_text: Some("80000000"), | ||
| 428 | sort_kind: 2, | ||
| 429 | sort_label: "unreachable", | ||
| 430 | }, | ||
| 431 | SortableMatch { | ||
| 432 | string_match: StringMatch { | ||
| 433 | candidate_id: 0, | ||
| 434 | score: 0.4666666666666667, | ||
| 435 | positions: vec![], | ||
| 436 | string: "unreachable!(…)".to_string(), | ||
| 437 | }, | ||
| 438 | is_snippet: true, | ||
| 439 | sort_text: Some("7fffffff"), | ||
| 440 | sort_kind: 2, | ||
| 441 | sort_label: "unreachable!(…)", | ||
| 442 | }, | ||
| 443 | SortableMatch { | ||
| 444 | string_match: StringMatch { | ||
| 445 | candidate_id: 0, | ||
| 446 | score: 0.3333333333333333, | ||
| 447 | positions: vec![], | ||
| 448 | string: "unreachable_unchecked".to_string(), | ||
| 449 | }, | ||
| 450 | is_snippet: false, | ||
| 451 | sort_text: Some("80000000"), | ||
| 452 | sort_kind: 2, | ||
| 453 | sort_label: "unreachable_unchecked", | ||
| 454 | }, | ||
| 455 | ]; | 100 | ]; |
| 456 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 101 | let matches = sort_matches("unreach", &completions, SnippetSortOrder::default(), cx).await; |
| 457 | assert_eq!( | 102 | assert_eq!(matches[0], "unreachable!(…)"); |
| 458 | matches[0].string_match.string.as_str(), | ||
| 459 | "unreachable!(…)", | ||
| 460 | "Match order not expected" | ||
| 461 | ); | ||
| 462 | 103 | ||
| 463 | // Case 4: "unreachabl" | 104 | // Case 4: "unreachabl" |
| 464 | let query: Option<&str> = Some("unreachable"); | 105 | let completions = vec![ |
| 465 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 106 | CompletionBuilder::function("unreachable", "80000000"), |
| 466 | SortableMatch { | 107 | CompletionBuilder::function("unreachable!(…)", "7fffffff"), |
| 467 | string_match: StringMatch { | 108 | CompletionBuilder::function("unreachable_unchecked", "80000000"), |
| 468 | candidate_id: 0, | ||
| 469 | score: 0.9090909090909092, | ||
| 470 | positions: vec![], | ||
| 471 | string: "unreachable".to_string(), | ||
| 472 | }, | ||
| 473 | is_snippet: false, | ||
| 474 | sort_text: Some("80000000"), | ||
| 475 | sort_kind: 3, | ||
| 476 | sort_label: "unreachable", | ||
| 477 | }, | ||
| 478 | SortableMatch { | ||
| 479 | string_match: StringMatch { | ||
| 480 | candidate_id: 0, | ||
| 481 | score: 0.6666666666666666, | ||
| 482 | positions: vec![], | ||
| 483 | string: "unreachable!(…)".to_string(), | ||
| 484 | }, | ||
| 485 | is_snippet: false, | ||
| 486 | sort_text: Some("7fffffff"), | ||
| 487 | sort_kind: 3, | ||
| 488 | sort_label: "unreachable!(…)", | ||
| 489 | }, | ||
| 490 | SortableMatch { | ||
| 491 | string_match: StringMatch { | ||
| 492 | candidate_id: 0, | ||
| 493 | score: 0.47619047619047616, | ||
| 494 | positions: vec![], | ||
| 495 | string: "unreachable_unchecked".to_string(), | ||
| 496 | }, | ||
| 497 | is_snippet: false, | ||
| 498 | sort_text: Some("80000000"), | ||
| 499 | sort_kind: 3, | ||
| 500 | sort_label: "unreachable_unchecked", | ||
| 501 | }, | ||
| 502 | ]; | 109 | ]; |
| 503 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 110 | let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await; |
| 504 | assert_eq!( | 111 | assert_eq!(matches[0], "unreachable!(…)"); |
| 505 | matches[0].string_match.string.as_str(), | ||
| 506 | "unreachable!(…)", | ||
| 507 | "Match order not expected" | ||
| 508 | ); | ||
| 509 | 112 | ||
| 510 | // Case 5: "unreachable" | 113 | // Case 5: "unreachable" |
| 511 | let query: Option<&str> = Some("unreachable"); | 114 | let completions = vec![ |
| 512 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 115 | CompletionBuilder::function("unreachable", "80000000"), |
| 513 | SortableMatch { | 116 | CompletionBuilder::function("unreachable!(…)", "7fffffff"), |
| 514 | string_match: StringMatch { | 117 | CompletionBuilder::function("unreachable_unchecked", "80000000"), |
| 515 | candidate_id: 0, | ||
| 516 | score: 1.0, | ||
| 517 | positions: vec![], | ||
| 518 | string: "unreachable".to_string(), | ||
| 519 | }, | ||
| 520 | is_snippet: false, | ||
| 521 | sort_text: Some("80000000"), | ||
| 522 | sort_kind: 2, | ||
| 523 | sort_label: "unreachable", | ||
| 524 | }, | ||
| 525 | SortableMatch { | ||
| 526 | string_match: StringMatch { | ||
| 527 | candidate_id: 0, | ||
| 528 | score: 0.7333333333333333, | ||
| 529 | positions: vec![], | ||
| 530 | string: "unreachable!(…)".to_string(), | ||
| 531 | }, | ||
| 532 | is_snippet: false, | ||
| 533 | sort_text: Some("7fffffff"), | ||
| 534 | sort_kind: 2, | ||
| 535 | sort_label: "unreachable!(…)", | ||
| 536 | }, | ||
| 537 | SortableMatch { | ||
| 538 | string_match: StringMatch { | ||
| 539 | candidate_id: 0, | ||
| 540 | score: 0.5238095238095237, | ||
| 541 | positions: vec![], | ||
| 542 | string: "unreachable_unchecked".to_string(), | ||
| 543 | }, | ||
| 544 | is_snippet: false, | ||
| 545 | sort_text: Some("80000000"), | ||
| 546 | sort_kind: 2, | ||
| 547 | sort_label: "unreachable_unchecked", | ||
| 548 | }, | ||
| 549 | ]; | 118 | ]; |
| 550 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 119 | let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await; |
| 551 | assert_eq!( | 120 | assert_eq!(matches[0], "unreachable!(…)"); |
| 552 | matches[0].string_match.string.as_str(), | ||
| 553 | "unreachable!(…)", | ||
| 554 | "LSP should take over even when fuzzy perfect matches" | ||
| 555 | ); | ||
| 556 | } | 121 | } |
| 557 | 122 | ||
| 558 | #[gpui::test] | 123 | #[gpui::test] |
| 559 | fn test_sort_matches_variable_and_constants_over_function(_cx: &mut TestAppContext) { | 124 | async fn test_sort_matches_variable_and_constants_over_function(cx: &mut TestAppContext) { |
| 560 | // Case 1: "var" as variable | 125 | // Case 1: "var" as variable |
| 561 | let query: Option<&str> = Some("var"); | 126 | let completions = vec![ |
| 562 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 127 | CompletionBuilder::function("var", "7fffffff"), |
| 563 | SortableMatch { | 128 | CompletionBuilder::variable("var", "7fffffff"), |
| 564 | string_match: StringMatch { | ||
| 565 | candidate_id: 0, | ||
| 566 | score: 1.0, | ||
| 567 | positions: vec![], | ||
| 568 | string: "var".to_string(), | ||
| 569 | }, | ||
| 570 | is_snippet: false, | ||
| 571 | sort_text: Some("7fffffff"), | ||
| 572 | sort_kind: 3, | ||
| 573 | sort_label: "var", // function | ||
| 574 | }, | ||
| 575 | SortableMatch { | ||
| 576 | string_match: StringMatch { | ||
| 577 | candidate_id: 1, | ||
| 578 | score: 1.0, | ||
| 579 | positions: vec![], | ||
| 580 | string: "var".to_string(), | ||
| 581 | }, | ||
| 582 | is_snippet: false, | ||
| 583 | sort_text: Some("7fffffff"), | ||
| 584 | sort_kind: 1, | ||
| 585 | sort_label: "var", // variable | ||
| 586 | }, | ||
| 587 | ]; | 129 | ]; |
| 588 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 130 | let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await; |
| 589 | assert_eq!( | 131 | assert_eq!(matches[0], "var"); |
| 590 | matches[0].string_match.candidate_id, 1, | 132 | assert_eq!(matches[1], "var"); |
| 591 | "Match order not expected" | ||
| 592 | ); | ||
| 593 | assert_eq!( | ||
| 594 | matches[1].string_match.candidate_id, 0, | ||
| 595 | "Match order not expected" | ||
| 596 | ); | ||
| 597 | 133 | ||
| 598 | // Case 2: "var" as constant | 134 | // Case 2: "var" as constant |
| 599 | let query: Option<&str> = Some("var"); | 135 | let completions = vec![ |
| 600 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 136 | CompletionBuilder::function("var", "7fffffff"), |
| 601 | SortableMatch { | 137 | CompletionBuilder::constant("var", "7fffffff"), |
| 602 | string_match: StringMatch { | ||
| 603 | candidate_id: 0, | ||
| 604 | score: 1.0, | ||
| 605 | positions: vec![], | ||
| 606 | string: "var".to_string(), | ||
| 607 | }, | ||
| 608 | is_snippet: false, | ||
| 609 | sort_text: Some("7fffffff"), | ||
| 610 | sort_kind: 3, | ||
| 611 | sort_label: "var", // function | ||
| 612 | }, | ||
| 613 | SortableMatch { | ||
| 614 | string_match: StringMatch { | ||
| 615 | candidate_id: 1, | ||
| 616 | score: 1.0, | ||
| 617 | positions: vec![], | ||
| 618 | string: "var".to_string(), | ||
| 619 | }, | ||
| 620 | is_snippet: false, | ||
| 621 | sort_text: Some("7fffffff"), | ||
| 622 | sort_kind: 2, | ||
| 623 | sort_label: "var", // constant | ||
| 624 | }, | ||
| 625 | ]; | 138 | ]; |
| 626 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 139 | let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await; |
| 627 | assert_eq!( | 140 | assert_eq!(matches[0], "var"); |
| 628 | matches[0].string_match.candidate_id, 1, | 141 | assert_eq!(matches[1], "var"); |
| 629 | "Match order not expected" | ||
| 630 | ); | ||
| 631 | assert_eq!( | ||
| 632 | matches[1].string_match.candidate_id, 0, | ||
| 633 | "Match order not expected" | ||
| 634 | ); | ||
| 635 | } | 142 | } |
| 636 | 143 | ||
| 637 | #[gpui::test] | 144 | #[gpui::test] |
| 638 | fn test_sort_matches_for_jsx_event_handler(_cx: &mut TestAppContext) { | 145 | async fn test_sort_matches_for_jsx_event_handler(cx: &mut TestAppContext) { |
| 639 | // Case 1: "on" | 146 | // Case 1: "on" |
| 640 | let query: Option<&str> = Some("on"); | 147 | let completions = vec![ |
| 641 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 148 | CompletionBuilder::function("onCut?", "12"), |
| 642 | SortableMatch { | 149 | CompletionBuilder::function("onPlay?", "12"), |
| 643 | string_match: StringMatch { | 150 | CompletionBuilder::function("color?", "12"), |
| 644 | candidate_id: 0, | 151 | CompletionBuilder::function("defaultValue?", "12"), |
| 645 | score: 0.3333333333333333, | 152 | CompletionBuilder::function("style?", "12"), |
| 646 | positions: vec![], | 153 | CompletionBuilder::function("className?", "12"), |
| 647 | string: "onCut?".to_string(), | ||
| 648 | }, | ||
| 649 | is_snippet: false, | ||
| 650 | sort_text: Some("12"), | ||
| 651 | sort_kind: 3, | ||
| 652 | sort_label: "onCut?", | ||
| 653 | }, | ||
| 654 | SortableMatch { | ||
| 655 | string_match: StringMatch { | ||
| 656 | candidate_id: 0, | ||
| 657 | score: 0.2857142857142857, | ||
| 658 | positions: vec![], | ||
| 659 | string: "onPlay?".to_string(), | ||
| 660 | }, | ||
| 661 | is_snippet: false, | ||
| 662 | sort_text: Some("12"), | ||
| 663 | sort_kind: 3, | ||
| 664 | sort_label: "onPlay?", | ||
| 665 | }, | ||
| 666 | SortableMatch { | ||
| 667 | string_match: StringMatch { | ||
| 668 | candidate_id: 0, | ||
| 669 | score: 0.25, | ||
| 670 | positions: vec![], | ||
| 671 | string: "color?".to_string(), | ||
| 672 | }, | ||
| 673 | is_snippet: false, | ||
| 674 | sort_text: Some("12"), | ||
| 675 | sort_kind: 3, | ||
| 676 | sort_label: "color?", | ||
| 677 | }, | ||
| 678 | SortableMatch { | ||
| 679 | string_match: StringMatch { | ||
| 680 | candidate_id: 0, | ||
| 681 | score: 0.25, | ||
| 682 | positions: vec![], | ||
| 683 | string: "defaultValue?".to_string(), | ||
| 684 | }, | ||
| 685 | is_snippet: false, | ||
| 686 | sort_text: Some("12"), | ||
| 687 | sort_kind: 3, | ||
| 688 | sort_label: "defaultValue?", | ||
| 689 | }, | ||
| 690 | SortableMatch { | ||
| 691 | string_match: StringMatch { | ||
| 692 | candidate_id: 0, | ||
| 693 | score: 0.25, | ||
| 694 | positions: vec![], | ||
| 695 | string: "style?".to_string(), | ||
| 696 | }, | ||
| 697 | is_snippet: false, | ||
| 698 | sort_text: Some("12"), | ||
| 699 | sort_kind: 3, | ||
| 700 | sort_label: "style?", | ||
| 701 | }, | ||
| 702 | SortableMatch { | ||
| 703 | string_match: StringMatch { | ||
| 704 | candidate_id: 0, | ||
| 705 | score: 0.20, | ||
| 706 | positions: vec![], | ||
| 707 | string: "className?".to_string(), | ||
| 708 | }, | ||
| 709 | is_snippet: false, | ||
| 710 | sort_text: Some("12"), | ||
| 711 | sort_kind: 3, | ||
| 712 | sort_label: "className?", | ||
| 713 | }, | ||
| 714 | ]; | 154 | ]; |
| 715 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 155 | let matches = sort_matches("on", &completions, SnippetSortOrder::default(), cx).await; |
| 716 | assert_eq!( | 156 | assert_eq!(matches[0], "onCut?"); |
| 717 | matches[0].string_match.string, "onCut?", | 157 | assert_eq!(matches[1], "onPlay?"); |
| 718 | "Match order not expected" | ||
| 719 | ); | ||
| 720 | assert_eq!( | ||
| 721 | matches[1].string_match.string, "onPlay?", | ||
| 722 | "Match order not expected" | ||
| 723 | ); | ||
| 724 | 158 | ||
| 725 | // Case 2: "ona" | 159 | // Case 2: "ona" |
| 726 | let query: Option<&str> = Some("ona"); | 160 | let completions = vec![ |
| 727 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 161 | CompletionBuilder::function("onAbort?", "12"), |
| 728 | SortableMatch { | 162 | CompletionBuilder::function("onAuxClick?", "12"), |
| 729 | string_match: StringMatch { | 163 | CompletionBuilder::function("onPlay?", "12"), |
| 730 | candidate_id: 0, | 164 | CompletionBuilder::function("onLoad?", "12"), |
| 731 | score: 0.375, | 165 | CompletionBuilder::function("onDrag?", "12"), |
| 732 | positions: vec![], | 166 | CompletionBuilder::function("onPause?", "12"), |
| 733 | string: "onAbort?".to_string(), | 167 | CompletionBuilder::function("onPaste?", "12"), |
| 734 | }, | 168 | CompletionBuilder::function("onAnimationEnd?", "12"), |
| 735 | is_snippet: false, | 169 | CompletionBuilder::function("onAbortCapture?", "12"), |
| 736 | sort_text: Some("12"), | 170 | CompletionBuilder::function("onChange?", "12"), |
| 737 | sort_kind: 3, | 171 | CompletionBuilder::function("onWaiting?", "12"), |
| 738 | sort_label: "onAbort?", | 172 | CompletionBuilder::function("onCanPlay?", "12"), |
| 739 | }, | ||
| 740 | SortableMatch { | ||
| 741 | string_match: StringMatch { | ||
| 742 | candidate_id: 0, | ||
| 743 | score: 0.2727272727272727, | ||
| 744 | positions: vec![], | ||
| 745 | string: "onAuxClick?".to_string(), | ||
| 746 | }, | ||
| 747 | is_snippet: false, | ||
| 748 | sort_text: Some("12"), | ||
| 749 | sort_kind: 3, | ||
| 750 | sort_label: "onAuxClick?", | ||
| 751 | }, | ||
| 752 | SortableMatch { | ||
| 753 | string_match: StringMatch { | ||
| 754 | candidate_id: 0, | ||
| 755 | score: 0.23571428571428565, | ||
| 756 | positions: vec![], | ||
| 757 | string: "onPlay?".to_string(), | ||
| 758 | }, | ||
| 759 | is_snippet: false, | ||
| 760 | sort_text: Some("12"), | ||
| 761 | sort_kind: 3, | ||
| 762 | sort_label: "onPlay?", | ||
| 763 | }, | ||
| 764 | SortableMatch { | ||
| 765 | string_match: StringMatch { | ||
| 766 | candidate_id: 0, | ||
| 767 | score: 0.23571428571428565, | ||
| 768 | positions: vec![], | ||
| 769 | string: "onLoad?".to_string(), | ||
| 770 | }, | ||
| 771 | is_snippet: false, | ||
| 772 | sort_text: Some("12"), | ||
| 773 | sort_kind: 3, | ||
| 774 | sort_label: "onLoad?", | ||
| 775 | }, | ||
| 776 | SortableMatch { | ||
| 777 | string_match: StringMatch { | ||
| 778 | candidate_id: 0, | ||
| 779 | score: 0.23571428571428565, | ||
| 780 | positions: vec![], | ||
| 781 | string: "onDrag?".to_string(), | ||
| 782 | }, | ||
| 783 | is_snippet: false, | ||
| 784 | sort_text: Some("12"), | ||
| 785 | sort_kind: 3, | ||
| 786 | sort_label: "onDrag?", | ||
| 787 | }, | ||
| 788 | SortableMatch { | ||
| 789 | string_match: StringMatch { | ||
| 790 | candidate_id: 0, | ||
| 791 | score: 0.22499999999999998, | ||
| 792 | positions: vec![], | ||
| 793 | string: "onPause?".to_string(), | ||
| 794 | }, | ||
| 795 | is_snippet: false, | ||
| 796 | sort_text: Some("12"), | ||
| 797 | sort_kind: 3, | ||
| 798 | sort_label: "onPause?", | ||
| 799 | }, | ||
| 800 | SortableMatch { | ||
| 801 | string_match: StringMatch { | ||
| 802 | candidate_id: 0, | ||
| 803 | score: 0.22499999999999998, | ||
| 804 | positions: vec![], | ||
| 805 | string: "onPaste?".to_string(), | ||
| 806 | }, | ||
| 807 | is_snippet: false, | ||
| 808 | sort_text: Some("12"), | ||
| 809 | sort_kind: 3, | ||
| 810 | sort_label: "onPaste?", | ||
| 811 | }, | ||
| 812 | SortableMatch { | ||
| 813 | string_match: StringMatch { | ||
| 814 | candidate_id: 0, | ||
| 815 | score: 0.2, | ||
| 816 | positions: vec![], | ||
| 817 | string: "onAnimationEnd?".to_string(), | ||
| 818 | }, | ||
| 819 | is_snippet: false, | ||
| 820 | sort_text: Some("12"), | ||
| 821 | sort_kind: 3, | ||
| 822 | sort_label: "onAnimationEnd?", | ||
| 823 | }, | ||
| 824 | SortableMatch { | ||
| 825 | string_match: StringMatch { | ||
| 826 | candidate_id: 0, | ||
| 827 | score: 0.2, | ||
| 828 | positions: vec![], | ||
| 829 | string: "onAbortCapture?".to_string(), | ||
| 830 | }, | ||
| 831 | is_snippet: false, | ||
| 832 | sort_text: Some("12"), | ||
| 833 | sort_kind: 3, | ||
| 834 | sort_label: "onAbortCapture?", | ||
| 835 | }, | ||
| 836 | SortableMatch { | ||
| 837 | string_match: StringMatch { | ||
| 838 | candidate_id: 0, | ||
| 839 | score: 0.1833333333333333, | ||
| 840 | positions: vec![], | ||
| 841 | string: "onChange?".to_string(), | ||
| 842 | }, | ||
| 843 | is_snippet: false, | ||
| 844 | sort_text: Some("12"), | ||
| 845 | sort_kind: 3, | ||
| 846 | sort_label: "onChange?", | ||
| 847 | }, | ||
| 848 | SortableMatch { | ||
| 849 | string_match: StringMatch { | ||
| 850 | candidate_id: 0, | ||
| 851 | score: 0.18, | ||
| 852 | positions: vec![], | ||
| 853 | string: "onWaiting?".to_string(), | ||
| 854 | }, | ||
| 855 | is_snippet: false, | ||
| 856 | sort_text: Some("12"), | ||
| 857 | sort_kind: 3, | ||
| 858 | sort_label: "onWaiting?", | ||
| 859 | }, | ||
| 860 | SortableMatch { | ||
| 861 | string_match: StringMatch { | ||
| 862 | candidate_id: 0, | ||
| 863 | score: 0.18, | ||
| 864 | positions: vec![], | ||
| 865 | string: "onCanPlay?".to_string(), | ||
| 866 | }, | ||
| 867 | is_snippet: false, | ||
| 868 | sort_text: Some("12"), | ||
| 869 | sort_kind: 3, | ||
| 870 | sort_label: "onCanPlay?", | ||
| 871 | }, | ||
| 872 | SortableMatch { | ||
| 873 | string_match: StringMatch { | ||
| 874 | candidate_id: 0, | ||
| 875 | score: 0.1764705882352941, | ||
| 876 | positions: vec![], | ||
| 877 | string: "onAnimationStart?".to_string(), | ||
| 878 | }, | ||
| 879 | is_snippet: false, | ||
| 880 | sort_text: Some("12"), | ||
| 881 | sort_kind: 3, | ||
| 882 | sort_label: "onAnimationStart?", | ||
| 883 | }, | ||
| 884 | SortableMatch { | ||
| 885 | string_match: StringMatch { | ||
| 886 | candidate_id: 0, | ||
| 887 | score: 0.16666666666666666, | ||
| 888 | positions: vec![], | ||
| 889 | string: "onAuxClickCapture?".to_string(), | ||
| 890 | }, | ||
| 891 | is_snippet: false, | ||
| 892 | sort_text: Some("12"), | ||
| 893 | sort_kind: 3, | ||
| 894 | sort_label: "onAuxClickCapture?", | ||
| 895 | }, | ||
| 896 | SortableMatch { | ||
| 897 | string_match: StringMatch { | ||
| 898 | candidate_id: 0, | ||
| 899 | score: 0.16499999999999998, | ||
| 900 | positions: vec![], | ||
| 901 | string: "onStalled?".to_string(), | ||
| 902 | }, | ||
| 903 | is_snippet: false, | ||
| 904 | sort_text: Some("12"), | ||
| 905 | sort_kind: 3, | ||
| 906 | sort_label: "onStalled?", | ||
| 907 | }, | ||
| 908 | SortableMatch { | ||
| 909 | string_match: StringMatch { | ||
| 910 | candidate_id: 0, | ||
| 911 | score: 0.16499999999999998, | ||
| 912 | positions: vec![], | ||
| 913 | string: "onPlaying?".to_string(), | ||
| 914 | }, | ||
| 915 | is_snippet: false, | ||
| 916 | sort_text: Some("12"), | ||
| 917 | sort_kind: 3, | ||
| 918 | sort_label: "onPlaying?", | ||
| 919 | }, | ||
| 920 | SortableMatch { | ||
| 921 | string_match: StringMatch { | ||
| 922 | candidate_id: 0, | ||
| 923 | score: 0.16499999999999998, | ||
| 924 | positions: vec![], | ||
| 925 | string: "onDragEnd?".to_string(), | ||
| 926 | }, | ||
| 927 | is_snippet: false, | ||
| 928 | sort_text: Some("12"), | ||
| 929 | sort_kind: 3, | ||
| 930 | sort_label: "onDragEnd?", | ||
| 931 | }, | ||
| 932 | SortableMatch { | ||
| 933 | string_match: StringMatch { | ||
| 934 | candidate_id: 0, | ||
| 935 | score: 0.15000000000000002, | ||
| 936 | positions: vec![], | ||
| 937 | string: "onInvalid?".to_string(), | ||
| 938 | }, | ||
| 939 | is_snippet: false, | ||
| 940 | sort_text: Some("12"), | ||
| 941 | sort_kind: 3, | ||
| 942 | sort_label: "onInvalid?", | ||
| 943 | }, | ||
| 944 | SortableMatch { | ||
| 945 | string_match: StringMatch { | ||
| 946 | candidate_id: 0, | ||
| 947 | score: 0.15, | ||
| 948 | positions: vec![], | ||
| 949 | string: "onDragOver?".to_string(), | ||
| 950 | }, | ||
| 951 | is_snippet: false, | ||
| 952 | sort_text: Some("12"), | ||
| 953 | sort_kind: 3, | ||
| 954 | sort_label: "onDragOver?", | ||
| 955 | }, | ||
| 956 | SortableMatch { | ||
| 957 | string_match: StringMatch { | ||
| 958 | candidate_id: 0, | ||
| 959 | score: 0.15, | ||
| 960 | positions: vec![], | ||
| 961 | string: "onDragExit?".to_string(), | ||
| 962 | }, | ||
| 963 | is_snippet: false, | ||
| 964 | sort_text: Some("12"), | ||
| 965 | sort_kind: 3, | ||
| 966 | sort_label: "onDragExit?", | ||
| 967 | }, | ||
| 968 | SortableMatch { | ||
| 969 | string_match: StringMatch { | ||
| 970 | candidate_id: 0, | ||
| 971 | score: 0.14285714285714285, | ||
| 972 | positions: vec![], | ||
| 973 | string: "onAnimationIteration?".to_string(), | ||
| 974 | }, | ||
| 975 | is_snippet: false, | ||
| 976 | sort_text: Some("12"), | ||
| 977 | sort_kind: 3, | ||
| 978 | sort_label: "onAnimationIteration?", | ||
| 979 | }, | ||
| 980 | SortableMatch { | ||
| 981 | string_match: StringMatch { | ||
| 982 | candidate_id: 0, | ||
| 983 | score: 0.13846153846153847, | ||
| 984 | positions: vec![], | ||
| 985 | string: "onRateChange?".to_string(), | ||
| 986 | }, | ||
| 987 | is_snippet: false, | ||
| 988 | sort_text: Some("12"), | ||
| 989 | sort_kind: 3, | ||
| 990 | sort_label: "onRateChange?", | ||
| 991 | }, | ||
| 992 | SortableMatch { | ||
| 993 | string_match: StringMatch { | ||
| 994 | candidate_id: 0, | ||
| 995 | score: 0.13749999999999996, | ||
| 996 | positions: vec![], | ||
| 997 | string: "onLoadStart?".to_string(), | ||
| 998 | }, | ||
| 999 | is_snippet: false, | ||
| 1000 | sort_text: Some("12"), | ||
| 1001 | sort_kind: 3, | ||
| 1002 | sort_label: "onLoadStart?", | ||
| 1003 | }, | ||
| 1004 | SortableMatch { | ||
| 1005 | string_match: StringMatch { | ||
| 1006 | candidate_id: 0, | ||
| 1007 | score: 0.13749999999999996, | ||
| 1008 | positions: vec![], | ||
| 1009 | string: "onDragStart?".to_string(), | ||
| 1010 | }, | ||
| 1011 | is_snippet: false, | ||
| 1012 | sort_text: Some("12"), | ||
| 1013 | sort_kind: 3, | ||
| 1014 | sort_label: "onDragStart?", | ||
| 1015 | }, | ||
| 1016 | SortableMatch { | ||
| 1017 | string_match: StringMatch { | ||
| 1018 | candidate_id: 0, | ||
| 1019 | score: 0.13749999999999996, | ||
| 1020 | positions: vec![], | ||
| 1021 | string: "onDragLeave?".to_string(), | ||
| 1022 | }, | ||
| 1023 | is_snippet: false, | ||
| 1024 | sort_text: Some("12"), | ||
| 1025 | sort_kind: 3, | ||
| 1026 | sort_label: "onDragLeave?", | ||
| 1027 | }, | ||
| 1028 | SortableMatch { | ||
| 1029 | string_match: StringMatch { | ||
| 1030 | candidate_id: 0, | ||
| 1031 | score: 0.13749999999999996, | ||
| 1032 | positions: vec![], | ||
| 1033 | string: "onDragEnter?".to_string(), | ||
| 1034 | }, | ||
| 1035 | is_snippet: false, | ||
| 1036 | sort_text: Some("12"), | ||
| 1037 | sort_kind: 3, | ||
| 1038 | sort_label: "onDragEnter?", | ||
| 1039 | }, | ||
| 1040 | SortableMatch { | ||
| 1041 | string_match: StringMatch { | ||
| 1042 | candidate_id: 0, | ||
| 1043 | score: 0.13636363636363635, | ||
| 1044 | positions: vec![], | ||
| 1045 | string: "onAnimationEndCapture?".to_string(), | ||
| 1046 | }, | ||
| 1047 | is_snippet: false, | ||
| 1048 | sort_text: Some("12"), | ||
| 1049 | sort_kind: 3, | ||
| 1050 | sort_label: "onAnimationEndCapture?", | ||
| 1051 | }, | ||
| 1052 | SortableMatch { | ||
| 1053 | string_match: StringMatch { | ||
| 1054 | candidate_id: 0, | ||
| 1055 | score: 0.12692307692307692, | ||
| 1056 | positions: vec![], | ||
| 1057 | string: "onLoadedData?".to_string(), | ||
| 1058 | }, | ||
| 1059 | is_snippet: false, | ||
| 1060 | sort_text: Some("12"), | ||
| 1061 | sort_kind: 3, | ||
| 1062 | sort_label: "onLoadedData?", | ||
| 1063 | }, | ||
| 1064 | ]; | 173 | ]; |
| 1065 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 174 | let matches = sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await; |
| 1066 | assert_eq!( | 175 | assert_eq!(matches[0], "onAbort?"); |
| 1067 | matches | 176 | assert_eq!(matches[1], "onAuxClick?"); |
| 1068 | .iter() | ||
| 1069 | .take(12) | ||
| 1070 | .map(|m| m.string_match.string.as_str()) | ||
| 1071 | .collect::<Vec<&str>>(), | ||
| 1072 | vec![ | ||
| 1073 | "onAbort?", | ||
| 1074 | "onAuxClick?", | ||
| 1075 | "onAbortCapture?", | ||
| 1076 | "onAnimationEnd?", | ||
| 1077 | "onAnimationStart?", | ||
| 1078 | "onAuxClickCapture?", | ||
| 1079 | "onAnimationIteration?", | ||
| 1080 | "onAnimationEndCapture?", | ||
| 1081 | "onDrag?", | ||
| 1082 | "onLoad?", | ||
| 1083 | "onPlay?", | ||
| 1084 | "onPaste?", | ||
| 1085 | ] | ||
| 1086 | ); | ||
| 1087 | } | 177 | } |
| 1088 | 178 | ||
| 1089 | #[gpui::test] | 179 | #[gpui::test] |
| 1090 | fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) { | 180 | async fn test_sort_matches_for_snippets(cx: &mut TestAppContext) { |
| 1091 | // Case 1: "prin" | 181 | // Case 1: "prin" |
| 1092 | let query: Option<&str> = Some("prin"); | 182 | let completions = vec![ |
| 1093 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 183 | CompletionBuilder::constant("println", "80000000"), |
| 1094 | SortableMatch { | 184 | CompletionBuilder::snippet("println!(…)", "80000000"), |
| 1095 | string_match: StringMatch { | ||
| 1096 | candidate_id: 0, | ||
| 1097 | score: 0.2, | ||
| 1098 | positions: vec![], | ||
| 1099 | string: "println".to_string(), | ||
| 1100 | }, | ||
| 1101 | is_snippet: false, | ||
| 1102 | sort_text: Some("80000000"), | ||
| 1103 | sort_kind: 2, | ||
| 1104 | sort_label: "println", | ||
| 1105 | }, | ||
| 1106 | SortableMatch { | ||
| 1107 | string_match: StringMatch { | ||
| 1108 | candidate_id: 0, | ||
| 1109 | score: 0.2, | ||
| 1110 | positions: vec![], | ||
| 1111 | string: "println!(…)".to_string(), | ||
| 1112 | }, | ||
| 1113 | is_snippet: true, | ||
| 1114 | sort_text: Some("80000000"), | ||
| 1115 | sort_kind: 2, | ||
| 1116 | sort_label: "println!(…)", | ||
| 1117 | }, | ||
| 1118 | ]; | 185 | ]; |
| 1119 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | 186 | let matches = sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await; |
| 1120 | assert_eq!( | 187 | assert_eq!(matches[0], "println!(…)"); |
| 1121 | matches[0].string_match.string.as_str(), | ||
| 1122 | "println!(…)", | ||
| 1123 | "Match order not expected" | ||
| 1124 | ); | ||
| 1125 | } | 188 | } |
| 1126 | 189 | ||
| 1127 | #[gpui::test] | 190 | #[gpui::test] |
| 1128 | fn test_sort_matches_for_exact_match(_cx: &mut TestAppContext) { | 191 | async fn test_sort_matches_for_exact_match(cx: &mut TestAppContext) { |
| 1129 | // Case 1: "set_text" | 192 | // Case 1: "set_text" |
| 1130 | let query: Option<&str> = Some("set_text"); | 193 | let completions = vec![ |
| 1131 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 194 | CompletionBuilder::function("set_text", "7fffffff"), |
| 1132 | SortableMatch { | 195 | CompletionBuilder::function("set_placeholder_text", "7fffffff"), |
| 1133 | string_match: StringMatch { | 196 | CompletionBuilder::function("set_text_style_refinement", "7fffffff"), |
| 1134 | candidate_id: 0, | 197 | CompletionBuilder::function("set_context_menu_options", "7fffffff"), |
| 1135 | score: 1.0, | 198 | CompletionBuilder::function("select_to_next_word_end", "7fffffff"), |
| 1136 | positions: vec![], | 199 | CompletionBuilder::function("select_to_next_subword_end", "7fffffff"), |
| 1137 | string: "set_text".to_string(), | 200 | CompletionBuilder::function("set_custom_context_menu", "7fffffff"), |
| 1138 | }, | 201 | CompletionBuilder::function("select_to_end_of_excerpt", "7fffffff"), |
| 1139 | is_snippet: false, | 202 | CompletionBuilder::function("select_to_start_of_excerpt", "7fffffff"), |
| 1140 | sort_text: Some("7fffffff"), | 203 | CompletionBuilder::function("select_to_start_of_next_excerpt", "7fffffff"), |
| 1141 | sort_kind: 3, | 204 | CompletionBuilder::function("select_to_end_of_previous_excerpt", "7fffffff"), |
| 1142 | sort_label: "set_text", | ||
| 1143 | }, | ||
| 1144 | SortableMatch { | ||
| 1145 | string_match: StringMatch { | ||
| 1146 | candidate_id: 0, | ||
| 1147 | score: 0.32000000000000006, | ||
| 1148 | positions: vec![], | ||
| 1149 | string: "set_placeholder_text".to_string(), | ||
| 1150 | }, | ||
| 1151 | is_snippet: false, | ||
| 1152 | sort_text: Some("7fffffff"), | ||
| 1153 | sort_kind: 3, | ||
| 1154 | sort_label: "set_placeholder_text", | ||
| 1155 | }, | ||
| 1156 | SortableMatch { | ||
| 1157 | string_match: StringMatch { | ||
| 1158 | candidate_id: 0, | ||
| 1159 | score: 0.32, | ||
| 1160 | positions: vec![], | ||
| 1161 | string: "set_text_style_refinement".to_string(), | ||
| 1162 | }, | ||
| 1163 | is_snippet: false, | ||
| 1164 | sort_text: Some("7fffffff"), | ||
| 1165 | sort_kind: 3, | ||
| 1166 | sort_label: "set_text_style_refinement", | ||
| 1167 | }, | ||
| 1168 | SortableMatch { | ||
| 1169 | string_match: StringMatch { | ||
| 1170 | candidate_id: 0, | ||
| 1171 | score: 0.16666666666666666, | ||
| 1172 | positions: vec![], | ||
| 1173 | string: "set_context_menu_options".to_string(), | ||
| 1174 | }, | ||
| 1175 | is_snippet: false, | ||
| 1176 | sort_text: Some("7fffffff"), | ||
| 1177 | sort_kind: 3, | ||
| 1178 | sort_label: "set_context_menu_options", | ||
| 1179 | }, | ||
| 1180 | SortableMatch { | ||
| 1181 | string_match: StringMatch { | ||
| 1182 | candidate_id: 0, | ||
| 1183 | score: 0.08695652173913043, | ||
| 1184 | positions: vec![], | ||
| 1185 | string: "select_to_next_word_end".to_string(), | ||
| 1186 | }, | ||
| 1187 | is_snippet: false, | ||
| 1188 | sort_text: Some("7fffffff"), | ||
| 1189 | sort_kind: 3, | ||
| 1190 | sort_label: "select_to_next_word_end", | ||
| 1191 | }, | ||
| 1192 | SortableMatch { | ||
| 1193 | string_match: StringMatch { | ||
| 1194 | candidate_id: 0, | ||
| 1195 | score: 0.07692307692307693, | ||
| 1196 | positions: vec![], | ||
| 1197 | string: "select_to_next_subword_end".to_string(), | ||
| 1198 | }, | ||
| 1199 | is_snippet: false, | ||
| 1200 | sort_text: Some("7fffffff"), | ||
| 1201 | sort_kind: 3, | ||
| 1202 | sort_label: "select_to_next_subword_end", | ||
| 1203 | }, | ||
| 1204 | SortableMatch { | ||
| 1205 | string_match: StringMatch { | ||
| 1206 | candidate_id: 0, | ||
| 1207 | score: 0.06956521739130435, | ||
| 1208 | positions: vec![], | ||
| 1209 | string: "set_custom_context_menu".to_string(), | ||
| 1210 | }, | ||
| 1211 | is_snippet: false, | ||
| 1212 | sort_text: Some("7fffffff"), | ||
| 1213 | sort_kind: 3, | ||
| 1214 | sort_label: "set_custom_context_menu", | ||
| 1215 | }, | ||
| 1216 | SortableMatch { | ||
| 1217 | string_match: StringMatch { | ||
| 1218 | candidate_id: 0, | ||
| 1219 | score: 0.06, | ||
| 1220 | positions: vec![], | ||
| 1221 | string: "select_to_end_of_excerpt".to_string(), | ||
| 1222 | }, | ||
| 1223 | is_snippet: false, | ||
| 1224 | sort_text: Some("7fffffff"), | ||
| 1225 | sort_kind: 3, | ||
| 1226 | sort_label: "select_to_end_of_excerpt", | ||
| 1227 | }, | ||
| 1228 | SortableMatch { | ||
| 1229 | string_match: StringMatch { | ||
| 1230 | candidate_id: 0, | ||
| 1231 | score: 0.055384615384615386, | ||
| 1232 | positions: vec![], | ||
| 1233 | string: "select_to_start_of_excerpt".to_string(), | ||
| 1234 | }, | ||
| 1235 | is_snippet: false, | ||
| 1236 | sort_text: Some("7fffffff"), | ||
| 1237 | sort_kind: 3, | ||
| 1238 | sort_label: "select_to_start_of_excerpt", | ||
| 1239 | }, | ||
| 1240 | SortableMatch { | ||
| 1241 | string_match: StringMatch { | ||
| 1242 | candidate_id: 0, | ||
| 1243 | score: 0.0464516129032258, | ||
| 1244 | positions: vec![], | ||
| 1245 | string: "select_to_start_of_next_excerpt".to_string(), | ||
| 1246 | }, | ||
| 1247 | is_snippet: false, | ||
| 1248 | sort_text: Some("7fffffff"), | ||
| 1249 | sort_kind: 3, | ||
| 1250 | sort_label: "select_to_start_of_next_excerpt", | ||
| 1251 | }, | ||
| 1252 | SortableMatch { | ||
| 1253 | string_match: StringMatch { | ||
| 1254 | candidate_id: 0, | ||
| 1255 | score: 0.04363636363636363, | ||
| 1256 | positions: vec![], | ||
| 1257 | string: "select_to_end_of_previous_excerpt".to_string(), | ||
| 1258 | }, | ||
| 1259 | is_snippet: false, | ||
| 1260 | sort_text: Some("7fffffff"), | ||
| 1261 | sort_kind: 3, | ||
| 1262 | sort_label: "select_to_end_of_previous_excerpt", | ||
| 1263 | }, | ||
| 1264 | ]; | 205 | ]; |
| 1265 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | 206 | let matches = sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await; |
| 1266 | assert_eq!( | 207 | assert_eq!(matches[0], "set_text"); |
| 1267 | matches | 208 | assert_eq!(matches[1], "set_text_style_refinement"); |
| 1268 | .iter() | 209 | assert_eq!(matches[2], "set_context_menu_options"); |
| 1269 | .map(|m| m.string_match.string.as_str()) | ||
| 1270 | .collect::<Vec<&str>>(), | ||
| 1271 | vec![ | ||
| 1272 | "set_text", | ||
| 1273 | "set_text_style_refinement", | ||
| 1274 | "set_placeholder_text", | ||
| 1275 | "set_context_menu_options", | ||
| 1276 | "set_custom_context_menu", | ||
| 1277 | "select_to_next_word_end", | ||
| 1278 | "select_to_next_subword_end", | ||
| 1279 | "select_to_end_of_excerpt", | ||
| 1280 | "select_to_start_of_excerpt", | ||
| 1281 | "select_to_start_of_next_excerpt", | ||
| 1282 | "select_to_end_of_previous_excerpt", | ||
| 1283 | ] | ||
| 1284 | ); | ||
| 1285 | } | 210 | } |
| 1286 | 211 | ||
| 1287 | #[gpui::test] | 212 | #[gpui::test] |
| 1288 | fn test_sort_matches_for_prefix_matches(_cx: &mut TestAppContext) { | 213 | async fn test_sort_matches_for_prefix_matches(cx: &mut TestAppContext) { |
| 1289 | // Case 1: "set" | 214 | // Case 1: "set" |
| 1290 | let query: Option<&str> = Some("set"); | 215 | let completions = vec![ |
| 1291 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 216 | CompletionBuilder::function("select_to_beginning", "7fffffff"), |
| 1292 | SortableMatch { | 217 | CompletionBuilder::function("set_collapse_matches", "7fffffff"), |
| 1293 | string_match: StringMatch { | 218 | CompletionBuilder::function("set_autoindent", "7fffffff"), |
| 1294 | candidate_id: 0, | 219 | CompletionBuilder::function("set_all_diagnostics_active", "7fffffff"), |
| 1295 | score: 0.12631578947368421, | 220 | CompletionBuilder::function("select_to_end_of_line", "7fffffff"), |
| 1296 | positions: vec![], | 221 | CompletionBuilder::function("select_all", "7fffffff"), |
| 1297 | string: "select_to_beginning".to_string(), | 222 | CompletionBuilder::function("select_line", "7fffffff"), |
| 1298 | }, | 223 | CompletionBuilder::function("select_left", "7fffffff"), |
| 1299 | is_snippet: false, | 224 | CompletionBuilder::function("select_down", "7fffffff"), |
| 1300 | sort_text: Some("7fffffff"), | ||
| 1301 | sort_kind: 3, | ||
| 1302 | sort_label: "select_to_beginning", | ||
| 1303 | }, | ||
| 1304 | SortableMatch { | ||
| 1305 | string_match: StringMatch { | ||
| 1306 | candidate_id: 0, | ||
| 1307 | score: 0.15000000000000002, | ||
| 1308 | positions: vec![], | ||
| 1309 | string: "set_collapse_matches".to_string(), | ||
| 1310 | }, | ||
| 1311 | is_snippet: false, | ||
| 1312 | sort_text: Some("7fffffff"), | ||
| 1313 | sort_kind: 3, | ||
| 1314 | sort_label: "set_collapse_matches", | ||
| 1315 | }, | ||
| 1316 | SortableMatch { | ||
| 1317 | string_match: StringMatch { | ||
| 1318 | candidate_id: 0, | ||
| 1319 | score: 0.21428571428571427, | ||
| 1320 | positions: vec![], | ||
| 1321 | string: "set_autoindent".to_string(), | ||
| 1322 | }, | ||
| 1323 | is_snippet: false, | ||
| 1324 | sort_text: Some("7fffffff"), | ||
| 1325 | sort_kind: 3, | ||
| 1326 | sort_label: "set_autoindent", | ||
| 1327 | }, | ||
| 1328 | SortableMatch { | ||
| 1329 | string_match: StringMatch { | ||
| 1330 | candidate_id: 0, | ||
| 1331 | score: 0.11538461538461539, | ||
| 1332 | positions: vec![], | ||
| 1333 | string: "set_all_diagnostics_active".to_string(), | ||
| 1334 | }, | ||
| 1335 | is_snippet: false, | ||
| 1336 | sort_text: Some("7fffffff"), | ||
| 1337 | sort_kind: 3, | ||
| 1338 | sort_label: "set_all_diagnostics_active", | ||
| 1339 | }, | ||
| 1340 | SortableMatch { | ||
| 1341 | string_match: StringMatch { | ||
| 1342 | candidate_id: 0, | ||
| 1343 | score: 0.1142857142857143, | ||
| 1344 | positions: vec![], | ||
| 1345 | string: "select_to_end_of_line".to_string(), | ||
| 1346 | }, | ||
| 1347 | is_snippet: false, | ||
| 1348 | sort_text: Some("7fffffff"), | ||
| 1349 | sort_kind: 3, | ||
| 1350 | sort_label: "select_to_end_of_line", | ||
| 1351 | }, | ||
| 1352 | SortableMatch { | ||
| 1353 | string_match: StringMatch { | ||
| 1354 | candidate_id: 0, | ||
| 1355 | score: 0.15000000000000002, | ||
| 1356 | positions: vec![], | ||
| 1357 | string: "select_all".to_string(), | ||
| 1358 | }, | ||
| 1359 | is_snippet: false, | ||
| 1360 | sort_text: Some("7fffffff"), | ||
| 1361 | sort_kind: 3, | ||
| 1362 | sort_label: "select_all", | ||
| 1363 | }, | ||
| 1364 | SortableMatch { | ||
| 1365 | string_match: StringMatch { | ||
| 1366 | candidate_id: 0, | ||
| 1367 | score: 0.13636363636363635, | ||
| 1368 | positions: vec![], | ||
| 1369 | string: "select_line".to_string(), | ||
| 1370 | }, | ||
| 1371 | is_snippet: false, | ||
| 1372 | sort_text: Some("7fffffff"), | ||
| 1373 | sort_kind: 3, | ||
| 1374 | sort_label: "select_line", | ||
| 1375 | }, | ||
| 1376 | SortableMatch { | ||
| 1377 | string_match: StringMatch { | ||
| 1378 | candidate_id: 0, | ||
| 1379 | score: 0.13636363636363635, | ||
| 1380 | positions: vec![], | ||
| 1381 | string: "select_left".to_string(), | ||
| 1382 | }, | ||
| 1383 | is_snippet: false, | ||
| 1384 | sort_text: Some("7fffffff"), | ||
| 1385 | sort_kind: 3, | ||
| 1386 | sort_label: "select_left", | ||
| 1387 | }, | ||
| 1388 | SortableMatch { | ||
| 1389 | string_match: StringMatch { | ||
| 1390 | candidate_id: 0, | ||
| 1391 | score: 0.13636363636363635, | ||
| 1392 | positions: vec![], | ||
| 1393 | string: "select_down".to_string(), | ||
| 1394 | }, | ||
| 1395 | is_snippet: false, | ||
| 1396 | sort_text: Some("7fffffff"), | ||
| 1397 | sort_kind: 3, | ||
| 1398 | sort_label: "select_down", | ||
| 1399 | }, | ||
| 1400 | ]; | 225 | ]; |
| 1401 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | 226 | let matches = sort_matches("set", &completions, SnippetSortOrder::Top, cx).await; |
| 1402 | assert_eq!( | 227 | assert_eq!(matches[0], "set_autoindent"); |
| 1403 | matches | 228 | assert_eq!(matches[1], "set_collapse_matches"); |
| 1404 | .iter() | 229 | assert_eq!(matches[2], "set_all_diagnostics_active"); |
| 1405 | .map(|m| m.string_match.string.as_str()) | ||
| 1406 | .collect::<Vec<&str>>(), | ||
| 1407 | vec![ | ||
| 1408 | "set_autoindent", | ||
| 1409 | "set_collapse_matches", | ||
| 1410 | "set_all_diagnostics_active", | ||
| 1411 | "select_all", | ||
| 1412 | "select_down", | ||
| 1413 | "select_left", | ||
| 1414 | "select_line", | ||
| 1415 | "select_to_beginning", | ||
| 1416 | "select_to_end_of_line", | ||
| 1417 | ] | ||
| 1418 | ); | ||
| 1419 | } | 230 | } |
| 1420 | 231 | ||
| 1421 | #[gpui::test] | 232 | #[gpui::test] |
| 1422 | fn test_sort_matches_for_await(_cx: &mut TestAppContext) { | 233 | async fn test_sort_matches_for_await(cx: &mut TestAppContext) { |
| 1423 | // Case 1: "awa" | 234 | // Case 1: "awa" |
| 1424 | let query: Option<&str> = Some("awa"); | 235 | let completions = vec![ |
| 1425 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 236 | CompletionBuilder::keyword("await", "7fffffff"), |
| 1426 | SortableMatch { | 237 | CompletionBuilder::function("await.ne", "80000010"), |
| 1427 | string_match: StringMatch { | 238 | CompletionBuilder::function("await.eq", "80000010"), |
| 1428 | candidate_id: 0, | 239 | CompletionBuilder::function("await.or", "7ffffff8"), |
| 1429 | score: 0.6000000000000001, | 240 | CompletionBuilder::function("await.zip", "80000006"), |
| 1430 | positions: vec![], | 241 | CompletionBuilder::function("await.xor", "7ffffff8"), |
| 1431 | string: "await".to_string(), | 242 | CompletionBuilder::function("await.and", "80000006"), |
| 1432 | }, | 243 | CompletionBuilder::function("await.map", "80000006"), |
| 1433 | is_snippet: false, | 244 | CompletionBuilder::function("await.take", "7ffffff8"), |
| 1434 | sort_text: Some("7fffffff"), | ||
| 1435 | sort_kind: 0, | ||
| 1436 | sort_label: "await", | ||
| 1437 | }, | ||
| 1438 | SortableMatch { | ||
| 1439 | string_match: StringMatch { | ||
| 1440 | candidate_id: 35, | ||
| 1441 | score: 0.375, | ||
| 1442 | positions: vec![], | ||
| 1443 | string: "await.ne".to_string(), | ||
| 1444 | }, | ||
| 1445 | is_snippet: false, | ||
| 1446 | sort_text: Some("80000010"), | ||
| 1447 | sort_kind: 3, | ||
| 1448 | sort_label: "await.ne", | ||
| 1449 | }, | ||
| 1450 | SortableMatch { | ||
| 1451 | string_match: StringMatch { | ||
| 1452 | candidate_id: 34, | ||
| 1453 | score: 0.375, | ||
| 1454 | positions: vec![], | ||
| 1455 | string: "await.eq".to_string(), | ||
| 1456 | }, | ||
| 1457 | is_snippet: false, | ||
| 1458 | sort_text: Some("80000010"), | ||
| 1459 | sort_kind: 3, | ||
| 1460 | sort_label: "await.eq", | ||
| 1461 | }, | ||
| 1462 | SortableMatch { | ||
| 1463 | string_match: StringMatch { | ||
| 1464 | candidate_id: 18, | ||
| 1465 | score: 0.375, | ||
| 1466 | positions: vec![], | ||
| 1467 | string: "await.or".to_string(), | ||
| 1468 | }, | ||
| 1469 | is_snippet: false, | ||
| 1470 | sort_text: Some("7ffffff8"), | ||
| 1471 | sort_kind: 3, | ||
| 1472 | sort_label: "await.or", | ||
| 1473 | }, | ||
| 1474 | SortableMatch { | ||
| 1475 | string_match: StringMatch { | ||
| 1476 | candidate_id: 21, | ||
| 1477 | score: 0.3333333333333333, | ||
| 1478 | positions: vec![], | ||
| 1479 | string: "await.zip".to_string(), | ||
| 1480 | }, | ||
| 1481 | is_snippet: false, | ||
| 1482 | sort_text: Some("80000006"), | ||
| 1483 | sort_kind: 3, | ||
| 1484 | sort_label: "await.zip", | ||
| 1485 | }, | ||
| 1486 | SortableMatch { | ||
| 1487 | string_match: StringMatch { | ||
| 1488 | candidate_id: 20, | ||
| 1489 | score: 0.3333333333333333, | ||
| 1490 | positions: vec![], | ||
| 1491 | string: "await.xor".to_string(), | ||
| 1492 | }, | ||
| 1493 | is_snippet: false, | ||
| 1494 | sort_text: Some("7ffffff8"), | ||
| 1495 | sort_kind: 3, | ||
| 1496 | sort_label: "await.xor", | ||
| 1497 | }, | ||
| 1498 | SortableMatch { | ||
| 1499 | string_match: StringMatch { | ||
| 1500 | candidate_id: 15, | ||
| 1501 | score: 0.3333333333333333, | ||
| 1502 | positions: vec![], | ||
| 1503 | string: "await.and".to_string(), | ||
| 1504 | }, | ||
| 1505 | is_snippet: false, | ||
| 1506 | sort_text: Some("80000006"), | ||
| 1507 | sort_kind: 3, | ||
| 1508 | sort_label: "await.and", | ||
| 1509 | }, | ||
| 1510 | SortableMatch { | ||
| 1511 | string_match: StringMatch { | ||
| 1512 | candidate_id: 9, | ||
| 1513 | score: 0.3333333333333333, | ||
| 1514 | positions: vec![], | ||
| 1515 | string: "await.map".to_string(), | ||
| 1516 | }, | ||
| 1517 | is_snippet: false, | ||
| 1518 | sort_text: Some("80000006"), | ||
| 1519 | sort_kind: 3, | ||
| 1520 | sort_label: "await.map", | ||
| 1521 | }, | ||
| 1522 | SortableMatch { | ||
| 1523 | string_match: StringMatch { | ||
| 1524 | candidate_id: 47, | ||
| 1525 | score: 0.30000000000000004, | ||
| 1526 | positions: vec![], | ||
| 1527 | string: "await.take".to_string(), | ||
| 1528 | }, | ||
| 1529 | is_snippet: false, | ||
| 1530 | sort_text: Some("7ffffff8"), | ||
| 1531 | sort_kind: 3, | ||
| 1532 | sort_label: "await.take", | ||
| 1533 | }, | ||
| 1534 | ]; | 245 | ]; |
| 1535 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | 246 | let matches = sort_matches("awa", &completions, SnippetSortOrder::Top, cx).await; |
| 1536 | assert_eq!( | 247 | assert_eq!(matches[0], "await"); |
| 1537 | matches | 248 | |
| 1538 | .iter() | ||
| 1539 | .map(|m| m.string_match.string.as_str()) | ||
| 1540 | .collect::<Vec<&str>>(), | ||
| 1541 | vec![ | ||
| 1542 | "await", | ||
| 1543 | "await.or", | ||
| 1544 | "await.eq", | ||
| 1545 | "await.ne", | ||
| 1546 | "await.xor", | ||
| 1547 | "await.take", | ||
| 1548 | "await.and", | ||
| 1549 | "await.map", | ||
| 1550 | "await.zip" | ||
| 1551 | ] | ||
| 1552 | ); | ||
| 1553 | // Case 2: "await" | 249 | // Case 2: "await" |
| 1554 | let query: Option<&str> = Some("await"); | 250 | let completions = vec![ |
| 1555 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 251 | CompletionBuilder::keyword("await", "7fffffff"), |
| 1556 | SortableMatch { | 252 | CompletionBuilder::function("await.ne", "80000010"), |
| 1557 | string_match: StringMatch { | 253 | CompletionBuilder::function("await.eq", "80000010"), |
| 1558 | candidate_id: 0, | 254 | CompletionBuilder::function("await.or", "7ffffff8"), |
| 1559 | score: 1.0, | 255 | CompletionBuilder::function("await.zip", "80000006"), |
| 1560 | positions: vec![], | 256 | CompletionBuilder::function("await.xor", "7ffffff8"), |
| 1561 | string: "await".to_string(), | 257 | CompletionBuilder::function("await.and", "80000006"), |
| 1562 | }, | 258 | CompletionBuilder::function("await.map", "80000006"), |
| 1563 | is_snippet: false, | 259 | CompletionBuilder::function("await.take", "7ffffff8"), |
| 1564 | sort_text: Some("7fffffff"), | ||
| 1565 | sort_kind: 0, | ||
| 1566 | sort_label: "await", | ||
| 1567 | }, | ||
| 1568 | SortableMatch { | ||
| 1569 | string_match: StringMatch { | ||
| 1570 | candidate_id: 35, | ||
| 1571 | score: 0.625, | ||
| 1572 | positions: vec![], | ||
| 1573 | string: "await.ne".to_string(), | ||
| 1574 | }, | ||
| 1575 | is_snippet: false, | ||
| 1576 | sort_text: Some("80000010"), | ||
| 1577 | sort_kind: 3, | ||
| 1578 | sort_label: "await.ne", | ||
| 1579 | }, | ||
| 1580 | SortableMatch { | ||
| 1581 | string_match: StringMatch { | ||
| 1582 | candidate_id: 34, | ||
| 1583 | score: 0.625, | ||
| 1584 | positions: vec![], | ||
| 1585 | string: "await.eq".to_string(), | ||
| 1586 | }, | ||
| 1587 | is_snippet: false, | ||
| 1588 | sort_text: Some("80000010"), | ||
| 1589 | sort_kind: 3, | ||
| 1590 | sort_label: "await.eq", | ||
| 1591 | }, | ||
| 1592 | SortableMatch { | ||
| 1593 | string_match: StringMatch { | ||
| 1594 | candidate_id: 18, | ||
| 1595 | score: 0.625, | ||
| 1596 | positions: vec![], | ||
| 1597 | string: "await.or".to_string(), | ||
| 1598 | }, | ||
| 1599 | is_snippet: false, | ||
| 1600 | sort_text: Some("7ffffff8"), | ||
| 1601 | sort_kind: 3, | ||
| 1602 | sort_label: "await.or", | ||
| 1603 | }, | ||
| 1604 | SortableMatch { | ||
| 1605 | string_match: StringMatch { | ||
| 1606 | candidate_id: 21, | ||
| 1607 | score: 0.5555555555555556, | ||
| 1608 | positions: vec![], | ||
| 1609 | string: "await.zip".to_string(), | ||
| 1610 | }, | ||
| 1611 | is_snippet: false, | ||
| 1612 | sort_text: Some("80000006"), | ||
| 1613 | sort_kind: 3, | ||
| 1614 | sort_label: "await.zip", | ||
| 1615 | }, | ||
| 1616 | SortableMatch { | ||
| 1617 | string_match: StringMatch { | ||
| 1618 | candidate_id: 20, | ||
| 1619 | score: 0.5555555555555556, | ||
| 1620 | positions: vec![], | ||
| 1621 | string: "await.xor".to_string(), | ||
| 1622 | }, | ||
| 1623 | is_snippet: false, | ||
| 1624 | sort_text: Some("7ffffff8"), | ||
| 1625 | sort_kind: 3, | ||
| 1626 | sort_label: "await.xor", | ||
| 1627 | }, | ||
| 1628 | SortableMatch { | ||
| 1629 | string_match: StringMatch { | ||
| 1630 | candidate_id: 15, | ||
| 1631 | score: 0.5555555555555556, | ||
| 1632 | positions: vec![], | ||
| 1633 | string: "await.and".to_string(), | ||
| 1634 | }, | ||
| 1635 | is_snippet: false, | ||
| 1636 | sort_text: Some("80000006"), | ||
| 1637 | sort_kind: 3, | ||
| 1638 | sort_label: "await.and", | ||
| 1639 | }, | ||
| 1640 | SortableMatch { | ||
| 1641 | string_match: StringMatch { | ||
| 1642 | candidate_id: 9, | ||
| 1643 | score: 0.5555555555555556, | ||
| 1644 | positions: vec![], | ||
| 1645 | string: "await.map".to_string(), | ||
| 1646 | }, | ||
| 1647 | is_snippet: false, | ||
| 1648 | sort_text: Some("80000006"), | ||
| 1649 | sort_kind: 3, | ||
| 1650 | sort_label: "await.map", | ||
| 1651 | }, | ||
| 1652 | SortableMatch { | ||
| 1653 | string_match: StringMatch { | ||
| 1654 | candidate_id: 47, | ||
| 1655 | score: 0.5, | ||
| 1656 | positions: vec![], | ||
| 1657 | string: "await.take".to_string(), | ||
| 1658 | }, | ||
| 1659 | is_snippet: false, | ||
| 1660 | sort_text: Some("7ffffff8"), | ||
| 1661 | sort_kind: 3, | ||
| 1662 | sort_label: "await.take", | ||
| 1663 | }, | ||
| 1664 | ]; | 260 | ]; |
| 1665 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | 261 | let matches = sort_matches("await", &completions, SnippetSortOrder::Top, cx).await; |
| 1666 | assert_eq!( | 262 | assert_eq!(matches[0], "await"); |
| 1667 | matches | ||
| 1668 | .iter() | ||
| 1669 | .map(|m| m.string_match.string.as_str()) | ||
| 1670 | .collect::<Vec<&str>>(), | ||
| 1671 | vec![ | ||
| 1672 | "await", | ||
| 1673 | "await.or", | ||
| 1674 | "await.eq", | ||
| 1675 | "await.ne", | ||
| 1676 | "await.xor", | ||
| 1677 | "await.take", | ||
| 1678 | "await.and", | ||
| 1679 | "await.map", | ||
| 1680 | "await.zip" | ||
| 1681 | ] | ||
| 1682 | ); | ||
| 1683 | } | 263 | } |
| 1684 | 264 | ||
| 1685 | #[gpui::test] | 265 | #[gpui::test] |
| 1686 | fn test_sort_matches_for_python_init(_cx: &mut TestAppContext) { | 266 | async fn test_sort_matches_for_python_init(cx: &mut TestAppContext) { |
| 1687 | // Case 1: "__in" | 267 | // Case 1: "__in" |
| 1688 | let query: Option<&str> = Some("__in"); | 268 | let completions = vec![ |
| 1689 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 269 | CompletionBuilder::function("__init__", "05.0003.__init__"), |
| 1690 | SortableMatch { | 270 | CompletionBuilder::function("__init__", "05.0003"), |
| 1691 | string_match: StringMatch { | 271 | CompletionBuilder::function("__instancecheck__", "05.0005.__instancecheck__"), |
| 1692 | candidate_id: 211, | 272 | CompletionBuilder::function("__init_subclass__", "05.0004.__init_subclass__"), |
| 1693 | score: 0.5, | 273 | CompletionBuilder::function("__instancecheck__", "05.0005"), |
| 1694 | positions: vec![], | 274 | CompletionBuilder::function("__init_subclass__", "05.0004"), |
| 1695 | string: "__init__".to_string(), | ||
| 1696 | }, | ||
| 1697 | is_snippet: false, | ||
| 1698 | sort_text: Some("05.0003.__init__"), | ||
| 1699 | sort_kind: 3, | ||
| 1700 | sort_label: "__init__", | ||
| 1701 | }, | ||
| 1702 | SortableMatch { | ||
| 1703 | string_match: StringMatch { | ||
| 1704 | candidate_id: 0, | ||
| 1705 | score: 0.5, | ||
| 1706 | positions: vec![], | ||
| 1707 | string: "__init__".to_string(), | ||
| 1708 | }, | ||
| 1709 | is_snippet: false, | ||
| 1710 | sort_text: Some("05.0003"), | ||
| 1711 | sort_kind: 3, | ||
| 1712 | sort_label: "__init__", | ||
| 1713 | }, | ||
| 1714 | SortableMatch { | ||
| 1715 | string_match: StringMatch { | ||
| 1716 | candidate_id: 215, | ||
| 1717 | score: 0.23529411764705882, | ||
| 1718 | positions: vec![], | ||
| 1719 | string: "__instancecheck__".to_string(), | ||
| 1720 | }, | ||
| 1721 | is_snippet: false, | ||
| 1722 | sort_text: Some("05.0005.__instancecheck__"), | ||
| 1723 | sort_kind: 3, | ||
| 1724 | sort_label: "__instancecheck__", | ||
| 1725 | }, | ||
| 1726 | SortableMatch { | ||
| 1727 | string_match: StringMatch { | ||
| 1728 | candidate_id: 213, | ||
| 1729 | score: 0.23529411764705882, | ||
| 1730 | positions: vec![], | ||
| 1731 | string: "__init_subclass__".to_string(), | ||
| 1732 | }, | ||
| 1733 | is_snippet: false, | ||
| 1734 | sort_text: Some("05.0004.__init_subclass__"), | ||
| 1735 | sort_kind: 3, | ||
| 1736 | sort_label: "__init_subclass__", | ||
| 1737 | }, | ||
| 1738 | SortableMatch { | ||
| 1739 | string_match: StringMatch { | ||
| 1740 | candidate_id: 4, | ||
| 1741 | score: 0.23529411764705882, | ||
| 1742 | positions: vec![], | ||
| 1743 | string: "__instancecheck__".to_string(), | ||
| 1744 | }, | ||
| 1745 | is_snippet: false, | ||
| 1746 | sort_text: Some("05.0005"), | ||
| 1747 | sort_kind: 3, | ||
| 1748 | sort_label: "__instancecheck__", | ||
| 1749 | }, | ||
| 1750 | SortableMatch { | ||
| 1751 | string_match: StringMatch { | ||
| 1752 | candidate_id: 2, | ||
| 1753 | score: 0.23529411764705882, | ||
| 1754 | positions: vec![], | ||
| 1755 | string: "__init_subclass__".to_string(), | ||
| 1756 | }, | ||
| 1757 | is_snippet: false, | ||
| 1758 | sort_text: Some("05.0004"), | ||
| 1759 | sort_kind: 3, | ||
| 1760 | sort_label: "__init_subclass__", | ||
| 1761 | }, | ||
| 1762 | ]; | 275 | ]; |
| 1763 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | 276 | let matches = sort_matches("__in", &completions, SnippetSortOrder::Top, cx).await; |
| 1764 | assert_eq!( | 277 | assert_eq!(matches[0], "__init__"); |
| 1765 | matches | 278 | assert_eq!(matches[1], "__init__"); |
| 1766 | .iter() | 279 | |
| 1767 | .map(|m| m.string_match.string.as_str()) | ||
| 1768 | .collect::<Vec<&str>>(), | ||
| 1769 | vec![ | ||
| 1770 | "__init__", | ||
| 1771 | "__init__", | ||
| 1772 | "__init_subclass__", | ||
| 1773 | "__init_subclass__", | ||
| 1774 | "__instancecheck__", | ||
| 1775 | "__instancecheck__", | ||
| 1776 | ] | ||
| 1777 | ); | ||
| 1778 | // Case 2: "__ini" | 280 | // Case 2: "__ini" |
| 1779 | let query: Option<&str> = Some("__ini"); | 281 | let completions = vec![ |
| 1780 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 282 | CompletionBuilder::function("__init__", "05.0004.__init__"), |
| 1781 | SortableMatch { | 283 | CompletionBuilder::function("__init__", "05.0004"), |
| 1782 | string_match: StringMatch { | 284 | CompletionBuilder::function("__init_subclass__", "05.0003.__init_subclass__"), |
| 1783 | candidate_id: 9, | 285 | CompletionBuilder::function("__init_subclass__", "05.0003"), |
| 1784 | score: 0.625, | ||
| 1785 | positions: vec![], | ||
| 1786 | string: "__init__".to_string(), | ||
| 1787 | }, | ||
| 1788 | is_snippet: false, | ||
| 1789 | sort_text: Some("05.0004.__init__"), | ||
| 1790 | sort_kind: 3, | ||
| 1791 | sort_label: "__init__", | ||
| 1792 | }, | ||
| 1793 | SortableMatch { | ||
| 1794 | string_match: StringMatch { | ||
| 1795 | candidate_id: 0, | ||
| 1796 | score: 0.625, | ||
| 1797 | positions: vec![], | ||
| 1798 | string: "__init__".to_string(), | ||
| 1799 | }, | ||
| 1800 | is_snippet: false, | ||
| 1801 | sort_text: Some("05.0004"), | ||
| 1802 | sort_kind: 3, | ||
| 1803 | sort_label: "__init__", | ||
| 1804 | }, | ||
| 1805 | SortableMatch { | ||
| 1806 | string_match: StringMatch { | ||
| 1807 | candidate_id: 10, | ||
| 1808 | score: 0.29411764705882354, | ||
| 1809 | positions: vec![], | ||
| 1810 | string: "__init_subclass__".to_string(), | ||
| 1811 | }, | ||
| 1812 | is_snippet: false, | ||
| 1813 | sort_text: Some("05.0003.__init_subclass__"), | ||
| 1814 | sort_kind: 3, | ||
| 1815 | sort_label: "__init_subclass__", | ||
| 1816 | }, | ||
| 1817 | SortableMatch { | ||
| 1818 | string_match: StringMatch { | ||
| 1819 | candidate_id: 1, | ||
| 1820 | score: 0.29411764705882354, | ||
| 1821 | positions: vec![], | ||
| 1822 | string: "__init_subclass__".to_string(), | ||
| 1823 | }, | ||
| 1824 | is_snippet: false, | ||
| 1825 | sort_text: Some("05.0003"), | ||
| 1826 | sort_kind: 3, | ||
| 1827 | sort_label: "__init_subclass__", | ||
| 1828 | }, | ||
| 1829 | ]; | 286 | ]; |
| 1830 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | 287 | let matches = sort_matches("__ini", &completions, SnippetSortOrder::Top, cx).await; |
| 1831 | assert_eq!( | 288 | assert_eq!(matches[0], "__init__"); |
| 1832 | matches | 289 | assert_eq!(matches[1], "__init__"); |
| 1833 | .iter() | 290 | |
| 1834 | .map(|m| m.string_match.string.as_str()) | ||
| 1835 | .collect::<Vec<&str>>(), | ||
| 1836 | vec![ | ||
| 1837 | "__init__", | ||
| 1838 | "__init__", | ||
| 1839 | "__init_subclass__", | ||
| 1840 | "__init_subclass__", | ||
| 1841 | ] | ||
| 1842 | ); | ||
| 1843 | // Case 3: "__init" | 291 | // Case 3: "__init" |
| 1844 | let query: Option<&str> = Some("__init"); | 292 | let completions = vec![ |
| 1845 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 293 | CompletionBuilder::function("__init__", "05.0000.__init__"), |
| 1846 | SortableMatch { | 294 | CompletionBuilder::function("__init__", "05.0000"), |
| 1847 | string_match: StringMatch { | 295 | CompletionBuilder::function("__init_subclass__", "05.0001.__init_subclass__"), |
| 1848 | candidate_id: 7, | 296 | CompletionBuilder::function("__init_subclass__", "05.0001"), |
| 1849 | score: 0.75, | ||
| 1850 | positions: vec![], | ||
| 1851 | string: "__init__".to_string(), | ||
| 1852 | }, | ||
| 1853 | is_snippet: false, | ||
| 1854 | sort_text: Some("05.0000.__init__"), | ||
| 1855 | sort_kind: 3, | ||
| 1856 | sort_label: "__init__", | ||
| 1857 | }, | ||
| 1858 | SortableMatch { | ||
| 1859 | string_match: StringMatch { | ||
| 1860 | candidate_id: 0, | ||
| 1861 | score: 0.75, | ||
| 1862 | positions: vec![], | ||
| 1863 | string: "__init__".to_string(), | ||
| 1864 | }, | ||
| 1865 | is_snippet: false, | ||
| 1866 | sort_text: Some("05.0000"), | ||
| 1867 | sort_kind: 3, | ||
| 1868 | sort_label: "__init__", | ||
| 1869 | }, | ||
| 1870 | SortableMatch { | ||
| 1871 | string_match: StringMatch { | ||
| 1872 | candidate_id: 8, | ||
| 1873 | score: 0.3529411764705882, | ||
| 1874 | positions: vec![], | ||
| 1875 | string: "__init_subclass__".to_string(), | ||
| 1876 | }, | ||
| 1877 | is_snippet: false, | ||
| 1878 | sort_text: Some("05.0001.__init_subclass__"), | ||
| 1879 | sort_kind: 3, | ||
| 1880 | sort_label: "__init_subclass__", | ||
| 1881 | }, | ||
| 1882 | SortableMatch { | ||
| 1883 | string_match: StringMatch { | ||
| 1884 | candidate_id: 1, | ||
| 1885 | score: 0.3529411764705882, | ||
| 1886 | positions: vec![], | ||
| 1887 | string: "__init_subclass__".to_string(), | ||
| 1888 | }, | ||
| 1889 | is_snippet: false, | ||
| 1890 | sort_text: Some("05.0001"), | ||
| 1891 | sort_kind: 3, | ||
| 1892 | sort_label: "__init_subclass__", | ||
| 1893 | }, | ||
| 1894 | ]; | 297 | ]; |
| 1895 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | 298 | let matches = sort_matches("__init", &completions, SnippetSortOrder::Top, cx).await; |
| 1896 | assert_eq!( | 299 | assert_eq!(matches[0], "__init__"); |
| 1897 | matches | 300 | assert_eq!(matches[1], "__init__"); |
| 1898 | .iter() | 301 | |
| 1899 | .map(|m| m.string_match.string.as_str()) | ||
| 1900 | .collect::<Vec<&str>>(), | ||
| 1901 | vec![ | ||
| 1902 | "__init__", | ||
| 1903 | "__init__", | ||
| 1904 | "__init_subclass__", | ||
| 1905 | "__init_subclass__", | ||
| 1906 | ] | ||
| 1907 | ); | ||
| 1908 | // Case 4: "__init_" | 302 | // Case 4: "__init_" |
| 1909 | let query: Option<&str> = Some("__init_"); | 303 | let completions = vec![ |
| 1910 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 304 | CompletionBuilder::function("__init__", "11.9999.__init__"), |
| 1911 | SortableMatch { | 305 | CompletionBuilder::function("__init__", "11.9999"), |
| 1912 | string_match: StringMatch { | 306 | CompletionBuilder::function("__init_subclass__", "05.0000.__init_subclass__"), |
| 1913 | candidate_id: 4, | 307 | CompletionBuilder::function("__init_subclass__", "05.0000"), |
| 1914 | score: 0.875, | ||
| 1915 | positions: vec![], | ||
| 1916 | string: "__init__".to_string(), | ||
| 1917 | }, | ||
| 1918 | is_snippet: false, | ||
| 1919 | sort_text: Some("11.9999.__init__"), | ||
| 1920 | sort_kind: 3, | ||
| 1921 | sort_label: "__init__", | ||
| 1922 | }, | ||
| 1923 | SortableMatch { | ||
| 1924 | string_match: StringMatch { | ||
| 1925 | candidate_id: 0, | ||
| 1926 | score: 0.875, | ||
| 1927 | positions: vec![], | ||
| 1928 | string: "__init__".to_string(), | ||
| 1929 | }, | ||
| 1930 | is_snippet: false, | ||
| 1931 | sort_text: Some("11.9999"), | ||
| 1932 | sort_kind: 3, | ||
| 1933 | sort_label: "__init__", | ||
| 1934 | }, | ||
| 1935 | SortableMatch { | ||
| 1936 | string_match: StringMatch { | ||
| 1937 | candidate_id: 5, | ||
| 1938 | score: 0.4117647058823529, | ||
| 1939 | positions: vec![], | ||
| 1940 | string: "__init_subclass__".to_string(), | ||
| 1941 | }, | ||
| 1942 | is_snippet: false, | ||
| 1943 | sort_text: Some("05.0000.__init_subclass__"), | ||
| 1944 | sort_kind: 3, | ||
| 1945 | sort_label: "__init_subclass__", | ||
| 1946 | }, | ||
| 1947 | SortableMatch { | ||
| 1948 | string_match: StringMatch { | ||
| 1949 | candidate_id: 1, | ||
| 1950 | score: 0.4117647058823529, | ||
| 1951 | positions: vec![], | ||
| 1952 | string: "__init_subclass__".to_string(), | ||
| 1953 | }, | ||
| 1954 | is_snippet: false, | ||
| 1955 | sort_text: Some("05.0000"), | ||
| 1956 | sort_kind: 3, | ||
| 1957 | sort_label: "__init_subclass__", | ||
| 1958 | }, | ||
| 1959 | ]; | 308 | ]; |
| 1960 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | 309 | let matches = sort_matches("__init_", &completions, SnippetSortOrder::Top, cx).await; |
| 1961 | assert_eq!( | 310 | assert_eq!(matches[0], "__init__"); |
| 1962 | matches | 311 | assert_eq!(matches[1], "__init__"); |
| 1963 | .iter() | ||
| 1964 | .map(|m| m.string_match.string.as_str()) | ||
| 1965 | .collect::<Vec<&str>>(), | ||
| 1966 | vec![ | ||
| 1967 | "__init__", | ||
| 1968 | "__init__", | ||
| 1969 | "__init_subclass__", | ||
| 1970 | "__init_subclass__", | ||
| 1971 | ] | ||
| 1972 | ); | ||
| 1973 | } | 312 | } |
| 1974 | 313 | ||
| 1975 | #[gpui::test] | 314 | #[gpui::test] |
| 1976 | fn test_sort_matches_for_rust_into(_cx: &mut TestAppContext) { | 315 | async fn test_sort_matches_for_rust_into(cx: &mut TestAppContext) { |
| 1977 | // Case 1: "int" | 316 | // Case 1: "int" |
| 1978 | let query: Option<&str> = Some("int"); | 317 | let completions = vec![ |
| 1979 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 318 | CompletionBuilder::function("into", "80000004"), |
| 1980 | SortableMatch { | 319 | CompletionBuilder::function("try_into", "80000004"), |
| 1981 | string_match: StringMatch { | 320 | CompletionBuilder::snippet("println", "80000004"), |
| 1982 | candidate_id: 67, | 321 | CompletionBuilder::function("clone_into", "80000004"), |
| 1983 | score: 0.75, | 322 | CompletionBuilder::function("into_searcher", "80000000"), |
| 1984 | positions: vec![], | 323 | CompletionBuilder::snippet("eprintln", "80000004"), |
| 1985 | string: "into".to_string(), | ||
| 1986 | }, | ||
| 1987 | is_snippet: false, | ||
| 1988 | sort_text: Some("80000004"), | ||
| 1989 | sort_kind: 3, | ||
| 1990 | sort_label: "into", | ||
| 1991 | }, | ||
| 1992 | SortableMatch { | ||
| 1993 | string_match: StringMatch { | ||
| 1994 | candidate_id: 68, | ||
| 1995 | score: 0.30000000000000004, | ||
| 1996 | positions: vec![], | ||
| 1997 | string: "try_into".to_string(), | ||
| 1998 | }, | ||
| 1999 | is_snippet: false, | ||
| 2000 | sort_text: Some("80000004"), | ||
| 2001 | sort_kind: 3, | ||
| 2002 | sort_label: "try_into", | ||
| 2003 | }, | ||
| 2004 | SortableMatch { | ||
| 2005 | string_match: StringMatch { | ||
| 2006 | candidate_id: 108, | ||
| 2007 | score: 0.2571428571428571, | ||
| 2008 | positions: vec![], | ||
| 2009 | string: "println".to_string(), | ||
| 2010 | }, | ||
| 2011 | is_snippet: true, | ||
| 2012 | sort_text: Some("80000004"), | ||
| 2013 | sort_kind: 3, | ||
| 2014 | sort_label: "println", | ||
| 2015 | }, | ||
| 2016 | SortableMatch { | ||
| 2017 | string_match: StringMatch { | ||
| 2018 | candidate_id: 73, | ||
| 2019 | score: 0.24, | ||
| 2020 | positions: vec![], | ||
| 2021 | string: "clone_into".to_string(), | ||
| 2022 | }, | ||
| 2023 | is_snippet: false, | ||
| 2024 | sort_text: Some("80000004"), | ||
| 2025 | sort_kind: 3, | ||
| 2026 | sort_label: "clone_into", | ||
| 2027 | }, | ||
| 2028 | SortableMatch { | ||
| 2029 | string_match: StringMatch { | ||
| 2030 | candidate_id: 1, | ||
| 2031 | score: 0.23076923076923078, | ||
| 2032 | positions: vec![], | ||
| 2033 | string: "into_searcher".to_string(), | ||
| 2034 | }, | ||
| 2035 | is_snippet: false, | ||
| 2036 | sort_text: Some("80000000"), | ||
| 2037 | sort_kind: 3, | ||
| 2038 | sort_label: "into_searcher", | ||
| 2039 | }, | ||
| 2040 | SortableMatch { | ||
| 2041 | string_match: StringMatch { | ||
| 2042 | candidate_id: 109, | ||
| 2043 | score: 0.22499999999999998, | ||
| 2044 | positions: vec![], | ||
| 2045 | string: "eprintln".to_string(), | ||
| 2046 | }, | ||
| 2047 | is_snippet: true, | ||
| 2048 | sort_text: Some("80000004"), | ||
| 2049 | sort_kind: 3, | ||
| 2050 | sort_label: "eprintln", | ||
| 2051 | }, | ||
| 2052 | ]; | 324 | ]; |
| 2053 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 325 | let matches = sort_matches("int", &completions, SnippetSortOrder::default(), cx).await; |
| 2054 | assert_eq!( | 326 | assert_eq!(matches[0], "into"); |
| 2055 | matches[0].string_match.string.as_str(), | 327 | |
| 2056 | "into", | ||
| 2057 | "Match order not expected" | ||
| 2058 | ); | ||
| 2059 | // Case 2: "into" | 328 | // Case 2: "into" |
| 2060 | let query: Option<&str> = Some("into"); | 329 | let completions = vec![ |
| 2061 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 330 | CompletionBuilder::function("into", "80000004"), |
| 2062 | SortableMatch { | 331 | CompletionBuilder::function("try_into", "80000004"), |
| 2063 | string_match: StringMatch { | 332 | CompletionBuilder::function("clone_into", "80000004"), |
| 2064 | candidate_id: 65, | 333 | CompletionBuilder::function("into_searcher", "80000000"), |
| 2065 | score: 1.0, | 334 | CompletionBuilder::function("split_terminator", "7fffffff"), |
| 2066 | positions: vec![], | 335 | CompletionBuilder::function("rsplit_terminator", "7fffffff"), |
| 2067 | string: "into".to_string(), | ||
| 2068 | }, | ||
| 2069 | is_snippet: false, | ||
| 2070 | sort_text: Some("80000004"), | ||
| 2071 | sort_kind: 3, | ||
| 2072 | sort_label: "into", | ||
| 2073 | }, | ||
| 2074 | SortableMatch { | ||
| 2075 | string_match: StringMatch { | ||
| 2076 | candidate_id: 66, | ||
| 2077 | score: 0.4, | ||
| 2078 | positions: vec![], | ||
| 2079 | string: "try_into".to_string(), | ||
| 2080 | }, | ||
| 2081 | is_snippet: false, | ||
| 2082 | sort_text: Some("80000004"), | ||
| 2083 | sort_kind: 3, | ||
| 2084 | sort_label: "try_into", | ||
| 2085 | }, | ||
| 2086 | SortableMatch { | ||
| 2087 | string_match: StringMatch { | ||
| 2088 | candidate_id: 71, | ||
| 2089 | score: 0.32, | ||
| 2090 | positions: vec![], | ||
| 2091 | string: "clone_into".to_string(), | ||
| 2092 | }, | ||
| 2093 | is_snippet: false, | ||
| 2094 | sort_text: Some("80000004"), | ||
| 2095 | sort_kind: 3, | ||
| 2096 | sort_label: "clone_into", | ||
| 2097 | }, | ||
| 2098 | SortableMatch { | ||
| 2099 | string_match: StringMatch { | ||
| 2100 | candidate_id: 0, | ||
| 2101 | score: 0.3076923076923077, | ||
| 2102 | positions: vec![], | ||
| 2103 | string: "into_searcher".to_string(), | ||
| 2104 | }, | ||
| 2105 | is_snippet: false, | ||
| 2106 | sort_text: Some("80000000"), | ||
| 2107 | sort_kind: 3, | ||
| 2108 | sort_label: "into_searcher", | ||
| 2109 | }, | ||
| 2110 | SortableMatch { | ||
| 2111 | string_match: StringMatch { | ||
| 2112 | candidate_id: 27, | ||
| 2113 | score: 0.09, | ||
| 2114 | positions: vec![], | ||
| 2115 | string: "split_terminator".to_string(), | ||
| 2116 | }, | ||
| 2117 | is_snippet: false, | ||
| 2118 | sort_text: Some("7fffffff"), | ||
| 2119 | sort_kind: 3, | ||
| 2120 | sort_label: "split_terminator", | ||
| 2121 | }, | ||
| 2122 | SortableMatch { | ||
| 2123 | string_match: StringMatch { | ||
| 2124 | candidate_id: 28, | ||
| 2125 | score: 0.08470588235294117, | ||
| 2126 | positions: vec![], | ||
| 2127 | string: "rsplit_terminator".to_string(), | ||
| 2128 | }, | ||
| 2129 | is_snippet: false, | ||
| 2130 | sort_text: Some("7fffffff"), | ||
| 2131 | sort_kind: 3, | ||
| 2132 | sort_label: "rsplit_terminator", | ||
| 2133 | }, | ||
| 2134 | ]; | 336 | ]; |
| 2135 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 337 | let matches = sort_matches("into", &completions, SnippetSortOrder::default(), cx).await; |
| 2136 | assert_eq!( | 338 | assert_eq!(matches[0], "into"); |
| 2137 | matches[0].string_match.string.as_str(), | ||
| 2138 | "into", | ||
| 2139 | "Match order not expected" | ||
| 2140 | ); | ||
| 2141 | } | 339 | } |
| 2142 | 340 | ||
| 2143 | #[gpui::test] | 341 | #[gpui::test] |
| 2144 | fn test_sort_matches_for_variable_over_function(_cx: &mut TestAppContext) { | 342 | async fn test_sort_matches_for_variable_over_function(cx: &mut TestAppContext) { |
| 2145 | // Case 1: "serial" | 343 | // Case 1: "serial" |
| 2146 | let query: Option<&str> = Some("serial"); | 344 | let completions = vec![ |
| 2147 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 345 | CompletionBuilder::function("serialize", "80000000"), |
| 2148 | SortableMatch { | 346 | CompletionBuilder::function("serialize", "80000000"), |
| 2149 | string_match: StringMatch { | 347 | CompletionBuilder::variable("serialization_key", "7ffffffe"), |
| 2150 | candidate_id: 33, | 348 | CompletionBuilder::function("serialize_version", "80000000"), |
| 2151 | score: 0.6666666666666666, | 349 | CompletionBuilder::function("deserialize", "80000000"), |
| 2152 | positions: vec![], | ||
| 2153 | string: "serialize".to_string(), | ||
| 2154 | }, | ||
| 2155 | is_snippet: false, | ||
| 2156 | sort_text: Some("80000000"), | ||
| 2157 | sort_kind: 3, | ||
| 2158 | sort_label: "serialize", | ||
| 2159 | }, | ||
| 2160 | SortableMatch { | ||
| 2161 | string_match: StringMatch { | ||
| 2162 | candidate_id: 32, | ||
| 2163 | score: 0.6666666666666666, | ||
| 2164 | positions: vec![], | ||
| 2165 | string: "serialize".to_string(), | ||
| 2166 | }, | ||
| 2167 | is_snippet: false, | ||
| 2168 | sort_text: Some("80000000"), | ||
| 2169 | sort_kind: 3, | ||
| 2170 | sort_label: "serialize", | ||
| 2171 | }, | ||
| 2172 | SortableMatch { | ||
| 2173 | string_match: StringMatch { | ||
| 2174 | candidate_id: 103, | ||
| 2175 | score: 0.3529411764705882, | ||
| 2176 | positions: vec![], | ||
| 2177 | string: "serialization_key".to_string(), | ||
| 2178 | }, | ||
| 2179 | is_snippet: false, | ||
| 2180 | sort_text: Some("7ffffffe"), | ||
| 2181 | sort_kind: 1, | ||
| 2182 | sort_label: "serialization_key", | ||
| 2183 | }, | ||
| 2184 | SortableMatch { | ||
| 2185 | string_match: StringMatch { | ||
| 2186 | candidate_id: 18, | ||
| 2187 | score: 0.3529411764705882, | ||
| 2188 | positions: vec![], | ||
| 2189 | string: "serialize_version".to_string(), | ||
| 2190 | }, | ||
| 2191 | is_snippet: false, | ||
| 2192 | sort_text: Some("80000000"), | ||
| 2193 | sort_kind: 3, | ||
| 2194 | sort_label: "serialize_version", | ||
| 2195 | }, | ||
| 2196 | SortableMatch { | ||
| 2197 | string_match: StringMatch { | ||
| 2198 | candidate_id: 65, | ||
| 2199 | score: 0.32727272727272727, | ||
| 2200 | positions: vec![], | ||
| 2201 | string: "deserialize".to_string(), | ||
| 2202 | }, | ||
| 2203 | is_snippet: false, | ||
| 2204 | sort_text: Some("80000000"), | ||
| 2205 | sort_kind: 3, | ||
| 2206 | sort_label: "deserialize", | ||
| 2207 | }, | ||
| 2208 | ]; | 350 | ]; |
| 2209 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 351 | let matches = sort_matches("serial", &completions, SnippetSortOrder::default(), cx).await; |
| 2210 | assert_eq!( | 352 | assert_eq!(matches[0], "serialization_key"); |
| 2211 | matches | 353 | assert_eq!(matches[1], "serialize"); |
| 2212 | .iter() | 354 | assert_eq!(matches[2], "serialize"); |
| 2213 | .map(|m| m.string_match.string.as_str()) | 355 | assert_eq!(matches[3], "serialize_version"); |
| 2214 | .collect::<Vec<&str>>(), | 356 | assert_eq!(matches[4], "deserialize"); |
| 2215 | vec![ | ||
| 2216 | "serialization_key", | ||
| 2217 | "serialize", | ||
| 2218 | "serialize", | ||
| 2219 | "serialize_version", | ||
| 2220 | "deserialize" | ||
| 2221 | ] | ||
| 2222 | ); | ||
| 2223 | } | 357 | } |
| 2224 | 358 | ||
| 2225 | #[gpui::test] | 359 | #[gpui::test] |
| 2226 | fn test_sort_matches_for_local_methods_over_library(_cx: &mut TestAppContext) { | 360 | async fn test_sort_matches_for_local_methods_over_library(cx: &mut TestAppContext) { |
| 2227 | // Case 1: "setis" | 361 | // Case 1: "setis" |
| 2228 | let query: Option<&str> = Some("setis"); | 362 | let completions = vec![ |
| 2229 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 363 | CompletionBuilder::variable("setISODay", "16"), |
| 2230 | SortableMatch { | 364 | CompletionBuilder::variable("setISOWeek", "16"), |
| 2231 | string_match: StringMatch { | 365 | CompletionBuilder::variable("setISOWeekYear", "16"), |
| 2232 | candidate_id: 1200, | 366 | CompletionBuilder::function("setISOWeekYear", "16"), |
| 2233 | score: 0.5555555555555556, | 367 | CompletionBuilder::variable("setIsRefreshing", "11"), |
| 2234 | positions: vec![], | 368 | CompletionBuilder::function("setFips", "16"), |
| 2235 | string: "setISODay".to_string(), | ||
| 2236 | }, | ||
| 2237 | is_snippet: false, | ||
| 2238 | sort_text: Some("16"), | ||
| 2239 | sort_kind: 1, | ||
| 2240 | sort_label: "setISODay", | ||
| 2241 | }, | ||
| 2242 | SortableMatch { | ||
| 2243 | string_match: StringMatch { | ||
| 2244 | candidate_id: 1216, | ||
| 2245 | score: 0.5, | ||
| 2246 | positions: vec![], | ||
| 2247 | string: "setISOWeek".to_string(), | ||
| 2248 | }, | ||
| 2249 | is_snippet: false, | ||
| 2250 | sort_text: Some("16"), | ||
| 2251 | sort_kind: 1, | ||
| 2252 | sort_label: "setISOWeek", | ||
| 2253 | }, | ||
| 2254 | SortableMatch { | ||
| 2255 | string_match: StringMatch { | ||
| 2256 | candidate_id: 1232, | ||
| 2257 | score: 0.3571428571428571, | ||
| 2258 | positions: vec![], | ||
| 2259 | string: "setISOWeekYear".to_string(), | ||
| 2260 | }, | ||
| 2261 | is_snippet: false, | ||
| 2262 | sort_text: Some("16"), | ||
| 2263 | sort_kind: 1, | ||
| 2264 | sort_label: "setISOWeekYear", | ||
| 2265 | }, | ||
| 2266 | SortableMatch { | ||
| 2267 | string_match: StringMatch { | ||
| 2268 | candidate_id: 1217, | ||
| 2269 | score: 0.3571428571428571, | ||
| 2270 | positions: vec![], | ||
| 2271 | string: "setISOWeekYear".to_string(), | ||
| 2272 | }, | ||
| 2273 | is_snippet: false, | ||
| 2274 | sort_text: Some("16"), | ||
| 2275 | sort_kind: 3, | ||
| 2276 | sort_label: "setISOWeekYear", | ||
| 2277 | }, | ||
| 2278 | SortableMatch { | ||
| 2279 | string_match: StringMatch { | ||
| 2280 | candidate_id: 53, | ||
| 2281 | score: 0.3333333333333333, | ||
| 2282 | positions: vec![], | ||
| 2283 | string: "setIsRefreshing".to_string(), | ||
| 2284 | }, | ||
| 2285 | is_snippet: false, | ||
| 2286 | sort_text: Some("11"), | ||
| 2287 | sort_kind: 1, | ||
| 2288 | sort_label: "setIsRefreshing", | ||
| 2289 | }, | ||
| 2290 | SortableMatch { | ||
| 2291 | string_match: StringMatch { | ||
| 2292 | candidate_id: 1180, | ||
| 2293 | score: 0.2571428571428571, | ||
| 2294 | positions: vec![], | ||
| 2295 | string: "setFips".to_string(), | ||
| 2296 | }, | ||
| 2297 | is_snippet: false, | ||
| 2298 | sort_text: Some("16"), | ||
| 2299 | sort_kind: 3, | ||
| 2300 | sort_label: "setFips", | ||
| 2301 | }, | ||
| 2302 | ]; | 369 | ]; |
| 2303 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 370 | let matches = sort_matches("setis", &completions, SnippetSortOrder::default(), cx).await; |
| 2304 | assert_eq!( | 371 | assert_eq!(matches[0], "setIsRefreshing"); |
| 2305 | matches | 372 | assert_eq!(matches[1], "setISODay"); |
| 2306 | .iter() | 373 | assert_eq!(matches[2], "setISOWeek"); |
| 2307 | .map(|m| m.string_match.string.as_str()) | ||
| 2308 | .collect::<Vec<&str>>(), | ||
| 2309 | vec![ | ||
| 2310 | "setIsRefreshing", | ||
| 2311 | "setISODay", | ||
| 2312 | "setISOWeek", | ||
| 2313 | "setISOWeekYear", | ||
| 2314 | "setISOWeekYear", | ||
| 2315 | "setFips" | ||
| 2316 | ] | ||
| 2317 | ); | ||
| 2318 | } | 374 | } |
| 2319 | 375 | ||
| 2320 | #[gpui::test] | 376 | #[gpui::test] |
| 2321 | fn test_sort_matches_for_priotize_not_exact_match(_cx: &mut TestAppContext) { | 377 | async fn test_sort_matches_for_prioritize_not_exact_match(cx: &mut TestAppContext) { |
| 2322 | // Case 1: "item" | 378 | // Case 1: "item" |
| 2323 | let query: Option<&str> = Some("item"); | 379 | let completions = vec![ |
| 2324 | let mut matches: Vec<SortableMatch<'_>> = vec![ | 380 | CompletionBuilder::function("Item", "16"), |
| 2325 | SortableMatch { | 381 | CompletionBuilder::variable("Item", "16"), |
| 2326 | string_match: StringMatch { | 382 | CompletionBuilder::variable("items", "11"), |
| 2327 | candidate_id: 1115, | 383 | CompletionBuilder::function("ItemText", "16"), |
| 2328 | score: 1.0, | ||
| 2329 | positions: vec![], | ||
| 2330 | string: "Item".to_string(), | ||
| 2331 | }, | ||
| 2332 | is_snippet: false, | ||
| 2333 | sort_text: Some("16"), | ||
| 2334 | sort_kind: 3, | ||
| 2335 | sort_label: "Item", | ||
| 2336 | }, | ||
| 2337 | SortableMatch { | ||
| 2338 | string_match: StringMatch { | ||
| 2339 | candidate_id: 1108, | ||
| 2340 | score: 1.0, | ||
| 2341 | positions: vec![], | ||
| 2342 | string: "Item".to_string(), | ||
| 2343 | }, | ||
| 2344 | is_snippet: false, | ||
| 2345 | sort_text: Some("16"), | ||
| 2346 | sort_kind: 1, | ||
| 2347 | sort_label: "Item", | ||
| 2348 | }, | ||
| 2349 | SortableMatch { | ||
| 2350 | string_match: StringMatch { | ||
| 2351 | candidate_id: 26, | ||
| 2352 | score: 0.8, | ||
| 2353 | positions: vec![], | ||
| 2354 | string: "items".to_string(), | ||
| 2355 | }, | ||
| 2356 | is_snippet: false, | ||
| 2357 | sort_text: Some("11"), | ||
| 2358 | sort_kind: 1, | ||
| 2359 | sort_label: "items", | ||
| 2360 | }, | ||
| 2361 | SortableMatch { | ||
| 2362 | string_match: StringMatch { | ||
| 2363 | candidate_id: 1138, | ||
| 2364 | score: 0.5, | ||
| 2365 | positions: vec![], | ||
| 2366 | string: "ItemText".to_string(), | ||
| 2367 | }, | ||
| 2368 | is_snippet: false, | ||
| 2369 | sort_text: Some("16"), | ||
| 2370 | sort_kind: 3, | ||
| 2371 | sort_label: "ItemText", | ||
| 2372 | }, | ||
| 2373 | ]; | 384 | ]; |
| 2374 | CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | 385 | let matches = sort_matches("item", &completions, SnippetSortOrder::default(), cx).await; |
| 2375 | assert_eq!( | 386 | assert_eq!(matches[0], "items"); |
| 2376 | matches | 387 | assert_eq!(matches[1], "Item"); |
| 2377 | .iter() | 388 | assert_eq!(matches[2], "Item"); |
| 2378 | .map(|m| m.string_match.string.as_str()) | 389 | assert_eq!(matches[3], "ItemText"); |
| 2379 | .collect::<Vec<&str>>(), | 390 | } |
| 2380 | vec!["items", "Item", "Item", "ItemText"] | 391 | |
| 2381 | ); | 392 | #[gpui::test] |
| 393 | async fn test_sort_matches_for_tailwind_classes(cx: &mut TestAppContext) { | ||
| 394 | let completions = vec![ | ||
| 395 | CompletionBuilder::function("rounded-full", "15788"), | ||
| 396 | CompletionBuilder::variable("rounded-t-full", "15846"), | ||
| 397 | CompletionBuilder::variable("rounded-b-full", "15731"), | ||
| 398 | CompletionBuilder::function("rounded-tr-full", "15866"), | ||
| 399 | ]; | ||
| 400 | // Case 1: "rounded-full" | ||
| 401 | let matches = sort_matches( | ||
| 402 | "rounded-full", | ||
| 403 | &completions, | ||
| 404 | SnippetSortOrder::default(), | ||
| 405 | cx, | ||
| 406 | ) | ||
| 407 | .await; | ||
| 408 | assert_eq!(matches[0], "rounded-full"); | ||
| 409 | // Case 2: "roundedfull" | ||
| 410 | let matches = sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await; | ||
| 411 | assert_eq!(matches[0], "rounded-full"); | ||
| 412 | } | ||
| 413 | |||
| 414 | struct CompletionBuilder; | ||
| 415 | |||
| 416 | impl CompletionBuilder { | ||
| 417 | fn constant(label: &str, sort_text: &str) -> Completion { | ||
| 418 | Self::new(label, sort_text, CompletionItemKind::CONSTANT) | ||
| 419 | } | ||
| 420 | |||
| 421 | fn function(label: &str, sort_text: &str) -> Completion { | ||
| 422 | Self::new(label, sort_text, CompletionItemKind::FUNCTION) | ||
| 423 | } | ||
| 424 | |||
| 425 | fn variable(label: &str, sort_text: &str) -> Completion { | ||
| 426 | Self::new(label, sort_text, CompletionItemKind::VARIABLE) | ||
| 427 | } | ||
| 428 | |||
| 429 | fn keyword(label: &str, sort_text: &str) -> Completion { | ||
| 430 | Self::new(label, sort_text, CompletionItemKind::KEYWORD) | ||
| 431 | } | ||
| 432 | |||
| 433 | fn snippet(label: &str, sort_text: &str) -> Completion { | ||
| 434 | Self::new(label, sort_text, CompletionItemKind::SNIPPET) | ||
| 435 | } | ||
| 436 | |||
| 437 | fn new(label: &str, sort_text: &str, kind: CompletionItemKind) -> Completion { | ||
| 438 | Completion { | ||
| 439 | replace_range: Anchor::MIN..Anchor::MAX, | ||
| 440 | new_text: label.to_string(), | ||
| 441 | label: CodeLabel { | ||
| 442 | text: label.to_string(), | ||
| 443 | runs: Default::default(), | ||
| 444 | filter_range: 0..label.len(), | ||
| 445 | }, | ||
| 446 | documentation: None, | ||
| 447 | source: CompletionSource::Lsp { | ||
| 448 | insert_range: None, | ||
| 449 | server_id: LanguageServerId(0), | ||
| 450 | lsp_completion: Box::new(CompletionItem { | ||
| 451 | label: label.to_string(), | ||
| 452 | kind: Some(kind), | ||
| 453 | sort_text: Some(sort_text.to_string()), | ||
| 454 | ..Default::default() | ||
| 455 | }), | ||
| 456 | lsp_defaults: None, | ||
| 457 | resolved: false, | ||
| 458 | }, | ||
| 459 | icon_path: None, | ||
| 460 | insert_text_mode: None, | ||
| 461 | confirm: None, | ||
| 462 | } | ||
| 463 | } | ||
| 464 | } | ||
| 465 | |||
| 466 | async fn sort_matches( | ||
| 467 | query: &str, | ||
| 468 | completions: &Vec<Completion>, | ||
| 469 | snippet_sort_order: SnippetSortOrder, | ||
| 470 | cx: &mut TestAppContext, | ||
| 471 | ) -> Vec<String> { | ||
| 472 | let candidates: Arc<[StringMatchCandidate]> = completions | ||
| 473 | .iter() | ||
| 474 | .enumerate() | ||
| 475 | .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.text)) | ||
| 476 | .collect(); | ||
| 477 | let cancel_flag = Arc::new(AtomicBool::new(false)); | ||
| 478 | let background_executor = cx.executor(); | ||
| 479 | let matches = fuzzy::match_strings( | ||
| 480 | &candidates, | ||
| 481 | query, | ||
| 482 | query.chars().any(|c| c.is_uppercase()), | ||
| 483 | 100, | ||
| 484 | &cancel_flag, | ||
| 485 | background_executor, | ||
| 486 | ) | ||
| 487 | .await; | ||
| 488 | let sorted_matches = CompletionsMenu::sort_string_matches( | ||
| 489 | matches, | ||
| 490 | Some(query), | ||
| 491 | snippet_sort_order, | ||
| 492 | &completions, | ||
| 493 | ); | ||
| 494 | sorted_matches.into_iter().map(|m| m.string).collect() | ||
| 2382 | } | 495 | } |
| @@ -722,10 +722,9 @@ impl CompletionsMenu { · 721 unmodified lines | |||
| 722 | let last_rendered_range = self.last_rendered_range.clone(); | 722 | let last_rendered_range = self.last_rendered_range.clone(); |
| 723 | let style = style.clone(); | 723 | let style = style.clone(); |
| 724 | let list = uniform_list( | 724 | let list = uniform_list( |
| 725 | cx.entity().clone(), | ||
| 726 | "completions", | 725 | "completions", |
| 727 | self.entries.borrow().len(), | 726 | self.entries.borrow().len(), |
| 728 | move |_editor, range, _window, cx| { | 727 | cx.processor(move |_editor, range: Range<usize>, _window, cx| { |
| 729 | last_rendered_range.borrow_mut().replace(range.clone()); | 728 | last_rendered_range.borrow_mut().replace(range.clone()); |
| 730 | let start_ix = range.start; | 729 | let start_ix = range.start; |
| 731 | let completions_guard = completions.borrow_mut(); | 730 | let completions_guard = completions.borrow_mut(); |
| @@ -837,7 +836,7 @@ impl CompletionsMenu { · 105 unmodified lines | |||
| 837 | ) | 836 | ) |
| 838 | }) | 837 | }) |
| 839 | .collect() | 838 | .collect() |
| 840 | }, | 839 | }), |
| 841 | ) | 840 | ) |
| 842 | .occlude() | 841 | .occlude() |
| 843 | .max_h(max_height_in_lines as f32 * window.line_height()) | 842 | .max_h(max_height_in_lines as f32 * window.line_height()) |
| @@ -1045,59 +1044,19 @@ impl CompletionsMenu { · 201 unmodified lines | |||
| 1045 | self.handle_selection_changed(provider.as_deref(), window, cx); | 1044 | self.handle_selection_changed(provider.as_deref(), window, cx); |
| 1046 | } | 1045 | } |
| 1047 | 1046 | ||
| 1048 | fn sort_string_matches( | 1047 | pub fn sort_string_matches( |
| 1049 | matches: Vec<StringMatch>, | 1048 | matches: Vec<StringMatch>, |
| 1050 | query: Option<&str>, | 1049 | query: Option<&str>, |
| 1051 | snippet_sort_order: SnippetSortOrder, | 1050 | snippet_sort_order: SnippetSortOrder, |
| 1052 | completions: &[Completion], | 1051 | completions: &[Completion], |
| 1053 | ) -> Vec<StringMatch> { | 1052 | ) -> Vec<StringMatch> { |
| 1054 | let mut sortable_items: Vec<SortableMatch<'_>> = matches | 1053 | let mut matches = matches; |
| 1055 | .into_iter() | ||
| 1056 | .map(|string_match| { | ||
| 1057 | let completion = &completions[string_match.candidate_id]; | ||
| 1058 | |||
| 1059 | let is_snippet = matches!( | ||
| 1060 | &completion.source, | ||
| 1061 | CompletionSource::Lsp { lsp_completion, .. } | ||
| 1062 | if lsp_completion.kind == Some(CompletionItemKind::SNIPPET) | ||
| 1063 | ); | ||
| 1064 | |||
| 1065 | let sort_text = | ||
| 1066 | if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source { | ||
| 1067 | lsp_completion.sort_text.as_deref() | ||
| 1068 | } else { | ||
| 1069 | None | ||
| 1070 | }; | ||
| 1071 | |||
| 1072 | let (sort_kind, sort_label) = completion.sort_key(); | ||
| 1073 | |||
| 1074 | SortableMatch { | ||
| 1075 | string_match, | ||
| 1076 | is_snippet, | ||
| 1077 | sort_text, | ||
| 1078 | sort_kind, | ||
| 1079 | sort_label, | ||
| 1080 | } | ||
| 1081 | }) | ||
| 1082 | .collect(); | ||
| 1083 | |||
| 1084 | Self::sort_matches(&mut sortable_items, query, snippet_sort_order); | ||
| 1085 | |||
| 1086 | sortable_items | ||
| 1087 | .into_iter() | ||
| 1088 | .map(|sortable| sortable.string_match) | ||
| 1089 | .collect() | ||
| 1090 | } | ||
| 1091 | 1054 | ||
| 1092 | pub fn sort_matches( | ||
| 1093 | matches: &mut Vec<SortableMatch<'_>>, | ||
| 1094 | query: Option<&str>, | ||
| 1095 | snippet_sort_order: SnippetSortOrder, | ||
| 1096 | ) { | ||
| 1097 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] | 1055 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] |
| 1098 | enum MatchTier<'a> { | 1056 | enum MatchTier<'a> { |
| 1099 | WordStartMatch { | 1057 | WordStartMatch { |
| 1100 | sort_mixed_case_prefix_length: Reverse<usize>, | 1058 | sort_capitalize: Reverse<usize>, |
| 1059 | sort_positions: Vec<usize>, | ||
| 1101 | sort_snippet: Reverse<i32>, | 1060 | sort_snippet: Reverse<i32>, |
| 1102 | sort_kind: usize, | 1061 | sort_kind: usize, |
| 1103 | sort_fuzzy_bracket: Reverse<usize>, | 1062 | sort_fuzzy_bracket: Reverse<usize>, |
| @@ -1115,10 +1074,7 @@ impl CompletionsMenu { · 11 unmodified lines | |||
| 1115 | 1074 | ||
| 1116 | // In a fuzzy bracket, matches with a score of 1.0 are prioritized. | 1075 | // In a fuzzy bracket, matches with a score of 1.0 are prioritized. |
| 1117 | // The remaining matches are partitioned into two groups at 3/5 of the max_score. | 1076 | // The remaining matches are partitioned into two groups at 3/5 of the max_score. |
| 1118 | let max_score = matches | 1077 | let max_score = matches.iter().map(|mat| mat.score).fold(0.0, f64::max); |
| 1119 | .iter() | ||
| 1120 | .map(|mat| mat.string_match.score) | ||
| 1121 | .fold(0.0, f64::max); | ||
| 1122 | let fuzzy_bracket_threshold = max_score * (3.0 / 5.0); | 1078 | let fuzzy_bracket_threshold = max_score * (3.0 / 5.0); |
| 1123 | 1079 | ||
| 1124 | let query_start_lower = query | 1080 | let query_start_lower = query |
| @@ -1126,13 +1082,30 @@ impl CompletionsMenu { · 1 unmodified line | |||
| 1126 | .and_then(|q| q.chars().next()) | 1082 | .and_then(|q| q.chars().next()) |
| 1127 | .and_then(|c| c.to_lowercase().next()); | 1083 | .and_then(|c| c.to_lowercase().next()); |
| 1128 | 1084 | ||
| 1129 | matches.sort_unstable_by_key(|mat| { | 1085 | matches.sort_unstable_by_key(|string_match| { |
| 1130 | let score = mat.string_match.score; | 1086 | let completion = &completions[string_match.candidate_id]; |
| 1087 | |||
| 1088 | let is_snippet = matches!( | ||
| 1089 | &completion.source, | ||
| 1090 | CompletionSource::Lsp { lsp_completion, .. } | ||
| 1091 | if lsp_completion.kind == Some(CompletionItemKind::SNIPPET) | ||
| 1092 | ); | ||
| 1093 | |||
| 1094 | let sort_text = if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source | ||
| 1095 | { | ||
| 1096 | lsp_completion.sort_text.as_deref() | ||
| 1097 | } else { | ||
| 1098 | None | ||
| 1099 | }; | ||
| 1100 | |||
| 1101 | let (sort_kind, sort_label) = completion.sort_key(); | ||
| 1102 | |||
| 1103 | let score = string_match.score; | ||
| 1131 | let sort_score = Reverse(OrderedFloat(score)); | 1104 | let sort_score = Reverse(OrderedFloat(score)); |
| 1132 | 1105 | ||
| 1133 | let query_start_doesnt_match_split_words = query_start_lower | 1106 | let query_start_doesnt_match_split_words = query_start_lower |
| 1134 | .map(|query_char| { | 1107 | .map(|query_char| { |
| 1135 | !split_words(&mat.string_match.string).any(|word| { | 1108 | !split_words(&string_match.string).any(|word| { |
| 1136 | word.chars() | 1109 | word.chars() |
| 1137 | .next() | 1110 | .next() |
| 1138 | .and_then(|c| c.to_lowercase().next()) | 1111 | .and_then(|c| c.to_lowercase().next()) |
| @@ -1150,41 +1123,34 @@ impl CompletionsMenu { · 11 unmodified lines | |||
| 1150 | 0 | 1123 | 0 |
| 1151 | }); | 1124 | }); |
| 1152 | let sort_snippet = match snippet_sort_order { | 1125 | let sort_snippet = match snippet_sort_order { |
| 1153 | SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }), | 1126 | SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }), |
| 1154 | SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }), | 1127 | SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }), |
| 1155 | SnippetSortOrder::Inline => Reverse(0), | 1128 | SnippetSortOrder::Inline => Reverse(0), |
| 1156 | }; | 1129 | }; |
| 1157 | let sort_mixed_case_prefix_length = Reverse( | 1130 | let sort_capitalize = Reverse( |
| 1158 | query | 1131 | query |
| 1159 | .as_ref() | 1132 | .as_ref() |
| 1160 | .map(|q| { | 1133 | .and_then(|q| q.chars().next()) |
| 1161 | q.chars() | 1134 | .zip(string_match.string.chars().next()) |
| 1162 | .zip(mat.string_match.string.chars()) | 1135 | .map(|(q_char, s_char)| if q_char == s_char { 1 } else { 0 }) |
| 1163 | .enumerate() | ||
| 1164 | .take_while(|(i, (q_char, match_char))| { | ||
| 1165 | if *i == 0 { | ||
| 1166 | // Case-sensitive comparison for first character | ||
| 1167 | q_char == match_char | ||
| 1168 | } else { | ||
| 1169 | // Case-insensitive comparison for other characters | ||
| 1170 | q_char.to_lowercase().eq(match_char.to_lowercase()) | ||
| 1171 | } | ||
| 1172 | }) | ||
| 1173 | .count() | ||
| 1174 | }) | ||
| 1175 | .unwrap_or(0), | 1136 | .unwrap_or(0), |
| 1176 | ); | 1137 | ); |
| 1138 | let sort_positions = string_match.positions.clone(); | ||
| 1139 | |||
| 1177 | MatchTier::WordStartMatch { | 1140 | MatchTier::WordStartMatch { |
| 1178 | sort_mixed_case_prefix_length, | 1141 | sort_capitalize, |
| 1142 | sort_positions, | ||
| 1179 | sort_snippet, | 1143 | sort_snippet, |
| 1180 | sort_kind: mat.sort_kind, | 1144 | sort_kind, |
| 1181 | sort_fuzzy_bracket, | 1145 | sort_fuzzy_bracket, |
| 1182 | sort_text: mat.sort_text, | 1146 | sort_text, |
| 1183 | sort_score, | 1147 | sort_score, |
| 1184 | sort_label: mat.sort_label, | 1148 | sort_label, |
| 1185 | } | 1149 | } |
| 1186 | } | 1150 | } |
| 1187 | }); | 1151 | }); |
| 1152 | |||
| 1153 | matches | ||
| 1188 | } | 1154 | } |
| 1189 | 1155 | ||
| 1190 | pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) { | 1156 | pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) { |
| @@ -1216,15 +1182,6 @@ impl CompletionsMenu { · 25 unmodified lines | |||
| 1216 | } | 1182 | } |
| 1217 | } | 1183 | } |
| 1218 | 1184 | ||
| 1219 | #[derive(Debug)] | ||
| 1220 | pub struct SortableMatch<'a> { | ||
| 1221 | pub string_match: StringMatch, | ||
| 1222 | pub is_snippet: bool, | ||
| 1223 | pub sort_text: Option<&'a str>, | ||
| 1224 | pub sort_kind: usize, | ||
| 1225 | pub sort_label: &'a str, | ||
| 1226 | } | ||
| 1227 | |||
| 1228 | #[derive(Clone)] | 1185 | #[derive(Clone)] |
| 1229 | pub struct AvailableCodeAction { | 1186 | pub struct AvailableCodeAction { |
| 1230 | pub excerpt_id: ExcerptId, | 1187 | pub excerpt_id: ExcerptId, |
| @@ -1436,7 +1393,9 @@ impl CodeActionsMenu { · 205 unmodified lines | |||
| 1436 | 1393 | ||
| 1437 | fn origin(&self) -> ContextMenuOrigin { | 1394 | fn origin(&self) -> ContextMenuOrigin { |
| 1438 | match &self.deployed_from { | 1395 | match &self.deployed_from { |
| 1439 | Some(CodeActionSource::Indicator(row)) => ContextMenuOrigin::GutterIndicator(*row), | 1396 | Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => { |
| 1397 | ContextMenuOrigin::GutterIndicator(*row) | ||
| 1398 | } | ||
| 1440 | Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar, | 1399 | Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar, |
| 1441 | None => ContextMenuOrigin::Cursor, | 1400 | None => ContextMenuOrigin::Cursor, |
| 1442 | } | 1401 | } |
| @@ -1452,10 +1411,9 @@ impl CodeActionsMenu { · 9 unmodified lines | |||
| 1452 | let actions = self.actions.clone(); | 1411 | let actions = self.actions.clone(); |
| 1453 | let selected_item = self.selected_item; | 1412 | let selected_item = self.selected_item; |
| 1454 | let list = uniform_list( | 1413 | let list = uniform_list( |
| 1455 | cx.entity().clone(), | ||
| 1456 | "code_actions_menu", | 1414 | "code_actions_menu", |
| 1457 | self.actions.len(), | 1415 | self.actions.len(), |
| 1458 | move |_this, range, _, cx| { | 1416 | cx.processor(move |_this, range: Range<usize>, _, cx| { |
| 1459 | actions | 1417 | actions |
| 1460 | .iter() | 1418 | .iter() |
| 1461 | .skip(range.start) | 1419 | .skip(range.start) |
| @@ -1518,7 +1476,7 @@ impl CodeActionsMenu { · 56 unmodified lines | |||
| 1518 | ) | 1476 | ) |
| 1519 | }) | 1477 | }) |
| 1520 | .collect() | 1478 | .collect() |
| 1521 | }, | 1479 | }), |
| 1522 | ) | 1480 | ) |
| 1523 | .occlude() | 1481 | .occlude() |
| 1524 | .max_h(max_height_in_lines as f32 * window.line_height()) | 1482 | .max_h(max_height_in_lines as f32 * window.line_height()) |
| @@ -464,7 +464,7 @@ impl BlockMap { · 463 unmodified lines | |||
| 464 | map | 464 | map |
| 465 | } | 465 | } |
| 466 | 466 | ||
| 467 | pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader { | 467 | pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader<'_> { |
| 468 | self.sync(&wrap_snapshot, edits); | 468 | self.sync(&wrap_snapshot, edits); |
| 469 | *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone(); | 469 | *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone(); |
| 470 | BlockMapReader { | 470 | BlockMapReader { |
| @@ -479,7 +479,7 @@ impl BlockMap { · 8 unmodified lines | |||
| 479 | } | 479 | } |
| 480 | } | 480 | } |
| 481 | 481 | ||
| 482 | pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter { | 482 | pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter<'_> { |
| 483 | self.sync(&wrap_snapshot, edits); | 483 | self.sync(&wrap_snapshot, edits); |
| 484 | *self.wrap_snapshot.borrow_mut() = wrap_snapshot; | 484 | *self.wrap_snapshot.borrow_mut() = wrap_snapshot; |
| 485 | BlockMapWriter(self) | 485 | BlockMapWriter(self) |
| @@ -1327,7 +1327,7 @@ impl BlockSnapshot { · 841 unmodified lines | |||
| 1327 | } | 1327 | } |
| 1328 | } | 1328 | } |
| 1329 | 1329 | ||
| 1330 | pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows { | 1330 | pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> { |
| 1331 | let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); | 1331 | let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); |
| 1332 | cursor.seek(&start_row, Bias::Right, &()); | 1332 | cursor.seek(&start_row, Bias::Right, &()); |
| 1333 | let (output_start, input_start) = cursor.start(); | 1333 | let (output_start, input_start) = cursor.start(); |
| @@ -357,7 +357,7 @@ impl FoldMap { · 356 unmodified lines | |||
| 357 | &mut self, | 357 | &mut self, |
| 358 | inlay_snapshot: InlaySnapshot, | 358 | inlay_snapshot: InlaySnapshot, |
| 359 | edits: Vec<InlayEdit>, | 359 | edits: Vec<InlayEdit>, |
| 360 | ) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) { | 360 | ) -> (FoldMapWriter<'_>, FoldSnapshot, Vec<FoldEdit>) { |
| 361 | let (snapshot, edits) = self.read(inlay_snapshot, edits); | 361 | let (snapshot, edits) = self.read(inlay_snapshot, edits); |
| 362 | (FoldMapWriter(self), snapshot, edits) | 362 | (FoldMapWriter(self), snapshot, edits) |
| 363 | } | 363 | } |
| @@ -730,7 +730,7 @@ impl FoldSnapshot { · 366 unmodified lines | |||
| 730 | (line_end - line_start) as u32 | 730 | (line_end - line_start) as u32 |
| 731 | } | 731 | } |
| 732 | 732 | ||
| 733 | pub fn row_infos(&self, start_row: u32) -> FoldRows { | 733 | pub fn row_infos(&self, start_row: u32) -> FoldRows<'_> { |
| 734 | if start_row > self.transforms.summary().output.lines.row { | 734 | if start_row > self.transforms.summary().output.lines.row { |
| 735 | panic!("invalid display row {}", start_row); | 735 | panic!("invalid display row {}", start_row); |
| 736 | } | 736 | } |
| @@ -726,7 +726,7 @@ impl WrapSnapshot { · 725 unmodified lines | |||
| 726 | self.transforms.summary().output.longest_row | 726 | self.transforms.summary().output.longest_row |
| 727 | } | 727 | } |
| 728 | 728 | ||
| 729 | pub fn row_infos(&self, start_row: u32) -> WrapRows { | 729 | pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> { |
| 730 | let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); | 730 | let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); |
| 731 | transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); | 731 | transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); |
| 732 | let mut input_row = transforms.start().1.row(); | 732 | let mut input_row = transforms.start().1.row(); |
| @@ -209,7 +209,7 @@ use workspace::{ · 208 unmodified lines | |||
| 209 | CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, | 209 | CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, |
| 210 | RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, | 210 | RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, |
| 211 | ViewId, Workspace, WorkspaceId, WorkspaceSettings, | 211 | ViewId, Workspace, WorkspaceId, WorkspaceSettings, |
| 212 | item::{ItemHandle, PreviewTabsSettings}, | 212 | item::{ItemHandle, PreviewTabsSettings, SaveOptions}, |
| 213 | notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, | 213 | notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, |
| 214 | searchable::SearchEvent, | 214 | searchable::SearchEvent, |
| 215 | }; | 215 | }; |
| @@ -241,7 +241,6 @@ pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration: · 25 unmodified lines | |||
| 241 | 241 | ||
| 242 | pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction"; | 242 | pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction"; |
| 243 | pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict"; | 243 | pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict"; |
| 244 | pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4; | ||
| 245 | pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.)); | 244 | pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.)); |
| 246 | 245 | ||
| 247 | pub type RenderDiffHunkControlsFn = Arc< | 246 | pub type RenderDiffHunkControlsFn = Arc< |
| @@ -450,6 +449,7 @@ pub enum SelectPhase { · 202 unmodified lines | |||
| 450 | BeginColumnar { | 449 | BeginColumnar { |
| 451 | position: DisplayPoint, | 450 | position: DisplayPoint, |
| 452 | reset: bool, | 451 | reset: bool, |
| 452 | mode: ColumnarMode, | ||
| 453 | goal_column: u32, | 453 | goal_column: u32, |
| 454 | }, | 454 | }, |
| 455 | Extend { | 455 | Extend { |
| @@ -464,6 +464,12 @@ pub enum SelectPhase { · 8 unmodified lines | |||
| 464 | End, | 464 | End, |
| 465 | } | 465 | } |
| 466 | 466 | ||
| 467 | #[derive(Clone, Debug, PartialEq)] | ||
| 468 | pub enum ColumnarMode { | ||
| 469 | FromMouse, | ||
| 470 | FromSelection, | ||
| 471 | } | ||
| 472 | |||
| 467 | #[derive(Clone, Debug)] | 473 | #[derive(Clone, Debug)] |
| 468 | pub enum SelectMode { | 474 | pub enum SelectMode { |
| 469 | Character, | 475 | Character, |
| @@ -478,6 +484,7 @@ pub enum EditorMode { · 8 unmodified lines | |||
| 478 | auto_width: bool, | 484 | auto_width: bool, |
| 479 | }, | 485 | }, |
| 480 | AutoHeight { | 486 | AutoHeight { |
| 487 | min_lines: usize, | ||
| 481 | max_lines: usize, | 488 | max_lines: usize, |
| 482 | }, | 489 | }, |
| 483 | Full { | 490 | Full { |
| @@ -700,7 +707,7 @@ impl EditorActionId { · 216 unmodified lines | |||
| 700 | // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>; | 707 | // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>; |
| 701 | 708 | ||
| 702 | type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>); | 709 | type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>); |
| 703 | type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range<Anchor>]>); | 710 | type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>); |
| 704 | 711 | ||
| 705 | #[derive(Default)] | 712 | #[derive(Default)] |
| 706 | struct ScrollbarMarkerState { | 713 | struct ScrollbarMarkerState { |
| @@ -902,7 +909,6 @@ struct InlineBlamePopoverState { · 195 unmodified lines | |||
| 902 | 909 | ||
| 903 | struct InlineBlamePopover { | 910 | struct InlineBlamePopover { |
| 904 | position: gpui::Point<Pixels>, | 911 | position: gpui::Point<Pixels>, |
| 905 | show_task: Option<Task<()>>, | ||
| 906 | hide_task: Option<Task<()>>, | 912 | hide_task: Option<Task<()>>, |
| 907 | popover_bounds: Option<Bounds<Pixels>>, | 913 | popover_bounds: Option<Bounds<Pixels>>, |
| 908 | popover_state: InlineBlamePopoverState, | 914 | popover_state: InlineBlamePopoverState, |
| @@ -915,17 +921,29 @@ enum SelectionDragState { · 6 unmodified lines | |||
| 915 | ReadyToDrag { | 921 | ReadyToDrag { |
| 916 | selection: Selection<Anchor>, | 922 | selection: Selection<Anchor>, |
| 917 | click_position: gpui::Point<Pixels>, | 923 | click_position: gpui::Point<Pixels>, |
| 924 | mouse_down_time: Instant, | ||
| 918 | }, | 925 | }, |
| 919 | /// State when the mouse is dragging the selection in the editor. | 926 | /// State when the mouse is dragging the selection in the editor. |
| 920 | Dragging { | 927 | Dragging { |
| 921 | selection: Selection<Anchor>, | 928 | selection: Selection<Anchor>, |
| 922 | drop_cursor: Selection<Anchor>, | 929 | drop_cursor: Selection<Anchor>, |
| 930 | hide_drop_cursor: bool, | ||
| 931 | }, | ||
| 932 | } | ||
| 933 | |||
| 934 | enum ColumnarSelectionState { | ||
| 935 | FromMouse { | ||
| 936 | selection_tail: Anchor, | ||
| 937 | display_point: Option<DisplayPoint>, | ||
| 938 | }, | ||
| 939 | FromSelection { | ||
| 940 | selection_tail: Anchor, | ||
| 923 | }, | 941 | }, |
| 924 | } | 942 | } |
| 925 | 943 | ||
| 926 | /// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have | 944 | /// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have |
| 927 | /// a breakpoint on them. | 945 | /// a breakpoint on them. |
| 928 | #[derive(Clone, Copy, Debug)] | 946 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 929 | struct PhantomBreakpointIndicator { | 947 | struct PhantomBreakpointIndicator { |
| 930 | display_row: DisplayRow, | 948 | display_row: DisplayRow, |
| 931 | /// There's a small debounce between hovering over the line and showing the indicator. | 949 | /// There's a small debounce between hovering over the line and showing the indicator. |
| @@ -933,6 +951,7 @@ struct PhantomBreakpointIndicator { · 1 unmodified line | |||
| 933 | is_active: bool, | 951 | is_active: bool, |
| 934 | collides_with_existing_breakpoint: bool, | 952 | collides_with_existing_breakpoint: bool, |
| 935 | } | 953 | } |
| 954 | |||
| 936 | /// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. | 955 | /// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. |
| 937 | /// | 956 | /// |
| 938 | /// See the [module level documentation](self) for more information. | 957 | /// See the [module level documentation](self) for more information. |
| @@ -949,8 +968,7 @@ pub struct Editor { · 10 unmodified lines | |||
| 949 | /// When inline assist editors are linked, they all render cursors because | 968 | /// When inline assist editors are linked, they all render cursors because |
| 950 | /// typing enters text into each of them, even the ones that aren't focused. | 969 | /// typing enters text into each of them, even the ones that aren't focused. |
| 951 | pub(crate) show_cursor_when_unfocused: bool, | 970 | pub(crate) show_cursor_when_unfocused: bool, |
| 952 | columnar_selection_tail: Option<Anchor>, | 971 | columnar_selection_state: Option<ColumnarSelectionState>, |
| 953 | columnar_display_point: Option<DisplayPoint>, | ||
| 954 | add_selections_state: Option<AddSelectionsState>, | 972 | add_selections_state: Option<AddSelectionsState>, |
| 955 | select_next_state: Option<SelectNextState>, | 973 | select_next_state: Option<SelectNextState>, |
| 956 | select_prev_state: Option<SelectNextState>, | 974 | select_prev_state: Option<SelectNextState>, |
| @@ -1007,6 +1025,7 @@ pub struct Editor { · 50 unmodified lines | |||
| 1007 | mouse_context_menu: Option<MouseContextMenu>, | 1025 | mouse_context_menu: Option<MouseContextMenu>, |
| 1008 | completion_tasks: Vec<(CompletionId, Task<()>)>, | 1026 | completion_tasks: Vec<(CompletionId, Task<()>)>, |
| 1009 | inline_blame_popover: Option<InlineBlamePopover>, | 1027 | inline_blame_popover: Option<InlineBlamePopover>, |
| 1028 | inline_blame_popover_show_task: Option<Task<()>>, | ||
| 1010 | signature_help_state: SignatureHelpState, | 1029 | signature_help_state: SignatureHelpState, |
| 1011 | auto_signature_help: Option<bool>, | 1030 | auto_signature_help: Option<bool>, |
| 1012 | find_all_references_task_sources: Vec<Anchor>, | 1031 | find_all_references_task_sources: Vec<Anchor>, |
| @@ -1054,8 +1073,9 @@ pub struct Editor { · 41 unmodified lines | |||
| 1054 | style: Option<EditorStyle>, | 1073 | style: Option<EditorStyle>, |
| 1055 | text_style_refinement: Option<TextStyleRefinement>, | 1074 | text_style_refinement: Option<TextStyleRefinement>, |
| 1056 | next_editor_action_id: EditorActionId, | 1075 | next_editor_action_id: EditorActionId, |
| 1057 | editor_actions: | 1076 | editor_actions: Rc< |
| 1058 | Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut Window, &mut Context<Self>)>>>>, | 1077 | RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&Editor, &mut Window, &mut Context<Self>)>>>, |
| 1078 | >, | ||
| 1059 | use_autoclose: bool, | 1079 | use_autoclose: bool, |
| 1060 | use_auto_surround: bool, | 1080 | use_auto_surround: bool, |
| 1061 | auto_replace_emoji_shortcode: bool, | 1081 | auto_replace_emoji_shortcode: bool, |
| @@ -1087,6 +1107,7 @@ pub struct Editor { · 25 unmodified lines | |||
| 1087 | tasks_update_task: Option<Task<()>>, | 1107 | tasks_update_task: Option<Task<()>>, |
| 1088 | breakpoint_store: Option<Entity<BreakpointStore>>, | 1108 | breakpoint_store: Option<Entity<BreakpointStore>>, |
| 1089 | gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>), | 1109 | gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>), |
| 1110 | hovered_diff_hunk_row: Option<DisplayRow>, | ||
| 1090 | pull_diagnostics_task: Task<()>, | 1111 | pull_diagnostics_task: Task<()>, |
| 1091 | in_project_search: bool, | 1112 | in_project_search: bool, |
| 1092 | previous_search_ranges: Option<Arc<[Range<Anchor>]>>, | 1113 | previous_search_ranges: Option<Arc<[Range<Anchor>]>>, |
| @@ -1201,10 +1222,12 @@ struct SelectionHistoryEntry { · 108 unmodified lines | |||
| 1201 | add_selections_state: Option<AddSelectionsState>, | 1222 | add_selections_state: Option<AddSelectionsState>, |
| 1202 | } | 1223 | } |
| 1203 | 1224 | ||
| 1225 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] | ||
| 1204 | enum SelectionHistoryMode { | 1226 | enum SelectionHistoryMode { |
| 1205 | Normal, | 1227 | Normal, |
| 1206 | Undoing, | 1228 | Undoing, |
| 1207 | Redoing, | 1229 | Redoing, |
| 1230 | Skipping, | ||
| 1208 | } | 1231 | } |
| 1209 | 1232 | ||
| 1210 | #[derive(Clone, PartialEq, Eq, Hash)] | 1233 | #[derive(Clone, PartialEq, Eq, Hash)] |
| @@ -1219,10 +1242,55 @@ impl Default for SelectionHistoryMode { · 8 unmodified lines | |||
| 1219 | } | 1242 | } |
| 1220 | } | 1243 | } |
| 1221 | 1244 | ||
| 1245 | #[derive(Debug)] | ||
| 1246 | pub struct SelectionEffects { | ||
| 1247 | nav_history: bool, | ||
| 1248 | completions: bool, | ||
| 1249 | scroll: Option<Autoscroll>, | ||
| 1250 | } | ||
| 1251 | |||
| 1252 | impl Default for SelectionEffects { | ||
| 1253 | fn default() -> Self { | ||
| 1254 | Self { | ||
| 1255 | nav_history: true, | ||
| 1256 | completions: true, | ||
| 1257 | scroll: Some(Autoscroll::fit()), | ||
| 1258 | } | ||
| 1259 | } | ||
| 1260 | } | ||
| 1261 | impl SelectionEffects { | ||
| 1262 | pub fn scroll(scroll: Autoscroll) -> Self { | ||
| 1263 | Self { | ||
| 1264 | scroll: Some(scroll), | ||
| 1265 | ..Default::default() | ||
| 1266 | } | ||
| 1267 | } | ||
| 1268 | |||
| 1269 | pub fn no_scroll() -> Self { | ||
| 1270 | Self { | ||
| 1271 | scroll: None, | ||
| 1272 | ..Default::default() | ||
| 1273 | } | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | pub fn completions(self, completions: bool) -> Self { | ||
| 1277 | Self { | ||
| 1278 | completions, | ||
| 1279 | ..self | ||
| 1280 | } | ||
| 1281 | } | ||
| 1282 | |||
| 1283 | pub fn nav_history(self, nav_history: bool) -> Self { | ||
| 1284 | Self { | ||
| 1285 | nav_history, | ||
| 1286 | ..self | ||
| 1287 | } | ||
| 1288 | } | ||
| 1289 | } | ||
| 1290 | |||
| 1222 | struct DeferredSelectionEffectsState { | 1291 | struct DeferredSelectionEffectsState { |
| 1223 | changed: bool, | 1292 | changed: bool, |
| 1224 | should_update_completions: bool, | 1293 | effects: SelectionEffects, |
| 1225 | autoscroll: Option<Autoscroll>, | ||
| 1226 | old_cursor_position: Anchor, | 1294 | old_cursor_position: Anchor, |
| 1227 | history_entry: SelectionHistoryEntry, | 1295 | history_entry: SelectionHistoryEntry, |
| 1228 | } | 1296 | } |
| @@ -1238,11 +1306,19 @@ struct SelectionHistory { · 9 unmodified lines | |||
| 1238 | } | 1306 | } |
| 1239 | 1307 | ||
| 1240 | impl SelectionHistory { | 1308 | impl SelectionHistory { |
| 1309 | #[track_caller] | ||
| 1241 | fn insert_transaction( | 1310 | fn insert_transaction( |
| 1242 | &mut self, | 1311 | &mut self, |
| 1243 | transaction_id: TransactionId, | 1312 | transaction_id: TransactionId, |
| 1244 | selections: Arc<[Selection<Anchor>]>, | 1313 | selections: Arc<[Selection<Anchor>]>, |
| 1245 | ) { | 1314 | ) { |
| 1315 | if selections.is_empty() { | ||
| 1316 | log::error!( | ||
| 1317 | "SelectionHistory::insert_transaction called with empty selections. Caller: {}", | ||
| 1318 | std::panic::Location::caller() | ||
| 1319 | ); | ||
| 1320 | return; | ||
| 1321 | } | ||
| 1246 | self.selections_by_transaction | 1322 | self.selections_by_transaction |
| 1247 | .insert(transaction_id, (selections, None)); | 1323 | .insert(transaction_id, (selections, None)); |
| 1248 | } | 1324 | } |
| @@ -1272,6 +1348,7 @@ impl SelectionHistory { · 23 unmodified lines | |||
| 1272 | } | 1348 | } |
| 1273 | SelectionHistoryMode::Undoing => self.push_redo(entry), | 1349 | SelectionHistoryMode::Undoing => self.push_redo(entry), |
| 1274 | SelectionHistoryMode::Redoing => self.push_undo(entry), | 1350 | SelectionHistoryMode::Redoing => self.push_undo(entry), |
| 1351 | SelectionHistoryMode::Skipping => {} | ||
| 1275 | } | 1352 | } |
| 1276 | } | 1353 | } |
| 1277 | } | 1354 | } |
| @@ -1486,7 +1563,7 @@ impl InlayHintRefreshReason { · 208 unmodified lines | |||
| 1486 | } | 1563 | } |
| 1487 | 1564 | ||
| 1488 | pub enum FormatTarget { | 1565 | pub enum FormatTarget { |
| 1489 | Buffers, | 1566 | Buffers(HashSet<Entity<Buffer>>), |
| 1490 | Ranges(Vec<Range<MultiBufferPoint>>), | 1567 | Ranges(Vec<Range<MultiBufferPoint>>), |
| 1491 | } | 1568 | } |
| 1492 | 1569 | ||
| @@ -1551,11 +1628,19 @@ impl Editor { · 58 unmodified lines | |||
| 1551 | ) | 1628 | ) |
| 1552 | } | 1629 | } |
| 1553 | 1630 | ||
| 1554 | pub fn auto_height(max_lines: usize, window: &mut Window, cx: &mut Context<Self>) -> Self { | 1631 | pub fn auto_height( |
| 1632 | min_lines: usize, | ||
| 1633 | max_lines: usize, | ||
| 1634 | window: &mut Window, | ||
| 1635 | cx: &mut Context<Self>, | ||
| 1636 | ) -> Self { | ||
| 1555 | let buffer = cx.new(|cx| Buffer::local("", cx)); | 1637 | let buffer = cx.new(|cx| Buffer::local("", cx)); |
| 1556 | let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); | 1638 | let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); |
| 1557 | Self::new( | 1639 | Self::new( |
| 1558 | EditorMode::AutoHeight { max_lines }, | 1640 | EditorMode::AutoHeight { |
| 1641 | min_lines, | ||
| 1642 | max_lines, | ||
| 1643 | }, | ||
| 1559 | buffer, | 1644 | buffer, |
| 1560 | None, | 1645 | None, |
| 1561 | window, | 1646 | window, |
| @@ -1817,8 +1902,7 @@ impl Editor { · 255 unmodified lines | |||
| 1817 | display_map: display_map.clone(), | 1902 | display_map: display_map.clone(), |
| 1818 | selections, | 1903 | selections, |
| 1819 | scroll_manager: ScrollManager::new(cx), | 1904 | scroll_manager: ScrollManager::new(cx), |
| 1820 | columnar_selection_tail: None, | 1905 | columnar_selection_state: None, |
| 1821 | columnar_display_point: None, | ||
| 1822 | add_selections_state: None, | 1906 | add_selections_state: None, |
| 1823 | select_next_state: None, | 1907 | select_next_state: None, |
| 1824 | select_prev_state: None, | 1908 | select_prev_state: None, |
| @@ -1872,6 +1956,7 @@ impl Editor { · 47 unmodified lines | |||
| 1872 | mouse_context_menu: None, | 1956 | mouse_context_menu: None, |
| 1873 | completion_tasks: Vec::new(), | 1957 | completion_tasks: Vec::new(), |
| 1874 | inline_blame_popover: None, | 1958 | inline_blame_popover: None, |
| 1959 | inline_blame_popover_show_task: None, | ||
| 1875 | signature_help_state: SignatureHelpState::default(), | 1960 | signature_help_state: SignatureHelpState::default(), |
| 1876 | auto_signature_help: None, | 1961 | auto_signature_help: None, |
| 1877 | find_all_references_task_sources: Vec::new(), | 1962 | find_all_references_task_sources: Vec::new(), |
| @@ -1949,6 +2034,7 @@ impl Editor { · 71 unmodified lines | |||
| 1949 | 2034 | ||
| 1950 | breakpoint_store, | 2035 | breakpoint_store, |
| 1951 | gutter_breakpoint_indicator: (None, None), | 2036 | gutter_breakpoint_indicator: (None, None), |
| 2037 | hovered_diff_hunk_row: None, | ||
| 1952 | _subscriptions: vec![ | 2038 | _subscriptions: vec![ |
| 1953 | cx.observe(&buffer, Self::on_buffer_changed), | 2039 | cx.observe(&buffer, Self::on_buffer_changed), |
| 1954 | cx.subscribe_in(&buffer, window, Self::on_buffer_event), | 2040 | cx.subscribe_in(&buffer, window, Self::on_buffer_event), |
| @@ -2082,7 +2168,11 @@ impl Editor { · 127 unmodified lines | |||
| 2082 | } | 2168 | } |
| 2083 | } | 2169 | } |
| 2084 | 2170 | ||
| 2171 | // skip adding the initial selection to selection history | ||
| 2172 | editor.selection_history.mode = SelectionHistoryMode::Skipping; | ||
| 2085 | editor.end_selection(window, cx); | 2173 | editor.end_selection(window, cx); |
| 2174 | editor.selection_history.mode = SelectionHistoryMode::Normal; | ||
| 2175 | |||
| 2086 | editor.scroll_manager.show_scrollbars(window, cx); | 2176 | editor.scroll_manager.show_scrollbars(window, cx); |
| 2087 | jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx); | 2177 | jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx); |
| 2088 | 2178 | ||
| @@ -2682,7 +2772,7 @@ impl Editor { · 593 unmodified lines | |||
| 2682 | &mut self, | 2772 | &mut self, |
| 2683 | local: bool, | 2773 | local: bool, |
| 2684 | old_cursor_position: &Anchor, | 2774 | old_cursor_position: &Anchor, |
| 2685 | should_update_completions: bool, | 2775 | effects: SelectionEffects, |
| 2686 | window: &mut Window, | 2776 | window: &mut Window, |
| 2687 | cx: &mut Context<Self>, | 2777 | cx: &mut Context<Self>, |
| 2688 | ) { | 2778 | ) { |
| @@ -2740,12 +2830,14 @@ impl Editor { · 51 unmodified lines | |||
| 2740 | let new_cursor_position = newest_selection.head(); | 2830 | let new_cursor_position = newest_selection.head(); |
| 2741 | let selection_start = newest_selection.start; | 2831 | let selection_start = newest_selection.start; |
| 2742 | 2832 | ||
| 2743 | self.push_to_nav_history( | 2833 | if effects.nav_history { |
| 2744 | *old_cursor_position, | 2834 | self.push_to_nav_history( |
| 2745 | Some(new_cursor_position.to_point(buffer)), | 2835 | *old_cursor_position, |
| 2746 | false, | 2836 | Some(new_cursor_position.to_point(buffer)), |
| 2747 | cx, | 2837 | false, |
| 2748 | ); | 2838 | cx, |
| 2839 | ); | ||
| 2840 | } | ||
| 2749 | 2841 | ||
| 2750 | if local { | 2842 | if local { |
| 2751 | if let Some(buffer_id) = new_cursor_position.buffer_id { | 2843 | if let Some(buffer_id) = new_cursor_position.buffer_id { |
| @@ -2776,7 +2868,7 @@ impl Editor { · 24 unmodified lines | |||
| 2776 | let completion_position = completion_menu.map(|menu| menu.initial_position); | 2868 | let completion_position = completion_menu.map(|menu| menu.initial_position); |
| 2777 | drop(context_menu); | 2869 | drop(context_menu); |
| 2778 | 2870 | ||
| 2779 | if should_update_completions { | 2871 | if effects.completions { |
| 2780 | if let Some(completion_position) = completion_position { | 2872 | if let Some(completion_position) = completion_position { |
| 2781 | let start_offset = selection_start.to_offset(buffer); | 2873 | let start_offset = selection_start.to_offset(buffer); |
| 2782 | let position_matches = start_offset == completion_position.to_offset(buffer); | 2874 | let position_matches = start_offset == completion_position.to_offset(buffer); |
| @@ -2785,7 +2877,8 @@ impl Editor { · 2 unmodified lines | |||
| 2785 | buffer.char_kind_before(start_offset, true) == Some(CharKind::Word) | 2877 | buffer.char_kind_before(start_offset, true) == Some(CharKind::Word) |
| 2786 | } else { | 2878 | } else { |
| 2787 | // Snippet choices can be shown even when the cursor is in whitespace. | 2879 | // Snippet choices can be shown even when the cursor is in whitespace. |
| 2788 | // Dismissing the menu when actions like backspace | 2880 | // Dismissing the menu with actions like backspace is handled by |
| 2881 | // invalidation regions. | ||
| 2789 | true | 2882 | true |
| 2790 | } | 2883 | } |
| 2791 | } else { | 2884 | } else { |
| @@ -2851,22 +2944,22 @@ impl Editor { · 59 unmodified lines | |||
| 2851 | let background_executor = cx.background_executor().clone(); | 2944 | let background_executor = cx.background_executor().clone(); |
| 2852 | let editor_id = cx.entity().entity_id().as_u64() as ItemId; | 2945 | let editor_id = cx.entity().entity_id().as_u64() as ItemId; |
| 2853 | self.serialize_selections = cx.background_spawn(async move { | 2946 | self.serialize_selections = cx.background_spawn(async move { |
| 2854 | background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; | 2947 | background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; |
| 2855 | let db_selections = selections | 2948 | let db_selections = selections |
| 2856 | .iter() | 2949 | .iter() |
| 2857 | .map(|selection| { | 2950 | .map(|selection| { |
| 2858 | ( | 2951 | ( |
| 2859 | selection.start.to_offset(&snapshot), | 2952 | selection.start.to_offset(&snapshot), |
| 2860 | selection.end.to_offset(&snapshot), | 2953 | selection.end.to_offset(&snapshot), |
| 2861 | ) | 2954 | ) |
| 2862 | }) | 2955 | }) |
| 2863 | .collect(); | 2956 | .collect(); |
| 2864 | 2957 | ||
| 2865 | DB.save_editor_selections(editor_id, workspace_id, db_selections) | 2958 | DB.save_editor_selections(editor_id, workspace_id, db_selections) |
| 2866 | .await | 2959 | .await |
| 2867 | .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) | 2960 | .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) |
| 2868 | .log_err(); | 2961 | .log_err(); |
| 2869 | }); | 2962 | }); |
| 2870 | } | 2963 | } |
| 2871 | } | 2964 | } |
| 2872 | } | 2965 | } |
| @@ -2983,43 +3076,23 @@ impl Editor { · 110 unmodified lines | |||
| 2983 | /// effects of selection change occur at the end of the transaction. | 3076 | /// effects of selection change occur at the end of the transaction. |
| 2984 | pub fn change_selections<R>( | 3077 | pub fn change_selections<R>( |
| 2985 | &mut self, | 3078 | &mut self, |
| 2986 | autoscroll: Option<Autoscroll>, | 3079 | effects: impl Into<SelectionEffects>, |
| 2987 | window: &mut Window, | ||
| 2988 | cx: &mut Context<Self>, | ||
| 2989 | change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, | ||
| 2990 | ) -> R { | ||
| 2991 | self.change_selections_inner(true, autoscroll, window, cx, change) | ||
| 2992 | } | ||
| 2993 | |||
| 2994 | pub(crate) fn change_selections_without_updating_completions<R>( | ||
| 2995 | &mut self, | ||
| 2996 | autoscroll: Option<Autoscroll>, | ||
| 2997 | window: &mut Window, | ||
| 2998 | cx: &mut Context<Self>, | ||
| 2999 | change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, | ||
| 3000 | ) -> R { | ||
| 3001 | self.change_selections_inner(false, autoscroll, window, cx, change) | ||
| 3002 | } | ||
| 3003 | |||
| 3004 | fn change_selections_inner<R>( | ||
| 3005 | &mut self, | ||
| 3006 | should_update_completions: bool, | ||
| 3007 | autoscroll: Option<Autoscroll>, | ||
| 3008 | window: &mut Window, | 3080 | window: &mut Window, |
| 3009 | cx: &mut Context<Self>, | 3081 | cx: &mut Context<Self>, |
| 3010 | change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, | 3082 | change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, |
| 3011 | ) -> R { | 3083 | ) -> R { |
| 3084 | let effects = effects.into(); | ||
| 3012 | if let Some(state) = &mut self.deferred_selection_effects_state { | 3085 | if let Some(state) = &mut self.deferred_selection_effects_state { |
| 3013 | state.autoscroll = autoscroll.or(state.autoscroll); | 3086 | state.effects.scroll = effects.scroll.or(state.effects.scroll); |
| 3014 | state.should_update_completions = should_update_completions; | 3087 | state.effects.completions = effects.completions; |
| 3088 | state.effects.nav_history |= effects.nav_history; | ||
| 3015 | let (changed, result) = self.selections.change_with(cx, change); | 3089 | let (changed, result) = self.selections.change_with(cx, change); |
| 3016 | state.changed |= changed; | 3090 | state.changed |= changed; |
| 3017 | return result; | 3091 | return result; |
| 3018 | } | 3092 | } |
| 3019 | let mut state = DeferredSelectionEffectsState { | 3093 | let mut state = DeferredSelectionEffectsState { |
| 3020 | changed: false, | 3094 | changed: false, |
| 3021 | should_update_completions, | 3095 | effects, |
| 3022 | autoscroll, | ||
| 3023 | old_cursor_position: self.selections.newest_anchor().head(), | 3096 | old_cursor_position: self.selections.newest_anchor().head(), |
| 3024 | history_entry: SelectionHistoryEntry { | 3097 | history_entry: SelectionHistoryEntry { |
| 3025 | selections: self.selections.disjoint_anchors(), | 3098 | selections: self.selections.disjoint_anchors(), |
| @@ -3069,19 +3142,13 @@ impl Editor { · 43 unmodified lines | |||
| 3069 | if state.changed { | 3142 | if state.changed { |
| 3070 | self.selection_history.push(state.history_entry); | 3143 | self.selection_history.push(state.history_entry); |
| 3071 | 3144 | ||
| 3072 | if let Some(autoscroll) = state.autoscroll { | 3145 | if let Some(autoscroll) = state.effects.scroll { |
| 3073 | self.request_autoscroll(autoscroll, cx); | 3146 | self.request_autoscroll(autoscroll, cx); |
| 3074 | } | 3147 | } |
| 3075 | 3148 | ||
| 3076 | let old_cursor_position = &state.old_cursor_position; | 3149 | let old_cursor_position = &state.old_cursor_position; |
| 3077 | 3150 | ||
| 3078 | self.selections_did_change( | 3151 | self.selections_did_change(true, &old_cursor_position, state.effects, window, cx); |
| 3079 | true, | ||
| 3080 | &old_cursor_position, | ||
| 3081 | state.should_update_completions, | ||
| 3082 | window, | ||
| 3083 | cx, | ||
| 3084 | ); | ||
| 3085 | 3152 | ||
| 3086 | if self.should_open_signature_help_automatically(&old_cursor_position, cx) { | 3153 | if self.should_open_signature_help_automatically(&old_cursor_position, cx) { |
| 3087 | self.show_signature_help(&ShowSignatureHelp, window, cx); | 3154 | self.show_signature_help(&ShowSignatureHelp, window, cx); |
| @@ -3156,7 +3223,8 @@ impl Editor { · 68 unmodified lines | |||
| 3156 | position, | 3223 | position, |
| 3157 | goal_column, | 3224 | goal_column, |
| 3158 | reset, | 3225 | reset, |
| 3159 | } => self.begin_columnar_selection(position, goal_column, reset, window, cx), | 3226 | mode, |
| 3227 | } => self.begin_columnar_selection(position, goal_column, reset, mode, window, cx), | ||
| 3160 | SelectPhase::Extend { | 3228 | SelectPhase::Extend { |
| 3161 | position, | 3229 | position, |
| 3162 | click_count, | 3230 | click_count, |
| @@ -3201,9 +3269,13 @@ impl Editor { · 38 unmodified lines | |||
| 3201 | _ => {} | 3269 | _ => {} |
| 3202 | } | 3270 | } |
| 3203 | 3271 | ||
| 3204 | let auto_scroll = EditorSettings::get_global(cx).autoscroll_on_clicks; | 3272 | let effects = if EditorSettings::get_global(cx).autoscroll_on_clicks { |
| 3273 | SelectionEffects::scroll(Autoscroll::fit()) | ||
| 3274 | } else { | ||
| 3275 | SelectionEffects::no_scroll() | ||
| 3276 | }; | ||
| 3205 | 3277 | ||
| 3206 | self.change_selections(auto_scroll.then(Autoscroll::fit), window, cx, |s| { | 3278 | self.change_selections(effects, window, cx, |s| { |
| 3207 | s.set_pending(pending_selection, pending_mode) | 3279 | s.set_pending(pending_selection, pending_mode) |
| 3208 | }); | 3280 | }); |
| 3209 | } | 3281 | } |
| @@ -3309,6 +3381,7 @@ impl Editor { · 99 unmodified lines | |||
| 3309 | position: DisplayPoint, | 3381 | position: DisplayPoint, |
| 3310 | goal_column: u32, | 3382 | goal_column: u32, |
| 3311 | reset: bool, | 3383 | reset: bool, |
| 3384 | mode: ColumnarMode, | ||
| 3312 | window: &mut Window, | 3385 | window: &mut Window, |
| 3313 | cx: &mut Context<Self>, | 3386 | cx: &mut Context<Self>, |
| 3314 | ) { | 3387 | ) { |
| @@ -3331,26 +3404,30 @@ impl Editor { · 16 unmodified lines | |||
| 3331 | SelectMode::Character, | 3404 | SelectMode::Character, |
| 3332 | ); | 3405 | ); |
| 3333 | }); | 3406 | }); |
| 3334 | if position.column() != goal_column { | 3407 | }; |
| 3335 | self.columnar_display_point = Some(DisplayPoint::new(position.row(), goal_column)); | ||
| 3336 | } else { | ||
| 3337 | self.columnar_display_point = None; | ||
| 3338 | } | ||
| 3339 | } | ||
| 3340 | 3408 | ||
| 3341 | let tail = self.selections.newest::<Point>(cx).tail(); | 3409 | let tail = self.selections.newest::<Point>(cx).tail(); |
| 3342 | self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); | 3410 | let selection_anchor = display_map.buffer_snapshot.anchor_before(tail); |
| 3411 | self.columnar_selection_state = match mode { | ||
| 3412 | ColumnarMode::FromMouse => Some(ColumnarSelectionState::FromMouse { | ||
| 3413 | selection_tail: selection_anchor, | ||
| 3414 | display_point: if reset { | ||
| 3415 | if position.column() != goal_column { | ||
| 3416 | Some(DisplayPoint::new(position.row(), goal_column)) | ||
| 3417 | } else { | ||
| 3418 | None | ||
| 3419 | } | ||
| 3420 | } else { | ||
| 3421 | None | ||
| 3422 | }, | ||
| 3423 | }), | ||
| 3424 | ColumnarMode::FromSelection => Some(ColumnarSelectionState::FromSelection { | ||
| 3425 | selection_tail: selection_anchor, | ||
| 3426 | }), | ||
| 3427 | }; | ||
| 3343 | 3428 | ||
| 3344 | if !reset { | 3429 | if !reset { |
| 3345 | self.columnar_display_point = None; | 3430 | self.select_columns(position, goal_column, &display_map, window, cx); |
| 3346 | self.select_columns( | ||
| 3347 | tail.to_display_point(&display_map), | ||
| 3348 | position, | ||
| 3349 | goal_column, | ||
| 3350 | &display_map, | ||
| 3351 | window, | ||
| 3352 | cx, | ||
| 3353 | ); | ||
| 3354 | } | 3431 | } |
| 3355 | } | 3432 | } |
| 3356 | 3433 | ||
| @@ -3364,11 +3441,8 @@ impl Editor { · 7 unmodified lines | |||
| 3364 | ) { | 3441 | ) { |
| 3365 | let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); | 3442 | let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); |
| 3366 | 3443 | ||
| 3367 | if let Some(tail) = self.columnar_selection_tail.as_ref() { | 3444 | if self.columnar_selection_state.is_some() { |
| 3368 | let tail = self | 3445 | self.select_columns(position, goal_column, &display_map, window, cx); |
| 3369 | .columnar_display_point | ||
| 3370 | .unwrap_or_else(|| tail.to_display_point(&display_map)); | ||
| 3371 | self.select_columns(tail, position, goal_column, &display_map, window, cx); | ||
| 3372 | } else if let Some(mut pending) = self.selections.pending_anchor() { | 3446 | } else if let Some(mut pending) = self.selections.pending_anchor() { |
| 3373 | let buffer = self.buffer.read(cx).snapshot(cx); | 3447 | let buffer = self.buffer.read(cx).snapshot(cx); |
| 3374 | let head; | 3448 | let head; |
| @@ -3455,7 +3529,7 @@ impl Editor { · 80 unmodified lines | |||
| 3455 | } | 3529 | } |
| 3456 | 3530 | ||
| 3457 | fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) { | 3531 | fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) { |
| 3458 | self.columnar_selection_tail.take(); | 3532 | self.columnar_selection_state.take(); |
| 3459 | if self.selections.pending_anchor().is_some() { | 3533 | if self.selections.pending_anchor().is_some() { |
| 3460 | let selections = self.selections.all::<usize>(cx); | 3534 | let selections = self.selections.all::<usize>(cx); |
| 3461 | self.change_selections(None, window, cx, |s| { | 3535 | self.change_selections(None, window, cx, |s| { |
| @@ -3467,13 +3541,26 @@ impl Editor { · 5 unmodified lines | |||
| 3467 | 3541 | ||
| 3468 | fn select_columns( | 3542 | fn select_columns( |
| 3469 | &mut self, | 3543 | &mut self, |
| 3470 | tail: DisplayPoint, | ||
| 3471 | head: DisplayPoint, | 3544 | head: DisplayPoint, |
| 3472 | goal_column: u32, | 3545 | goal_column: u32, |
| 3473 | display_map: &DisplaySnapshot, | 3546 | display_map: &DisplaySnapshot, |
| 3474 | window: &mut Window, | 3547 | window: &mut Window, |
| 3475 | cx: &mut Context<Self>, | 3548 | cx: &mut Context<Self>, |
| 3476 | ) { | 3549 | ) { |
| 3550 | let Some(columnar_state) = self.columnar_selection_state.as_ref() else { | ||
| 3551 | return; | ||
| 3552 | }; | ||
| 3553 | |||
| 3554 | let tail = match columnar_state { | ||
| 3555 | ColumnarSelectionState::FromMouse { | ||
| 3556 | selection_tail, | ||
| 3557 | display_point, | ||
| 3558 | } => display_point.unwrap_or_else(|| selection_tail.to_display_point(&display_map)), | ||
| 3559 | ColumnarSelectionState::FromSelection { selection_tail } => { | ||
| 3560 | selection_tail.to_display_point(&display_map) | ||
| 3561 | } | ||
| 3562 | }; | ||
| 3563 | |||
| 3477 | let start_row = cmp::min(tail.row(), head.row()); | 3564 | let start_row = cmp::min(tail.row(), head.row()); |
| 3478 | let end_row = cmp::max(tail.row(), head.row()); | 3565 | let end_row = cmp::max(tail.row(), head.row()); |
| 3479 | let start_column = cmp::min(tail.column(), goal_column); | 3566 | let start_column = cmp::min(tail.column(), goal_column); |
| @@ -3483,7 +3570,10 @@ impl Editor { · 3 unmodified lines | |||
| 3483 | let selection_ranges = (start_row.0..=end_row.0) | 3570 | let selection_ranges = (start_row.0..=end_row.0) |
| 3484 | .map(DisplayRow) | 3571 | .map(DisplayRow) |
| 3485 | .filter_map(|row| { | 3572 | .filter_map(|row| { |
| 3486 | if !display_map.is_block_line(row) { | 3573 | if (matches!(columnar_state, ColumnarSelectionState::FromMouse { .. }) |
| 3574 | || start_column <= display_map.line_len(row)) | ||
| 3575 | && !display_map.is_block_line(row) | ||
| 3576 | { | ||
| 3487 | let start = display_map | 3577 | let start = display_map |
| 3488 | .clip_point(DisplayPoint::new(row, start_column), Bias::Left) | 3578 | .clip_point(DisplayPoint::new(row, start_column), Bias::Left) |
| 3489 | .to_point(display_map); | 3579 | .to_point(display_map); |
| @@ -3501,15 +3591,19 @@ impl Editor { · 11 unmodified lines | |||
| 3501 | }) | 3591 | }) |
| 3502 | .collect::<Vec<_>>(); | 3592 | .collect::<Vec<_>>(); |
| 3503 | 3593 | ||
| 3504 | let mut non_empty_ranges = selection_ranges | 3594 | let ranges = match columnar_state { |
| 3505 | .iter() | 3595 | ColumnarSelectionState::FromMouse { .. } => { |
| 3506 | .filter(|selection_range| selection_range.start != selection_range.end) | 3596 | let mut non_empty_ranges = selection_ranges |
| 3507 | .peekable(); | 3597 | .iter() |
| 3508 | 3598 | .filter(|selection_range| selection_range.start != selection_range.end) | |
| 3509 | let ranges = if non_empty_ranges.peek().is_some() { | 3599 | .peekable(); |
| 3510 | non_empty_ranges.cloned().collect() | 3600 | if non_empty_ranges.peek().is_some() { |
| 3511 | } else { | 3601 | non_empty_ranges.cloned().collect() |
| 3512 | selection_ranges | 3602 | } else { |
| 3603 | selection_ranges | ||
| 3604 | } | ||
| 3605 | } | ||
| 3606 | _ => selection_ranges, | ||
| 3513 | }; | 3607 | }; |
| 3514 | 3608 | ||
| 3515 | self.change_selections(None, window, cx, |s| { | 3609 | self.change_selections(None, window, cx, |s| { |
| @@ -3532,11 +3626,11 @@ impl Editor { · 16 unmodified lines | |||
| 3532 | }; | 3626 | }; |
| 3533 | 3627 | ||
| 3534 | pending_nonempty_selection | 3628 | pending_nonempty_selection |
| 3535 | || (self.columnar_selection_tail.is_some() && self.selections.disjoint.len() > 1) | 3629 | || (self.columnar_selection_state.is_some() && self.selections.disjoint.len() > 1) |
| 3536 | } | 3630 | } |
| 3537 | 3631 | ||
| 3538 | pub fn has_pending_selection(&self) -> bool { | 3632 | pub fn has_pending_selection(&self) -> bool { |
| 3539 | self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some() | 3633 | self.selections.pending_anchor().is_some() || self.columnar_selection_state.is_some() |
| 3540 | } | 3634 | } |
| 3541 | 3635 | ||
| 3542 | pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) { | 3636 | pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) { |
| @@ -3990,8 +4084,8 @@ impl Editor { · 447 unmodified lines | |||
| 3990 | } | 4084 | } |
| 3991 | 4085 | ||
| 3992 | let had_active_inline_completion = this.has_active_inline_completion(); | 4086 | let had_active_inline_completion = this.has_active_inline_completion(); |
| 3993 | this.change_selections_without_updating_completions( | 4087 | this.change_selections( |
| 3994 | Some(Autoscroll::fit()), | 4088 | SelectionEffects::scroll(Autoscroll::fit()).completions(false), |
| 3995 | window, | 4089 | window, |
| 3996 | cx, | 4090 | cx, |
| 3997 | |s| s.select(new_selections), | 4091 | |s| s.select(new_selections), |
| @@ -5114,10 +5208,13 @@ impl Editor { · 1116 unmodified lines | |||
| 5114 | .as_ref() | 5208 | .as_ref() |
| 5115 | .map_or(true, |provider| provider.filter_completions()); | 5209 | .map_or(true, |provider| provider.filter_completions()); |
| 5116 | 5210 | ||
| 5117 | // When `is_incomplete` is false, can filter completions instead of re-querying when the | ||
| 5118 | // current query is a suffix of the initial query. | ||
| 5119 | if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() { | 5211 | if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() { |
| 5120 | if !menu.is_incomplete && filter_completions { | 5212 | if filter_completions { |
| 5213 | menu.filter(query.clone(), provider.clone(), window, cx); | ||
| 5214 | } | ||
| 5215 | // When `is_incomplete` is false, no need to re-query completions when the current query | ||
| 5216 | // is a suffix of the initial query. | ||
| 5217 | if !menu.is_incomplete { | ||
| 5121 | // If the new query is a suffix of the old query (typing more characters) and | 5218 | // If the new query is a suffix of the old query (typing more characters) and |
| 5122 | // the previous result was complete, the existing completions can be filtered. | 5219 | // the previous result was complete, the existing completions can be filtered. |
| 5123 | // | 5220 | // |
| @@ -5135,7 +5232,6 @@ impl Editor { · 11 unmodified lines | |||
| 5135 | menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot) | 5232 | menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot) |
| 5136 | }; | 5233 | }; |
| 5137 | if position_matches { | 5234 | if position_matches { |
| 5138 | menu.filter(query.clone(), provider.clone(), window, cx); | ||
| 5139 | return; | 5235 | return; |
| 5140 | } | 5236 | } |
| 5141 | } | 5237 | } |
| @@ -5684,70 +5780,60 @@ impl Editor { · 542 unmodified lines | |||
| 5684 | drop(context_menu); | 5780 | drop(context_menu); |
| 5685 | let snapshot = self.snapshot(window, cx); | 5781 | let snapshot = self.snapshot(window, cx); |
| 5686 | let deployed_from = action.deployed_from.clone(); | 5782 | let deployed_from = action.deployed_from.clone(); |
| 5687 | let mut task = self.code_actions_task.take(); | ||
| 5688 | let action = action.clone(); | 5783 | let action = action.clone(); |
| 5689 | cx.spawn_in(window, async move |editor, cx| { | 5784 | self.completion_tasks.clear(); |
| 5690 | while let Some(prev_task) = task { | 5785 | self.discard_inline_completion(false, cx); |
| 5691 | prev_task.await.log_err(); | 5786 | |
| 5692 | task = editor.update(cx, |this, _| this.code_actions_task.take())?; | 5787 | let multibuffer_point = match &action.deployed_from { |
| 5788 | Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => { | ||
| 5789 | DisplayPoint::new(*row, 0).to_point(&snapshot) | ||
| 5693 | } | 5790 | } |
| 5791 | _ => self.selections.newest::<Point>(cx).head(), | ||
| 5792 | }; | ||
| 5793 | let Some((buffer, buffer_row)) = snapshot | ||
| 5794 | .buffer_snapshot | ||
| 5795 | .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) | ||
| 5796 | .and_then(|(buffer_snapshot, range)| { | ||
| 5797 | self.buffer() | ||
| 5798 | .read(cx) | ||
| 5799 | .buffer(buffer_snapshot.remote_id()) | ||
| 5800 | .map(|buffer| (buffer, range.start.row)) | ||
| 5801 | }) | ||
| 5802 | else { | ||
| 5803 | return; | ||
| 5804 | }; | ||
| 5805 | let buffer_id = buffer.read(cx).remote_id(); | ||
| 5806 | let tasks = self | ||
| 5807 | .tasks | ||
| 5808 | .get(&(buffer_id, buffer_row)) | ||
| 5809 | .map(|t| Arc::new(t.to_owned())); | ||
| 5694 | 5810 | ||
| 5695 | let spawned_test_task = editor.update_in(cx, |editor, window, cx| { | 5811 | if !self.focus_handle.is_focused(window) { |
| 5696 | if editor.focus_handle.is_focused(window) { | 5812 | return; |
| 5697 | let multibuffer_point = match &action.deployed_from { | 5813 | } |
| 5698 | Some(CodeActionSource::Indicator(row)) => { | 5814 | let project = self.project.clone(); |
| 5699 | DisplayPoint::new(*row, 0).to_point(&snapshot) | 5815 | |
| 5700 | } | 5816 | let code_actions_task = match deployed_from { |
| 5701 | _ => editor.selections.newest::<Point>(cx).head(), | 5817 | Some(CodeActionSource::RunMenu(_)) => Task::ready(None), |
| 5702 | }; | 5818 | _ => self.code_actions(buffer_row, window, cx), |
| 5703 | let (buffer, buffer_row) = snapshot | 5819 | }; |
| 5704 | .buffer_snapshot | 5820 | |
| 5705 | .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) | 5821 | let runnable_task = match deployed_from { |
| 5706 | .and_then(|(buffer_snapshot, range)| { | 5822 | Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())), |
| 5707 | editor | 5823 | _ => { |
| 5708 | .buffer | 5824 | let mut task_context_task = Task::ready(None); |
| 5709 | .read(cx) | 5825 | if let Some(tasks) = &tasks { |
| 5710 | .buffer(buffer_snapshot.remote_id()) | 5826 | if let Some(project) = project { |
| 5711 | .map(|buffer| (buffer, range.start.row)) | 5827 | task_context_task = |
| 5712 | })?; | 5828 | Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); |
| 5713 | let (_, code_actions) = editor | ||
| 5714 | .available_code_actions | ||
| 5715 | .clone() | ||
| 5716 | .and_then(|(location, code_actions)| { | ||
| 5717 | let snapshot = location.buffer.read(cx).snapshot(); | ||
| 5718 | let point_range = location.range.to_point(&snapshot); | ||
| 5719 | let point_range = point_range.start.row..=point_range.end.row; | ||
| 5720 | if point_range.contains(&buffer_row) { | ||
| 5721 | Some((location, code_actions)) | ||
| 5722 | } else { | ||
| 5723 | None | ||
| 5724 | } | ||
| 5725 | }) | ||
| 5726 | .unzip(); | ||
| 5727 | let buffer_id = buffer.read(cx).remote_id(); | ||
| 5728 | let tasks = editor | ||
| 5729 | .tasks | ||
| 5730 | .get(&(buffer_id, buffer_row)) | ||
| 5731 | .map(|t| Arc::new(t.to_owned())); | ||
| 5732 | if tasks.is_none() && code_actions.is_none() { | ||
| 5733 | return None; | ||
| 5734 | } | 5829 | } |
| 5830 | } | ||
| 5735 | 5831 | ||
| 5736 | editor.completion_tasks.clear(); | 5832 | cx.spawn_in(window, { |
| 5737 | editor.discard_inline_completion(false, cx); | 5833 | let buffer = buffer.clone(); |
| 5738 | let task_context = | 5834 | async move |editor, cx| { |
| 5739 | tasks | 5835 | let task_context = task_context_task.await; |
| 5740 | .as_ref() | ||
| 5741 | .zip(editor.project.clone()) | ||
| 5742 | .map(|(tasks, project)| { | ||
| 5743 | Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) | ||
| 5744 | }); | ||
| 5745 | 5836 | ||
| 5746 | Some(cx.spawn_in(window, async move |editor, cx| { | ||
| 5747 | let task_context = match task_context { | ||
| 5748 | Some(task_context) => task_context.await, | ||
| 5749 | None => None, | ||
| 5750 | }; | ||
| 5751 | let resolved_tasks = | 5837 | let resolved_tasks = |
| 5752 | tasks | 5838 | tasks |
| 5753 | .zip(task_context.clone()) | 5839 | .zip(task_context.clone()) |
| @@ -5758,104 +5844,144 @@ impl Editor { · 4 unmodified lines | |||
| 5758 | tasks.column, | 5844 | tasks.column, |
| 5759 | )), | 5845 | )), |
| 5760 | }); | 5846 | }); |
| 5761 | let debug_scenarios = editor.update(cx, |editor, cx| { | 5847 | let debug_scenarios = editor |
| 5762 | if cx.has_flag::<DebuggerFeatureFlag>() { | 5848 | .update(cx, |editor, cx| { |
| 5763 | maybe!({ | 5849 | editor.debug_scenarios(&resolved_tasks, &buffer, cx) |
| 5764 | let project = editor.project.as_ref()?; | 5850 | })? |
| 5765 | let dap_store = project.read(cx).dap_store(); | 5851 | .await; |
| 5766 | let mut scenarios = vec![]; | 5852 | anyhow::Ok((resolved_tasks, debug_scenarios, task_context)) |
| 5767 | let resolved_tasks = resolved_tasks.as_ref()?; | 5853 | } |
| 5768 | let buffer = buffer.read(cx); | 5854 | }) |
| 5769 | let language = buffer.language()?; | ||
| 5770 | let file = buffer.file(); | ||
| 5771 | let debug_adapter = | ||
| 5772 | language_settings(language.name().into(), file, cx) | ||
| 5773 | .debuggers | ||
| 5774 | .first() | ||
| 5775 | .map(SharedString::from) | ||
| 5776 | .or_else(|| { | ||
| 5777 | language | ||
| 5778 | .config() | ||
| 5779 | .debuggers | ||
| 5780 | .first() | ||
| 5781 | .map(SharedString::from) | ||
| 5782 | })?; | ||
| 5783 | |||
| 5784 | dap_store.update(cx, |dap_store, cx| { | ||
| 5785 | for (_, task) in &resolved_tasks.templates { | ||
| 5786 | if let Some(scenario) = dap_store | ||
| 5787 | .debug_scenario_for_build_task( | ||
| 5788 | task.original_task().clone(), | ||
| 5789 | debug_adapter.clone().into(), | ||
| 5790 | task.display_label().to_owned().into(), | ||
| 5791 | cx, | ||
| 5792 | ) | ||
| 5793 | { | ||
| 5794 | scenarios.push(scenario); | ||
| 5795 | } | ||
| 5796 | } | ||
| 5797 | }); | ||
| 5798 | Some(scenarios) | ||
| 5799 | }) | ||
| 5800 | .unwrap_or_default() | ||
| 5801 | } else { | ||
| 5802 | vec![] | ||
| 5803 | } | ||
| 5804 | })?; | ||
| 5805 | let spawn_straight_away = quick_launch | ||
| 5806 | && resolved_tasks | ||
| 5807 | .as_ref() | ||
| 5808 | .map_or(false, |tasks| tasks.templates.len() == 1) | ||
| 5809 | && code_actions | ||
| 5810 | .as_ref() | ||
| 5811 | .map_or(true, |actions| actions.is_empty()) | ||
| 5812 | && debug_scenarios.is_empty(); | ||
| 5813 | if let Ok(task) = editor.update_in(cx, |editor, window, cx| { | ||
| 5814 | crate::hover_popover::hide_hover(editor, cx); | ||
| 5815 | *editor.context_menu.borrow_mut() = | ||
| 5816 | Some(CodeContextMenu::CodeActions(CodeActionsMenu { | ||
| 5817 | buffer, | ||
| 5818 | actions: CodeActionContents::new( | ||
| 5819 | resolved_tasks, | ||
| 5820 | code_actions, | ||
| 5821 | debug_scenarios, | ||
| 5822 | task_context.unwrap_or_default(), | ||
| 5823 | ), | ||
| 5824 | selected_item: Default::default(), | ||
| 5825 | scroll_handle: UniformListScrollHandle::default(), | ||
| 5826 | deployed_from, | ||
| 5827 | })); | ||
| 5828 | if spawn_straight_away { | ||
| 5829 | if let Some(task) = editor.confirm_code_action( | ||
| 5830 | &ConfirmCodeAction { item_ix: Some(0) }, | ||
| 5831 | window, | ||
| 5832 | cx, | ||
| 5833 | ) { | ||
| 5834 | cx.notify(); | ||
| 5835 | return task; | ||
| 5836 | } | ||
| 5837 | } | ||
| 5838 | cx.notify(); | ||
| 5839 | Task::ready(Ok(())) | ||
| 5840 | }) { | ||
| 5841 | task.await | ||
| 5842 | } else { | ||
| 5843 | Ok(()) | ||
| 5844 | } | ||
| 5845 | })) | ||
| 5846 | } else { | ||
| 5847 | Some(Task::ready(Ok(()))) | ||
| 5848 | } | ||
| 5849 | })?; | ||
| 5850 | if let Some(task) = spawned_test_task { | ||
| 5851 | task.await?; | ||
| 5852 | } | 5855 | } |
| 5856 | }; | ||
| 5853 | 5857 | ||
| 5854 | anyhow::Ok(()) | 5858 | cx.spawn_in(window, async move |editor, cx| { |
| 5859 | let (resolved_tasks, debug_scenarios, task_context) = runnable_task.await?; | ||
| 5860 | let code_actions = code_actions_task.await; | ||
| 5861 | let spawn_straight_away = quick_launch | ||
| 5862 | && resolved_tasks | ||
| 5863 | .as_ref() | ||
| 5864 | .map_or(false, |tasks| tasks.templates.len() == 1) | ||
| 5865 | && code_actions | ||
| 5866 | .as_ref() | ||
| 5867 | .map_or(true, |actions| actions.is_empty()) | ||
| 5868 | && debug_scenarios.is_empty(); | ||
| 5869 | |||
| 5870 | editor.update_in(cx, |editor, window, cx| { | ||
| 5871 | crate::hover_popover::hide_hover(editor, cx); | ||
| 5872 | *editor.context_menu.borrow_mut() = | ||
| 5873 | Some(CodeContextMenu::CodeActions(CodeActionsMenu { | ||
| 5874 | buffer, | ||
| 5875 | actions: CodeActionContents::new( | ||
| 5876 | resolved_tasks, | ||
| 5877 | code_actions, | ||
| 5878 | debug_scenarios, | ||
| 5879 | task_context.unwrap_or_default(), | ||
| 5880 | ), | ||
| 5881 | selected_item: Default::default(), | ||
| 5882 | scroll_handle: UniformListScrollHandle::default(), | ||
| 5883 | deployed_from, | ||
| 5884 | })); | ||
| 5885 | if spawn_straight_away { | ||
| 5886 | if let Some(task) = editor.confirm_code_action( | ||
| 5887 | &ConfirmCodeAction { item_ix: Some(0) }, | ||
| 5888 | window, | ||
| 5889 | cx, | ||
| 5890 | ) { | ||
| 5891 | cx.notify(); | ||
| 5892 | return task; | ||
| 5893 | } | ||
| 5894 | } | ||
| 5895 | |||
| 5896 | Task::ready(Ok(())) | ||
| 5897 | }) | ||
| 5855 | }) | 5898 | }) |
| 5856 | .detach_and_log_err(cx); | 5899 | .detach_and_log_err(cx); |
| 5857 | } | 5900 | } |
| 5858 | 5901 | ||
| 5902 | fn debug_scenarios( | ||
| 5903 | &mut self, | ||
| 5904 | resolved_tasks: &Option<ResolvedTasks>, | ||
| 5905 | buffer: &Entity<Buffer>, | ||
| 5906 | cx: &mut App, | ||
| 5907 | ) -> Task<Vec<task::DebugScenario>> { | ||
| 5908 | if cx.has_flag::<DebuggerFeatureFlag>() { | ||
| 5909 | maybe!({ | ||
| 5910 | let project = self.project.as_ref()?; | ||
| 5911 | let dap_store = project.read(cx).dap_store(); | ||
| 5912 | let mut scenarios = vec![]; | ||
| 5913 | let resolved_tasks = resolved_tasks.as_ref()?; | ||
| 5914 | let buffer = buffer.read(cx); | ||
| 5915 | let language = buffer.language()?; | ||
| 5916 | let file = buffer.file(); | ||
| 5917 | let debug_adapter = language_settings(language.name().into(), file, cx) | ||
| 5918 | .debuggers | ||
| 5919 | .first() | ||
| 5920 | .map(SharedString::from) | ||
| 5921 | .or_else(|| language.config().debuggers.first().map(SharedString::from))?; | ||
| 5922 | |||
| 5923 | dap_store.update(cx, |dap_store, cx| { | ||
| 5924 | for (_, task) in &resolved_tasks.templates { | ||
| 5925 | let maybe_scenario = dap_store.debug_scenario_for_build_task( | ||
| 5926 | task.original_task().clone(), | ||
| 5927 | debug_adapter.clone().into(), | ||
| 5928 | task.display_label().to_owned().into(), | ||
| 5929 | cx, | ||
| 5930 | ); | ||
| 5931 | scenarios.push(maybe_scenario); | ||
| 5932 | } | ||
| 5933 | }); | ||
| 5934 | Some(cx.background_spawn(async move { | ||
| 5935 | let scenarios = futures::future::join_all(scenarios) | ||
| 5936 | .await | ||
| 5937 | .into_iter() | ||
| 5938 | .flatten() | ||
| 5939 | .collect::<Vec<_>>(); | ||
| 5940 | scenarios | ||
| 5941 | })) | ||
| 5942 | }) | ||
| 5943 | .unwrap_or_else(|| Task::ready(vec![])) | ||
| 5944 | } else { | ||
| 5945 | Task::ready(vec![]) | ||
| 5946 | } | ||
| 5947 | } | ||
| 5948 | |||
| 5949 | fn code_actions( | ||
| 5950 | &mut self, | ||
| 5951 | buffer_row: u32, | ||
| 5952 | window: &mut Window, | ||
| 5953 | cx: &mut Context<Self>, | ||
| 5954 | ) -> Task<Option<Rc<[AvailableCodeAction]>>> { | ||
| 5955 | let mut task = self.code_actions_task.take(); | ||
| 5956 | cx.spawn_in(window, async move |editor, cx| { | ||
| 5957 | while let Some(prev_task) = task { | ||
| 5958 | prev_task.await.log_err(); | ||
| 5959 | task = editor | ||
| 5960 | .update(cx, |this, _| this.code_actions_task.take()) | ||
| 5961 | .ok()?; | ||
| 5962 | } | ||
| 5963 | |||
| 5964 | editor | ||
| 5965 | .update(cx, |editor, cx| { | ||
| 5966 | editor | ||
| 5967 | .available_code_actions | ||
| 5968 | .clone() | ||
| 5969 | .and_then(|(location, code_actions)| { | ||
| 5970 | let snapshot = location.buffer.read(cx).snapshot(); | ||
| 5971 | let point_range = location.range.to_point(&snapshot); | ||
| 5972 | let point_range = point_range.start.row..=point_range.end.row; | ||
| 5973 | if point_range.contains(&buffer_row) { | ||
| 5974 | Some(code_actions) | ||
| 5975 | } else { | ||
| 5976 | None | ||
| 5977 | } | ||
| 5978 | }) | ||
| 5979 | }) | ||
| 5980 | .ok() | ||
| 5981 | .flatten() | ||
| 5982 | }) | ||
| 5983 | } | ||
| 5984 | |||
| 5859 | pub fn confirm_code_action( | 5985 | pub fn confirm_code_action( |
| 5860 | &mut self, | 5986 | &mut self, |
| 5861 | action: &ConfirmCodeAction, | 5987 | action: &ConfirmCodeAction, |
| @@ -6193,71 +6319,65 @@ impl Editor { · 331 unmodified lines | |||
| 6193 | ) { | 6319 | ) { |
| 6194 | if let Some(state) = &mut self.inline_blame_popover { | 6320 | if let Some(state) = &mut self.inline_blame_popover { |
| 6195 | state.hide_task.take(); | 6321 | state.hide_task.take(); |
| 6196 | cx.notify(); | ||
| 6197 | } else { | 6322 | } else { |
| 6198 | let delay = EditorSettings::get_global(cx).hover_popover_delay; | 6323 | let delay = EditorSettings::get_global(cx).hover_popover_delay; |
| 6324 | let blame_entry = blame_entry.clone(); | ||
| 6199 | let show_task = cx.spawn(async move |editor, cx| { | 6325 | let show_task = cx.spawn(async move |editor, cx| { |
| 6200 | cx.background_executor() | 6326 | cx.background_executor() |
| 6201 | .timer(std::time::Duration::from_millis(delay)) | 6327 | .timer(std::time::Duration::from_millis(delay)) |
| 6202 | .await; | 6328 | .await; |
| 6203 | editor | 6329 | editor |
| 6204 | .update(cx, |editor, cx| { | 6330 | .update(cx, |editor, cx| { |
| 6205 | if let Some(state) = &mut editor.inline_blame_popover { | 6331 | editor.inline_blame_popover_show_task.take(); |
| 6206 | state.show_task = None; | 6332 | let Some(blame) = editor.blame.as_ref() else { |
| 6207 | cx.notify(); | 6333 | return; |
| 6208 | } | 6334 | }; |
| 6335 | let blame = blame.read(cx); | ||
| 6336 | let details = blame.details_for_entry(&blame_entry); | ||
| 6337 | let markdown = cx.new(|cx| { | ||
| 6338 | Markdown::new( | ||
| 6339 | details | ||
| 6340 | .as_ref() | ||
| 6341 | .map(|message| message.message.clone()) | ||
| 6342 | .unwrap_or_default(), | ||
| 6343 | None, | ||
| 6344 | None, | ||
| 6345 | cx, | ||
| 6346 | ) | ||
| 6347 | }); | ||
| 6348 | editor.inline_blame_popover = Some(InlineBlamePopover { | ||
| 6349 | position, | ||
| 6350 | hide_task: None, | ||
| 6351 | popover_bounds: None, | ||
| 6352 | popover_state: InlineBlamePopoverState { | ||
| 6353 | scroll_handle: ScrollHandle::new(), | ||
| 6354 | commit_message: details, | ||
| 6355 | markdown, | ||
| 6356 | }, | ||
| 6357 | }); | ||
| 6358 | cx.notify(); | ||
| 6209 | }) | 6359 | }) |
| 6210 | .ok(); | 6360 | .ok(); |
| 6211 | }); | 6361 | }); |
| 6212 | let Some(blame) = self.blame.as_ref() else { | 6362 | self.inline_blame_popover_show_task = Some(show_task); |
| 6213 | return; | ||
| 6214 | }; | ||
| 6215 | let blame = blame.read(cx); | ||
| 6216 | let details = blame.details_for_entry(&blame_entry); | ||
| 6217 | let markdown = cx.new(|cx| { | ||
| 6218 | Markdown::new( | ||
| 6219 | details | ||
| 6220 | .as_ref() | ||
| 6221 | .map(|message| message.message.clone()) | ||
| 6222 | .unwrap_or_default(), | ||
| 6223 | None, | ||
| 6224 | None, | ||
| 6225 | cx, | ||
| 6226 | ) | ||
| 6227 | }); | ||
| 6228 | self.inline_blame_popover = Some(InlineBlamePopover { | ||
| 6229 | position, | ||
| 6230 | show_task: Some(show_task), | ||
| 6231 | hide_task: None, | ||
| 6232 | popover_bounds: None, | ||
| 6233 | popover_state: InlineBlamePopoverState { | ||
| 6234 | scroll_handle: ScrollHandle::new(), | ||
| 6235 | commit_message: details, | ||
| 6236 | markdown, | ||
| 6237 | }, | ||
| 6238 | }); | ||
| 6239 | } | 6363 | } |
| 6240 | } | 6364 | } |
| 6241 | 6365 | ||
| 6242 | fn hide_blame_popover(&mut self, cx: &mut Context<Self>) { | 6366 | fn hide_blame_popover(&mut self, cx: &mut Context<Self>) { |
| 6367 | self.inline_blame_popover_show_task.take(); | ||
| 6243 | if let Some(state) = &mut self.inline_blame_popover { | 6368 | if let Some(state) = &mut self.inline_blame_popover { |
| 6244 | if state.show_task.is_some() { | 6369 | let hide_task = cx.spawn(async move |editor, cx| { |
| 6245 | self.inline_blame_popover.take(); | 6370 | cx.background_executor() |
| 6246 | cx.notify(); | 6371 | .timer(std::time::Duration::from_millis(100)) |
| 6247 | } else { | 6372 | .await; |
| 6248 | let hide_task = cx.spawn(async move |editor, cx| { | 6373 | editor |
| 6249 | cx.background_executor() | 6374 | .update(cx, |editor, cx| { |
| 6250 | .timer(std::time::Duration::from_millis(100)) | 6375 | editor.inline_blame_popover.take(); |
| 6251 | .await; | 6376 | cx.notify(); |
| 6252 | editor | 6377 | }) |
| 6253 | .update(cx, |editor, cx| { | 6378 | .ok(); |
| 6254 | editor.inline_blame_popover.take(); | 6379 | }); |
| 6255 | cx.notify(); | 6380 | state.hide_task = Some(hide_task); |
| 6256 | }) | ||
| 6257 | .ok(); | ||
| 6258 | }); | ||
| 6259 | state.hide_task = Some(hide_task); | ||
| 6260 | } | ||
| 6261 | } | 6381 | } |
| 6262 | } | 6382 | } |
| 6263 | 6383 | ||
| @@ -7098,13 +7218,9 @@ impl Editor { · 834 unmodified lines | |||
| 7098 | ) | 7218 | ) |
| 7099 | } | 7219 | } |
| 7100 | 7220 | ||
| 7101 | fn multi_cursor_modifier( | 7221 | fn multi_cursor_modifier(invert: bool, modifiers: &Modifiers, cx: &mut Context<Self>) -> bool { |
| 7102 | cursor_event: bool, | ||
| 7103 | modifiers: &Modifiers, | ||
| 7104 | cx: &mut Context<Self>, | ||
| 7105 | ) -> bool { | ||
| 7106 | let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier; | 7222 | let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier; |
| 7107 | if cursor_event { | 7223 | if invert { |
| 7108 | match multi_cursor_setting { | 7224 | match multi_cursor_setting { |
| 7109 | MultiCursorModifier::Alt => modifiers.alt, | 7225 | MultiCursorModifier::Alt => modifiers.alt, |
| 7110 | MultiCursorModifier::CmdOrCtrl => modifiers.secondary(), | 7226 | MultiCursorModifier::CmdOrCtrl => modifiers.secondary(), |
| @@ -7117,8 +7233,21 @@ impl Editor { · 6 unmodified lines | |||
| 7117 | } | 7233 | } |
| 7118 | } | 7234 | } |
| 7119 | 7235 | ||
| 7120 | fn columnar_selection_modifiers(multi_cursor_modifier: bool, modifiers: &Modifiers) -> bool { | 7236 | fn columnar_selection_mode( |
| 7121 | modifiers.shift && multi_cursor_modifier && modifiers.number_of_modifiers() == 2 | 7237 | modifiers: &Modifiers, |
| 7238 | cx: &mut Context<Self>, | ||
| 7239 | ) -> Option<ColumnarMode> { | ||
| 7240 | if modifiers.shift && modifiers.number_of_modifiers() == 2 { | ||
| 7241 | if Self::multi_cursor_modifier(false, modifiers, cx) { | ||
| 7242 | Some(ColumnarMode::FromMouse) | ||
| 7243 | } else if Self::multi_cursor_modifier(true, modifiers, cx) { | ||
| 7244 | Some(ColumnarMode::FromSelection) | ||
| 7245 | } else { | ||
| 7246 | None | ||
| 7247 | } | ||
| 7248 | } else { | ||
| 7249 | None | ||
| 7250 | } | ||
| 7122 | } | 7251 | } |
| 7123 | 7252 | ||
| 7124 | fn update_selection_mode( | 7253 | fn update_selection_mode( |
| @@ -7128,10 +7257,10 @@ impl Editor { · 3 unmodified lines | |||
| 7128 | window: &mut Window, | 7257 | window: &mut Window, |
| 7129 | cx: &mut Context<Self>, | 7258 | cx: &mut Context<Self>, |
| 7130 | ) { | 7259 | ) { |
| 7131 | let multi_cursor_modifier = Self::multi_cursor_modifier(true, modifiers, cx); | 7260 | let Some(mode) = Self::columnar_selection_mode(modifiers, cx) else { |
| 7132 | if !Self::columnar_selection_modifiers(multi_cursor_modifier, modifiers) | 7261 | return; |
| 7133 | || self.selections.pending.is_none() | 7262 | }; |
| 7134 | { | 7263 | if self.selections.pending.is_none() { |
| 7135 | return; | 7264 | return; |
| 7136 | } | 7265 | } |
| 7137 | 7266 | ||
| @@ -7143,6 +7272,7 @@ impl Editor { · 5 unmodified lines | |||
| 7143 | SelectPhase::BeginColumnar { | 7272 | SelectPhase::BeginColumnar { |
| 7144 | position, | 7273 | position, |
| 7145 | reset: false, | 7274 | reset: false, |
| 7275 | mode, | ||
| 7146 | goal_column: point_for_position.exact_unclipped.column(), | 7276 | goal_column: point_for_position.exact_unclipped.column(), |
| 7147 | }, | 7277 | }, |
| 7148 | window, | 7278 | window, |
| @@ -7519,8 +7649,7 @@ impl Editor { · 370 unmodified lines | |||
| 7519 | "Set Breakpoint" | 7649 | "Set Breakpoint" |
| 7520 | }; | 7650 | }; |
| 7521 | 7651 | ||
| 7522 | let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx) | 7652 | let run_to_cursor = window.is_action_available(&RunToCursor, cx); |
| 7523 | .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor)); | ||
| 7524 | 7653 | ||
| 7525 | let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state { | 7654 | let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state { |
| 7526 | BreakpointState::Enabled => Some("Disable"), | 7655 | BreakpointState::Enabled => Some("Disable"), |
| @@ -7544,7 +7673,7 @@ impl Editor { · 17 unmodified lines | |||
| 7544 | }) | 7673 | }) |
| 7545 | .ok(); | 7674 | .ok(); |
| 7546 | 7675 | ||
| 7547 | window.dispatch_action(Box::new(DebuggerRunToCursor), cx); | 7676 | window.dispatch_action(Box::new(RunToCursor), cx); |
| 7548 | }) | 7677 | }) |
| 7549 | .separator() | 7678 | .separator() |
| 7550 | }) | 7679 | }) |
| @@ -7899,7 +8028,7 @@ impl Editor { · 348 unmodified lines | |||
| 7899 | window.focus(&editor.focus_handle(cx)); | 8028 | window.focus(&editor.focus_handle(cx)); |
| 7900 | editor.toggle_code_actions( | 8029 | editor.toggle_code_actions( |
| 7901 | &ToggleCodeActions { | 8030 | &ToggleCodeActions { |
| 7902 | deployed_from: Some(CodeActionSource::Indicator(row)), | 8031 | deployed_from: Some(CodeActionSource::RunMenu(row)), |
| 7903 | quick_launch, | 8032 | quick_launch, |
| 7904 | }, | 8033 | }, |
| 7905 | window, | 8034 | window, |
| @@ -14076,7 +14205,8 @@ impl Editor { · 6170 unmodified lines | |||
| 14076 | prefer_lsp && !lsp_tasks_by_rows.is_empty(), | 14205 | prefer_lsp && !lsp_tasks_by_rows.is_empty(), |
| 14077 | new_rows, | 14206 | new_rows, |
| 14078 | cx.clone(), | 14207 | cx.clone(), |
| 14079 | ); | 14208 | ) |
| 14209 | .await; | ||
| 14080 | editor | 14210 | editor |
| 14081 | .update(cx, |editor, _| { | 14211 | .update(cx, |editor, _| { |
| 14082 | editor.clear_tasks(); | 14212 | editor.clear_tasks(); |
| @@ -14106,35 +14236,40 @@ impl Editor { · 23 unmodified lines | |||
| 14106 | snapshot: DisplaySnapshot, | 14236 | snapshot: DisplaySnapshot, |
| 14107 | prefer_lsp: bool, | 14237 | prefer_lsp: bool, |
| 14108 | runnable_ranges: Vec<RunnableRange>, | 14238 | runnable_ranges: Vec<RunnableRange>, |
| 14109 | mut cx: AsyncWindowContext, | 14239 | cx: AsyncWindowContext, |
| 14110 | ) -> Vec<((BufferId, BufferRow), RunnableTasks)> { | 14240 | ) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> { |
| 14111 | runnable_ranges | 14241 | cx.spawn(async move |cx| { |
| 14112 | .into_iter() | 14242 | let mut runnable_rows = Vec::with_capacity(runnable_ranges.len()); |
| 14113 | .filter_map(|mut runnable| { | 14243 | for mut runnable in runnable_ranges { |
| 14114 | let mut tasks = cx | 14244 | let Some(tasks) = cx |
| 14115 | .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx)) | 14245 | .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx)) |
| 14116 | .ok()?; | 14246 | .ok() |
| 14247 | else { | ||
| 14248 | continue; | ||
| 14249 | }; | ||
| 14250 | let mut tasks = tasks.await; | ||
| 14251 | |||
| 14117 | if prefer_lsp { | 14252 | if prefer_lsp { |
| 14118 | tasks.retain(|(task_kind, _)| { | 14253 | tasks.retain(|(task_kind, _)| { |
| 14119 | !matches!(task_kind, TaskSourceKind::Language { .. }) | 14254 | !matches!(task_kind, TaskSourceKind::Language { .. }) |
| 14120 | }); | 14255 | }); |
| 14121 | } | 14256 | } |
| 14122 | if tasks.is_empty() { | 14257 | if tasks.is_empty() { |
| 14123 | return None; | 14258 | continue; |
| 14124 | } | 14259 | } |
| 14125 | 14260 | ||
| 14126 | let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot); | 14261 | let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot); |
| 14127 | 14262 | let Some(row) = snapshot | |
| 14128 | let row = snapshot | ||
| 14129 | .buffer_snapshot | 14263 | .buffer_snapshot |
| 14130 | .buffer_line_for_row(MultiBufferRow(point.row))? | 14264 | .buffer_line_for_row(MultiBufferRow(point.row)) |
| 14131 | .1 | 14265 | .map(|(_, range)| range.start.row) |
| 14132 | .start | 14266 | else { |
| 14133 | .row; | 14267 | continue; |
| 14268 | }; | ||
| 14134 | 14269 | ||
| 14135 | let context_range = | 14270 | let context_range = |
| 14136 | BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end); | 14271 | BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end); |
| 14137 | Some(( | 14272 | runnable_rows.push(( |
| 14138 | (runnable.buffer_id, row), | 14273 | (runnable.buffer_id, row), |
| 14139 | RunnableTasks { | 14274 | RunnableTasks { |
| 14140 | templates: tasks, | 14275 | templates: tasks, |
| @@ -14145,16 +14280,17 @@ impl Editor { · 4 unmodified lines | |||
| 14145 | column: point.column, | 14280 | column: point.column, |
| 14146 | extra_variables: runnable.extra_captures, | 14281 | extra_variables: runnable.extra_captures, |
| 14147 | }, | 14282 | }, |
| 14148 | )) | 14283 | )); |
| 14149 | }) | 14284 | } |
| 14150 | .collect() | 14285 | runnable_rows |
| 14286 | }) | ||
| 14151 | } | 14287 | } |
| 14152 | 14288 | ||
| 14153 | fn templates_with_tags( | 14289 | fn templates_with_tags( |
| 14154 | project: &Entity<Project>, | 14290 | project: &Entity<Project>, |
| 14155 | runnable: &mut Runnable, | 14291 | runnable: &mut Runnable, |
| 14156 | cx: &mut App, | 14292 | cx: &mut App, |
| 14157 | ) -> Vec<(TaskSourceKind, TaskTemplate)> { | 14293 | ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> { |
| 14158 | let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { | 14294 | let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { |
| 14159 | let (worktree_id, file) = project | 14295 | let (worktree_id, file) = project |
| 14160 | .buffer_for_id(runnable.buffer, cx) | 14296 | .buffer_for_id(runnable.buffer, cx) |
| @@ -14169,39 +14305,40 @@ impl Editor { · 8 unmodified lines | |||
| 14169 | ) | 14305 | ) |
| 14170 | }); | 14306 | }); |
| 14171 | 14307 | ||
| 14172 | let mut templates_with_tags = mem::take(&mut runnable.tags) | 14308 | let tags = mem::take(&mut runnable.tags); |
| 14173 | .into_iter() | 14309 | let language = runnable.language.clone(); |
| 14174 | .flat_map(|RunnableTag(tag)| { | 14310 | cx.spawn(async move |cx| { |
| 14175 | inventory | 14311 | let mut templates_with_tags = Vec::new(); |
| 14176 | .as_ref() | 14312 | if let Some(inventory) = inventory { |
| 14177 | .into_iter() | 14313 | for RunnableTag(tag) in tags { |
| 14178 | .flat_map(|inventory| { | 14314 | let Ok(new_tasks) = inventory.update(cx, |inventory, cx| { |
| 14179 | inventory.read(cx).list_tasks( | 14315 | inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx) |
| 14180 | file.clone(), | 14316 | }) else { |
| 14181 | Some(runnable.language.clone()), | 14317 | return templates_with_tags; |
| 14182 | worktree_id, | 14318 | }; |
| 14183 | cx, | 14319 | templates_with_tags.extend(new_tasks.await.into_iter().filter( |
| 14184 | ) | 14320 | move |(_, template)| { |
| 14185 | }) | 14321 | template.tags.iter().any(|source_tag| source_tag == &tag) |
| 14186 | .filter(move |(_, template)| { | 14322 | }, |
| 14187 | template.tags.iter().any(|source_tag| source_tag == &tag) | 14323 | )); |
| 14188 | }) | 14324 | } |
| 14189 | }) | ||
| 14190 | .sorted_by_key(|(kind, _)| kind.to_owned()) | ||
| 14191 | .collect::<Vec<_>>(); | ||
| 14192 | if let Some((leading_tag_source, _)) = templates_with_tags.first() { | ||
| 14193 | // Strongest source wins; if we have worktree tag binding, prefer that to | ||
| 14194 | // global and language bindings; | ||
| 14195 | // if we have a global binding, prefer that to language binding. | ||
| 14196 | let first_mismatch = templates_with_tags | ||
| 14197 | .iter() | ||
| 14198 | .position(|(tag_source, _)| tag_source != leading_tag_source); | ||
| 14199 | if let Some(index) = first_mismatch { | ||
| 14200 | templates_with_tags.truncate(index); | ||
| 14201 | } | 14325 | } |
| 14202 | } | 14326 | templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned()); |
| 14203 | 14327 | ||
| 14204 | templates_with_tags | 14328 | if let Some((leading_tag_source, _)) = templates_with_tags.first() { |
| 14329 | // Strongest source wins; if we have worktree tag binding, prefer that to | ||
| 14330 | // global and language bindings; | ||
| 14331 | // if we have a global binding, prefer that to language binding. | ||
| 14332 | let first_mismatch = templates_with_tags | ||
| 14333 | .iter() | ||
| 14334 | .position(|(tag_source, _)| tag_source != leading_tag_source); | ||
| 14335 | if let Some(index) = first_mismatch { | ||
| 14336 | templates_with_tags.truncate(index); | ||
| 14337 | } | ||
| 14338 | } | ||
| 14339 | |||
| 14340 | templates_with_tags | ||
| 14341 | }) | ||
| 14205 | } | 14342 | } |
| 14206 | 14343 | ||
| 14207 | pub fn move_to_enclosing_bracket( | 14344 | pub fn move_to_enclosing_bracket( |
| @@ -14268,18 +14405,20 @@ impl Editor { · 60 unmodified lines | |||
| 14268 | cx: &mut Context<Self>, | 14405 | cx: &mut Context<Self>, |
| 14269 | ) { | 14406 | ) { |
| 14270 | self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); | 14407 | self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); |
| 14271 | self.end_selection(window, cx); | ||
| 14272 | self.selection_history.mode = SelectionHistoryMode::Undoing; | ||
| 14273 | if let Some(entry) = self.selection_history.undo_stack.pop_back() { | 14408 | if let Some(entry) = self.selection_history.undo_stack.pop_back() { |
| 14274 | self.change_selections(None, window, cx, |s| { | 14409 | self.selection_history.mode = SelectionHistoryMode::Undoing; |
| 14275 | s.select_anchors(entry.selections.to_vec()) | 14410 | self.with_selection_effects_deferred(window, cx, |this, window, cx| { |
| 14411 | this.end_selection(window, cx); | ||
| 14412 | this.change_selections(Some(Autoscroll::newest()), window, cx, |s| { | ||
| 14413 | s.select_anchors(entry.selections.to_vec()) | ||
| 14414 | }); | ||
| 14276 | }); | 14415 | }); |
| 14416 | self.selection_history.mode = SelectionHistoryMode::Normal; | ||
| 14417 | |||
| 14277 | self.select_next_state = entry.select_next_state; | 14418 | self.select_next_state = entry.select_next_state; |
| 14278 | self.select_prev_state = entry.select_prev_state; | 14419 | self.select_prev_state = entry.select_prev_state; |
| 14279 | self.add_selections_state = entry.add_selections_state; | 14420 | self.add_selections_state = entry.add_selections_state; |
| 14280 | self.request_autoscroll(Autoscroll::newest(), cx); | ||
| 14281 | } | 14421 | } |
| 14282 | self.selection_history.mode = SelectionHistoryMode::Normal; | ||
| 14283 | } | 14422 | } |
| 14284 | 14423 | ||
| 14285 | pub fn redo_selection( | 14424 | pub fn redo_selection( |
| @@ -14289,18 +14428,20 @@ impl Editor { · 3 unmodified lines | |||
| 14289 | cx: &mut Context<Self>, | 14428 | cx: &mut Context<Self>, |
| 14290 | ) { | 14429 | ) { |
| 14291 | self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); | 14430 | self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); |
| 14292 | self.end_selection(window, cx); | ||
| 14293 | self.selection_history.mode = SelectionHistoryMode::Redoing; | ||
| 14294 | if let Some(entry) = self.selection_history.redo_stack.pop_back() { | 14431 | if let Some(entry) = self.selection_history.redo_stack.pop_back() { |
| 14295 | self.change_selections(None, window, cx, |s| { | 14432 | self.selection_history.mode = SelectionHistoryMode::Redoing; |
| 14296 | s.select_anchors(entry.selections.to_vec()) | 14433 | self.with_selection_effects_deferred(window, cx, |this, window, cx| { |
| 14434 | this.end_selection(window, cx); | ||
| 14435 | this.change_selections(Some(Autoscroll::newest()), window, cx, |s| { | ||
| 14436 | s.select_anchors(entry.selections.to_vec()) | ||
| 14437 | }); | ||
| 14297 | }); | 14438 | }); |
| 14439 | self.selection_history.mode = SelectionHistoryMode::Normal; | ||
| 14440 | |||
| 14298 | self.select_next_state = entry.select_next_state; | 14441 | self.select_next_state = entry.select_next_state; |
| 14299 | self.select_prev_state = entry.select_prev_state; | 14442 | self.select_prev_state = entry.select_prev_state; |
| 14300 | self.add_selections_state = entry.add_selections_state; | 14443 | self.add_selections_state = entry.add_selections_state; |
| 14301 | self.request_autoscroll(Autoscroll::newest(), cx); | ||
| 14302 | } | 14444 | } |
| 14303 | self.selection_history.mode = SelectionHistoryMode::Normal; | ||
| 14304 | } | 14445 | } |
| 14305 | 14446 | ||
| 14306 | pub fn expand_excerpts( | 14447 | pub fn expand_excerpts( |
| @@ -15605,7 +15746,7 @@ impl Editor { · 1298 unmodified lines | |||
| 15605 | Some(self.perform_format( | 15746 | Some(self.perform_format( |
| 15606 | project, | 15747 | project, |
| 15607 | FormatTrigger::Manual, | 15748 | FormatTrigger::Manual, |
| 15608 | FormatTarget::Buffers, | 15749 | FormatTarget::Buffers(self.buffer.read(cx).all_buffers()), |
| 15609 | window, | 15750 | window, |
| 15610 | cx, | 15751 | cx, |
| 15611 | )) | 15752 | )) |
| @@ -15650,13 +15791,7 @@ impl Editor { · 38 unmodified lines | |||
| 15650 | ) -> Task<Result<()>> { | 15791 | ) -> Task<Result<()>> { |
| 15651 | let buffer = self.buffer.clone(); | 15792 | let buffer = self.buffer.clone(); |
| 15652 | let (buffers, target) = match target { | 15793 | let (buffers, target) = match target { |
| 15653 | FormatTarget::Buffers => { | 15794 | FormatTarget::Buffers(buffers) => (buffers, LspFormatTarget::Buffers), |
| 15654 | let mut buffers = buffer.read(cx).all_buffers(); | ||
| 15655 | if trigger == FormatTrigger::Save { | ||
| 15656 | buffers.retain(|buffer| buffer.read(cx).is_dirty()); | ||
| 15657 | } | ||
| 15658 | (buffers, LspFormatTarget::Buffers) | ||
| 15659 | } | ||
| 15660 | FormatTarget::Ranges(selection_ranges) => { | 15795 | FormatTarget::Ranges(selection_ranges) => { |
| 15661 | let multi_buffer = buffer.read(cx); | 15796 | let multi_buffer = buffer.read(cx); |
| 15662 | let snapshot = multi_buffer.read(cx); | 15797 | let snapshot = multi_buffer.read(cx); |
| @@ -16113,13 +16248,16 @@ impl Editor { · 450 unmodified lines | |||
| 16113 | window: &Window, | 16248 | window: &Window, |
| 16114 | cx: &mut Context<Self>, | 16249 | cx: &mut Context<Self>, |
| 16115 | ) -> Option<()> { | 16250 | ) -> Option<()> { |
| 16116 | let project = self.project.as_ref()?.downgrade(); | 16251 | if !self.mode().is_full() { |
| 16252 | return None; | ||
| 16253 | } | ||
| 16117 | let pull_diagnostics_settings = ProjectSettings::get_global(cx) | 16254 | let pull_diagnostics_settings = ProjectSettings::get_global(cx) |
| 16118 | .diagnostics | 16255 | .diagnostics |
| 16119 | .lsp_pull_diagnostics; | 16256 | .lsp_pull_diagnostics; |
| 16120 | if !pull_diagnostics_settings.enabled { | 16257 | if !pull_diagnostics_settings.enabled { |
| 16121 | return None; | 16258 | return None; |
| 16122 | } | 16259 | } |
| 16260 | let project = self.project.as_ref()?.downgrade(); | ||
| 16123 | let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms); | 16261 | let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms); |
| 16124 | let mut buffers = self.buffer.read(cx).all_buffers(); | 16262 | let mut buffers = self.buffer.read(cx).all_buffers(); |
| 16125 | if let Some(buffer_id) = buffer_id { | 16263 | if let Some(buffer_id) = buffer_id { |
| @@ -16176,7 +16314,13 @@ impl Editor { · 50 unmodified lines | |||
| 16176 | s.clear_pending(); | 16314 | s.clear_pending(); |
| 16177 | } | 16315 | } |
| 16178 | }); | 16316 | }); |
| 16179 | self.selections_did_change(false, &old_cursor_position, true, window, cx); | 16317 | self.selections_did_change( |
| 16318 | false, | ||
| 16319 | &old_cursor_position, | ||
| 16320 | SelectionEffects::default(), | ||
| 16321 | window, | ||
| 16322 | cx, | ||
| 16323 | ); | ||
| 16180 | } | 16324 | } |
| 16181 | 16325 | ||
| 16182 | pub fn transact( | 16326 | pub fn transact( |
| @@ -17098,7 +17242,16 @@ impl Editor { · 915 unmodified lines | |||
| 17098 | } | 17242 | } |
| 17099 | 17243 | ||
| 17100 | if let Some(project) = self.project.clone() { | 17244 | if let Some(project) = self.project.clone() { |
| 17101 | self.save(true, project, window, cx).detach_and_log_err(cx); | 17245 | self.save( |
| 17246 | SaveOptions { | ||
| 17247 | format: true, | ||
| 17248 | autosave: false, | ||
| 17249 | }, | ||
| 17250 | project, | ||
| 17251 | window, | ||
| 17252 | cx, | ||
| 17253 | ) | ||
| 17254 | .detach_and_log_err(cx); | ||
| 17102 | } | 17255 | } |
| 17103 | } | 17256 | } |
| 17104 | 17257 | ||
| @@ -17130,7 +17283,16 @@ impl Editor { · 25 unmodified lines | |||
| 17130 | }); | 17283 | }); |
| 17131 | 17284 | ||
| 17132 | if let Some(project) = self.project.clone() { | 17285 | if let Some(project) = self.project.clone() { |
| 17133 | self.save(true, project, window, cx).detach_and_log_err(cx); | 17286 | self.save( |
| 17287 | SaveOptions { | ||
| 17288 | format: true, | ||
| 17289 | autosave: false, | ||
| 17290 | }, | ||
| 17291 | project, | ||
| 17292 | window, | ||
| 17293 | cx, | ||
| 17294 | ) | ||
| 17295 | .detach_and_log_err(cx); | ||
| 17134 | } | 17296 | } |
| 17135 | } | 17297 | } |
| 17136 | 17298 | ||
| @@ -18119,16 +18281,9 @@ impl Editor { · 982 unmodified lines | |||
| 18119 | .selections | 18281 | .selections |
| 18120 | .disjoint_anchors() | 18282 | .disjoint_anchors() |
| 18121 | .iter() | 18283 | .iter() |
| 18122 | .map(|selection| { | 18284 | .map(|range| Location { |
| 18123 | let range = if selection.reversed { | 18285 | buffer: buffer.clone(), |
| 18124 | selection.end.text_anchor..selection.start.text_anchor | 18286 | range: range.start.text_anchor..range.end.text_anchor, |
| 18125 | } else { | ||
| 18126 | selection.start.text_anchor..selection.end.text_anchor | ||
| 18127 | }; | ||
| 18128 | Location { | ||
| 18129 | buffer: buffer.clone(), | ||
| 18130 | range, | ||
| 18131 | } | ||
| 18132 | }) | 18287 | }) |
| 18133 | .collect::<Vec<_>>(); | 18288 | .collect::<Vec<_>>(); |
| 18134 | 18289 | ||
| @@ -18383,12 +18538,12 @@ impl Editor { · 248 unmodified lines | |||
| 18383 | 18538 | ||
| 18384 | pub fn highlight_gutter<T: 'static>( | 18539 | pub fn highlight_gutter<T: 'static>( |
| 18385 | &mut self, | 18540 | &mut self, |
| 18386 | ranges: &[Range<Anchor>], | 18541 | ranges: impl Into<Vec<Range<Anchor>>>, |
| 18387 | color_fetcher: fn(&App) -> Hsla, | 18542 | color_fetcher: fn(&App) -> Hsla, |
| 18388 | cx: &mut Context<Self>, | 18543 | cx: &mut Context<Self>, |
| 18389 | ) { | 18544 | ) { |
| 18390 | self.gutter_highlights | 18545 | self.gutter_highlights |
| 18391 | .insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges))); | 18546 | .insert(TypeId::of::<T>(), (color_fetcher, ranges.into())); |
| 18392 | cx.notify(); | 18547 | cx.notify(); |
| 18393 | } | 18548 | } |
| 18394 | 18549 | ||
| @@ -18400,6 +18555,65 @@ impl Editor { · 5 unmodified lines | |||
| 18400 | self.gutter_highlights.remove(&TypeId::of::<T>()) | 18555 | self.gutter_highlights.remove(&TypeId::of::<T>()) |
| 18401 | } | 18556 | } |
| 18402 | 18557 | ||
| 18558 | pub fn insert_gutter_highlight<T: 'static>( | ||
| 18559 | &mut self, | ||
| 18560 | range: Range<Anchor>, | ||
| 18561 | color_fetcher: fn(&App) -> Hsla, | ||
| 18562 | cx: &mut Context<Self>, | ||
| 18563 | ) { | ||
| 18564 | let snapshot = self.buffer().read(cx).snapshot(cx); | ||
| 18565 | let mut highlights = self | ||
| 18566 | .gutter_highlights | ||
| 18567 | .remove(&TypeId::of::<T>()) | ||
| 18568 | .map(|(_, highlights)| highlights) | ||
| 18569 | .unwrap_or_default(); | ||
| 18570 | let ix = highlights.binary_search_by(|highlight| { | ||
| 18571 | Ordering::Equal | ||
| 18572 | .then_with(|| highlight.start.cmp(&range.start, &snapshot)) | ||
| 18573 | .then_with(|| highlight.end.cmp(&range.end, &snapshot)) | ||
| 18574 | }); | ||
| 18575 | if let Err(ix) = ix { | ||
| 18576 | highlights.insert(ix, range); | ||
| 18577 | } | ||
| 18578 | self.gutter_highlights | ||
| 18579 | .insert(TypeId::of::<T>(), (color_fetcher, highlights)); | ||
| 18580 | } | ||
| 18581 | |||
| 18582 | pub fn remove_gutter_highlights<T: 'static>( | ||
| 18583 | &mut self, | ||
| 18584 | ranges_to_remove: Vec<Range<Anchor>>, | ||
| 18585 | cx: &mut Context<Self>, | ||
| 18586 | ) { | ||
| 18587 | let snapshot = self.buffer().read(cx).snapshot(cx); | ||
| 18588 | let Some((color_fetcher, mut gutter_highlights)) = | ||
| 18589 | self.gutter_highlights.remove(&TypeId::of::<T>()) | ||
| 18590 | else { | ||
| 18591 | return; | ||
| 18592 | }; | ||
| 18593 | let mut ranges_to_remove = ranges_to_remove.iter().peekable(); | ||
| 18594 | gutter_highlights.retain(|highlight| { | ||
| 18595 | while let Some(range_to_remove) = ranges_to_remove.peek() { | ||
| 18596 | match range_to_remove.end.cmp(&highlight.start, &snapshot) { | ||
| 18597 | Ordering::Less | Ordering::Equal => { | ||
| 18598 | ranges_to_remove.next(); | ||
| 18599 | } | ||
| 18600 | Ordering::Greater => { | ||
| 18601 | match range_to_remove.start.cmp(&highlight.end, &snapshot) { | ||
| 18602 | Ordering::Less | Ordering::Equal => { | ||
| 18603 | return false; | ||
| 18604 | } | ||
| 18605 | Ordering::Greater => break, | ||
| 18606 | } | ||
| 18607 | } | ||
| 18608 | } | ||
| 18609 | } | ||
| 18610 | |||
| 18611 | true | ||
| 18612 | }); | ||
| 18613 | self.gutter_highlights | ||
| 18614 | .insert(TypeId::of::<T>(), (color_fetcher, gutter_highlights)); | ||
| 18615 | } | ||
| 18616 | |||
| 18403 | #[cfg(feature = "test-support")] | 18617 | #[cfg(feature = "test-support")] |
| 18404 | pub fn all_text_background_highlights( | 18618 | pub fn all_text_background_highlights( |
| 18405 | &self, | 18619 | &self, |
| @@ -19662,7 +19876,7 @@ impl Editor { · 1256 unmodified lines | |||
| 19662 | .flatten() | 19876 | .flatten() |
| 19663 | .filter_map(|keystroke| { | 19877 | .filter_map(|keystroke| { |
| 19664 | if keystroke.modifiers.is_subset_of(&Modifiers::shift()) { | 19878 | if keystroke.modifiers.is_subset_of(&Modifiers::shift()) { |
| 19665 | Some(keystroke.key_char.clone().unwrap_or(keystroke.key.clone())) | 19879 | keystroke.key_char.clone() |
| 19666 | } else { | 19880 | } else { |
| 19667 | None | 19881 | None |
| 19668 | } | 19882 | } |
| @@ -19739,6 +19953,21 @@ impl Editor { · 70 unmodified lines | |||
| 19739 | } | 19953 | } |
| 19740 | } | 19954 | } |
| 19741 | 19955 | ||
| 19956 | pub fn register_action_renderer( | ||
| 19957 | &mut self, | ||
| 19958 | listener: impl Fn(&Editor, &mut Window, &mut Context<Editor>) + 'static, | ||
| 19959 | ) -> Subscription { | ||
| 19960 | let id = self.next_editor_action_id.post_inc(); | ||
| 19961 | self.editor_actions | ||
| 19962 | .borrow_mut() | ||
| 19963 | .insert(id, Box::new(listener)); | ||
| 19964 | |||
| 19965 | let editor_actions = self.editor_actions.clone(); | ||
| 19966 | Subscription::new(move || { | ||
| 19967 | editor_actions.borrow_mut().remove(&id); | ||
| 19968 | }) | ||
| 19969 | } | ||
| 19970 | |||
| 19742 | pub fn register_action<A: Action>( | 19971 | pub fn register_action<A: Action>( |
| 19743 | &mut self, | 19972 | &mut self, |
| 19744 | listener: impl Fn(&A, &mut Window, &mut App) + 'static, | 19973 | listener: impl Fn(&A, &mut Window, &mut App) + 'static, |
| @@ -19747,7 +19976,7 @@ impl Editor { · 2 unmodified lines | |||
| 19747 | let listener = Arc::new(listener); | 19976 | let listener = Arc::new(listener); |
| 19748 | self.editor_actions.borrow_mut().insert( | 19977 | self.editor_actions.borrow_mut().insert( |
| 19749 | id, | 19978 | id, |
| 19750 | Box::new(move |window, _| { | 19979 | Box::new(move |_, window, _| { |
| 19751 | let listener = listener.clone(); | 19980 | let listener = listener.clone(); |
| 19752 | window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| { | 19981 | window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| { |
| 19753 | let action = action.downcast_ref().unwrap(); | 19982 | let action = action.downcast_ref().unwrap(); |
| @@ -19945,12 +20174,15 @@ impl Editor { · 191 unmodified lines | |||
| 19945 | if !selections.is_empty() { | 20174 | if !selections.is_empty() { |
| 19946 | let snapshot = | 20175 | let snapshot = |
| 19947 | buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); | 20176 | buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); |
| 20177 | // skip adding the initial selection to selection history | ||
| 20178 | self.selection_history.mode = SelectionHistoryMode::Skipping; | ||
| 19948 | self.change_selections(None, window, cx, |s| { | 20179 | self.change_selections(None, window, cx, |s| { |
| 19949 | s.select_ranges(selections.into_iter().map(|(start, end)| { | 20180 | s.select_ranges(selections.into_iter().map(|(start, end)| { |
| 19950 | snapshot.clip_offset(start, Bias::Left) | 20181 | snapshot.clip_offset(start, Bias::Left) |
| 19951 | ..snapshot.clip_offset(end, Bias::Right) | 20182 | ..snapshot.clip_offset(end, Bias::Right) |
| 19952 | })); | 20183 | })); |
| 19953 | }); | 20184 | }); |
| 20185 | self.selection_history.mode = SelectionHistoryMode::Normal; | ||
| 19954 | } | 20186 | } |
| 19955 | }; | 20187 | }; |
| 19956 | } | 20188 | } |
| @@ -19976,8 +20208,13 @@ fn process_completion_for_edit( · 19 unmodified lines | |||
| 19976 | let buffer = buffer.read(cx); | 20208 | let buffer = buffer.read(cx); |
| 19977 | let buffer_snapshot = buffer.snapshot(); | 20209 | let buffer_snapshot = buffer.snapshot(); |
| 19978 | let (snippet, new_text) = if completion.is_snippet() { | 20210 | let (snippet, new_text) = if completion.is_snippet() { |
| 20211 | // Workaround for typescript language server issues so that methods don't expand within | ||
| 20212 | // strings and functions with type expressions. The previous point is used because the query | ||
| 20213 | // for function identifier doesn't match when the cursor is immediately after. See PR #30312 | ||
| 19979 | let mut snippet_source = completion.new_text.clone(); | 20214 | let mut snippet_source = completion.new_text.clone(); |
| 19980 | if let Some(scope) = buffer_snapshot.language_scope_at(cursor_position) { | 20215 | let mut previous_point = text::ToPoint::to_point(cursor_position, buffer); |
| 20216 | previous_point.column = previous_point.column.saturating_sub(1); | ||
| 20217 | if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) { | ||
| 19981 | if scope.prefers_label_for_snippet_in_completion() { | 20218 | if scope.prefers_label_for_snippet_in_completion() { |
| 19982 | if let Some(label) = completion.label() { | 20219 | if let Some(label) = completion.label() { |
| 19983 | if matches!( | 20220 | if matches!( |
| @@ -21440,8 +21677,8 @@ impl EditorSnapshot { · 1456 unmodified lines | |||
| 21440 | return None; | 21677 | return None; |
| 21441 | } | 21678 | } |
| 21442 | 21679 | ||
| 21443 | let em_width = cx.text_system().em_width(font_id, font_size).log_err()?; | 21680 | let ch_width = cx.text_system().ch_width(font_id, font_size).log_err()?; |
| 21444 | let em_advance = cx.text_system().em_advance(font_id, font_size).log_err()?; | 21681 | let ch_advance = cx.text_system().ch_advance(font_id, font_size).log_err()?; |
| 21445 | 21682 | ||
| 21446 | let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| { | 21683 | let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| { |
| 21447 | matches!( | 21684 | matches!( |
| @@ -21454,8 +21691,10 @@ impl EditorSnapshot { · 6 unmodified lines | |||
| 21454 | .show_line_numbers | 21691 | .show_line_numbers |
| 21455 | .unwrap_or(gutter_settings.line_numbers); | 21692 | .unwrap_or(gutter_settings.line_numbers); |
| 21456 | let line_gutter_width = if show_line_numbers { | 21693 | let line_gutter_width = if show_line_numbers { |
| 21457 | // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines. | 21694 | // Avoid flicker-like gutter resizes when the line number gains another digit by |
| 21458 | let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32; | 21695 | // only resizing the gutter on files with > 10**min_line_number_digits lines. |
| 21696 | let min_width_for_number_on_gutter = | ||
| 21697 | ch_advance * gutter_settings.min_line_number_digits as f32; | ||
| 21459 | max_line_number_width.max(min_width_for_number_on_gutter) | 21698 | max_line_number_width.max(min_width_for_number_on_gutter) |
| 21460 | } else { | 21699 | } else { |
| 21461 | 0.0.into() | 21700 | 0.0.into() |
| @@ -21478,20 +21717,20 @@ impl EditorSnapshot { · 16 unmodified lines | |||
| 21478 | + MAX_RELATIVE_TIMESTAMP.len() | 21717 | + MAX_RELATIVE_TIMESTAMP.len() |
| 21479 | + SPACING_WIDTH; | 21718 | + SPACING_WIDTH; |
| 21480 | 21719 | ||
| 21481 | em_advance * max_char_count | 21720 | ch_advance * max_char_count |
| 21482 | }); | 21721 | }); |
| 21483 | 21722 | ||
| 21484 | let is_singleton = self.buffer_snapshot.is_singleton(); | 21723 | let is_singleton = self.buffer_snapshot.is_singleton(); |
| 21485 | 21724 | ||
| 21486 | let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO); | 21725 | let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO); |
| 21487 | left_padding += if !is_singleton { | 21726 | left_padding += if !is_singleton { |
| 21488 | em_width * 4.0 | 21727 | ch_width * 4.0 |
| 21489 | } else if show_runnables || show_breakpoints { | 21728 | } else if show_runnables || show_breakpoints { |
| 21490 | em_width * 3.0 | 21729 | ch_width * 3.0 |
| 21491 | } else if show_git_gutter && show_line_numbers { | 21730 | } else if show_git_gutter && show_line_numbers { |
| 21492 | em_width * 2.0 | 21731 | ch_width * 2.0 |
| 21493 | } else if show_git_gutter || show_line_numbers { | 21732 | } else if show_git_gutter || show_line_numbers { |
| 21494 | em_width | 21733 | ch_width |
| 21495 | } else { | 21734 | } else { |
| 21496 | px(0.) | 21735 | px(0.) |
| 21497 | }; | 21736 | }; |
| @@ -21499,11 +21738,11 @@ impl EditorSnapshot { · 1 unmodified line | |||
| 21499 | let shows_folds = is_singleton && gutter_settings.folds; | 21738 | let shows_folds = is_singleton && gutter_settings.folds; |
| 21500 | 21739 | ||
| 21501 | let right_padding = if shows_folds && show_line_numbers { | 21740 | let right_padding = if shows_folds && show_line_numbers { |
| 21502 | em_width * 4.0 | 21741 | ch_width * 4.0 |
| 21503 | } else if shows_folds || (!is_singleton && show_line_numbers) { | 21742 | } else if shows_folds || (!is_singleton && show_line_numbers) { |
| 21504 | em_width * 3.0 | 21743 | ch_width * 3.0 |
| 21505 | } else if show_line_numbers { | 21744 | } else if show_line_numbers { |
| 21506 | em_width | 21745 | ch_width |
| 21507 | } else { | 21746 | } else { |
| 21508 | px(0.) | 21747 | px(0.) |
| 21509 | }; | 21748 | }; |
| @@ -21709,7 +21948,7 @@ impl Render for Editor { · 199 unmodified lines | |||
| 21709 | 21948 | ||
| 21710 | let background = match self.mode { | 21949 | let background = match self.mode { |
| 21711 | EditorMode::SingleLine { .. } => cx.theme().system().transparent, | 21950 | EditorMode::SingleLine { .. } => cx.theme().system().transparent, |
| 21712 | EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, | 21951 | EditorMode::AutoHeight { .. } => cx.theme().system().transparent, |
| 21713 | EditorMode::Full { .. } => cx.theme().colors().editor_background, | 21952 | EditorMode::Full { .. } => cx.theme().colors().editor_background, |
| 21714 | EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7), | 21953 | EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7), |
| 21715 | }; | 21954 | }; |
| @@ -22367,6 +22606,7 @@ impl BreakpointPromptEditor { · 651 unmodified lines | |||
| 22367 | let prompt = cx.new(|cx| { | 22606 | let prompt = cx.new(|cx| { |
| 22368 | let mut prompt = Editor::new( | 22607 | let mut prompt = Editor::new( |
| 22369 | EditorMode::AutoHeight { | 22608 | EditorMode::AutoHeight { |
| 22609 | min_lines: 1, | ||
| 22370 | max_lines: Self::MAX_LINES as usize, | 22610 | max_lines: Self::MAX_LINES as usize, |
| 22371 | }, | 22611 | }, |
| 22372 | buffer, | 22612 | buffer, |
| @@ -150,6 +150,7 @@ impl Minimap { · 149 unmodified lines | |||
| 150 | 150 | ||
| 151 | #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] | 151 | #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] |
| 152 | pub struct Gutter { | 152 | pub struct Gutter { |
| 153 | pub min_line_number_digits: usize, | ||
| 153 | pub line_numbers: bool, | 154 | pub line_numbers: bool, |
| 154 | pub runnables: bool, | 155 | pub runnables: bool, |
| 155 | pub breakpoints: bool, | 156 | pub breakpoints: bool, |
| @@ -609,6 +610,10 @@ pub struct GutterContent { · 453 unmodified lines | |||
| 609 | /// | 610 | /// |
| 610 | /// Default: true | 611 | /// Default: true |
| 611 | pub line_numbers: Option<bool>, | 612 | pub line_numbers: Option<bool>, |
| 613 | /// Minimum number of characters to reserve space for in the gutter. | ||
| 614 | /// | ||
| 615 | /// Default: 4 | ||
| 616 | pub min_line_number_digits: Option<usize>, | ||
| 612 | /// Whether to show runnable buttons in the gutter. | 617 | /// Whether to show runnable buttons in the gutter. |
| 613 | /// | 618 | /// |
| 614 | /// Default: true | 619 | /// Default: true |
| @@ -56,7 +56,7 @@ use util::{ · 55 unmodified lines | |||
| 56 | }; | 56 | }; |
| 57 | use workspace::{ | 57 | use workspace::{ |
| 58 | CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId, | 58 | CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId, |
| 59 | item::{FollowEvent, FollowableItem, Item, ItemHandle}, | 59 | item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, |
| 60 | }; | 60 | }; |
| 61 | 61 | ||
| 62 | #[gpui::test] | 62 | #[gpui::test] |
| @@ -1907,7 +1907,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { · 1844 unmodified lines | |||
| 1907 | DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4), | 1907 | DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4), |
| 1908 | ]) | 1908 | ]) |
| 1909 | }); | 1909 | }); |
| 1910 | |||
| 1911 | editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); | 1910 | editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); |
| 1912 | assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx); | 1911 | assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx); |
| 1913 | 1912 | ||
| @@ -1927,29 +1926,29 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { · 13 unmodified lines | |||
| 1927 | assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx); | 1926 | assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx); |
| 1928 | 1927 | ||
| 1929 | editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); | 1928 | editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); |
| 1930 | assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); | 1929 | assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx); |
| 1931 | 1930 | ||
| 1932 | editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); | 1931 | editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); |
| 1933 | assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx); | 1932 | assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); |
| 1934 | 1933 | ||
| 1935 | editor.move_right(&MoveRight, window, cx); | 1934 | editor.move_right(&MoveRight, window, cx); |
| 1936 | editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); | 1935 | editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); |
| 1937 | assert_selection_ranges( | 1936 | assert_selection_ranges( |
| 1938 | "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", | 1937 | "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}", |
| 1939 | editor, | 1938 | editor, |
| 1940 | cx, | 1939 | cx, |
| 1941 | ); | 1940 | ); |
| 1942 | 1941 | ||
| 1943 | editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); | 1942 | editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); |
| 1944 | assert_selection_ranges( | 1943 | assert_selection_ranges( |
| 1945 | "use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}", | 1944 | "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}", |
| 1946 | editor, | 1945 | editor, |
| 1947 | cx, | 1946 | cx, |
| 1948 | ); | 1947 | ); |
| 1949 | 1948 | ||
| 1950 | editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx); | 1949 | editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx); |
| 1951 | assert_selection_ranges( | 1950 | assert_selection_ranges( |
| 1952 | "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", | 1951 | "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}", |
| 1953 | editor, | 1952 | editor, |
| 1954 | cx, | 1953 | cx, |
| 1955 | ); | 1954 | ); |
| @@ -9042,7 +9041,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 7086 unmodified lines | |||
| 9042 | ); | 9041 | ); |
| 9043 | let save = editor | 9042 | let save = editor |
| 9044 | .update_in(cx, |editor, window, cx| { | 9043 | .update_in(cx, |editor, window, cx| { |
| 9045 | editor.save(true, project.clone(), window, cx) | 9044 | editor.save( |
| 9045 | SaveOptions { | ||
| 9046 | format: true, | ||
| 9047 | autosave: false, | ||
| 9048 | }, | ||
| 9049 | project.clone(), | ||
| 9050 | window, | ||
| 9051 | cx, | ||
| 9052 | ) | ||
| 9046 | }) | 9053 | }) |
| 9047 | .unwrap(); | 9054 | .unwrap(); |
| 9048 | cx.executor().start_waiting(); | 9055 | cx.executor().start_waiting(); |
| @@ -9074,7 +9081,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 25 unmodified lines | |||
| 9074 | ); | 9081 | ); |
| 9075 | let save = editor | 9082 | let save = editor |
| 9076 | .update_in(cx, |editor, window, cx| { | 9083 | .update_in(cx, |editor, window, cx| { |
| 9077 | editor.save(true, project.clone(), window, cx) | 9084 | editor.save( |
| 9085 | SaveOptions { | ||
| 9086 | format: true, | ||
| 9087 | autosave: false, | ||
| 9088 | }, | ||
| 9089 | project.clone(), | ||
| 9090 | window, | ||
| 9091 | cx, | ||
| 9092 | ) | ||
| 9078 | }) | 9093 | }) |
| 9079 | .unwrap(); | 9094 | .unwrap(); |
| 9080 | cx.executor().advance_clock(super::FORMAT_TIMEOUT); | 9095 | cx.executor().advance_clock(super::FORMAT_TIMEOUT); |
| @@ -9086,22 +9101,6 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 5 unmodified lines | |||
| 9086 | ); | 9101 | ); |
| 9087 | } | 9102 | } |
| 9088 | 9103 | ||
| 9089 | // For non-dirty buffer, no formatting request should be sent | ||
| 9090 | { | ||
| 9091 | assert!(!cx.read(|cx| editor.is_dirty(cx))); | ||
| 9092 | |||
| 9093 | fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move { | ||
| 9094 | panic!("Should not be invoked on non-dirty buffer"); | ||
| 9095 | }); | ||
| 9096 | let save = editor | ||
| 9097 | .update_in(cx, |editor, window, cx| { | ||
| 9098 | editor.save(true, project.clone(), window, cx) | ||
| 9099 | }) | ||
| 9100 | .unwrap(); | ||
| 9101 | cx.executor().start_waiting(); | ||
| 9102 | save.await; | ||
| 9103 | } | ||
| 9104 | |||
| 9105 | // Set rust language override and assert overridden tabsize is sent to language server | 9104 | // Set rust language override and assert overridden tabsize is sent to language server |
| 9106 | update_test_language_settings(cx, |settings| { | 9105 | update_test_language_settings(cx, |settings| { |
| 9107 | settings.languages.insert( | 9106 | settings.languages.insert( |
| @@ -9129,7 +9128,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 21 unmodified lines | |||
| 9129 | }); | 9128 | }); |
| 9130 | let save = editor | 9129 | let save = editor |
| 9131 | .update_in(cx, |editor, window, cx| { | 9130 | .update_in(cx, |editor, window, cx| { |
| 9132 | editor.save(true, project.clone(), window, cx) | 9131 | editor.save( |
| 9132 | SaveOptions { | ||
| 9133 | format: true, | ||
| 9134 | autosave: false, | ||
| 9135 | }, | ||
| 9136 | project.clone(), | ||
| 9137 | window, | ||
| 9138 | cx, | ||
| 9139 | ) | ||
| 9133 | }) | 9140 | }) |
| 9134 | .unwrap(); | 9141 | .unwrap(); |
| 9135 | cx.executor().start_waiting(); | 9142 | cx.executor().start_waiting(); |
| @@ -9297,7 +9304,15 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { · 161 unmodified lines | |||
| 9297 | cx.executor().start_waiting(); | 9304 | cx.executor().start_waiting(); |
| 9298 | let save = multi_buffer_editor | 9305 | let save = multi_buffer_editor |
| 9299 | .update_in(cx, |editor, window, cx| { | 9306 | .update_in(cx, |editor, window, cx| { |
| 9300 | editor.save(true, project.clone(), window, cx) | 9307 | editor.save( |
| 9308 | SaveOptions { | ||
| 9309 | format: true, | ||
| 9310 | autosave: false, | ||
| 9311 | }, | ||
| 9312 | project.clone(), | ||
| 9313 | window, | ||
| 9314 | cx, | ||
| 9315 | ) | ||
| 9301 | }) | 9316 | }) |
| 9302 | .unwrap(); | 9317 | .unwrap(); |
| 9303 | 9318 | ||
| @@ -9341,6 +9356,170 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { · 37 unmodified lines | |||
| 9341 | }); | 9356 | }); |
| 9342 | } | 9357 | } |
| 9343 | 9358 | ||
| 9359 | #[gpui::test] | ||
| 9360 | async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) { | ||
| 9361 | init_test(cx, |_| {}); | ||
| 9362 | |||
| 9363 | let fs = FakeFs::new(cx.executor()); | ||
| 9364 | fs.insert_tree( | ||
| 9365 | path!("/dir"), | ||
| 9366 | json!({ | ||
| 9367 | "file1.rs": "fn main() { println!(\"hello\"); }", | ||
| 9368 | "file2.rs": "fn test() { println!(\"test\"); }", | ||
| 9369 | "file3.rs": "fn other() { println!(\"other\"); }\n", | ||
| 9370 | }), | ||
| 9371 | ) | ||
| 9372 | .await; | ||
| 9373 | |||
| 9374 | let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; | ||
| 9375 | let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); | ||
| 9376 | let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); | ||
| 9377 | |||
| 9378 | let language_registry = project.read_with(cx, |project, _| project.languages().clone()); | ||
| 9379 | language_registry.add(rust_lang()); | ||
| 9380 | |||
| 9381 | let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); | ||
| 9382 | let worktree_id = worktree.update(cx, |worktree, _| worktree.id()); | ||
| 9383 | |||
| 9384 | // Open three buffers | ||
| 9385 | let buffer_1 = project | ||
| 9386 | .update(cx, |project, cx| { | ||
| 9387 | project.open_buffer((worktree_id, "file1.rs"), cx) | ||
| 9388 | }) | ||
| 9389 | .await | ||
| 9390 | .unwrap(); | ||
| 9391 | let buffer_2 = project | ||
| 9392 | .update(cx, |project, cx| { | ||
| 9393 | project.open_buffer((worktree_id, "file2.rs"), cx) | ||
| 9394 | }) | ||
| 9395 | .await | ||
| 9396 | .unwrap(); | ||
| 9397 | let buffer_3 = project | ||
| 9398 | .update(cx, |project, cx| { | ||
| 9399 | project.open_buffer((worktree_id, "file3.rs"), cx) | ||
| 9400 | }) | ||
| 9401 | .await | ||
| 9402 | .unwrap(); | ||
| 9403 | |||
| 9404 | // Create a multi-buffer with all three buffers | ||
| 9405 | let multi_buffer = cx.new(|cx| { | ||
| 9406 | let mut multi_buffer = MultiBuffer::new(ReadWrite); | ||
| 9407 | multi_buffer.push_excerpts( | ||
| 9408 | buffer_1.clone(), | ||
| 9409 | [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], | ||
| 9410 | cx, | ||
| 9411 | ); | ||
| 9412 | multi_buffer.push_excerpts( | ||
| 9413 | buffer_2.clone(), | ||
| 9414 | [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], | ||
| 9415 | cx, | ||
| 9416 | ); | ||
| 9417 | multi_buffer.push_excerpts( | ||
| 9418 | buffer_3.clone(), | ||
| 9419 | [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], | ||
| 9420 | cx, | ||
| 9421 | ); | ||
| 9422 | multi_buffer | ||
| 9423 | }); | ||
| 9424 | |||
| 9425 | let editor = cx.new_window_entity(|window, cx| { | ||
| 9426 | Editor::new( | ||
| 9427 | EditorMode::full(), | ||
| 9428 | multi_buffer, | ||
| 9429 | Some(project.clone()), | ||
| 9430 | window, | ||
| 9431 | cx, | ||
| 9432 | ) | ||
| 9433 | }); | ||
| 9434 | |||
| 9435 | // Edit only the first buffer | ||
| 9436 | editor.update_in(cx, |editor, window, cx| { | ||
| 9437 | editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { | ||
| 9438 | s.select_ranges(Some(10..10)) | ||
| 9439 | }); | ||
| 9440 | editor.insert("// edited", window, cx); | ||
| 9441 | }); | ||
| 9442 | |||
| 9443 | // Verify that only buffer 1 is dirty | ||
| 9444 | buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty())); | ||
| 9445 | buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty())); | ||
| 9446 | buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty())); | ||
| 9447 | |||
| 9448 | // Get write counts after file creation (files were created with initial content) | ||
| 9449 | // We expect each file to have been written once during creation | ||
| 9450 | let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs")); | ||
| 9451 | let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs")); | ||
| 9452 | let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs")); | ||
| 9453 | |||
| 9454 | // Perform autosave | ||
| 9455 | let save_task = editor.update_in(cx, |editor, window, cx| { | ||
| 9456 | editor.save( | ||
| 9457 | SaveOptions { | ||
| 9458 | format: true, | ||
| 9459 | autosave: true, | ||
| 9460 | }, | ||
| 9461 | project.clone(), | ||
| 9462 | window, | ||
| 9463 | cx, | ||
| 9464 | ) | ||
| 9465 | }); | ||
| 9466 | save_task.await.unwrap(); | ||
| 9467 | |||
| 9468 | // Only the dirty buffer should have been saved | ||
| 9469 | assert_eq!( | ||
| 9470 | fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1, | ||
| 9471 | 1, | ||
| 9472 | "Buffer 1 was dirty, so it should have been written once during autosave" | ||
| 9473 | ); | ||
| 9474 | assert_eq!( | ||
| 9475 | fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2, | ||
| 9476 | 0, | ||
| 9477 | "Buffer 2 was clean, so it should not have been written during autosave" | ||
| 9478 | ); | ||
| 9479 | assert_eq!( | ||
| 9480 | fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3, | ||
| 9481 | 0, | ||
| 9482 | "Buffer 3 was clean, so it should not have been written during autosave" | ||
| 9483 | ); | ||
| 9484 | |||
| 9485 | // Verify buffer states after autosave | ||
| 9486 | buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty())); | ||
| 9487 | buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty())); | ||
| 9488 | buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty())); | ||
| 9489 | |||
| 9490 | // Now perform a manual save (format = true) | ||
| 9491 | let save_task = editor.update_in(cx, |editor, window, cx| { | ||
| 9492 | editor.save( | ||
| 9493 | SaveOptions { | ||
| 9494 | format: true, | ||
| 9495 | autosave: false, | ||
| 9496 | }, | ||
| 9497 | project.clone(), | ||
| 9498 | window, | ||
| 9499 | cx, | ||
| 9500 | ) | ||
| 9501 | }); | ||
| 9502 | save_task.await.unwrap(); | ||
| 9503 | |||
| 9504 | // During manual save, clean buffers don't get written to disk | ||
| 9505 | // They just get did_save called for language server notifications | ||
| 9506 | assert_eq!( | ||
| 9507 | fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1, | ||
| 9508 | 1, | ||
| 9509 | "Buffer 1 should only have been written once total (during autosave, not manual save)" | ||
| 9510 | ); | ||
| 9511 | assert_eq!( | ||
| 9512 | fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2, | ||
| 9513 | 0, | ||
| 9514 | "Buffer 2 should not have been written at all" | ||
| 9515 | ); | ||
| 9516 | assert_eq!( | ||
| 9517 | fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3, | ||
| 9518 | 0, | ||
| 9519 | "Buffer 3 should not have been written at all" | ||
| 9520 | ); | ||
| 9521 | } | ||
| 9522 | |||
| 9344 | #[gpui::test] | 9523 | #[gpui::test] |
| 9345 | async fn test_range_format_during_save(cx: &mut TestAppContext) { | 9524 | async fn test_range_format_during_save(cx: &mut TestAppContext) { |
| 9346 | init_test(cx, |_| {}); | 9525 | init_test(cx, |_| {}); |
| @@ -9384,7 +9563,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 37 unmodified lines | |||
| 9384 | 9563 | ||
| 9385 | let save = editor | 9564 | let save = editor |
| 9386 | .update_in(cx, |editor, window, cx| { | 9565 | .update_in(cx, |editor, window, cx| { |
| 9387 | editor.save(true, project.clone(), window, cx) | 9566 | editor.save( |
| 9567 | SaveOptions { | ||
| 9568 | format: true, | ||
| 9569 | autosave: false, | ||
| 9570 | }, | ||
| 9571 | project.clone(), | ||
| 9572 | window, | ||
| 9573 | cx, | ||
| 9574 | ) | ||
| 9388 | }) | 9575 | }) |
| 9389 | .unwrap(); | 9576 | .unwrap(); |
| 9390 | fake_server | 9577 | fake_server |
| @@ -9427,7 +9614,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 36 unmodified lines | |||
| 9427 | ); | 9614 | ); |
| 9428 | let save = editor | 9615 | let save = editor |
| 9429 | .update_in(cx, |editor, window, cx| { | 9616 | .update_in(cx, |editor, window, cx| { |
| 9430 | editor.save(true, project.clone(), window, cx) | 9617 | editor.save( |
| 9618 | SaveOptions { | ||
| 9619 | format: true, | ||
| 9620 | autosave: false, | ||
| 9621 | }, | ||
| 9622 | project.clone(), | ||
| 9623 | window, | ||
| 9624 | cx, | ||
| 9625 | ) | ||
| 9431 | }) | 9626 | }) |
| 9432 | .unwrap(); | 9627 | .unwrap(); |
| 9433 | cx.executor().advance_clock(super::FORMAT_TIMEOUT); | 9628 | cx.executor().advance_clock(super::FORMAT_TIMEOUT); |
| @@ -9442,12 +9637,20 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 8 unmodified lines | |||
| 9442 | // For non-dirty buffer, no formatting request should be sent | 9637 | // For non-dirty buffer, no formatting request should be sent |
| 9443 | let save = editor | 9638 | let save = editor |
| 9444 | .update_in(cx, |editor, window, cx| { | 9639 | .update_in(cx, |editor, window, cx| { |
| 9445 | editor.save(true, project.clone(), window, cx) | 9640 | editor.save( |
| 9641 | SaveOptions { | ||
| 9642 | format: false, | ||
| 9643 | autosave: false, | ||
| 9644 | }, | ||
| 9645 | project.clone(), | ||
| 9646 | window, | ||
| 9647 | cx, | ||
| 9648 | ) | ||
| 9446 | }) | 9649 | }) |
| 9447 | .unwrap(); | 9650 | .unwrap(); |
| 9448 | let _pending_format_request = fake_server | 9651 | let _pending_format_request = fake_server |
| 9449 | .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move { | 9652 | .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move { |
| 9450 | panic!("Should not be invoked on non-dirty buffer"); | 9653 | panic!("Should not be invoked"); |
| 9451 | }) | 9654 | }) |
| 9452 | .next(); | 9655 | .next(); |
| 9453 | cx.executor().start_waiting(); | 9656 | cx.executor().start_waiting(); |
| @@ -9470,7 +9673,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 16 unmodified lines | |||
| 9470 | assert!(cx.read(|cx| editor.is_dirty(cx))); | 9673 | assert!(cx.read(|cx| editor.is_dirty(cx))); |
| 9471 | let save = editor | 9674 | let save = editor |
| 9472 | .update_in(cx, |editor, window, cx| { | 9675 | .update_in(cx, |editor, window, cx| { |
| 9473 | editor.save(true, project.clone(), window, cx) | 9676 | editor.save( |
| 9677 | SaveOptions { | ||
| 9678 | format: true, | ||
| 9679 | autosave: false, | ||
| 9680 | }, | ||
| 9681 | project.clone(), | ||
| 9682 | window, | ||
| 9683 | cx, | ||
| 9684 | ) | ||
| 9474 | }) | 9685 | }) |
| 9475 | .unwrap(); | 9686 | .unwrap(); |
| 9476 | fake_server | 9687 | fake_server |
| @@ -9554,7 +9765,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { · 77 unmodified lines | |||
| 9554 | editor.perform_format( | 9765 | editor.perform_format( |
| 9555 | project.clone(), | 9766 | project.clone(), |
| 9556 | FormatTrigger::Manual, | 9767 | FormatTrigger::Manual, |
| 9557 | FormatTarget::Buffers, | 9768 | FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), |
| 9558 | window, | 9769 | window, |
| 9559 | cx, | 9770 | cx, |
| 9560 | ) | 9771 | ) |
| @@ -9600,7 +9811,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { · 39 unmodified lines | |||
| 9600 | editor.perform_format( | 9811 | editor.perform_format( |
| 9601 | project, | 9812 | project, |
| 9602 | FormatTrigger::Manual, | 9813 | FormatTrigger::Manual, |
| 9603 | FormatTarget::Buffers, | 9814 | FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), |
| 9604 | window, | 9815 | window, |
| 9605 | cx, | 9816 | cx, |
| 9606 | ) | 9817 | ) |
| @@ -9778,7 +9989,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { · 171 unmodified lines | |||
| 9778 | editor.perform_format( | 9989 | editor.perform_format( |
| 9779 | project.clone(), | 9990 | project.clone(), |
| 9780 | FormatTrigger::Manual, | 9991 | FormatTrigger::Manual, |
| 9781 | FormatTarget::Buffers, | 9992 | FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), |
| 9782 | window, | 9993 | window, |
| 9783 | cx, | 9994 | cx, |
| 9784 | ) | 9995 | ) |
| @@ -9814,7 +10025,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { · 29 unmodified lines | |||
| 9814 | editor.perform_format( | 10025 | editor.perform_format( |
| 9815 | project.clone(), | 10026 | project.clone(), |
| 9816 | FormatTrigger::Manual, | 10027 | FormatTrigger::Manual, |
| 9817 | FormatTarget::Buffers, | 10028 | FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), |
| 9818 | window, | 10029 | window, |
| 9819 | cx, | 10030 | cx, |
| 9820 | ) | 10031 | ) |
| @@ -15356,7 +15567,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { · 5535 unmodified lines | |||
| 15356 | editor.perform_format( | 15567 | editor.perform_format( |
| 15357 | project.clone(), | 15568 | project.clone(), |
| 15358 | FormatTrigger::Manual, | 15569 | FormatTrigger::Manual, |
| 15359 | FormatTarget::Buffers, | 15570 | FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), |
| 15360 | window, | 15571 | window, |
| 15361 | cx, | 15572 | cx, |
| 15362 | ) | 15573 | ) |
| @@ -15376,7 +15587,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { · 13 unmodified lines | |||
| 15376 | editor.perform_format( | 15587 | editor.perform_format( |
| 15377 | project.clone(), | 15588 | project.clone(), |
| 15378 | FormatTrigger::Manual, | 15589 | FormatTrigger::Manual, |
| 15379 | FormatTarget::Buffers, | 15590 | FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), |
| 15380 | window, | 15591 | window, |
| 15381 | cx, | 15592 | cx, |
| 15382 | ) | 15593 | ) |
| @@ -21911,6 +22122,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { · 6528 unmodified lines | |||
| 21911 | .downcast::<Editor>() | 22122 | .downcast::<Editor>() |
| 21912 | .unwrap(); | 22123 | .unwrap(); |
| 21913 | let fake_server = fake_servers.next().await.unwrap(); | 22124 | let fake_server = fake_servers.next().await.unwrap(); |
| 22125 | let server_id = fake_server.server.server_id(); | ||
| 21914 | let mut first_request = fake_server | 22126 | let mut first_request = fake_server |
| 21915 | .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| { | 22127 | .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| { |
| 21916 | let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1; | 22128 | let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1; |
| @@ -21942,7 +22154,10 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { · 25 unmodified lines | |||
| 21942 | .expect("created a singleton buffer") | 22154 | .expect("created a singleton buffer") |
| 21943 | .read(cx) | 22155 | .read(cx) |
| 21944 | .remote_id(); | 22156 | .remote_id(); |
| 21945 | let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id); | 22157 | let buffer_result_id = project |
| 22158 | .lsp_store() | ||
| 22159 | .read(cx) | ||
| 22160 | .result_id(server_id, buffer_id, cx); | ||
| 21946 | assert_eq!(expected, buffer_result_id); | 22161 | assert_eq!(expected, buffer_result_id); |
| 21947 | }); | 22162 | }); |
| 21948 | }; | 22163 | }; |
| @@ -21988,7 +22203,6 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { · 39 unmodified lines | |||
| 21988 | "Cursor movement should not trigger diagnostic request" | 22203 | "Cursor movement should not trigger diagnostic request" |
| 21989 | ); | 22204 | ); |
| 21990 | ensure_result_id(Some("2".to_string()), cx); | 22205 | ensure_result_id(Some("2".to_string()), cx); |
| 21991 | |||
| 21992 | // Multiple rapid edits should be debounced | 22206 | // Multiple rapid edits should be debounced |
| 21993 | for _ in 0..5 { | 22207 | for _ in 0..5 { |
| 21994 | editor.update_in(cx, |editor, window, cx| { | 22208 | editor.update_in(cx, |editor, window, cx| { |
| @@ -1,15 +1,15 @@ | |||
| 1 | use crate::{ | 1 | use crate::{ |
| 2 | ActiveDiagnostic, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement, | 2 | ActiveDiagnostic, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement, |
| 3 | CodeActionSource, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, | 3 | CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, |
| 4 | ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, | 4 | ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, |
| 5 | DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, | 5 | DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, |
| 6 | Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, | 6 | EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, |
| 7 | FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, | 7 | FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, |
| 8 | InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, | 8 | HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, |
| 9 | MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, | 9 | LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, |
| 10 | OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, | 10 | PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, |
| 11 | SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, | 11 | SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint, |
| 12 | StickyHeaderExcerpt, ToPoint, ToggleFold, | 12 | ToggleFold, |
| 13 | code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, | 13 | code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, |
| 14 | display_map::{ | 14 | display_map::{ |
| 15 | Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk, | 15 | Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk, |
| @@ -75,7 +75,7 @@ use std::{ · 59 unmodified lines | |||
| 75 | ops::{Deref, Range}, | 75 | ops::{Deref, Range}, |
| 76 | rc::Rc, | 76 | rc::Rc, |
| 77 | sync::Arc, | 77 | sync::Arc, |
| 78 | time::Duration, | 78 | time::{Duration, Instant}, |
| 79 | }; | 79 | }; |
| 80 | use sum_tree::Bias; | 80 | use sum_tree::Bias; |
| 81 | use text::{BufferId, SelectionGoal}; | 81 | use text::{BufferId, SelectionGoal}; |
| @@ -87,6 +87,7 @@ use util::{RangeExt, ResultExt, debug_panic}; · 5 unmodified lines | |||
| 87 | use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; | 87 | use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; |
| 88 | 88 | ||
| 89 | const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; | 89 | const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; |
| 90 | const SELECTION_DRAG_DELAY: Duration = Duration::from_millis(300); | ||
| 90 | 91 | ||
| 91 | /// Determines what kinds of highlights should be applied to a lines background. | 92 | /// Determines what kinds of highlights should be applied to a lines background. |
| 92 | #[derive(Clone, Copy, Default)] | 93 | #[derive(Clone, Copy, Default)] |
| @@ -107,6 +108,12 @@ struct SelectionLayout { · 14 unmodified lines | |||
| 107 | user_name: Option<SharedString>, | 108 | user_name: Option<SharedString>, |
| 108 | } | 109 | } |
| 109 | 110 | ||
| 111 | struct InlineBlameLayout { | ||
| 112 | element: AnyElement, | ||
| 113 | bounds: Bounds<Pixels>, | ||
| 114 | entry: BlameEntry, | ||
| 115 | } | ||
| 116 | |||
| 110 | impl SelectionLayout { | 117 | impl SelectionLayout { |
| 111 | fn new<T: ToPoint + ToDisplayPoint + Clone>( | 118 | fn new<T: ToPoint + ToDisplayPoint + Clone>( |
| 112 | selection: Selection<T>, | 119 | selection: Selection<T>, |
| @@ -187,7 +194,7 @@ impl EditorElement { · 74 unmodified lines | |||
| 187 | let editor = &self.editor; | 194 | let editor = &self.editor; |
| 188 | editor.update(cx, |editor, cx| { | 195 | editor.update(cx, |editor, cx| { |
| 189 | for action in editor.editor_actions.borrow().values() { | 196 | for action in editor.editor_actions.borrow().values() { |
| 190 | (action)(window, cx) | 197 | (action)(editor, window, cx) |
| 191 | } | 198 | } |
| 192 | }); | 199 | }); |
| 193 | 200 | ||
| @@ -642,6 +649,7 @@ impl EditorElement { · 448 unmodified lines | |||
| 642 | editor.selection_drag_state = SelectionDragState::ReadyToDrag { | 649 | editor.selection_drag_state = SelectionDragState::ReadyToDrag { |
| 643 | selection: newest_anchor.clone(), | 650 | selection: newest_anchor.clone(), |
| 644 | click_position: event.position, | 651 | click_position: event.position, |
| 652 | mouse_down_time: Instant::now(), | ||
| 645 | }; | 653 | }; |
| 646 | cx.stop_propagation(); | 654 | cx.stop_propagation(); |
| 647 | return; | 655 | return; |
| @@ -692,12 +700,15 @@ impl EditorElement { · 44 unmodified lines | |||
| 692 | } | 700 | } |
| 693 | 701 | ||
| 694 | let position = point_for_position.previous_valid; | 702 | let position = point_for_position.previous_valid; |
| 695 | let multi_cursor_modifier = Editor::multi_cursor_modifier(true, &modifiers, cx); | 703 | if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) { |
| 696 | if Editor::columnar_selection_modifiers(multi_cursor_modifier, &modifiers) { | ||
| 697 | editor.select( | 704 | editor.select( |
| 698 | SelectPhase::BeginColumnar { | 705 | SelectPhase::BeginColumnar { |
| 699 | position, | 706 | position, |
| 700 | reset: true, | 707 | reset: match mode { |
| 708 | ColumnarMode::FromMouse => true, | ||
| 709 | ColumnarMode::FromSelection => false, | ||
| 710 | }, | ||
| 711 | mode: mode, | ||
| 701 | goal_column: point_for_position.exact_unclipped.column(), | 712 | goal_column: point_for_position.exact_unclipped.column(), |
| 702 | }, | 713 | }, |
| 703 | window, | 714 | window, |
| @@ -717,7 +728,7 @@ impl EditorElement { · 13 unmodified lines | |||
| 717 | editor.select( | 728 | editor.select( |
| 718 | SelectPhase::Begin { | 729 | SelectPhase::Begin { |
| 719 | position, | 730 | position, |
| 720 | add: multi_cursor_modifier, | 731 | add: Editor::multi_cursor_modifier(true, &modifiers, cx), |
| 721 | click_count, | 732 | click_count, |
| 722 | }, | 733 | }, |
| 723 | window, | 734 | window, |
| @@ -814,6 +825,7 @@ impl EditorElement { · 90 unmodified lines | |||
| 814 | SelectPhase::BeginColumnar { | 825 | SelectPhase::BeginColumnar { |
| 815 | position, | 826 | position, |
| 816 | reset: true, | 827 | reset: true, |
| 828 | mode: ColumnarMode::FromMouse, | ||
| 817 | goal_column: point_for_position.exact_unclipped.column(), | 829 | goal_column: point_for_position.exact_unclipped.column(), |
| 818 | }, | 830 | }, |
| 819 | window, | 831 | window, |
| @@ -837,6 +849,7 @@ impl EditorElement { · 17 unmodified lines | |||
| 837 | SelectionDragState::ReadyToDrag { | 849 | SelectionDragState::ReadyToDrag { |
| 838 | selection: _, | 850 | selection: _, |
| 839 | ref click_position, | 851 | ref click_position, |
| 852 | mouse_down_time: _, | ||
| 840 | } => { | 853 | } => { |
| 841 | if event.position == *click_position { | 854 | if event.position == *click_position { |
| 842 | editor.select( | 855 | editor.select( |
| @@ -851,13 +864,18 @@ impl EditorElement { · 8 unmodified lines | |||
| 851 | editor.selection_drag_state = SelectionDragState::None; | 864 | editor.selection_drag_state = SelectionDragState::None; |
| 852 | cx.stop_propagation(); | 865 | cx.stop_propagation(); |
| 853 | return; | 866 | return; |
| 867 | } else { | ||
| 868 | debug_panic!("drag state can never be in ready state after drag") | ||
| 854 | } | 869 | } |
| 855 | } | 870 | } |
| 856 | SelectionDragState::Dragging { ref selection, .. } => { | 871 | SelectionDragState::Dragging { ref selection, .. } => { |
| 857 | let snapshot = editor.snapshot(window, cx); | 872 | let snapshot = editor.snapshot(window, cx); |
| 858 | let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot)); | 873 | let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot)); |
| 859 | if !point_for_position.intersects_selection(&selection_display) { | 874 | if !point_for_position.intersects_selection(&selection_display) |
| 860 | let is_cut = !event.modifiers.control; | 875 | && text_hitbox.is_hovered(window) |
| 876 | { | ||
| 877 | let is_cut = !(cfg!(target_os = "macos") && event.modifiers.alt | ||
| 878 | || cfg!(not(target_os = "macos")) && event.modifiers.control); | ||
| 861 | editor.move_selection_on_drop( | 879 | editor.move_selection_on_drop( |
| 862 | &selection.clone(), | 880 | &selection.clone(), |
| 863 | point_for_position.previous_valid, | 881 | point_for_position.previous_valid, |
| @@ -865,10 +883,11 @@ impl EditorElement { · 1 unmodified line | |||
| 865 | window, | 883 | window, |
| 866 | cx, | 884 | cx, |
| 867 | ); | 885 | ); |
| 868 | editor.selection_drag_state = SelectionDragState::None; | ||
| 869 | cx.stop_propagation(); | ||
| 870 | return; | ||
| 871 | } | 886 | } |
| 887 | editor.selection_drag_state = SelectionDragState::None; | ||
| 888 | cx.stop_propagation(); | ||
| 889 | cx.notify(); | ||
| 890 | return; | ||
| 872 | } | 891 | } |
| 873 | _ => {} | 892 | _ => {} |
| 874 | } | 893 | } |
| @@ -941,39 +960,43 @@ impl EditorElement { · 66 unmodified lines | |||
| 941 | return; | 960 | return; |
| 942 | } | 961 | } |
| 943 | 962 | ||
| 944 | let text_bounds = position_map.text_hitbox.bounds; | ||
| 945 | let point_for_position = position_map.point_for_position(event.position); | 963 | let point_for_position = position_map.point_for_position(event.position); |
| 964 | let text_hitbox = &position_map.text_hitbox; | ||
| 946 | 965 | ||
| 947 | let mut scroll_delta = gpui::Point::<f32>::default(); | 966 | let scroll_delta = { |
| 948 | let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); | 967 | let text_bounds = text_hitbox.bounds; |
| 949 | let top = text_bounds.origin.y + vertical_margin; | 968 | let mut scroll_delta = gpui::Point::<f32>::default(); |
| 950 | let bottom = text_bounds.bottom_left().y - vertical_margin; | 969 | let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); |
| 951 | if event.position.y < top { | 970 | let top = text_bounds.origin.y + vertical_margin; |
| 952 | scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); | 971 | let bottom = text_bounds.bottom_left().y - vertical_margin; |
| 953 | } | 972 | if event.position.y < top { |
| 954 | if event.position.y > bottom { | 973 | scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); |
| 955 | scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); | 974 | } |
| 956 | } | 975 | if event.position.y > bottom { |
| 976 | scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); | ||
| 977 | } | ||
| 957 | 978 | ||
| 958 | // We need horizontal width of text | 979 | // We need horizontal width of text |
| 959 | let style = editor.style.clone().unwrap_or_default(); | 980 | let style = editor.style.clone().unwrap_or_default(); |
| 960 | let font_id = window.text_system().resolve_font(&style.text.font()); | 981 | let font_id = window.text_system().resolve_font(&style.text.font()); |
| 961 | let font_size = style.text.font_size.to_pixels(window.rem_size()); | 982 | let font_size = style.text.font_size.to_pixels(window.rem_size()); |
| 962 | let em_width = window.text_system().em_width(font_id, font_size).unwrap(); | 983 | let em_width = window.text_system().em_width(font_id, font_size).unwrap(); |
| 963 | 984 | ||
| 964 | let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin; | 985 | let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin; |
| 965 | 986 | ||
| 966 | let scroll_space: Pixels = scroll_margin_x * em_width; | 987 | let scroll_space: Pixels = scroll_margin_x * em_width; |
| 967 | 988 | ||
| 968 | let left = text_bounds.origin.x + scroll_space; | 989 | let left = text_bounds.origin.x + scroll_space; |
| 969 | let right = text_bounds.top_right().x - scroll_space; | 990 | let right = text_bounds.top_right().x - scroll_space; |
| 970 | 991 | ||
| 971 | if event.position.x < left { | 992 | if event.position.x < left { |
| 972 | scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); | 993 | scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); |
| 973 | } | 994 | } |
| 974 | if event.position.x > right { | 995 | if event.position.x > right { |
| 975 | scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); | 996 | scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); |
| 976 | } | 997 | } |
| 998 | scroll_delta | ||
| 999 | }; | ||
| 977 | 1000 | ||
| 978 | if !editor.has_pending_selection() { | 1001 | if !editor.has_pending_selection() { |
| 979 | let drop_anchor = position_map | 1002 | let drop_anchor = position_map |
| @@ -982,28 +1005,60 @@ impl EditorElement { · 2 unmodified lines | |||
| 982 | match editor.selection_drag_state { | 1005 | match editor.selection_drag_state { |
| 983 | SelectionDragState::Dragging { | 1006 | SelectionDragState::Dragging { |
| 984 | ref mut drop_cursor, | 1007 | ref mut drop_cursor, |
| 1008 | ref mut hide_drop_cursor, | ||
| 985 | .. | 1009 | .. |
| 986 | } => { | 1010 | } => { |
| 987 | drop_cursor.start = drop_anchor; | 1011 | drop_cursor.start = drop_anchor; |
| 988 | drop_cursor.end = drop_anchor; | 1012 | drop_cursor.end = drop_anchor; |
| 1013 | *hide_drop_cursor = !text_hitbox.is_hovered(window); | ||
| 1014 | editor.apply_scroll_delta(scroll_delta, window, cx); | ||
| 1015 | cx.notify(); | ||
| 989 | } | 1016 | } |
| 990 | SelectionDragState::ReadyToDrag { ref selection, .. } => { | 1017 | SelectionDragState::ReadyToDrag { |
| 991 | let drop_cursor = Selection { | 1018 | ref selection, |
| 992 | id: post_inc(&mut editor.selections.next_selection_id), | 1019 | ref click_position, |
| 993 | start: drop_anchor, | 1020 | ref mouse_down_time, |
| 994 | end: drop_anchor, | 1021 | } => { |
| 995 | reversed: false, | 1022 | if mouse_down_time.elapsed() >= SELECTION_DRAG_DELAY { |
| 996 | goal: SelectionGoal::None, | 1023 | let drop_cursor = Selection { |
| 997 | }; | 1024 | id: post_inc(&mut editor.selections.next_selection_id), |
| 998 | editor.selection_drag_state = SelectionDragState::Dragging { | 1025 | start: drop_anchor, |
| 999 | selection: selection.clone(), | 1026 | end: drop_anchor, |
| 1000 | drop_cursor, | 1027 | reversed: false, |
| 1001 | }; | 1028 | goal: SelectionGoal::None, |
| 1029 | }; | ||
| 1030 | editor.selection_drag_state = SelectionDragState::Dragging { | ||
| 1031 | selection: selection.clone(), | ||
| 1032 | drop_cursor, | ||
| 1033 | hide_drop_cursor: false, | ||
| 1034 | }; | ||
| 1035 | editor.apply_scroll_delta(scroll_delta, window, cx); | ||
| 1036 | cx.notify(); | ||
| 1037 | } else { | ||
| 1038 | let click_point = position_map.point_for_position(*click_position); | ||
| 1039 | editor.selection_drag_state = SelectionDragState::None; | ||
| 1040 | editor.select( | ||
| 1041 | SelectPhase::Begin { | ||
| 1042 | position: click_point.previous_valid, | ||
| 1043 | add: false, | ||
| 1044 | click_count: 1, | ||
| 1045 | }, | ||
| 1046 | window, | ||
| 1047 | cx, | ||
| 1048 | ); | ||
| 1049 | editor.select( | ||
| 1050 | SelectPhase::Update { | ||
| 1051 | position: point_for_position.previous_valid, | ||
| 1052 | goal_column: point_for_position.exact_unclipped.column(), | ||
| 1053 | scroll_delta, | ||
| 1054 | }, | ||
| 1055 | window, | ||
| 1056 | cx, | ||
| 1057 | ); | ||
| 1058 | } | ||
| 1002 | } | 1059 | } |
| 1003 | _ => {} | 1060 | _ => {} |
| 1004 | } | 1061 | } |
| 1005 | editor.apply_scroll_delta(scroll_delta, window, cx); | ||
| 1006 | cx.notify(); | ||
| 1007 | } else { | 1062 | } else { |
| 1008 | editor.select( | 1063 | editor.select( |
| 1009 | SelectPhase::Update { | 1064 | SelectPhase::Update { |
| @@ -1027,17 +1082,70 @@ impl EditorElement { · 17 unmodified lines | |||
| 1027 | let text_hitbox = &position_map.text_hitbox; | 1082 | let text_hitbox = &position_map.text_hitbox; |
| 1028 | let gutter_hitbox = &position_map.gutter_hitbox; | 1083 | let gutter_hitbox = &position_map.gutter_hitbox; |
| 1029 | let modifiers = event.modifiers; | 1084 | let modifiers = event.modifiers; |
| 1085 | let text_hovered = text_hitbox.is_hovered(window); | ||
| 1030 | let gutter_hovered = gutter_hitbox.is_hovered(window); | 1086 | let gutter_hovered = gutter_hitbox.is_hovered(window); |
| 1031 | editor.set_gutter_hovered(gutter_hovered, cx); | 1087 | editor.set_gutter_hovered(gutter_hovered, cx); |
| 1032 | editor.mouse_cursor_hidden = false; | 1088 | editor.mouse_cursor_hidden = false; |
| 1033 | 1089 | ||
| 1034 | if gutter_hovered { | 1090 | let point_for_position = position_map.point_for_position(event.position); |
| 1035 | let new_point = position_map | 1091 | let valid_point = point_for_position.previous_valid; |
| 1036 | .point_for_position(event.position) | 1092 | |
| 1037 | .previous_valid; | 1093 | let hovered_diff_control = position_map |
| 1094 | .diff_hunk_control_bounds | ||
| 1095 | .iter() | ||
| 1096 | .find(|(_, bounds)| bounds.contains(&event.position)) | ||
| 1097 | .map(|(row, _)| *row); | ||
| 1098 | |||
| 1099 | let hovered_diff_hunk_row = if let Some(control_row) = hovered_diff_control { | ||
| 1100 | Some(control_row) | ||
| 1101 | } else { | ||
| 1102 | if text_hovered { | ||
| 1103 | let current_row = valid_point.row(); | ||
| 1104 | position_map.display_hunks.iter().find_map(|(hunk, _)| { | ||
| 1105 | if let DisplayDiffHunk::Unfolded { | ||
| 1106 | display_row_range, .. | ||
| 1107 | } = hunk | ||
| 1108 | { | ||
| 1109 | if display_row_range.contains(¤t_row) { | ||
| 1110 | Some(display_row_range.start) | ||
| 1111 | } else { | ||
| 1112 | None | ||
| 1113 | } | ||
| 1114 | } else { | ||
| 1115 | None | ||
| 1116 | } | ||
| 1117 | }) | ||
| 1118 | } else { | ||
| 1119 | None | ||
| 1120 | } | ||
| 1121 | }; | ||
| 1122 | |||
| 1123 | if hovered_diff_hunk_row != editor.hovered_diff_hunk_row { | ||
| 1124 | editor.hovered_diff_hunk_row = hovered_diff_hunk_row; | ||
| 1125 | cx.notify(); | ||
| 1126 | } | ||
| 1127 | |||
| 1128 | if let Some((bounds, blame_entry)) = &position_map.inline_blame_bounds { | ||
| 1129 | let mouse_over_inline_blame = bounds.contains(&event.position); | ||
| 1130 | let mouse_over_popover = editor | ||
| 1131 | .inline_blame_popover | ||
| 1132 | .as_ref() | ||
| 1133 | .and_then(|state| state.popover_bounds) | ||
| 1134 | .map_or(false, |bounds| bounds.contains(&event.position)); | ||
| 1135 | |||
| 1136 | if mouse_over_inline_blame || mouse_over_popover { | ||
| 1137 | editor.show_blame_popover(&blame_entry, event.position, cx); | ||
| 1138 | } else { | ||
| 1139 | editor.hide_blame_popover(cx); | ||
| 1140 | } | ||
| 1141 | } else { | ||
| 1142 | editor.hide_blame_popover(cx); | ||
| 1143 | } | ||
| 1144 | |||
| 1145 | let breakpoint_indicator = if gutter_hovered { | ||
| 1038 | let buffer_anchor = position_map | 1146 | let buffer_anchor = position_map |
| 1039 | .snapshot | 1147 | .snapshot |
| 1040 | .display_point_to_anchor(new_point, Bias::Left); | 1148 | .display_point_to_anchor(valid_point, Bias::Left); |
| 1041 | 1149 | ||
| 1042 | if let Some((buffer_snapshot, file)) = position_map | 1150 | if let Some((buffer_snapshot, file)) = position_map |
| 1043 | .snapshot | 1151 | .snapshot |
| @@ -1045,7 +1153,6 @@ impl EditorElement { · 1 unmodified line | |||
| 1045 | .buffer_for_excerpt(buffer_anchor.excerpt_id) | 1153 | .buffer_for_excerpt(buffer_anchor.excerpt_id) |
| 1046 | .and_then(|buffer| buffer.file().map(|file| (buffer, file))) | 1154 | .and_then(|buffer| buffer.file().map(|file| (buffer, file))) |
| 1047 | { | 1155 | { |
| 1048 | let was_hovered = editor.gutter_breakpoint_indicator.0.is_some(); | ||
| 1049 | let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot); | 1156 | let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot); |
| 1050 | 1157 | ||
| 1051 | let is_visible = editor | 1158 | let is_visible = editor |
| @@ -1073,43 +1180,46 @@ impl EditorElement { · 21 unmodified lines | |||
| 1073 | .is_some() | 1180 | .is_some() |
| 1074 | }); | 1181 | }); |
| 1075 | 1182 | ||
| 1076 | editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator { | 1183 | if !is_visible { |
| 1077 | display_row: new_point.row(), | 1184 | editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| { |
| 1078 | is_active: is_visible, | 1185 | cx.spawn(async move |this, cx| { |
| 1079 | collides_with_existing_breakpoint: has_existing_breakpoint, | ||
| 1080 | }); | ||
| 1081 | |||
| 1082 | editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| { | ||
| 1083 | cx.spawn(async move |this, cx| { | ||
| 1084 | if !was_hovered { | ||
| 1085 | cx.background_executor() | 1186 | cx.background_executor() |
| 1086 | .timer(Duration::from_millis(200)) | 1187 | .timer(Duration::from_millis(200)) |
| 1087 | .await; | 1188 | .await; |
| 1088 | } | ||
| 1089 | |||
| 1090 | this.update(cx, |this, cx| { | ||
| 1091 | if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() { | ||
| 1092 | indicator.is_active = true; | ||
| 1093 | } | ||
| 1094 | 1189 | ||
| 1095 | cx.notify(); | 1190 | this.update(cx, |this, cx| { |
| 1191 | if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() | ||
| 1192 | { | ||
| 1193 | indicator.is_active = true; | ||
| 1194 | cx.notify(); | ||
| 1195 | } | ||
| 1196 | }) | ||
| 1197 | .ok(); | ||
| 1096 | }) | 1198 | }) |
| 1097 | .ok(); | 1199 | }); |
| 1098 | }) | 1200 | } |
| 1099 | }); | 1201 | |
| 1202 | Some(PhantomBreakpointIndicator { | ||
| 1203 | display_row: valid_point.row(), | ||
| 1204 | is_active: is_visible, | ||
| 1205 | collides_with_existing_breakpoint: has_existing_breakpoint, | ||
| 1206 | }) | ||
| 1100 | } else { | 1207 | } else { |
| 1101 | editor.gutter_breakpoint_indicator = (None, None); | 1208 | editor.gutter_breakpoint_indicator.1 = None; |
| 1209 | None | ||
| 1102 | } | 1210 | } |
| 1103 | } else { | 1211 | } else { |
| 1104 | editor.gutter_breakpoint_indicator = (None, None); | 1212 | editor.gutter_breakpoint_indicator.1 = None; |
| 1105 | } | 1213 | None |
| 1214 | }; | ||
| 1106 | 1215 | ||
| 1107 | cx.notify(); | 1216 | if &breakpoint_indicator != &editor.gutter_breakpoint_indicator.0 { |
| 1217 | editor.gutter_breakpoint_indicator.0 = breakpoint_indicator; | ||
| 1218 | cx.notify(); | ||
| 1219 | } | ||
| 1108 | 1220 | ||
| 1109 | // Don't trigger hover popover if mouse is hovering over context menu | 1221 | // Don't trigger hover popover if mouse is hovering over context menu |
| 1110 | if text_hitbox.is_hovered(window) { | 1222 | if text_hovered { |
| 1111 | let point_for_position = position_map.point_for_position(event.position); | ||
| 1112 | |||
| 1113 | editor.update_hovered_link( | 1223 | editor.update_hovered_link( |
| 1114 | point_for_position, | 1224 | point_for_position, |
| 1115 | &position_map.snapshot, | 1225 | &position_map.snapshot, |
| @@ -1247,16 +1357,18 @@ impl EditorElement { · 131 unmodified lines | |||
| 1247 | if let SelectionDragState::Dragging { | 1357 | if let SelectionDragState::Dragging { |
| 1248 | ref selection, | 1358 | ref selection, |
| 1249 | ref drop_cursor, | 1359 | ref drop_cursor, |
| 1360 | ref hide_drop_cursor, | ||
| 1250 | } = editor.selection_drag_state | 1361 | } = editor.selection_drag_state |
| 1251 | { | 1362 | { |
| 1252 | if drop_cursor | 1363 | if !hide_drop_cursor |
| 1253 | .start | 1364 | && (drop_cursor |
| 1254 | .cmp(&selection.start, &snapshot.buffer_snapshot) | 1365 | .start |
| 1255 | .eq(&Ordering::Less) | 1366 | .cmp(&selection.start, &snapshot.buffer_snapshot) |
| 1256 | || drop_cursor | 1367 | .eq(&Ordering::Less) |
| 1257 | .end | 1368 | || drop_cursor |
| 1258 | .cmp(&selection.end, &snapshot.buffer_snapshot) | 1369 | .end |
| 1259 | .eq(&Ordering::Greater) | 1370 | .cmp(&selection.end, &snapshot.buffer_snapshot) |
| 1371 | .eq(&Ordering::Greater)) | ||
| 1260 | { | 1372 | { |
| 1261 | let drag_cursor_layout = SelectionLayout::new( | 1373 | let drag_cursor_layout = SelectionLayout::new( |
| 1262 | drop_cursor.clone(), | 1374 | drop_cursor.clone(), |
| @@ -1451,7 +1563,7 @@ impl EditorElement { · 188 unmodified lines | |||
| 1451 | snapshot | 1563 | snapshot |
| 1452 | .grapheme_at(cursor_position) | 1564 | .grapheme_at(cursor_position) |
| 1453 | .or_else(|| { | 1565 | .or_else(|| { |
| 1454 | if cursor_column == 0 { | 1566 | if snapshot.is_empty() { |
| 1455 | snapshot.placeholder_text().and_then(|s| { | 1567 | snapshot.placeholder_text().and_then(|s| { |
| 1456 | s.graphemes(true).next().map(|s| s.to_string().into()) | 1568 | s.graphemes(true).next().map(|s| s.to_string().into()) |
| 1457 | }) | 1569 | }) |
| @@ -2230,7 +2342,7 @@ impl EditorElement { · 772 unmodified lines | |||
| 2230 | text_hitbox: &Hitbox, | 2342 | text_hitbox: &Hitbox, |
| 2231 | window: &mut Window, | 2343 | window: &mut Window, |
| 2232 | cx: &mut App, | 2344 | cx: &mut App, |
| 2233 | ) -> Option<AnyElement> { | 2345 | ) -> Option<InlineBlameLayout> { |
| 2234 | if !self | 2346 | if !self |
| 2235 | .editor | 2347 | .editor |
| 2236 | .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx)) | 2348 | .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx)) |
| @@ -2259,13 +2371,13 @@ impl EditorElement { · 22 unmodified lines | |||
| 2259 | padding * em_width | 2371 | padding * em_width |
| 2260 | }; | 2372 | }; |
| 2261 | 2373 | ||
| 2262 | let blame_entry = blame | 2374 | let entry = blame |
| 2263 | .update(cx, |blame, cx| { | 2375 | .update(cx, |blame, cx| { |
| 2264 | blame.blame_for_rows(&[*row_info], cx).next() | 2376 | blame.blame_for_rows(&[*row_info], cx).next() |
| 2265 | }) | 2377 | }) |
| 2266 | .flatten()?; | 2378 | .flatten()?; |
| 2267 | 2379 | ||
| 2268 | let mut element = render_inline_blame_entry(blame_entry.clone(), &self.style, cx)?; | 2380 | let mut element = render_inline_blame_entry(entry.clone(), &self.style, cx)?; |
| 2269 | 2381 | ||
| 2270 | let start_y = content_origin.y | 2382 | let start_y = content_origin.y |
| 2271 | + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); | 2383 | + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); |
| @@ -2294,24 +2406,19 @@ impl EditorElement { · 22 unmodified lines | |||
| 2294 | let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); | 2406 | let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); |
| 2295 | let bounds = Bounds::new(absolute_offset, size); | 2407 | let bounds = Bounds::new(absolute_offset, size); |
| 2296 | 2408 | ||
| 2297 | self.layout_blame_entry_popover( | 2409 | self.layout_blame_entry_popover(entry.clone(), blame, line_height, text_hitbox, window, cx); |
| 2298 | bounds, | ||
| 2299 | blame_entry, | ||
| 2300 | blame, | ||
| 2301 | line_height, | ||
| 2302 | text_hitbox, | ||
| 2303 | window, | ||
| 2304 | cx, | ||
| 2305 | ); | ||
| 2306 | 2410 | ||
| 2307 | element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx); | 2411 | element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx); |
| 2308 | 2412 | ||
| 2309 | Some(element) | 2413 | Some(InlineBlameLayout { |
| 2414 | element, | ||
| 2415 | bounds, | ||
| 2416 | entry, | ||
| 2417 | }) | ||
| 2310 | } | 2418 | } |
| 2311 | 2419 | ||
| 2312 | fn layout_blame_entry_popover( | 2420 | fn layout_blame_entry_popover( |
| 2313 | &self, | 2421 | &self, |
| 2314 | parent_bounds: Bounds<Pixels>, | ||
| 2315 | blame_entry: BlameEntry, | 2422 | blame_entry: BlameEntry, |
| 2316 | blame: Entity<GitBlame>, | 2423 | blame: Entity<GitBlame>, |
| 2317 | line_height: Pixels, | 2424 | line_height: Pixels, |
| @@ -2319,91 +2426,59 @@ impl EditorElement { · 1 unmodified line | |||
| 2319 | window: &mut Window, | 2426 | window: &mut Window, |
| 2320 | cx: &mut App, | 2427 | cx: &mut App, |
| 2321 | ) { | 2428 | ) { |
| 2322 | let mouse_position = window.mouse_position(); | 2429 | let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| { |
| 2323 | let mouse_over_inline_blame = parent_bounds.contains(&mouse_position); | ||
| 2324 | let mouse_over_popover = self.editor.read_with(cx, |editor, _| { | ||
| 2325 | editor | 2430 | editor |
| 2326 | .inline_blame_popover | 2431 | .inline_blame_popover |
| 2327 | .as_ref() | 2432 | .as_ref() |
| 2328 | .and_then(|state| state.popover_bounds) | 2433 | .map(|state| (state.popover_state.clone(), state.position)) |
| 2329 | .map_or(false, |bounds| bounds.contains(&mouse_position)) | 2434 | }) else { |
| 2330 | }); | 2435 | return; |
| 2331 | 2436 | }; | |
| 2332 | self.editor.update(cx, |editor, cx| { | ||
| 2333 | if mouse_over_inline_blame || mouse_over_popover { | ||
| 2334 | editor.show_blame_popover(&blame_entry, mouse_position, cx); | ||
| 2335 | } else { | ||
| 2336 | editor.hide_blame_popover(cx); | ||
| 2337 | } | ||
| 2338 | }); | ||
| 2339 | 2437 | ||
| 2340 | let should_draw = self.editor.read_with(cx, |editor, _| { | 2438 | let workspace = self |
| 2341 | editor | 2439 | .editor |
| 2342 | .inline_blame_popover | 2440 | .read_with(cx, |editor, _| editor.workspace().map(|w| w.downgrade())); |
| 2343 | .as_ref() | 2441 | |
| 2344 | .map_or(false, |state| state.show_task.is_none()) | 2442 | let maybe_element = workspace.and_then(|workspace| { |
| 2443 | render_blame_entry_popover( | ||
| 2444 | blame_entry, | ||
| 2445 | popover_state.scroll_handle, | ||
| 2446 | popover_state.commit_message, | ||
| 2447 | popover_state.markdown, | ||
| 2448 | workspace, | ||
| 2449 | &blame, | ||
| 2450 | window, | ||
| 2451 | cx, | ||
| 2452 | ) | ||
| 2345 | }); | 2453 | }); |
| 2346 | 2454 | ||
| 2347 | if should_draw { | 2455 | if let Some(mut element) = maybe_element { |
| 2348 | let maybe_element = self.editor.update(cx, |editor, cx| { | 2456 | let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); |
| 2349 | editor | 2457 | let overall_height = size.height + HOVER_POPOVER_GAP; |
| 2350 | .workspace() | 2458 | let popover_origin = if target_point.y > overall_height { |
| 2351 | .map(|workspace| workspace.downgrade()) | 2459 | point(target_point.x, target_point.y - size.height) |
| 2352 | .zip( | 2460 | } else { |
| 2353 | editor | 2461 | point( |
| 2354 | .inline_blame_popover | 2462 | target_point.x, |
| 2355 | .as_ref() | 2463 | target_point.y + line_height + HOVER_POPOVER_GAP, |
| 2356 | .map(|p| p.popover_state.clone()), | 2464 | ) |
| 2357 | ) | 2465 | }; |
| 2358 | .and_then(|(workspace, popover_state)| { | ||
| 2359 | render_blame_entry_popover( | ||
| 2360 | blame_entry, | ||
| 2361 | popover_state.scroll_handle, | ||
| 2362 | popover_state.commit_message, | ||
| 2363 | popover_state.markdown, | ||
| 2364 | workspace, | ||
| 2365 | &blame, | ||
| 2366 | window, | ||
| 2367 | cx, | ||
| 2368 | ) | ||
| 2369 | }) | ||
| 2370 | }); | ||
| 2371 | |||
| 2372 | if let Some(mut element) = maybe_element { | ||
| 2373 | let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); | ||
| 2374 | let origin = self.editor.read_with(cx, |editor, _| { | ||
| 2375 | let target_point = editor | ||
| 2376 | .inline_blame_popover | ||
| 2377 | .as_ref() | ||
| 2378 | .map_or(mouse_position, |state| state.position); | ||
| 2379 | |||
| 2380 | let overall_height = size.height + HOVER_POPOVER_GAP; | ||
| 2381 | let popover_origin = if target_point.y > overall_height { | ||
| 2382 | point(target_point.x, target_point.y - size.height) | ||
| 2383 | } else { | ||
| 2384 | point( | ||
| 2385 | target_point.x, | ||
| 2386 | target_point.y + line_height + HOVER_POPOVER_GAP, | ||
| 2387 | ) | ||
| 2388 | }; | ||
| 2389 | 2466 | ||
| 2390 | let horizontal_offset = (text_hitbox.top_right().x | 2467 | let horizontal_offset = (text_hitbox.top_right().x |
| 2391 | - POPOVER_RIGHT_OFFSET | 2468 | - POPOVER_RIGHT_OFFSET |
| 2392 | - (popover_origin.x + size.width)) | 2469 | - (popover_origin.x + size.width)) |
| 2393 | .min(Pixels::ZERO); | 2470 | .min(Pixels::ZERO); |
| 2394 | 2471 | ||
| 2395 | point(popover_origin.x + horizontal_offset, popover_origin.y) | 2472 | let origin = point(popover_origin.x + horizontal_offset, popover_origin.y); |
| 2396 | }); | 2473 | let popover_bounds = Bounds::new(origin, size); |
| 2397 | 2474 | ||
| 2398 | let popover_bounds = Bounds::new(origin, size); | 2475 | self.editor.update(cx, |editor, _| { |
| 2399 | self.editor.update(cx, |editor, _| { | 2476 | if let Some(state) = &mut editor.inline_blame_popover { |
| 2400 | if let Some(state) = &mut editor.inline_blame_popover { | 2477 | state.popover_bounds = Some(popover_bounds); |
| 2401 | state.popover_bounds = Some(popover_bounds); | 2478 | } |
| 2402 | } | 2479 | }); |
| 2403 | }); | ||
| 2404 | 2480 | ||
| 2405 | window.defer_draw(element, origin, 2); | 2481 | window.defer_draw(element, origin, 2); |
| 2406 | } | ||
| 2407 | } | 2482 | } |
| 2408 | } | 2483 | } |
| 2409 | 2484 | ||
| @@ -2812,7 +2887,8 @@ impl EditorElement { · 402 unmodified lines | |||
| 2812 | let available_width = gutter_dimensions.left_padding - git_gutter_width; | 2887 | let available_width = gutter_dimensions.left_padding - git_gutter_width; |
| 2813 | 2888 | ||
| 2814 | let editor = self.editor.clone(); | 2889 | let editor = self.editor.clone(); |
| 2815 | let is_wide = max_line_number_length >= MIN_LINE_NUMBER_DIGITS | 2890 | let is_wide = max_line_number_length |
| 2891 | >= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32 | ||
| 2816 | && row_info | 2892 | && row_info |
| 2817 | .buffer_row | 2893 | .buffer_row |
| 2818 | .is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length) | 2894 | .is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length) |
| @@ -4730,7 +4806,6 @@ impl EditorElement { · 1911 unmodified lines | |||
| 4730 | row_range: Range<DisplayRow>, | 4806 | row_range: Range<DisplayRow>, |
| 4731 | row_infos: &[RowInfo], | 4807 | row_infos: &[RowInfo], |
| 4732 | text_hitbox: &Hitbox, | 4808 | text_hitbox: &Hitbox, |
| 4733 | position_map: &PositionMap, | ||
| 4734 | newest_cursor_position: Option<DisplayPoint>, | 4809 | newest_cursor_position: Option<DisplayPoint>, |
| 4735 | line_height: Pixels, | 4810 | line_height: Pixels, |
| 4736 | right_margin: Pixels, | 4811 | right_margin: Pixels, |
| @@ -4740,14 +4815,15 @@ impl EditorElement { · 3 unmodified lines | |||
| 4740 | editor: Entity<Editor>, | 4815 | editor: Entity<Editor>, |
| 4741 | window: &mut Window, | 4816 | window: &mut Window, |
| 4742 | cx: &mut App, | 4817 | cx: &mut App, |
| 4743 | ) -> Vec<AnyElement> { | 4818 | ) -> (Vec<AnyElement>, Vec<(DisplayRow, Bounds<Pixels>)>) { |
| 4744 | let render_diff_hunk_controls = editor.read(cx).render_diff_hunk_controls.clone(); | 4819 | let render_diff_hunk_controls = editor.read(cx).render_diff_hunk_controls.clone(); |
| 4745 | let point_for_position = position_map.point_for_position(window.mouse_position()); | 4820 | let hovered_diff_hunk_row = editor.read(cx).hovered_diff_hunk_row; |
| 4746 | 4821 | ||
| 4747 | let mut controls = vec![]; | 4822 | let mut controls = vec![]; |
| 4823 | let mut control_bounds = vec![]; | ||
| 4748 | 4824 | ||
| 4749 | let active_positions = [ | 4825 | let active_positions = [ |
| 4750 | Some(point_for_position.previous_valid), | 4826 | hovered_diff_hunk_row.map(|row| DisplayPoint::new(row, 0)), |
| 4751 | newest_cursor_position, | 4827 | newest_cursor_position, |
| 4752 | ]; | 4828 | ]; |
| 4753 | 4829 | ||
| @@ -4792,6 +4868,7 @@ impl EditorElement { · 38 unmodified lines | |||
| 4792 | { | 4868 | { |
| 4793 | continue; | 4869 | continue; |
| 4794 | } | 4870 | } |
| 4871 | |||
| 4795 | if active_positions | 4872 | if active_positions |
| 4796 | .iter() | 4873 | .iter() |
| 4797 | .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row()))) | 4874 | .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row()))) |
| @@ -4815,6 +4892,9 @@ impl EditorElement { · 17 unmodified lines | |||
| 4815 | 4892 | ||
| 4816 | let x = text_hitbox.bounds.right() - right_margin - px(10.) - size.width; | 4893 | let x = text_hitbox.bounds.right() - right_margin - px(10.) - size.width; |
| 4817 | 4894 | ||
| 4895 | let bounds = Bounds::new(gpui::Point::new(x, y), size); | ||
| 4896 | control_bounds.push((display_row_range.start, bounds)); | ||
| 4897 | |||
| 4818 | window.with_absolute_element_offset(gpui::Point::new(x, y), |window| { | 4898 | window.with_absolute_element_offset(gpui::Point::new(x, y), |window| { |
| 4819 | element.prepaint(window, cx) | 4899 | element.prepaint(window, cx) |
| 4820 | }); | 4900 | }); |
| @@ -4823,7 +4903,7 @@ impl EditorElement { · 2 unmodified lines | |||
| 4823 | } | 4903 | } |
| 4824 | } | 4904 | } |
| 4825 | 4905 | ||
| 4826 | controls | 4906 | (controls, control_bounds) |
| 4827 | } | 4907 | } |
| 4828 | 4908 | ||
| 4829 | fn layout_signature_help( | 4909 | fn layout_signature_help( |
| @@ -5562,6 +5642,12 @@ impl EditorElement { · 732 unmodified lines | |||
| 5562 | let editor = self.editor.read(cx); | 5642 | let editor = self.editor.read(cx); |
| 5563 | if editor.mouse_cursor_hidden { | 5643 | if editor.mouse_cursor_hidden { |
| 5564 | window.set_window_cursor_style(CursorStyle::None); | 5644 | window.set_window_cursor_style(CursorStyle::None); |
| 5645 | } else if matches!( | ||
| 5646 | editor.selection_drag_state, | ||
| 5647 | SelectionDragState::Dragging { .. } | ||
| 5648 | ) { | ||
| 5649 | window | ||
| 5650 | .set_cursor_style(CursorStyle::DragCopy, &layout.position_map.text_hitbox); | ||
| 5565 | } else if editor | 5651 | } else if editor |
| 5566 | .hovered_link_state | 5652 | .hovered_link_state |
| 5567 | .as_ref() | 5653 | .as_ref() |
| @@ -6203,9 +6289,9 @@ impl EditorElement { · 635 unmodified lines | |||
| 6203 | } | 6289 | } |
| 6204 | 6290 | ||
| 6205 | fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { | 6291 | fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { |
| 6206 | if let Some(mut inline_blame) = layout.inline_blame.take() { | 6292 | if let Some(mut blame_layout) = layout.inline_blame_layout.take() { |
| 6207 | window.paint_layer(layout.position_map.text_hitbox.bounds, |window| { | 6293 | window.paint_layer(layout.position_map.text_hitbox.bounds, |window| { |
| 6208 | inline_blame.paint(window, cx); | 6294 | blame_layout.element.paint(window, cx); |
| 6209 | }) | 6295 | }) |
| 6210 | } | 6296 | } |
| 6211 | } | 6297 | } |
| @@ -7325,6 +7411,17 @@ impl LineWithInvisibles { · 1113 unmodified lines | |||
| 7325 | paint(window, cx); | 7411 | paint(window, cx); |
| 7326 | }), | 7412 | }), |
| 7327 | 7413 | ||
| 7414 | ShowWhitespaceSetting::Trailing => { | ||
| 7415 | let mut previous_start = self.len; | ||
| 7416 | for ([start, end], paint) in invisible_iter.rev() { | ||
| 7417 | if previous_start != end { | ||
| 7418 | break; | ||
| 7419 | } | ||
| 7420 | previous_start = start; | ||
| 7421 | paint(window, cx); | ||
| 7422 | } | ||
| 7423 | } | ||
| 7424 | |||
| 7328 | // For a whitespace to be on a boundary, any of the following conditions need to be met: | 7425 | // For a whitespace to be on a boundary, any of the following conditions need to be met: |
| 7329 | // - It is a tab | 7426 | // - It is a tab |
| 7330 | // - It is adjacent to an edge (start or end) | 7427 | // - It is adjacent to an edge (start or end) |
| @@ -7537,7 +7634,7 @@ impl Element for EditorElement { · 206 unmodified lines | |||
| 7537 | fn request_layout( | 7634 | fn request_layout( |
| 7538 | &mut self, | 7635 | &mut self, |
| 7539 | _: Option<&GlobalElementId>, | 7636 | _: Option<&GlobalElementId>, |
| 7540 | __inspector_id: Option<&gpui::InspectorElementId>, | 7637 | _inspector_id: Option<&gpui::InspectorElementId>, |
| 7541 | window: &mut Window, | 7638 | window: &mut Window, |
| 7542 | cx: &mut App, | 7639 | cx: &mut App, |
| 7543 | ) -> (gpui::LayoutId, ()) { | 7640 | ) -> (gpui::LayoutId, ()) { |
| @@ -7588,7 +7685,10 @@ impl Element for EditorElement { · 44 unmodified lines | |||
| 7588 | window.request_layout(style, None, cx) | 7685 | window.request_layout(style, None, cx) |
| 7589 | } | 7686 | } |
| 7590 | } | 7687 | } |
| 7591 | EditorMode::AutoHeight { max_lines } => { | 7688 | EditorMode::AutoHeight { |
| 7689 | min_lines, | ||
| 7690 | max_lines, | ||
| 7691 | } => { | ||
| 7592 | let editor_handle = cx.entity().clone(); | 7692 | let editor_handle = cx.entity().clone(); |
| 7593 | let max_line_number_width = | 7693 | let max_line_number_width = |
| 7594 | self.max_line_number_width(&editor.snapshot(window, cx), window, cx); | 7694 | self.max_line_number_width(&editor.snapshot(window, cx), window, cx); |
| @@ -7599,6 +7699,7 @@ impl Element for EditorElement { · 4 unmodified lines | |||
| 7599 | .update(cx, |editor, cx| { | 7699 | .update(cx, |editor, cx| { |
| 7600 | compute_auto_height_layout( | 7700 | compute_auto_height_layout( |
| 7601 | editor, | 7701 | editor, |
| 7702 | min_lines, | ||
| 7602 | max_lines, | 7703 | max_lines, |
| 7603 | max_line_number_width, | 7704 | max_line_number_width, |
| 7604 | known_dimensions, | 7705 | known_dimensions, |
| @@ -7673,8 +7774,7 @@ impl Element for EditorElement { · 68 unmodified lines | |||
| 7673 | let line_height = style.text.line_height_in_pixels(window.rem_size()); | 7774 | let line_height = style.text.line_height_in_pixels(window.rem_size()); |
| 7674 | let em_width = window.text_system().em_width(font_id, font_size).unwrap(); | 7775 | let em_width = window.text_system().em_width(font_id, font_size).unwrap(); |
| 7675 | let em_advance = window.text_system().em_advance(font_id, font_size).unwrap(); | 7776 | let em_advance = window.text_system().em_advance(font_id, font_size).unwrap(); |
| 7676 | 7777 | let glyph_grid_cell = size(em_advance, line_height); | |
| 7677 | let glyph_grid_cell = size(em_width, line_height); | ||
| 7678 | 7778 | ||
| 7679 | let gutter_dimensions = snapshot | 7779 | let gutter_dimensions = snapshot |
| 7680 | .gutter_dimensions( | 7780 | .gutter_dimensions( |
| @@ -8233,7 +8333,7 @@ impl Element for EditorElement { · 552 unmodified lines | |||
| 8233 | MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row); | 8333 | MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row); |
| 8234 | 8334 | ||
| 8235 | let scroll_max = point( | 8335 | let scroll_max = point( |
| 8236 | ((scroll_width - editor_content_width) / em_width).max(0.0), | 8336 | ((scroll_width - editor_content_width) / em_advance).max(0.0), |
| 8237 | max_scroll_top, | 8337 | max_scroll_top, |
| 8238 | ); | 8338 | ); |
| 8239 | 8339 | ||
| @@ -8245,7 +8345,7 @@ impl Element for EditorElement { · 5 unmodified lines | |||
| 8245 | start_row, | 8345 | start_row, |
| 8246 | editor_content_width, | 8346 | editor_content_width, |
| 8247 | scroll_width, | 8347 | scroll_width, |
| 8248 | em_width, | 8348 | em_advance, |
| 8249 | &line_layouts, | 8349 | &line_layouts, |
| 8250 | cx, | 8350 | cx, |
| 8251 | ) | 8351 | ) |
| @@ -8260,10 +8360,9 @@ impl Element for EditorElement { · 8 unmodified lines | |||
| 8260 | }); | 8360 | }); |
| 8261 | 8361 | ||
| 8262 | let scroll_pixel_position = point( | 8362 | let scroll_pixel_position = point( |
| 8263 | scroll_position.x * em_width, | 8363 | scroll_position.x * em_advance, |
| 8264 | scroll_position.y * line_height, | 8364 | scroll_position.y * line_height, |
| 8265 | ); | 8365 | ); |
| 8266 | |||
| 8267 | let indent_guides = self.layout_indent_guides( | 8366 | let indent_guides = self.layout_indent_guides( |
| 8268 | content_origin, | 8367 | content_origin, |
| 8269 | text_hitbox.origin, | 8368 | text_hitbox.origin, |
| @@ -8328,7 +8427,7 @@ impl Element for EditorElement { · 58 unmodified lines | |||
| 8328 | cx, | 8427 | cx, |
| 8329 | ); | 8428 | ); |
| 8330 | 8429 | ||
| 8331 | let mut inline_blame = None; | 8430 | let mut inline_blame_layout = None; |
| 8332 | let mut inline_code_actions = None; | 8431 | let mut inline_code_actions = None; |
| 8333 | if let Some(newest_selection_head) = newest_selection_head { | 8432 | if let Some(newest_selection_head) = newest_selection_head { |
| 8334 | let display_row = newest_selection_head.row(); | 8433 | let display_row = newest_selection_head.row(); |
| @@ -8350,7 +8449,7 @@ impl Element for EditorElement { · 15 unmodified lines | |||
| 8350 | let line_layout = &line_layouts[line_ix]; | 8449 | let line_layout = &line_layouts[line_ix]; |
| 8351 | let crease_trailer_layout = crease_trailers[line_ix].as_ref(); | 8450 | let crease_trailer_layout = crease_trailers[line_ix].as_ref(); |
| 8352 | 8451 | ||
| 8353 | inline_blame = self.layout_inline_blame( | 8452 | if let Some(layout) = self.layout_inline_blame( |
| 8354 | display_row, | 8453 | display_row, |
| 8355 | row_info, | 8454 | row_info, |
| 8356 | line_layout, | 8455 | line_layout, |
| @@ -8362,8 +8461,8 @@ impl Element for EditorElement { · 5 unmodified lines | |||
| 8362 | &text_hitbox, | 8461 | &text_hitbox, |
| 8363 | window, | 8462 | window, |
| 8364 | cx, | 8463 | cx, |
| 8365 | ); | 8464 | ) { |
| 8366 | if inline_blame.is_some() { | 8465 | inline_blame_layout = Some(layout); |
| 8367 | // Blame overrides inline diagnostics | 8466 | // Blame overrides inline diagnostics |
| 8368 | inline_diagnostics.remove(&display_row); | 8467 | inline_diagnostics.remove(&display_row); |
| 8369 | } | 8468 | } |
| @@ -8633,6 +8732,25 @@ impl Element for EditorElement { · 263 unmodified lines | |||
| 8633 | 8732 | ||
| 8634 | let mode = snapshot.mode.clone(); | 8733 | let mode = snapshot.mode.clone(); |
| 8635 | 8734 | ||
| 8735 | let (diff_hunk_controls, diff_hunk_control_bounds) = if is_read_only { | ||
| 8736 | (vec![], vec![]) | ||
| 8737 | } else { | ||
| 8738 | self.layout_diff_hunk_controls( | ||
| 8739 | start_row..end_row, | ||
| 8740 | &row_infos, | ||
| 8741 | &text_hitbox, | ||
| 8742 | newest_selection_head, | ||
| 8743 | line_height, | ||
| 8744 | right_margin, | ||
| 8745 | scroll_pixel_position, | ||
| 8746 | &display_hunks, | ||
| 8747 | &highlighted_rows, | ||
| 8748 | self.editor.clone(), | ||
| 8749 | window, | ||
| 8750 | cx, | ||
| 8751 | ) | ||
| 8752 | }; | ||
| 8753 | |||
| 8636 | let position_map = Rc::new(PositionMap { | 8754 | let position_map = Rc::new(PositionMap { |
| 8637 | size: bounds.size, | 8755 | size: bounds.size, |
| 8638 | visible_row_range, | 8756 | visible_row_range, |
| @@ -8645,32 +8763,17 @@ impl Element for EditorElement { · 6 unmodified lines | |||
| 8645 | snapshot, | 8763 | snapshot, |
| 8646 | gutter_hitbox: gutter_hitbox.clone(), | 8764 | gutter_hitbox: gutter_hitbox.clone(), |
| 8647 | text_hitbox: text_hitbox.clone(), | 8765 | text_hitbox: text_hitbox.clone(), |
| 8766 | inline_blame_bounds: inline_blame_layout | ||
| 8767 | .as_ref() | ||
| 8768 | .map(|layout| (layout.bounds, layout.entry.clone())), | ||
| 8769 | display_hunks: display_hunks.clone(), | ||
| 8770 | diff_hunk_control_bounds: diff_hunk_control_bounds.clone(), | ||
| 8648 | }); | 8771 | }); |
| 8649 | 8772 | ||
| 8650 | self.editor.update(cx, |editor, _| { | 8773 | self.editor.update(cx, |editor, _| { |
| 8651 | editor.last_position_map = Some(position_map.clone()) | 8774 | editor.last_position_map = Some(position_map.clone()) |
| 8652 | }); | 8775 | }); |
| 8653 | 8776 | ||
| 8654 | let diff_hunk_controls = if is_read_only { | ||
| 8655 | vec![] | ||
| 8656 | } else { | ||
| 8657 | self.layout_diff_hunk_controls( | ||
| 8658 | start_row..end_row, | ||
| 8659 | &row_infos, | ||
| 8660 | &text_hitbox, | ||
| 8661 | &position_map, | ||
| 8662 | newest_selection_head, | ||
| 8663 | line_height, | ||
| 8664 | right_margin, | ||
| 8665 | scroll_pixel_position, | ||
| 8666 | &display_hunks, | ||
| 8667 | &highlighted_rows, | ||
| 8668 | self.editor.clone(), | ||
| 8669 | window, | ||
| 8670 | cx, | ||
| 8671 | ) | ||
| 8672 | }; | ||
| 8673 | |||
| 8674 | EditorLayout { | 8777 | EditorLayout { |
| 8675 | mode, | 8778 | mode, |
| 8676 | position_map, | 8779 | position_map, |
| @@ -8692,7 +8795,7 @@ impl Element for EditorElement { · 15 unmodified lines | |||
| 8692 | line_numbers, | 8795 | line_numbers, |
| 8693 | blamed_display_rows, | 8796 | blamed_display_rows, |
| 8694 | inline_diagnostics, | 8797 | inline_diagnostics, |
| 8695 | inline_blame, | 8798 | inline_blame_layout, |
| 8696 | inline_code_actions, | 8799 | inline_code_actions, |
| 8697 | blocks, | 8800 | blocks, |
| 8698 | cursors, | 8801 | cursors, |
| @@ -8718,7 +8821,7 @@ impl Element for EditorElement { · 19 unmodified lines | |||
| 8718 | fn paint( | 8821 | fn paint( |
| 8719 | &mut self, | 8822 | &mut self, |
| 8720 | _: Option<&GlobalElementId>, | 8823 | _: Option<&GlobalElementId>, |
| 8721 | __inspector_id: Option<&gpui::InspectorElementId>, | 8824 | _inspector_id: Option<&gpui::InspectorElementId>, |
| 8722 | bounds: Bounds<gpui::Pixels>, | 8825 | bounds: Bounds<gpui::Pixels>, |
| 8723 | _: &mut Self::RequestLayoutState, | 8826 | _: &mut Self::RequestLayoutState, |
| 8724 | layout: &mut Self::PrepaintState, | 8827 | layout: &mut Self::PrepaintState, |
| @@ -8873,7 +8976,7 @@ pub struct EditorLayout { · 148 unmodified lines | |||
| 8873 | display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>, | 8976 | display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>, |
| 8874 | blamed_display_rows: Option<Vec<AnyElement>>, | 8977 | blamed_display_rows: Option<Vec<AnyElement>>, |
| 8875 | inline_diagnostics: HashMap<DisplayRow, AnyElement>, | 8978 | inline_diagnostics: HashMap<DisplayRow, AnyElement>, |
| 8876 | inline_blame: Option<AnyElement>, | 8979 | inline_blame_layout: Option<InlineBlameLayout>, |
| 8877 | inline_code_actions: Option<AnyElement>, | 8980 | inline_code_actions: Option<AnyElement>, |
| 8878 | blocks: Vec<BlockLayout>, | 8981 | blocks: Vec<BlockLayout>, |
| 8879 | highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>, | 8982 | highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>, |
| @@ -9333,6 +9436,9 @@ pub(crate) struct PositionMap { · 453 unmodified lines | |||
| 9333 | pub snapshot: EditorSnapshot, | 9436 | pub snapshot: EditorSnapshot, |
| 9334 | pub text_hitbox: Hitbox, | 9437 | pub text_hitbox: Hitbox, |
| 9335 | pub gutter_hitbox: Hitbox, | 9438 | pub gutter_hitbox: Hitbox, |
| 9439 | pub inline_blame_bounds: Option<(Bounds<Pixels>, BlameEntry)>, | ||
| 9440 | pub display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>, | ||
| 9441 | pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds<Pixels>)>, | ||
| 9336 | } | 9442 | } |
| 9337 | 9443 | ||
| 9338 | #[derive(Debug, Copy, Clone)] | 9444 | #[derive(Debug, Copy, Clone)] |
| @@ -9388,7 +9494,7 @@ impl PositionMap { · 49 unmodified lines | |||
| 9388 | let scroll_position = self.snapshot.scroll_position(); | 9494 | let scroll_position = self.snapshot.scroll_position(); |
| 9389 | let position = position - text_bounds.origin; | 9495 | let position = position - text_bounds.origin; |
| 9390 | let y = position.y.max(px(0.)).min(self.size.height); | 9496 | let y = position.y.max(px(0.)).min(self.size.height); |
| 9391 | let x = position.x + (scroll_position.x * self.em_width); | 9497 | let x = position.x + (scroll_position.x * self.em_advance); |
| 9392 | let row = ((y / self.line_height) + scroll_position.y) as u32; | 9498 | let row = ((y / self.line_height) + scroll_position.y) as u32; |
| 9393 | 9499 | ||
| 9394 | let (column, x_overshoot_after_line_end) = if let Some(line) = self | 9500 | let (column, x_overshoot_after_line_end) = if let Some(line) = self |
| @@ -9766,6 +9872,7 @@ pub fn register_action<T: Action>( · 371 unmodified lines | |||
| 9766 | 9872 | ||
| 9767 | fn compute_auto_height_layout( | 9873 | fn compute_auto_height_layout( |
| 9768 | editor: &mut Editor, | 9874 | editor: &mut Editor, |
| 9875 | min_lines: usize, | ||
| 9769 | max_lines: usize, | 9876 | max_lines: usize, |
| 9770 | max_line_number_width: Pixels, | 9877 | max_line_number_width: Pixels, |
| 9771 | known_dimensions: Size<Option<Pixels>>, | 9878 | known_dimensions: Size<Option<Pixels>>, |
| @@ -9813,7 +9920,7 @@ fn compute_auto_height_layout( · 41 unmodified lines | |||
| 9813 | 9920 | ||
| 9814 | let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height; | 9921 | let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height; |
| 9815 | let height = scroll_height | 9922 | let height = scroll_height |
| 9816 | .max(line_height) | 9923 | .max(line_height * min_lines as f32) |
| 9817 | .min(line_height * max_lines as f32); | 9924 | .min(line_height * max_lines as f32); |
| 9818 | 9925 | ||
| 9819 | Some(size(width, height)) | 9926 | Some(size(width, height)) |
| @@ -10116,7 +10223,10 @@ mod tests { · 296 unmodified lines | |||
| 10116 | 10223 | ||
| 10117 | for editor_mode_without_invisibles in [ | 10224 | for editor_mode_without_invisibles in [ |
| 10118 | EditorMode::SingleLine { auto_width: false }, | 10225 | EditorMode::SingleLine { auto_width: false }, |
| 10119 | EditorMode::AutoHeight { max_lines: 100 }, | 10226 | EditorMode::AutoHeight { |
| 10227 | min_lines: 1, | ||
| 10228 | max_lines: 100, | ||
| 10229 | }, | ||
| 10120 | ] { | 10230 | ] { |
| 10121 | for show_line_numbers in [true, false] { | 10231 | for show_line_numbers in [true, false] { |
| 10122 | let invisibles = collect_invisibles_from_new_editor( | 10232 | let invisibles = collect_invisibles_from_new_editor( |
| @@ -1,6 +1,7 @@ | |||
| 1 | use crate::{ | 1 | use crate::{ |
| 2 | Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget, | 2 | Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget, |
| 3 | MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, ToPoint as _, | 3 | MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, SelectionEffects, |
| 4 | ToPoint as _, | ||
| 4 | editor_settings::SeedQuerySetting, | 5 | editor_settings::SeedQuerySetting, |
| 5 | persistence::{DB, SerializedEditor}, | 6 | persistence::{DB, SerializedEditor}, |
| 6 | scroll::ScrollAnchor, | 7 | scroll::ScrollAnchor, |
| @@ -40,7 +41,7 @@ use ui::{IconDecorationKind, prelude::*}; · 33 unmodified lines | |||
| 40 | use util::{ResultExt, TryFutureExt, paths::PathExt}; | 41 | use util::{ResultExt, TryFutureExt, paths::PathExt}; |
| 41 | use workspace::{ | 42 | use workspace::{ |
| 42 | CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, | 43 | CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, |
| 43 | item::{FollowableItem, Item, ItemEvent, ProjectItem}, | 44 | item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, |
| 44 | searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, | 45 | searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, |
| 45 | }; | 46 | }; |
| 46 | use workspace::{ | 47 | use workspace::{ |
| @@ -611,12 +612,13 @@ impl Item for Editor { · 564 unmodified lines | |||
| 611 | if newest_selection.head() == offset { | 612 | if newest_selection.head() == offset { |
| 612 | false | 613 | false |
| 613 | } else { | 614 | } else { |
| 614 | let nav_history = self.nav_history.take(); | ||
| 615 | self.set_scroll_anchor(scroll_anchor, window, cx); | 615 | self.set_scroll_anchor(scroll_anchor, window, cx); |
| 616 | self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { | 616 | self.change_selections( |
| 617 | s.select_ranges([offset..offset]) | 617 | SelectionEffects::default().nav_history(false), |
| 618 | }); | 618 | window, |
| 619 | self.nav_history = nav_history; | 619 | cx, |
| 620 | |s| s.select_ranges([offset..offset]), | ||
| 621 | ); | ||
| 620 | true | 622 | true |
| 621 | } | 623 | } |
| 622 | } else { | 624 | } else { |
| @@ -805,7 +807,7 @@ impl Item for Editor { · 182 unmodified lines | |||
| 805 | 807 | ||
| 806 | fn save( | 808 | fn save( |
| 807 | &mut self, | 809 | &mut self, |
| 808 | format: bool, | 810 | options: SaveOptions, |
| 809 | project: Entity<Project>, | 811 | project: Entity<Project>, |
| 810 | window: &mut Window, | 812 | window: &mut Window, |
| 811 | cx: &mut Context<Self>, | 813 | cx: &mut Context<Self>, |
| @@ -816,13 +818,25 @@ impl Item for Editor { · 4 unmodified lines | |||
| 816 | .into_iter() | 818 | .into_iter() |
| 817 | .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone())) | 819 | .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone())) |
| 818 | .collect::<HashSet<_>>(); | 820 | .collect::<HashSet<_>>(); |
| 821 | |||
| 822 | // let mut buffers_to_save = | ||
| 823 | let buffers_to_save = if self.buffer.read(cx).is_singleton() && !options.autosave { | ||
| 824 | buffers.clone() | ||
| 825 | } else { | ||
| 826 | buffers | ||
| 827 | .iter() | ||
| 828 | .filter(|buffer| buffer.read(cx).is_dirty()) | ||
| 829 | .cloned() | ||
| 830 | .collect() | ||
| 831 | }; | ||
| 832 | |||
| 819 | cx.spawn_in(window, async move |this, cx| { | 833 | cx.spawn_in(window, async move |this, cx| { |
| 820 | if format { | 834 | if options.format { |
| 821 | this.update_in(cx, |editor, window, cx| { | 835 | this.update_in(cx, |editor, window, cx| { |
| 822 | editor.perform_format( | 836 | editor.perform_format( |
| 823 | project.clone(), | 837 | project.clone(), |
| 824 | FormatTrigger::Save, | 838 | FormatTrigger::Save, |
| 825 | FormatTarget::Buffers, | 839 | FormatTarget::Buffers(buffers_to_save.clone()), |
| 826 | window, | 840 | window, |
| 827 | cx, | 841 | cx, |
| 828 | ) | 842 | ) |
| @@ -830,33 +844,28 @@ impl Item for Editor { · 1 unmodified line | |||
| 830 | .await?; | 844 | .await?; |
| 831 | } | 845 | } |
| 832 | 846 | ||
| 833 | if buffers.len() == 1 { | 847 | if !buffers_to_save.is_empty() { |
| 834 | // Apply full save routine for singleton buffers, to allow to `touch` the file via the editor. | ||
| 835 | project | 848 | project |
| 836 | .update(cx, |project, cx| project.save_buffers(buffers, cx))? | 849 | .update(cx, |project, cx| { |
| 850 | project.save_buffers(buffers_to_save.clone(), cx) | ||
| 851 | })? | ||
| 837 | .await?; | 852 | .await?; |
| 838 | } else { | 853 | } |
| 839 | // For multi-buffers, only format and save the buffers with changes. | ||
| 840 | // For clean buffers, we simulate saving by calling `Buffer::did_save`, | ||
| 841 | // so that language servers or other downstream listeners of save events get notified. | ||
| 842 | let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| { | ||
| 843 | buffer | ||
| 844 | .read_with(cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict()) | ||
| 845 | .unwrap_or(false) | ||
| 846 | }); | ||
| 847 | 854 | ||
| 848 | project | 855 | // Notify about clean buffers for language server events |
| 849 | .update(cx, |project, cx| project.save_buffers(dirty_buffers, cx))? | 856 | let buffers_that_were_not_saved: Vec<_> = buffers |
| 850 | .await?; | 857 | .into_iter() |
| 851 | for buffer in clean_buffers { | 858 | .filter(|b| !buffers_to_save.contains(b)) |
| 852 | buffer | 859 | .collect(); |
| 853 | .update(cx, |buffer, cx| { | 860 | |
| 854 | let version = buffer.saved_version().clone(); | 861 | for buffer in buffers_that_were_not_saved { |
| 855 | let mtime = buffer.saved_mtime(); | 862 | buffer |
| 856 | buffer.did_save(version, mtime, cx); | 863 | .update(cx, |buffer, cx| { |
| 857 | }) | 864 | let version = buffer.saved_version().clone(); |
| 858 | .ok(); | 865 | let mtime = buffer.saved_mtime(); |
| 859 | } | 866 | buffer.did_save(version, mtime, cx); |
| 867 | }) | ||
| 868 | .ok(); | ||
| 860 | } | 869 | } |
| 861 | 870 | ||
| 862 | Ok(()) | 871 | Ok(()) |
| @@ -8,7 +8,7 @@ use util::ResultExt as _; · 7 unmodified lines | |||
| 8 | use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node}; | 8 | use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node}; |
| 9 | use text::{Anchor, OffsetRangeExt as _}; | 9 | use text::{Anchor, OffsetRangeExt as _}; |
| 10 | 10 | ||
| 11 | use crate::Editor; | 11 | use crate::{Editor, SelectionEffects}; |
| 12 | 12 | ||
| 13 | pub struct JsxTagCompletionState { | 13 | pub struct JsxTagCompletionState { |
| 14 | edit_index: usize, | 14 | edit_index: usize, |
| @@ -600,9 +600,14 @@ pub(crate) fn handle_from( · 585 unmodified lines | |||
| 600 | }) | 600 | }) |
| 601 | .collect::<Vec<_>>(); | 601 | .collect::<Vec<_>>(); |
| 602 | this.update_in(cx, |this, window, cx| { | 602 | this.update_in(cx, |this, window, cx| { |
| 603 | this.change_selections_without_updating_completions(None, window, cx, |s| { | 603 | this.change_selections( |
| 604 | s.select(base_selections); | 604 | SelectionEffects::no_scroll().completions(false), |
| 605 | }); | 605 | window, |
| 606 | cx, | ||
| 607 | |s| { | ||
| 608 | s.select(base_selections); | ||
| 609 | }, | ||
| 610 | ); | ||
| 606 | }) | 611 | }) |
| 607 | .ok()?; | 612 | .ok()?; |
| 608 | } | 613 | } |
| @@ -1,8 +1,8 @@ | |||
| 1 | use crate::{ | 1 | use crate::{ |
| 2 | Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint, | 2 | Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor, |
| 3 | DisplaySnapshot, Editor, FindAllReferences, GoToDeclaration, GoToDefinition, | 3 | EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation, |
| 4 | GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, | 4 | GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt, |
| 5 | SelectionExt, ToDisplayPoint, ToggleCodeActions, | 5 | ToDisplayPoint, ToggleCodeActions, |
| 6 | actions::{Format, FormatSelections}, | 6 | actions::{Format, FormatSelections}, |
| 7 | selections_collection::SelectionsCollection, | 7 | selections_collection::SelectionsCollection, |
| 8 | }; | 8 | }; |
| @@ -199,17 +199,14 @@ pub fn deploy_context_menu( · 190 unmodified lines | |||
| 199 | .is_some() | 199 | .is_some() |
| 200 | }); | 200 | }); |
| 201 | 201 | ||
| 202 | let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx) | 202 | let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx); |
| 203 | .map_or(false, |filter| { | ||
| 204 | !filter.is_hidden(&DebuggerEvaluateSelectedText) | ||
| 205 | }); | ||
| 206 | 203 | ||
| 207 | ui::ContextMenu::build(window, cx, |menu, _window, _cx| { | 204 | ui::ContextMenu::build(window, cx, |menu, _window, _cx| { |
| 208 | let builder = menu | 205 | let builder = menu |
| 209 | .on_blur_subscription(Subscription::new(|| {})) | 206 | .on_blur_subscription(Subscription::new(|| {})) |
| 210 | .when(evaluate_selection && has_selections, |builder| { | 207 | .when(evaluate_selection && has_selections, |builder| { |
| 211 | builder | 208 | builder |
| 212 | .action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText)) | 209 | .action("Evaluate Selection", Box::new(EvaluateSelectedText)) |
| 213 | .separator() | 210 | .separator() |
| 214 | }) | 211 | }) |
| 215 | .action("Go to Definition", Box::new(GoToDefinition)) | 212 | .action("Go to Definition", Box::new(GoToDefinition)) |
| @@ -58,8 +58,8 @@ pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Displa · 57 unmodified lines | |||
| 58 | map.clip_point(point, Bias::Left) | 58 | map.clip_point(point, Bias::Left) |
| 59 | } | 59 | } |
| 60 | 60 | ||
| 61 | /// Returns a column to the right of the current point, doing nothing | 61 | /// Returns a column to the right of the current point, wrapping |
| 62 | // if that point is at the end of the line. | 62 | /// to the next line if that point is at the end of line. |
| 63 | pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { | 63 | pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { |
| 64 | if point.column() < map.line_len(point.row()) { | 64 | if point.column() < map.line_len(point.row()) { |
| 65 | *point.column_mut() += 1; | 65 | *point.column_mut() += 1; |
| @@ -266,10 +266,11 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa · 200 unmodified lines | |||
| 266 | 266 | ||
| 267 | let mut is_first_iteration = true; | 267 | let mut is_first_iteration = true; |
| 268 | find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| { | 268 | find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| { |
| 269 | // Make alt-left skip punctuation on Mac OS to respect Mac VSCode behaviour. For example: hello.| goes to |hello. | 269 | // Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello. |
| 270 | if is_first_iteration | 270 | if is_first_iteration |
| 271 | && classifier.is_punctuation(right) | 271 | && classifier.is_punctuation(right) |
| 272 | && !classifier.is_punctuation(left) | 272 | && !classifier.is_punctuation(left) |
| 273 | && left != '\n' | ||
| 273 | { | 274 | { |
| 274 | is_first_iteration = false; | 275 | is_first_iteration = false; |
| 275 | return false; | 276 | return false; |
| @@ -318,10 +319,11 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint · 42 unmodified lines | |||
| 318 | let classifier = map.buffer_snapshot.char_classifier_at(raw_point); | 319 | let classifier = map.buffer_snapshot.char_classifier_at(raw_point); |
| 319 | let mut is_first_iteration = true; | 320 | let mut is_first_iteration = true; |
| 320 | find_boundary(map, point, FindRange::MultiLine, |left, right| { | 321 | find_boundary(map, point, FindRange::MultiLine, |left, right| { |
| 321 | // Make alt-right skip punctuation on Mac OS to respect the Mac behaviour. For example: |.hello goes to .hello| | 322 | // Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello| |
| 322 | if is_first_iteration | 323 | if is_first_iteration |
| 323 | && classifier.is_punctuation(left) | 324 | && classifier.is_punctuation(left) |
| 324 | && !classifier.is_punctuation(right) | 325 | && !classifier.is_punctuation(right) |
| 326 | && right != '\n' | ||
| 325 | { | 327 | { |
| 326 | is_first_iteration = false; | 328 | is_first_iteration = false; |
| 327 | return false; | 329 | return false; |
| @@ -12,7 +12,7 @@ use text::ToOffset; · 11 unmodified lines | |||
| 12 | use ui::{ButtonLike, KeyBinding, prelude::*}; | 12 | use ui::{ButtonLike, KeyBinding, prelude::*}; |
| 13 | use workspace::{ | 13 | use workspace::{ |
| 14 | Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, | 14 | Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, |
| 15 | searchable::SearchableItemHandle, | 15 | item::SaveOptions, searchable::SearchableItemHandle, |
| 16 | }; | 16 | }; |
| 17 | 17 | ||
| 18 | pub struct ProposedChangesEditor { | 18 | pub struct ProposedChangesEditor { |
| @@ -351,13 +351,13 @@ impl Item for ProposedChangesEditor { · 332 unmodified lines | |||
| 351 | 351 | ||
| 352 | fn save( | 352 | fn save( |
| 353 | &mut self, | 353 | &mut self, |
| 354 | format: bool, | 354 | options: SaveOptions, |
| 355 | project: Entity<Project>, | 355 | project: Entity<Project>, |
| 356 | window: &mut Window, | 356 | window: &mut Window, |
| 357 | cx: &mut Context<Self>, | 357 | cx: &mut Context<Self>, |
| 358 | ) -> Task<anyhow::Result<()>> { | 358 | ) -> Task<anyhow::Result<()>> { |
| 359 | self.editor.update(cx, |editor, cx| { | 359 | self.editor.update(cx, |editor, cx| { |
| 360 | Item::save(editor, format, project, window, cx) | 360 | Item::save(editor, options, project, window, cx) |
| 361 | }) | 361 | }) |
| 362 | } | 362 | } |
| 363 | } | 363 | } |
| @@ -669,12 +669,23 @@ impl Editor { · 668 unmodified lines | |||
| 669 | return; | 669 | return; |
| 670 | } | 670 | } |
| 671 | 671 | ||
| 672 | let cur_position = self.scroll_position(cx); | 672 | let mut current_position = self.scroll_position(cx); |
| 673 | let Some(visible_line_count) = self.visible_line_count() else { | 673 | let Some(visible_line_count) = self.visible_line_count() else { |
| 674 | return; | 674 | return; |
| 675 | }; | 675 | }; |
| 676 | let new_pos = cur_position + point(0., amount.lines(visible_line_count)); | 676 | |
| 677 | self.set_scroll_position(new_pos, window, cx); | 677 | // If the scroll position is currently at the left edge of the document |
| 678 | // (x == 0.0) and the intent is to scroll right, the gutter's margin | ||
| 679 | // should first be added to the current position, otherwise the cursor | ||
| 680 | // will end at the column position minus the margin, which looks off. | ||
| 681 | if current_position.x == 0.0 && amount.columns() > 0. { | ||
| 682 | if let Some(last_position_map) = &self.last_position_map { | ||
| 683 | current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance; | ||
| 684 | } | ||
| 685 | } | ||
| 686 | let new_position = | ||
| 687 | current_position + point(amount.columns(), amount.lines(visible_line_count)); | ||
| 688 | self.set_scroll_position(new_position, window, cx); | ||
| 678 | } | 689 | } |
| 679 | 690 | ||
| 680 | /// Returns an ordering. The newest selection is: | 691 | /// Returns an ordering. The newest selection is: |
| @@ -1,12 +1,13 @@ | |||
| 1 | use crate::{ | 1 | use crate::{ |
| 2 | DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, display_map::ToDisplayPoint, | 2 | DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects, |
| 3 | display_map::ToDisplayPoint, | ||
| 3 | }; | 4 | }; |
| 4 | use gpui::{Bounds, Context, Pixels, Window, px}; | 5 | use gpui::{Bounds, Context, Pixels, Window, px}; |
| 5 | use language::Point; | 6 | use language::Point; |
| 6 | use multi_buffer::Anchor; | 7 | use multi_buffer::Anchor; |
| 7 | use std::{cmp, f32}; | 8 | use std::{cmp, f32}; |
| 8 | 9 | ||
| 9 | #[derive(PartialEq, Eq, Clone, Copy)] | 10 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] |
| 10 | pub enum Autoscroll { | 11 | pub enum Autoscroll { |
| 11 | Next, | 12 | Next, |
| 12 | Strategy(AutoscrollStrategy, Option<Anchor>), | 13 | Strategy(AutoscrollStrategy, Option<Anchor>), |
| @@ -66,7 +67,16 @@ impl Autoscroll { · 53 unmodified lines | |||
| 66 | } | 67 | } |
| 67 | } | 68 | } |
| 68 | 69 | ||
| 69 | #[derive(PartialEq, Eq, Default, Clone, Copy)] | 70 | impl Into<SelectionEffects> for Option<Autoscroll> { |
| 71 | fn into(self) -> SelectionEffects { | ||
| 72 | match self { | ||
| 73 | Some(autoscroll) => SelectionEffects::scroll(autoscroll), | ||
| 74 | None => SelectionEffects::no_scroll(), | ||
| 75 | } | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | #[derive(Debug, PartialEq, Eq, Default, Clone, Copy)] | ||
| 70 | pub enum AutoscrollStrategy { | 80 | pub enum AutoscrollStrategy { |
| 71 | Fit, | 81 | Fit, |
| 72 | Newest, | 82 | Newest, |
| @@ -5,6 +5,8 @@ use ui::{Pixels, px}; · 4 unmodified lines | |||
| 5 | pub enum ScrollDirection { | 5 | pub enum ScrollDirection { |
| 6 | Upwards, | 6 | Upwards, |
| 7 | Downwards, | 7 | Downwards, |
| 8 | Rightwards, | ||
| 9 | Leftwards, | ||
| 8 | } | 10 | } |
| 9 | 11 | ||
| 10 | impl ScrollDirection { | 12 | impl ScrollDirection { |
| @@ -19,6 +21,8 @@ pub enum ScrollAmount { · 8 unmodified lines | |||
| 19 | Line(f32), | 21 | Line(f32), |
| 20 | // Scroll N pages (positive is towards the end of the document) | 22 | // Scroll N pages (positive is towards the end of the document) |
| 21 | Page(f32), | 23 | Page(f32), |
| 24 | // Scroll N columns (positive is towards the right of the document) | ||
| 25 | Column(f32), | ||
| 22 | } | 26 | } |
| 23 | 27 | ||
| 24 | impl ScrollAmount { | 28 | impl ScrollAmount { |
| @@ -32,6 +36,15 @@ impl ScrollAmount { · 7 unmodified lines | |||
| 32 | } | 36 | } |
| 33 | (visible_line_count * count).trunc() | 37 | (visible_line_count * count).trunc() |
| 34 | } | 38 | } |
| 39 | Self::Column(_count) => 0.0, | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | pub fn columns(&self) -> f32 { | ||
| 44 | match self { | ||
| 45 | Self::Line(_count) => 0.0, | ||
| 46 | Self::Page(_count) => 0.0, | ||
| 47 | Self::Column(count) => *count, | ||
| 35 | } | 48 | } |
| 36 | } | 49 | } |
| 37 | 50 | ||
| @@ -39,6 +52,12 @@ impl ScrollAmount { · 1 unmodified line | |||
| 39 | match self { | 52 | match self { |
| 40 | ScrollAmount::Line(x) => px(line_height.0 * x), | 53 | ScrollAmount::Line(x) => px(line_height.0 * x), |
| 41 | ScrollAmount::Page(x) => px(height.0 * x), | 54 | ScrollAmount::Page(x) => px(height.0 * x), |
| 55 | // This function seems to only be leveraged by the popover that is | ||
| 56 | // displayed by the editor when, for example, viewing a function's | ||
| 57 | // documentation. Right now that only supports vertical scrolling, | ||
| 58 | // so I'm leaving this at 0.0 for now to try and make it clear that | ||
| 59 | // this should not have an impact on that? | ||
| 60 | ScrollAmount::Column(_) => px(0.0), | ||
| 42 | } | 61 | } |
| 43 | } | 62 | } |
| 44 | 63 | ||
| @@ -53,6 +72,8 @@ impl ScrollAmount { · 8 unmodified lines | |||
| 53 | match self { | 72 | match self { |
| 54 | Self::Line(amount) if amount.is_sign_positive() => ScrollDirection::Downwards, | 73 | Self::Line(amount) if amount.is_sign_positive() => ScrollDirection::Downwards, |
| 55 | Self::Page(amount) if amount.is_sign_positive() => ScrollDirection::Downwards, | 74 | Self::Page(amount) if amount.is_sign_positive() => ScrollDirection::Downwards, |
| 75 | Self::Column(amount) if amount.is_sign_positive() => ScrollDirection::Rightwards, | ||
| 76 | Self::Column(amount) if amount.is_sign_negative() => ScrollDirection::Leftwards, | ||
| 56 | _ => ScrollDirection::Upwards, | 77 | _ => ScrollDirection::Upwards, |
| 57 | } | 78 | } |
| 58 | } | 79 | } |
| @@ -411,7 +411,7 @@ impl<'a> MutableSelectionsCollection<'a> { · 410 unmodified lines | |||
| 411 | self.collection.display_map(self.cx) | 411 | self.collection.display_map(self.cx) |
| 412 | } | 412 | } |
| 413 | 413 | ||
| 414 | pub fn buffer(&self) -> Ref<MultiBufferSnapshot> { | 414 | pub fn buffer(&self) -> Ref<'_, MultiBufferSnapshot> { |
| 415 | self.collection.buffer(self.cx) | 415 | self.collection.buffer(self.cx) |
| 416 | } | 416 | } |
| 417 | 417 | ||
| @@ -659,6 +659,7 @@ impl<'a> MutableSelectionsCollection<'a> { · 241 unmodified lines | |||
| 659 | .collect(); | 659 | .collect(); |
| 660 | self.select(selections); | 660 | self.select(selections); |
| 661 | } | 661 | } |
| 662 | |||
| 662 | pub fn reverse_selections(&mut self) { | 663 | pub fn reverse_selections(&mut self) { |
| 663 | let map = &self.display_map(); | 664 | let map = &self.display_map(); |
| 664 | let mut new_selections: Vec<Selection<Point>> = Vec::new(); | 665 | let mut new_selections: Vec<Selection<Point>> = Vec::new(); |
| @@ -169,6 +169,12 @@ impl EditorLspTestContext { · 168 unmodified lines | |||
| 169 | .expect("Opened test file wasn't an editor") | 169 | .expect("Opened test file wasn't an editor") |
| 170 | }); | 170 | }); |
| 171 | editor.update_in(&mut cx, |editor, window, cx| { | 171 | editor.update_in(&mut cx, |editor, window, cx| { |
| 172 | let nav_history = workspace | ||
| 173 | .read(cx) | ||
| 174 | .active_pane() | ||
| 175 | .read(cx) | ||
| 176 | .nav_history_for_item(&cx.entity()); | ||
| 177 | editor.set_nav_history(Some(nav_history)); | ||
| 172 | window.focus(&editor.focus_handle(cx)) | 178 | window.focus(&editor.focus_handle(cx)) |
| 173 | }); | 179 | }); |
| 174 | 180 | ||
| @@ -36,6 +36,7 @@ impl ExtensionEvents { · 35 unmodified lines | |||
| 36 | #[derive(Clone)] | 36 | #[derive(Clone)] |
| 37 | pub enum Event { | 37 | pub enum Event { |
| 38 | ExtensionInstalled(Arc<ExtensionManifest>), | 38 | ExtensionInstalled(Arc<ExtensionManifest>), |
| 39 | ExtensionUninstalled(Arc<ExtensionManifest>), | ||
| 39 | ExtensionsInstalledChanged, | 40 | ExtensionsInstalledChanged, |
| 40 | ConfigureExtensionRequested(Arc<ExtensionManifest>), | 41 | ConfigureExtensionRequested(Arc<ExtensionManifest>), |
| 41 | } | 42 | } |
| @@ -50,7 +50,7 @@ interface dap { · 49 unmodified lines | |||
| 50 | } | 50 | } |
| 51 | 51 | ||
| 52 | record debug-adapter-binary { | 52 | record debug-adapter-binary { |
| 53 | command: string, | 53 | command: option<string>, |
| 54 | arguments: list<string>, | 54 | arguments: list<string>, |
| 55 | envs: env-vars, | 55 | envs: env-vars, |
| 56 | cwd: option<string>, | 56 | cwd: option<string>, |
| @@ -132,6 +132,7 @@ pub enum Event { · 131 unmodified lines | |||
| 132 | ExtensionsUpdated, | 132 | ExtensionsUpdated, |
| 133 | StartedReloading, | 133 | StartedReloading, |
| 134 | ExtensionInstalled(Arc<str>), | 134 | ExtensionInstalled(Arc<str>), |
| 135 | ExtensionUninstalled(Arc<str>), | ||
| 135 | ExtensionFailedToLoad(Arc<str>), | 136 | ExtensionFailedToLoad(Arc<str>), |
| 136 | } | 137 | } |
| 137 | 138 | ||
| @@ -842,6 +843,8 @@ impl ExtensionStore { · 704 unmodified lines | |||
| 842 | let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref()); | 843 | let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref()); |
| 843 | let fs = self.fs.clone(); | 844 | let fs = self.fs.clone(); |
| 844 | 845 | ||
| 846 | let extension_manifest = self.extension_manifest_for_id(&extension_id).cloned(); | ||
| 847 | |||
| 845 | match self.outstanding_operations.entry(extension_id.clone()) { | 848 | match self.outstanding_operations.entry(extension_id.clone()) { |
| 846 | btree_map::Entry::Occupied(_) => return, | 849 | btree_map::Entry::Occupied(_) => return, |
| 847 | btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove), | 850 | btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove), |
| @@ -878,6 +881,17 @@ impl ExtensionStore { · 30 unmodified lines | |||
| 878 | ) | 881 | ) |
| 879 | .await?; | 882 | .await?; |
| 880 | 883 | ||
| 884 | this.update(cx, |_, cx| { | ||
| 885 | cx.emit(Event::ExtensionUninstalled(extension_id.clone())); | ||
| 886 | if let Some(events) = ExtensionEvents::try_global(cx) { | ||
| 887 | if let Some(manifest) = extension_manifest { | ||
| 888 | events.update(cx, |this, cx| { | ||
| 889 | this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx) | ||
| 890 | }); | ||
| 891 | } | ||
| 892 | } | ||
| 893 | })?; | ||
| 894 | |||
| 881 | anyhow::Ok(()) | 895 | anyhow::Ok(()) |
| 882 | }) | 896 | }) |
| 883 | .detach_and_log_err(cx) | 897 | .detach_and_log_err(cx) |
| @@ -1682,12 +1696,15 @@ impl ExtensionStore { · 798 unmodified lines | |||
| 1682 | 1696 | ||
| 1683 | pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) { | 1697 | pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) { |
| 1684 | let connection_options = client.read(cx).connection_options(); | 1698 | let connection_options = client.read(cx).connection_options(); |
| 1685 | if self.ssh_clients.contains_key(&connection_options.ssh_url()) { | 1699 | let ssh_url = connection_options.ssh_url(); |
| 1686 | return; | 1700 | |
| 1701 | if let Some(existing_client) = self.ssh_clients.get(&ssh_url) { | ||
| 1702 | if existing_client.upgrade().is_some() { | ||
| 1703 | return; | ||
| 1704 | } | ||
| 1687 | } | 1705 | } |
| 1688 | 1706 | ||
| 1689 | self.ssh_clients | 1707 | self.ssh_clients.insert(ssh_url, client.downgrade()); |
| 1690 | .insert(connection_options.ssh_url(), client.downgrade()); | ||
| 1691 | self.ssh_registered_tx.unbounded_send(()).ok(); | 1708 | self.ssh_registered_tx.unbounded_send(()).ok(); |
| 1692 | } | 1709 | } |
| 1693 | } | 1710 | } |
| @@ -8,9 +8,9 @@ use collections::BTreeMap; · 7 unmodified lines | |||
| 8 | use extension::ExtensionHostProxy; | 8 | use extension::ExtensionHostProxy; |
| 9 | use fs::{FakeFs, Fs, RealFs}; | 9 | use fs::{FakeFs, Fs, RealFs}; |
| 10 | use futures::{AsyncReadExt, StreamExt, io::BufReader}; | 10 | use futures::{AsyncReadExt, StreamExt, io::BufReader}; |
| 11 | use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext}; | 11 | use gpui::{AppContext as _, SemanticVersion, TestAppContext}; |
| 12 | use http_client::{FakeHttpClient, Response}; | 12 | use http_client::{FakeHttpClient, Response}; |
| 13 | use language::{BinaryStatus, LanguageMatcher, LanguageRegistry}; | 13 | use language::{BinaryStatus, LanguageMatcher, LanguageRegistry, LanguageServerStatusUpdate}; |
| 14 | use lsp::LanguageServerName; | 14 | use lsp::LanguageServerName; |
| 15 | use node_runtime::NodeRuntime; | 15 | use node_runtime::NodeRuntime; |
| 16 | use parking_lot::Mutex; | 16 | use parking_lot::Mutex; |
| @@ -717,9 +717,18 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { · 700 unmodified lines | |||
| 717 | status_updates.next().await.unwrap(), | 717 | status_updates.next().await.unwrap(), |
| 718 | ], | 718 | ], |
| 719 | [ | 719 | [ |
| 720 | (SharedString::new("gleam"), BinaryStatus::CheckingForUpdate), | 720 | ( |
| 721 | (SharedString::new("gleam"), BinaryStatus::Downloading), | 721 | LanguageServerName::new_static("gleam"), |
| 722 | (SharedString::new("gleam"), BinaryStatus::None) | 722 | LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) |
| 723 | ), | ||
| 724 | ( | ||
| 725 | LanguageServerName::new_static("gleam"), | ||
| 726 | LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) | ||
| 727 | ), | ||
| 728 | ( | ||
| 729 | LanguageServerName::new_static("gleam"), | ||
| 730 | LanguageServerStatusUpdate::Binary(BinaryStatus::None) | ||
| 731 | ) | ||
| 723 | ] | 732 | ] |
| 724 | ); | 733 | ); |
| 725 | 734 | ||
| @@ -377,6 +377,7 @@ impl extension::Extension for WasmExtension { · 376 unmodified lines | |||
| 377 | }) | 377 | }) |
| 378 | .await | 378 | .await |
| 379 | } | 379 | } |
| 380 | |||
| 380 | async fn get_dap_binary( | 381 | async fn get_dap_binary( |
| 381 | &self, | 382 | &self, |
| 382 | dap_name: Arc<str>, | 383 | dap_name: Arc<str>, |
| @@ -724,7 +725,7 @@ impl IncrementalCompilationCache { · 341 unmodified lines | |||
| 724 | } | 725 | } |
| 725 | 726 | ||
| 726 | impl CacheStore for IncrementalCompilationCache { | 727 | impl CacheStore for IncrementalCompilationCache { |
| 727 | fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> { | 728 | fn get(&self, key: &[u8]) -> Option<Cow<'_, [u8]>> { |
| 728 | self.cache.get(key).map(|v| v.into()) | 729 | self.cache.get(key).map(|v| v.into()) |
| 729 | } | 730 | } |
| 730 | 731 | ||
| @@ -554,13 +554,15 @@ impl ExtensionsPage { · 553 unmodified lines | |||
| 554 | ) | 554 | ) |
| 555 | .child( | 555 | .child( |
| 556 | h_flex() | 556 | h_flex() |
| 557 | .gap_2() | 557 | .gap_1() |
| 558 | .justify_between() | 558 | .justify_between() |
| 559 | .child( | 559 | .child( |
| 560 | Button::new( | 560 | Button::new( |
| 561 | SharedString::from(format!("rebuild-{}", extension.id)), | 561 | SharedString::from(format!("rebuild-{}", extension.id)), |
| 562 | "Rebuild", | 562 | "Rebuild", |
| 563 | ) | 563 | ) |
| 564 | .color(Color::Accent) | ||
| 565 | .disabled(matches!(status, ExtensionStatus::Upgrading)) | ||
| 564 | .on_click({ | 566 | .on_click({ |
| 565 | let extension_id = extension.id.clone(); | 567 | let extension_id = extension.id.clone(); |
| 566 | move |_, _, cx| { | 568 | move |_, _, cx| { |
| @@ -568,12 +570,12 @@ impl ExtensionsPage { · 1 unmodified line | |||
| 568 | store.rebuild_dev_extension(extension_id.clone(), cx) | 570 | store.rebuild_dev_extension(extension_id.clone(), cx) |
| 569 | }); | 571 | }); |
| 570 | } | 572 | } |
| 571 | }) | 573 | }), |
| 572 | .color(Color::Accent) | ||
| 573 | .disabled(matches!(status, ExtensionStatus::Upgrading)), | ||
| 574 | ) | 574 | ) |
| 575 | .child( | 575 | .child( |
| 576 | Button::new(SharedString::from(extension.id.clone()), "Uninstall") | 576 | Button::new(SharedString::from(extension.id.clone()), "Uninstall") |
| 577 | .color(Color::Accent) | ||
| 578 | .disabled(matches!(status, ExtensionStatus::Removing)) | ||
| 577 | .on_click({ | 579 | .on_click({ |
| 578 | let extension_id = extension.id.clone(); | 580 | let extension_id = extension.id.clone(); |
| 579 | move |_, _, cx| { | 581 | move |_, _, cx| { |
| @@ -581,9 +583,7 @@ impl ExtensionsPage { · 1 unmodified line | |||
| 581 | store.uninstall_extension(extension_id.clone(), cx) | 583 | store.uninstall_extension(extension_id.clone(), cx) |
| 582 | }); | 584 | }); |
| 583 | } | 585 | } |
| 584 | }) | 586 | }), |
| 585 | .color(Color::Accent) | ||
| 586 | .disabled(matches!(status, ExtensionStatus::Removing)), | ||
| 587 | ) | 587 | ) |
| 588 | .when(can_configure, |this| { | 588 | .when(can_configure, |this| { |
| 589 | this.child( | 589 | this.child( |
| @@ -591,8 +591,8 @@ impl ExtensionsPage { · 1 unmodified line | |||
| 591 | SharedString::from(format!("configure-{}", extension.id)), | 591 | SharedString::from(format!("configure-{}", extension.id)), |
| 592 | "Configure", | 592 | "Configure", |
| 593 | ) | 593 | ) |
| 594 | 594 | .color(Color::Accent) | |
| 595 | 595 | .disabled(matches!(status, ExtensionStatus::Installing)) | |
| 596 | .on_click({ | 596 | .on_click({ |
| 597 | let manifest = Arc::new(extension.clone()); | 597 | let manifest = Arc::new(extension.clone()); |
| 598 | move |_, _, cx| { | 598 | move |_, _, cx| { |
| @@ -609,9 +609,7 @@ impl ExtensionsPage { · 10 unmodified lines | |||
| 609 | }); | 609 | }); |
| 610 | } | 610 | } |
| 611 | } | 611 | } |
| 612 | }) | 612 | }), |
| 613 | .color(Color::Accent) | ||
| 614 | .disabled(matches!(status, ExtensionStatus::Installing)), | ||
| 615 | ) | 613 | ) |
| 616 | }), | 614 | }), |
| 617 | ), | 615 | ), |
| @@ -1479,18 +1477,12 @@ impl Render for ExtensionsPage { · 861 unmodified lines | |||
| 1479 | return this.py_4().child(self.render_empty_state(cx)); | 1477 | return this.py_4().child(self.render_empty_state(cx)); |
| 1480 | } | 1478 | } |
| 1481 | 1479 | ||
| 1482 | let extensions_page = cx.entity().clone(); | ||
| 1483 | let scroll_handle = self.list.clone(); | 1480 | let scroll_handle = self.list.clone(); |
| 1484 | this.child( | 1481 | this.child( |
| 1485 | uniform_list( | 1482 | uniform_list("entries", count, cx.processor(Self::render_extensions)) |
| 1486 | extensions_page, | 1483 | .flex_grow() |
| 1487 | "entries", | 1484 | .pb_4() |
| 1488 | count, | 1485 | .track_scroll(scroll_handle), |
| 1489 | Self::render_extensions, | ||
| 1490 | ) | ||
| 1491 | .flex_grow() | ||
| 1492 | .pb_4() | ||
| 1493 | .track_scroll(scroll_handle), | ||
| 1494 | ) | 1486 | ) |
| 1495 | .child( | 1487 | .child( |
| 1496 | div() | 1488 | div() |
| @@ -209,7 +209,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { · 208 unmodified lines | |||
| 209 | "bandana", | 209 | "bandana", |
| 210 | "./bandana", | 210 | "./bandana", |
| 211 | ".\\bandana", | 211 | ".\\bandana", |
| 212 | util::separator!("a/bandana"), | 212 | util::path!("a/bandana"), |
| 213 | "b/bandana", | 213 | "b/bandana", |
| 214 | "b\\bandana", | 214 | "b\\bandana", |
| 215 | " bandana", | 215 | " bandana", |
| @@ -230,7 +230,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { · 14 unmodified lines | |||
| 230 | assert_eq!( | 230 | assert_eq!( |
| 231 | picker.delegate.matches.len(), | 231 | picker.delegate.matches.len(), |
| 232 | // existence of CreateNew option depends on whether path already exists | 232 | // existence of CreateNew option depends on whether path already exists |
| 233 | if bandana_query == util::separator!("a/bandana") { | 233 | if bandana_query == util::path!("a/bandana") { |
| 234 | 1 | 234 | 1 |
| 235 | } else { | 235 | } else { |
| 236 | 2 | 236 | 2 |
| @@ -897,6 +897,7 @@ struct FakeFsState { · 896 unmodified lines | |||
| 897 | buffered_events: Vec<PathEvent>, | 897 | buffered_events: Vec<PathEvent>, |
| 898 | metadata_call_count: usize, | 898 | metadata_call_count: usize, |
| 899 | read_dir_call_count: usize, | 899 | read_dir_call_count: usize, |
| 900 | path_write_counts: std::collections::HashMap<PathBuf, usize>, | ||
| 900 | moves: std::collections::HashMap<u64, PathBuf>, | 901 | moves: std::collections::HashMap<u64, PathBuf>, |
| 901 | home_dir: Option<PathBuf>, | 902 | home_dir: Option<PathBuf>, |
| 902 | } | 903 | } |
| @@ -1083,6 +1084,7 @@ impl FakeFs { · 180 unmodified lines | |||
| 1083 | events_paused: false, | 1084 | events_paused: false, |
| 1084 | read_dir_call_count: 0, | 1085 | read_dir_call_count: 0, |
| 1085 | metadata_call_count: 0, | 1086 | metadata_call_count: 0, |
| 1087 | path_write_counts: Default::default(), | ||
| 1086 | moves: Default::default(), | 1088 | moves: Default::default(), |
| 1087 | home_dir: None, | 1089 | home_dir: None, |
| 1088 | })), | 1090 | })), |
| @@ -1173,6 +1175,8 @@ impl FakeFs { · 84 unmodified lines | |||
| 1173 | recreate_inode: bool, | 1175 | recreate_inode: bool, |
| 1174 | ) -> Result<()> { | 1176 | ) -> Result<()> { |
| 1175 | let mut state = self.state.lock(); | 1177 | let mut state = self.state.lock(); |
| 1178 | let path_buf = path.as_ref().to_path_buf(); | ||
| 1179 | *state.path_write_counts.entry(path_buf).or_insert(0) += 1; | ||
| 1176 | let new_inode = state.get_and_increment_inode(); | 1180 | let new_inode = state.get_and_increment_inode(); |
| 1177 | let new_mtime = state.get_and_increment_mtime(); | 1181 | let new_mtime = state.get_and_increment_mtime(); |
| 1178 | let new_len = new_content.len() as u64; | 1182 | let new_len = new_content.len() as u64; |
| @@ -1727,6 +1731,17 @@ impl FakeFs { · 548 unmodified lines | |||
| 1727 | self.state.lock().metadata_call_count | 1731 | self.state.lock().metadata_call_count |
| 1728 | } | 1732 | } |
| 1729 | 1733 | ||
| 1734 | /// How many write operations have been issued for a specific path. | ||
| 1735 | pub fn write_count_for_path(&self, path: impl AsRef<Path>) -> usize { | ||
| 1736 | let path = path.as_ref().to_path_buf(); | ||
| 1737 | self.state | ||
| 1738 | .lock() | ||
| 1739 | .path_write_counts | ||
| 1740 | .get(&path) | ||
| 1741 | .copied() | ||
| 1742 | .unwrap_or(0) | ||
| 1743 | } | ||
| 1744 | |||
| 1730 | fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> { | 1745 | fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> { |
| 1731 | self.executor.simulate_random_delay() | 1746 | self.executor.simulate_random_delay() |
| 1732 | } | 1747 | } |
| @@ -57,6 +57,7 @@ actions!( · 56 unmodified lines | |||
| 57 | ExpandCommitEditor, | 57 | ExpandCommitEditor, |
| 58 | GenerateCommitMessage, | 58 | GenerateCommitMessage, |
| 59 | Init, | 59 | Init, |
| 60 | OpenModifiedFiles, | ||
| 60 | ] | 61 | ] |
| 61 | ); | 62 | ); |
| 62 | 63 | ||
| @@ -26,8 +26,8 @@ use std::{ · 25 unmodified lines | |||
| 26 | }; | 26 | }; |
| 27 | use sum_tree::MapSeekTarget; | 27 | use sum_tree::MapSeekTarget; |
| 28 | use thiserror::Error; | 28 | use thiserror::Error; |
| 29 | use util::ResultExt; | ||
| 30 | use util::command::{new_smol_command, new_std_command}; | 29 | use util::command::{new_smol_command, new_std_command}; |
| 30 | use util::{ResultExt, paths}; | ||
| 31 | use uuid::Uuid; | 31 | use uuid::Uuid; |
| 32 | 32 | ||
| 33 | pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession}; | 33 | pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession}; |
| @@ -323,7 +323,7 @@ pub trait GitRepository: Send + Sync { · 289 unmodified lines | |||
| 323 | /// Resolve a list of refs to SHAs. | 323 | /// Resolve a list of refs to SHAs. |
| 324 | fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>; | 324 | fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>; |
| 325 | 325 | ||
| 326 | fn head_sha(&self) -> BoxFuture<Option<String>> { | 326 | fn head_sha(&self) -> BoxFuture<'_, Option<String>> { |
| 327 | async move { | 327 | async move { |
| 328 | self.revparse_batch(vec!["HEAD".into()]) | 328 | self.revparse_batch(vec!["HEAD".into()]) |
| 329 | .await | 329 | .await |
| @@ -508,6 +508,50 @@ pub struct GitRepositoryCheckpoint { · 178 unmodified lines | |||
| 508 | pub commit_sha: Oid, | 508 | pub commit_sha: Oid, |
| 509 | } | 509 | } |
| 510 | 510 | ||
| 511 | #[derive(Debug)] | ||
| 512 | pub struct GitCommitter { | ||
| 513 | pub name: Option<String>, | ||
| 514 | pub email: Option<String>, | ||
| 515 | } | ||
| 516 | |||
| 517 | pub async fn get_git_committer(cx: &AsyncApp) -> GitCommitter { | ||
| 518 | if cfg!(any(feature = "test-support", test)) { | ||
| 519 | return GitCommitter { | ||
| 520 | name: None, | ||
| 521 | email: None, | ||
| 522 | }; | ||
| 523 | } | ||
| 524 | |||
| 525 | let git_binary_path = | ||
| 526 | if cfg!(target_os = "macos") && option_env!("ZED_BUNDLE").as_deref() == Some("true") { | ||
| 527 | cx.update(|cx| { | ||
| 528 | cx.path_for_auxiliary_executable("git") | ||
| 529 | .context("could not find git binary path") | ||
| 530 | .log_err() | ||
| 531 | }) | ||
| 532 | .ok() | ||
| 533 | .flatten() | ||
| 534 | } else { | ||
| 535 | None | ||
| 536 | }; | ||
| 537 | |||
| 538 | let git = GitBinary::new( | ||
| 539 | git_binary_path.unwrap_or(PathBuf::from("git")), | ||
| 540 | paths::home_dir().clone(), | ||
| 541 | cx.background_executor().clone(), | ||
| 542 | ); | ||
| 543 | |||
| 544 | cx.background_spawn(async move { | ||
| 545 | let name = git.run(["config", "--global", "user.name"]).await.log_err(); | ||
| 546 | let email = git | ||
| 547 | .run(["config", "--global", "user.email"]) | ||
| 548 | .await | ||
| 549 | .log_err(); | ||
| 550 | GitCommitter { name, email } | ||
| 551 | }) | ||
| 552 | .await | ||
| 553 | } | ||
| 554 | |||
| 511 | impl GitRepository for RealGitRepository { | 555 | impl GitRepository for RealGitRepository { |
| 512 | fn reload_index(&self) { | 556 | fn reload_index(&self) { |
| 513 | if let Ok(mut index) = self.repository.lock().index() { | 557 | if let Ok(mut index) = self.repository.lock().index() { |
| @@ -525,7 +569,7 @@ impl GitRepository for RealGitRepository { · 11 unmodified lines | |||
| 525 | repo.commondir().into() | 569 | repo.commondir().into() |
| 526 | } | 570 | } |
| 527 | 571 | ||
| 528 | fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> { | 572 | fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> { |
| 529 | let working_directory = self.working_directory(); | 573 | let working_directory = self.working_directory(); |
| 530 | self.executor | 574 | self.executor |
| 531 | .spawn(async move { | 575 | .spawn(async move { |
| @@ -561,7 +605,7 @@ impl GitRepository for RealGitRepository { · 29 unmodified lines | |||
| 561 | .boxed() | 605 | .boxed() |
| 562 | } | 606 | } |
| 563 | 607 | ||
| 564 | fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> { | 608 | fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>> { |
| 565 | let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned) | 609 | let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned) |
| 566 | else { | 610 | else { |
| 567 | return future::ready(Err(anyhow!("no working directory"))).boxed(); | 611 | return future::ready(Err(anyhow!("no working directory"))).boxed(); |
| @@ -668,7 +712,7 @@ impl GitRepository for RealGitRepository { · 100 unmodified lines | |||
| 668 | commit: String, | 712 | commit: String, |
| 669 | mode: ResetMode, | 713 | mode: ResetMode, |
| 670 | env: Arc<HashMap<String, String>>, | 714 | env: Arc<HashMap<String, String>>, |
| 671 | ) -> BoxFuture<Result<()>> { | 715 | ) -> BoxFuture<'_, Result<()>> { |
| 672 | async move { | 716 | async move { |
| 673 | let working_directory = self.working_directory(); | 717 | let working_directory = self.working_directory(); |
| 674 | 718 | ||
| @@ -698,7 +742,7 @@ impl GitRepository for RealGitRepository { · 23 unmodified lines | |||
| 698 | commit: String, | 742 | commit: String, |
| 699 | paths: Vec<RepoPath>, | 743 | paths: Vec<RepoPath>, |
| 700 | env: Arc<HashMap<String, String>>, | 744 | env: Arc<HashMap<String, String>>, |
| 701 | ) -> BoxFuture<Result<()>> { | 745 | ) -> BoxFuture<'_, Result<()>> { |
| 702 | let working_directory = self.working_directory(); | 746 | let working_directory = self.working_directory(); |
| 703 | let git_binary_path = self.git_binary_path.clone(); | 747 | let git_binary_path = self.git_binary_path.clone(); |
| 704 | async move { | 748 | async move { |
| @@ -723,7 +767,7 @@ impl GitRepository for RealGitRepository { · 18 unmodified lines | |||
| 723 | .boxed() | 767 | .boxed() |
| 724 | } | 768 | } |
| 725 | 769 | ||
| 726 | fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> { | 770 | fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> { |
| 727 | // https://git-scm.com/book/en/v2/Git-Internals-Git-Objects | 771 | // https://git-scm.com/book/en/v2/Git-Internals-Git-Objects |
| 728 | const GIT_MODE_SYMLINK: u32 = 0o120000; | 772 | const GIT_MODE_SYMLINK: u32 = 0o120000; |
| 729 | 773 | ||
| @@ -756,7 +800,7 @@ impl GitRepository for RealGitRepository { · 26 unmodified lines | |||
| 756 | .boxed() | 800 | .boxed() |
| 757 | } | 801 | } |
| 758 | 802 | ||
| 759 | fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> { | 803 | fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> { |
| 760 | let repo = self.repository.clone(); | 804 | let repo = self.repository.clone(); |
| 761 | self.executor | 805 | self.executor |
| 762 | .spawn(async move { | 806 | .spawn(async move { |
| @@ -777,7 +821,7 @@ impl GitRepository for RealGitRepository { · 14 unmodified lines | |||
| 777 | path: RepoPath, | 821 | path: RepoPath, |
| 778 | content: Option<String>, | 822 | content: Option<String>, |
| 779 | env: Arc<HashMap<String, String>>, | 823 | env: Arc<HashMap<String, String>>, |
| 780 | ) -> BoxFuture<anyhow::Result<()>> { | 824 | ) -> BoxFuture<'_, anyhow::Result<()>> { |
| 781 | let working_directory = self.working_directory(); | 825 | let working_directory = self.working_directory(); |
| 782 | let git_binary_path = self.git_binary_path.clone(); | 826 | let git_binary_path = self.git_binary_path.clone(); |
| 783 | self.executor | 827 | self.executor |
| @@ -841,7 +885,7 @@ impl GitRepository for RealGitRepository { · 57 unmodified lines | |||
| 841 | remote.url().map(|url| url.to_string()) | 885 | remote.url().map(|url| url.to_string()) |
| 842 | } | 886 | } |
| 843 | 887 | ||
| 844 | fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>> { | 888 | fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> { |
| 845 | let working_directory = self.working_directory(); | 889 | let working_directory = self.working_directory(); |
| 846 | self.executor | 890 | self.executor |
| 847 | .spawn(async move { | 891 | .spawn(async move { |
| @@ -891,14 +935,14 @@ impl GitRepository for RealGitRepository { · 43 unmodified lines | |||
| 891 | .boxed() | 935 | .boxed() |
| 892 | } | 936 | } |
| 893 | 937 | ||
| 894 | fn merge_message(&self) -> BoxFuture<Option<String>> { | 938 | fn merge_message(&self) -> BoxFuture<'_, Option<String>> { |
| 895 | let path = self.path().join("MERGE_MSG"); | 939 | let path = self.path().join("MERGE_MSG"); |
| 896 | self.executor | 940 | self.executor |
| 897 | .spawn(async move { std::fs::read_to_string(&path).ok() }) | 941 | .spawn(async move { std::fs::read_to_string(&path).ok() }) |
| 898 | .boxed() | 942 | .boxed() |
| 899 | } | 943 | } |
| 900 | 944 | ||
| 901 | fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> { | 945 | fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>> { |
| 902 | let git_binary_path = self.git_binary_path.clone(); | 946 | let git_binary_path = self.git_binary_path.clone(); |
| 903 | let working_directory = self.working_directory(); | 947 | let working_directory = self.working_directory(); |
| 904 | let path_prefixes = path_prefixes.to_owned(); | 948 | let path_prefixes = path_prefixes.to_owned(); |
| @@ -919,7 +963,7 @@ impl GitRepository for RealGitRepository { · 14 unmodified lines | |||
| 919 | .boxed() | 963 | .boxed() |
| 920 | } | 964 | } |
| 921 | 965 | ||
| 922 | fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> { | 966 | fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> { |
| 923 | let working_directory = self.working_directory(); | 967 | let working_directory = self.working_directory(); |
| 924 | let git_binary_path = self.git_binary_path.clone(); | 968 | let git_binary_path = self.git_binary_path.clone(); |
| 925 | self.executor | 969 | self.executor |
| @@ -986,7 +1030,7 @@ impl GitRepository for RealGitRepository { · 60 unmodified lines | |||
| 986 | .boxed() | 1030 | .boxed() |
| 987 | } | 1031 | } |
| 988 | 1032 | ||
| 989 | fn change_branch(&self, name: String) -> BoxFuture<Result<()>> { | 1033 | fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { |
| 990 | let repo = self.repository.clone(); | 1034 | let repo = self.repository.clone(); |
| 991 | self.executor | 1035 | self.executor |
| 992 | .spawn(async move { | 1036 | .spawn(async move { |
| @@ -1018,7 +1062,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines | |||
| 1018 | .boxed() | 1062 | .boxed() |
| 1019 | } | 1063 | } |
| 1020 | 1064 | ||
| 1021 | fn create_branch(&self, name: String) -> BoxFuture<Result<()>> { | 1065 | fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { |
| 1022 | let repo = self.repository.clone(); | 1066 | let repo = self.repository.clone(); |
| 1023 | self.executor | 1067 | self.executor |
| 1024 | .spawn(async move { | 1068 | .spawn(async move { |
| @@ -1030,7 +1074,7 @@ impl GitRepository for RealGitRepository { · 5 unmodified lines | |||
| 1030 | .boxed() | 1074 | .boxed() |
| 1031 | } | 1075 | } |
| 1032 | 1076 | ||
| 1033 | fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>> { | 1077 | fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>> { |
| 1034 | let working_directory = self.working_directory(); | 1078 | let working_directory = self.working_directory(); |
| 1035 | let git_binary_path = self.git_binary_path.clone(); | 1079 | let git_binary_path = self.git_binary_path.clone(); |
| 1036 | 1080 | ||
| @@ -1052,7 +1096,7 @@ impl GitRepository for RealGitRepository { · 15 unmodified lines | |||
| 1052 | .boxed() | 1096 | .boxed() |
| 1053 | } | 1097 | } |
| 1054 | 1098 | ||
| 1055 | fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>> { | 1099 | fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>> { |
| 1056 | let working_directory = self.working_directory(); | 1100 | let working_directory = self.working_directory(); |
| 1057 | let git_binary_path = self.git_binary_path.clone(); | 1101 | let git_binary_path = self.git_binary_path.clone(); |
| 1058 | self.executor | 1102 | self.executor |
| @@ -1083,7 +1127,7 @@ impl GitRepository for RealGitRepository { · 24 unmodified lines | |||
| 1083 | &self, | 1127 | &self, |
| 1084 | paths: Vec<RepoPath>, | 1128 | paths: Vec<RepoPath>, |
| 1085 | env: Arc<HashMap<String, String>>, | 1129 | env: Arc<HashMap<String, String>>, |
| 1086 | ) -> BoxFuture<Result<()>> { | 1130 | ) -> BoxFuture<'_, Result<()>> { |
| 1087 | let working_directory = self.working_directory(); | 1131 | let working_directory = self.working_directory(); |
| 1088 | let git_binary_path = self.git_binary_path.clone(); | 1132 | let git_binary_path = self.git_binary_path.clone(); |
| 1089 | self.executor | 1133 | self.executor |
| @@ -1111,7 +1155,7 @@ impl GitRepository for RealGitRepository { · 21 unmodified lines | |||
| 1111 | &self, | 1155 | &self, |
| 1112 | paths: Vec<RepoPath>, | 1156 | paths: Vec<RepoPath>, |
| 1113 | env: Arc<HashMap<String, String>>, | 1157 | env: Arc<HashMap<String, String>>, |
| 1114 | ) -> BoxFuture<Result<()>> { | 1158 | ) -> BoxFuture<'_, Result<()>> { |
| 1115 | let working_directory = self.working_directory(); | 1159 | let working_directory = self.working_directory(); |
| 1116 | let git_binary_path = self.git_binary_path.clone(); | 1160 | let git_binary_path = self.git_binary_path.clone(); |
| 1117 | 1161 | ||
| @@ -1143,7 +1187,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines | |||
| 1143 | name_and_email: Option<(SharedString, SharedString)>, | 1187 | name_and_email: Option<(SharedString, SharedString)>, |
| 1144 | options: CommitOptions, | 1188 | options: CommitOptions, |
| 1145 | env: Arc<HashMap<String, String>>, | 1189 | env: Arc<HashMap<String, String>>, |
| 1146 | ) -> BoxFuture<Result<()>> { | 1190 | ) -> BoxFuture<'_, Result<()>> { |
| 1147 | let working_directory = self.working_directory(); | 1191 | let working_directory = self.working_directory(); |
| 1148 | self.executor | 1192 | self.executor |
| 1149 | .spawn(async move { | 1193 | .spawn(async move { |
| @@ -1182,7 +1226,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines | |||
| 1182 | ask_pass: AskPassDelegate, | 1226 | ask_pass: AskPassDelegate, |
| 1183 | env: Arc<HashMap<String, String>>, | 1227 | env: Arc<HashMap<String, String>>, |
| 1184 | cx: AsyncApp, | 1228 | cx: AsyncApp, |
| 1185 | ) -> BoxFuture<Result<RemoteCommandOutput>> { | 1229 | ) -> BoxFuture<'_, Result<RemoteCommandOutput>> { |
| 1186 | let working_directory = self.working_directory(); | 1230 | let working_directory = self.working_directory(); |
| 1187 | let executor = cx.background_executor().clone(); | 1231 | let executor = cx.background_executor().clone(); |
| 1188 | async move { | 1232 | async move { |
| @@ -1214,7 +1258,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines | |||
| 1214 | ask_pass: AskPassDelegate, | 1258 | ask_pass: AskPassDelegate, |
| 1215 | env: Arc<HashMap<String, String>>, | 1259 | env: Arc<HashMap<String, String>>, |
| 1216 | cx: AsyncApp, | 1260 | cx: AsyncApp, |
| 1217 | ) -> BoxFuture<Result<RemoteCommandOutput>> { | 1261 | ) -> BoxFuture<'_, Result<RemoteCommandOutput>> { |
| 1218 | let working_directory = self.working_directory(); | 1262 | let working_directory = self.working_directory(); |
| 1219 | let executor = cx.background_executor().clone(); | 1263 | let executor = cx.background_executor().clone(); |
| 1220 | async move { | 1264 | async move { |
| @@ -1239,7 +1283,7 @@ impl GitRepository for RealGitRepository { · 18 unmodified lines | |||
| 1239 | ask_pass: AskPassDelegate, | 1283 | ask_pass: AskPassDelegate, |
| 1240 | env: Arc<HashMap<String, String>>, | 1284 | env: Arc<HashMap<String, String>>, |
| 1241 | cx: AsyncApp, | 1285 | cx: AsyncApp, |
| 1242 | ) -> BoxFuture<Result<RemoteCommandOutput>> { | 1286 | ) -> BoxFuture<'_, Result<RemoteCommandOutput>> { |
| 1243 | let working_directory = self.working_directory(); | 1287 | let working_directory = self.working_directory(); |
| 1244 | let remote_name = format!("{}", fetch_options); | 1288 | let remote_name = format!("{}", fetch_options); |
| 1245 | let executor = cx.background_executor().clone(); | 1289 | let executor = cx.background_executor().clone(); |
| @@ -1257,7 +1301,7 @@ impl GitRepository for RealGitRepository { · 11 unmodified lines | |||
| 1257 | .boxed() | 1301 | .boxed() |
| 1258 | } | 1302 | } |
| 1259 | 1303 | ||
| 1260 | fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>> { | 1304 | fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<'_, Result<Vec<Remote>>> { |
| 1261 | let working_directory = self.working_directory(); | 1305 | let working_directory = self.working_directory(); |
| 1262 | let git_binary_path = self.git_binary_path.clone(); | 1306 | let git_binary_path = self.git_binary_path.clone(); |
| 1263 | self.executor | 1307 | self.executor |
| @@ -1303,7 +1347,7 @@ impl GitRepository for RealGitRepository { · 39 unmodified lines | |||
| 1303 | .boxed() | 1347 | .boxed() |
| 1304 | } | 1348 | } |
| 1305 | 1349 | ||
| 1306 | fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>> { | 1350 | fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<SharedString>>> { |
| 1307 | let working_directory = self.working_directory(); | 1351 | let working_directory = self.working_directory(); |
| 1308 | let git_binary_path = self.git_binary_path.clone(); | 1352 | let git_binary_path = self.git_binary_path.clone(); |
| 1309 | self.executor | 1353 | self.executor |
| @@ -1396,7 +1440,7 @@ impl GitRepository for RealGitRepository { · 86 unmodified lines | |||
| 1396 | .boxed() | 1440 | .boxed() |
| 1397 | } | 1441 | } |
| 1398 | 1442 | ||
| 1399 | fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> { | 1443 | fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> { |
| 1400 | let working_directory = self.working_directory(); | 1444 | let working_directory = self.working_directory(); |
| 1401 | let git_binary_path = self.git_binary_path.clone(); | 1445 | let git_binary_path = self.git_binary_path.clone(); |
| 1402 | 1446 | ||
| @@ -1435,7 +1479,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines | |||
| 1435 | &self, | 1479 | &self, |
| 1436 | left: GitRepositoryCheckpoint, | 1480 | left: GitRepositoryCheckpoint, |
| 1437 | right: GitRepositoryCheckpoint, | 1481 | right: GitRepositoryCheckpoint, |
| 1438 | ) -> BoxFuture<Result<bool>> { | 1482 | ) -> BoxFuture<'_, Result<bool>> { |
| 1439 | let working_directory = self.working_directory(); | 1483 | let working_directory = self.working_directory(); |
| 1440 | let git_binary_path = self.git_binary_path.clone(); | 1484 | let git_binary_path = self.git_binary_path.clone(); |
| 1441 | 1485 | ||
| @@ -1474,7 +1518,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines | |||
| 1474 | &self, | 1518 | &self, |
| 1475 | base_checkpoint: GitRepositoryCheckpoint, | 1519 | base_checkpoint: GitRepositoryCheckpoint, |
| 1476 | target_checkpoint: GitRepositoryCheckpoint, | 1520 | target_checkpoint: GitRepositoryCheckpoint, |
| 1477 | ) -> BoxFuture<Result<String>> { | 1521 | ) -> BoxFuture<'_, Result<String>> { |
| 1478 | let working_directory = self.working_directory(); | 1522 | let working_directory = self.working_directory(); |
| 1479 | let git_binary_path = self.git_binary_path.clone(); | 1523 | let git_binary_path = self.git_binary_path.clone(); |
| 1480 | 1524 | ||
| @@ -21,6 +21,7 @@ agent_settings.workspace = true · 20 unmodified lines | |||
| 21 | anyhow.workspace = true | 21 | anyhow.workspace = true |
| 22 | askpass.workspace = true | 22 | askpass.workspace = true |
| 23 | buffer_diff.workspace = true | 23 | buffer_diff.workspace = true |
| 24 | call.workspace = true | ||
| 24 | chrono.workspace = true | 25 | chrono.workspace = true |
| 25 | collections.workspace = true | 26 | collections.workspace = true |
| 26 | command_palette_hooks.workspace = true | 27 | command_palette_hooks.workspace = true |
| @@ -148,6 +148,7 @@ impl CommitModal { · 147 unmodified lines | |||
| 148 | } | 148 | } |
| 149 | } | 149 | } |
| 150 | git_panel.set_modal_open(true, cx); | 150 | git_panel.set_modal_open(true, cx); |
| 151 | git_panel.load_local_committer(cx); | ||
| 151 | }); | 152 | }); |
| 152 | 153 | ||
| 153 | let dock = workspace.dock_at_position(git_panel.position(window, cx)); | 154 | let dock = workspace.dock_at_position(git_panel.position(window, cx)); |
| @@ -248,6 +248,8 @@ fn conflicts_updated( · 247 unmodified lines | |||
| 248 | removed_block_ids.insert(block_id); | 248 | removed_block_ids.insert(block_id); |
| 249 | } | 249 | } |
| 250 | 250 | ||
| 251 | editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx); | ||
| 252 | |||
| 251 | editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx); | 253 | editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx); |
| 252 | editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx); | 254 | editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx); |
| 253 | editor | 255 | editor |
| @@ -325,8 +327,7 @@ fn update_conflict_highlighting( · 71 unmodified lines | |||
| 325 | cx: &mut Context<Editor>, | 327 | cx: &mut Context<Editor>, |
| 326 | ) { | 328 | ) { |
| 327 | log::debug!("update conflict highlighting for {conflict:?}"); | 329 | log::debug!("update conflict highlighting for {conflict:?}"); |
| 328 | let theme = cx.theme().clone(); | 330 | |
| 329 | let colors = theme.colors(); | ||
| 330 | let outer_start = buffer | 331 | let outer_start = buffer |
| 331 | .anchor_in_excerpt(excerpt_id, conflict.range.start) | 332 | .anchor_in_excerpt(excerpt_id, conflict.range.start) |
| 332 | .unwrap(); | 333 | .unwrap(); |
| @@ -346,26 +347,29 @@ fn update_conflict_highlighting( · 13 unmodified lines | |||
| 346 | .anchor_in_excerpt(excerpt_id, conflict.theirs.end) | 347 | .anchor_in_excerpt(excerpt_id, conflict.theirs.end) |
| 347 | .unwrap(); | 348 | .unwrap(); |
| 348 | 349 | ||
| 349 | let ours_background = colors.version_control_conflict_ours_background; | 350 | let ours_background = cx.theme().colors().version_control_conflict_marker_ours; |
| 350 | let ours_marker = colors.version_control_conflict_ours_marker_background; | 351 | let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs; |
| 351 | let theirs_background = colors.version_control_conflict_theirs_background; | ||
| 352 | let theirs_marker = colors.version_control_conflict_theirs_marker_background; | ||
| 353 | let divider_background = colors.version_control_conflict_divider_background; | ||
| 354 | 352 | ||
| 355 | let options = RowHighlightOptions { | 353 | let options = RowHighlightOptions { |
| 356 | include_gutter: false, | 354 | include_gutter: true, |
| 357 | ..Default::default() | 355 | ..Default::default() |
| 358 | }; | 356 | }; |
| 359 | 357 | ||
| 358 | editor.insert_gutter_highlight::<ConflictsOuter>( | ||
| 359 | outer_start..their_end, | ||
| 360 | |cx| cx.theme().colors().editor_background, | ||
| 361 | cx, | ||
| 362 | ); | ||
| 363 | |||
| 360 | // Prevent diff hunk highlighting within the entire conflict region. | 364 | // Prevent diff hunk highlighting within the entire conflict region. |
| 361 | editor.highlight_rows::<ConflictsOuter>( | 365 | editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx); |
| 362 | outer_start..outer_end, | 366 | editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx); |
| 363 | divider_background, | 367 | editor.highlight_rows::<ConflictsOursMarker>( |
| 368 | outer_start..our_start, | ||
| 369 | ours_background, | ||
| 364 | options, | 370 | options, |
| 365 | cx, | 371 | cx, |
| 366 | ); | 372 | ); |
| 367 | editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx); | ||
| 368 | editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx); | ||
| 369 | editor.highlight_rows::<ConflictsTheirs>( | 373 | editor.highlight_rows::<ConflictsTheirs>( |
| 370 | their_start..their_end, | 374 | their_start..their_end, |
| 371 | theirs_background, | 375 | theirs_background, |
| @@ -374,7 +378,7 @@ fn update_conflict_highlighting( · 2 unmodified lines | |||
| 374 | ); | 378 | ); |
| 375 | editor.highlight_rows::<ConflictsTheirsMarker>( | 379 | editor.highlight_rows::<ConflictsTheirsMarker>( |
| 376 | their_end..outer_end, | 380 | their_end..outer_end, |
| 377 | theirs_marker, | 381 | theirs_background, |
| 378 | options, | 382 | options, |
| 379 | cx, | 383 | cx, |
| 380 | ); | 384 | ); |
| @@ -512,6 +516,9 @@ pub(crate) fn resolve_conflict( · 131 unmodified lines | |||
| 512 | let end = snapshot | 516 | let end = snapshot |
| 513 | .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end) | 517 | .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end) |
| 514 | .unwrap(); | 518 | .unwrap(); |
| 519 | |||
| 520 | editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx); | ||
| 521 | |||
| 515 | editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx); | 522 | editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx); |
| 516 | editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx); | 523 | editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx); |
| 517 | editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx); | 524 | editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx); |
| @@ -20,18 +20,20 @@ use editor::{ · 19 unmodified lines | |||
| 20 | use futures::StreamExt as _; | 20 | use futures::StreamExt as _; |
| 21 | use git::blame::ParsedCommitMessage; | 21 | use git::blame::ParsedCommitMessage; |
| 22 | use git::repository::{ | 22 | use git::repository::{ |
| 23 | Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, PushOptions, | 23 | Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter, |
| 24 | Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, | 24 | PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, |
| 25 | UpstreamTrackingStatus, get_git_committer, | ||
| 25 | }; | 26 | }; |
| 26 | use git::status::StageStatus; | 27 | use git::status::StageStatus; |
| 27 | use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus}; | 28 | use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus}; |
| 28 | use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll}; | 29 | use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll}; |
| 29 | use gpui::{ | 30 | use gpui::{ |
| 30 | Action, Animation, AnimationExt as _, Axis, ClickEvent, Corner, DismissEvent, Entity, | 31 | Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, |
| 31 | EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, | 32 | DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, |
| 32 | ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point, | 33 | ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent, |
| 33 | PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle, | 34 | MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task, |
| 34 | WeakEntity, actions, anchored, deferred, percentage, uniform_list, | 35 | Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage, |
| 36 | uniform_list, | ||
| 35 | }; | 37 | }; |
| 36 | use itertools::Itertools; | 38 | use itertools::Itertools; |
| 37 | use language::{Buffer, File}; | 39 | use language::{Buffer, File}; |
| @@ -54,6 +56,7 @@ use project::{ · 16 unmodified lines | |||
| 54 | use serde::{Deserialize, Serialize}; | 56 | use serde::{Deserialize, Serialize}; |
| 55 | use settings::{Settings as _, SettingsStore}; | 57 | use settings::{Settings as _, SettingsStore}; |
| 56 | use std::future::Future; | 58 | use std::future::Future; |
| 59 | use std::ops::Range; | ||
| 57 | use std::path::{Path, PathBuf}; | 60 | use std::path::{Path, PathBuf}; |
| 58 | use std::{collections::HashSet, sync::Arc, time::Duration, usize}; | 61 | use std::{collections::HashSet, sync::Arc, time::Duration, usize}; |
| 59 | use strum::{IntoEnumIterator, VariantNames}; | 62 | use strum::{IntoEnumIterator, VariantNames}; |
| @@ -63,11 +66,11 @@ use ui::{ · 3 unmodified lines | |||
| 63 | Tooltip, prelude::*, | 66 | Tooltip, prelude::*, |
| 64 | }; | 67 | }; |
| 65 | use util::{ResultExt, TryFutureExt, maybe}; | 68 | use util::{ResultExt, TryFutureExt, maybe}; |
| 66 | use workspace::AppState; | 69 | |
| 67 | use workspace::{ | 70 | use workspace::{ |
| 68 | Workspace, | 71 | Workspace, |
| 69 | dock::{DockPosition, Panel, PanelEvent}, | 72 | dock::{DockPosition, Panel, PanelEvent}, |
| 70 | notifications::DetachAndPromptErr, | 73 | notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId}, |
| 71 | }; | 74 | }; |
| 72 | use zed_llm_client::CompletionIntent; | 75 | use zed_llm_client::CompletionIntent; |
| 73 | 76 | ||
| @@ -356,6 +359,8 @@ pub struct GitPanel { · 282 unmodified lines | |||
| 356 | context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, | 359 | context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, |
| 357 | modal_open: bool, | 360 | modal_open: bool, |
| 358 | show_placeholders: bool, | 361 | show_placeholders: bool, |
| 362 | local_committer: Option<GitCommitter>, | ||
| 363 | local_committer_task: Option<Task<()>>, | ||
| 359 | _settings_subscription: Subscription, | 364 | _settings_subscription: Subscription, |
| 360 | } | 365 | } |
| 361 | 366 | ||
| @@ -372,7 +377,10 @@ pub(crate) fn commit_message_editor( · 10 unmodified lines | |||
| 372 | let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx)); | 377 | let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx)); |
| 373 | let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 }; | 378 | let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 }; |
| 374 | let mut commit_editor = Editor::new( | 379 | let mut commit_editor = Editor::new( |
| 375 | EditorMode::AutoHeight { max_lines }, | 380 | EditorMode::AutoHeight { |
| 381 | min_lines: 1, | ||
| 382 | max_lines, | ||
| 383 | }, | ||
| 376 | buffer, | 384 | buffer, |
| 377 | None, | 385 | None, |
| 378 | window, | 386 | window, |
| @@ -389,144 +397,150 @@ pub(crate) fn commit_message_editor( · 10 unmodified lines | |||
| 389 | } | 397 | } |
| 390 | 398 | ||
| 391 | impl GitPanel { | 399 | impl GitPanel { |
| 392 | pub fn new( | 400 | fn new( |
| 393 | workspace: Entity<Workspace>, | 401 | workspace: &mut Workspace, |
| 394 | project: Entity<Project>, | ||
| 395 | app_state: Arc<AppState>, | ||
| 396 | window: &mut Window, | 402 | window: &mut Window, |
| 397 | cx: &mut Context<Self>, | 403 | cx: &mut Context<Workspace>, |
| 398 | ) -> Self { | 404 | ) -> Entity<Self> { |
| 405 | let project = workspace.project().clone(); | ||
| 406 | let app_state = workspace.app_state().clone(); | ||
| 399 | let fs = app_state.fs.clone(); | 407 | let fs = app_state.fs.clone(); |
| 400 | let git_store = project.read(cx).git_store().clone(); | 408 | let git_store = project.read(cx).git_store().clone(); |
| 401 | let active_repository = project.read(cx).active_repository(cx); | 409 | let active_repository = project.read(cx).active_repository(cx); |
| 402 | let workspace = workspace.downgrade(); | ||
| 403 | 410 | ||
| 404 | let focus_handle = cx.focus_handle(); | 411 | let git_panel = cx.new(|cx| { |
| 405 | cx.on_focus(&focus_handle, window, Self::focus_in).detach(); | 412 | let focus_handle = cx.focus_handle(); |
| 406 | cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { | 413 | cx.on_focus(&focus_handle, window, Self::focus_in).detach(); |
| 407 | this.hide_scrollbars(window, cx); | 414 | cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { |
| 408 | }) | 415 | this.hide_scrollbars(window, cx); |
| 409 | .detach(); | 416 | }) |
| 417 | .detach(); | ||
| 410 | 418 | ||
| 411 | let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; | 419 | let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; |
| 412 | cx.observe_global::<SettingsStore>(move |this, cx| { | 420 | cx.observe_global::<SettingsStore>(move |this, cx| { |
| 413 | let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; | 421 | let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; |
| 414 | if is_sort_by_path != was_sort_by_path { | 422 | if is_sort_by_path != was_sort_by_path { |
| 415 | this.update_visible_entries(cx); | 423 | this.update_visible_entries(cx); |
| 416 | } | 424 | } |
| 417 | was_sort_by_path = is_sort_by_path | 425 | was_sort_by_path = is_sort_by_path |
| 418 | }) | 426 | }) |
| 419 | .detach(); | 427 | .detach(); |
| 420 | 428 | ||
| 421 | // just to let us render a placeholder editor. | 429 | // just to let us render a placeholder editor. |
| 422 | // Once the active git repo is set, this buffer will be replaced. | 430 | // Once the active git repo is set, this buffer will be replaced. |
| 423 | let temporary_buffer = cx.new(|cx| Buffer::local("", cx)); | 431 | let temporary_buffer = cx.new(|cx| Buffer::local("", cx)); |
| 424 | let commit_editor = cx.new(|cx| { | 432 | let commit_editor = cx.new(|cx| { |
| 425 | commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx) | 433 | commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx) |
| 426 | }); | 434 | }); |
| 427 | 435 | ||
| 428 | commit_editor.update(cx, |editor, cx| { | 436 | commit_editor.update(cx, |editor, cx| { |
| 429 | editor.clear(window, cx); | 437 | editor.clear(window, cx); |
| 430 | }); | 438 | }); |
| 431 | 439 | ||
| 432 | let scroll_handle = UniformListScrollHandle::new(); | 440 | let scroll_handle = UniformListScrollHandle::new(); |
| 433 | 441 | ||
| 434 | cx.subscribe_in( | 442 | let vertical_scrollbar = ScrollbarProperties { |
| 435 | &git_store, | 443 | axis: Axis::Vertical, |
| 436 | window, | 444 | state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), |
| 437 | move |this, git_store, event, window, cx| match event { | 445 | show_scrollbar: false, |
| 438 | GitStoreEvent::ActiveRepositoryChanged(_) => { | 446 | show_track: false, |
| 439 | this.active_repository = git_store.read(cx).active_repository(); | 447 | auto_hide: false, |
| 440 | this.schedule_update(true, window, cx); | 448 | hide_task: None, |
| 441 | } | 449 | }; |
| 442 | GitStoreEvent::RepositoryUpdated( | ||
| 443 | _, | ||
| 444 | RepositoryEvent::Updated { full_scan }, | ||
| 445 | true, | ||
| 446 | ) => { | ||
| 447 | this.schedule_update(*full_scan, window, cx); | ||
| 448 | } | ||
| 449 | 450 | ||
| 450 | GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => { | 451 | let horizontal_scrollbar = ScrollbarProperties { |
| 451 | this.schedule_update(false, window, cx); | 452 | axis: Axis::Horizontal, |
| 452 | } | 453 | state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), |
| 453 | GitStoreEvent::IndexWriteError(error) => { | 454 | show_scrollbar: false, |
| 454 | this.workspace | 455 | show_track: false, |
| 455 | .update(cx, |workspace, cx| { | 456 | auto_hide: false, |
| 456 | workspace.show_error(error, cx); | 457 | hide_task: None, |
| 457 | }) | 458 | }; |
| 458 | .ok(); | 459 | |
| 460 | let mut assistant_enabled = AgentSettings::get_global(cx).enabled; | ||
| 461 | let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| { | ||
| 462 | if assistant_enabled != AgentSettings::get_global(cx).enabled { | ||
| 463 | assistant_enabled = AgentSettings::get_global(cx).enabled; | ||
| 464 | cx.notify(); | ||
| 459 | } | 465 | } |
| 460 | GitStoreEvent::RepositoryUpdated(_, _, _) => {} | 466 | }); |
| 461 | GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {} | ||
| 462 | }, | ||
| 463 | ) | ||
| 464 | .detach(); | ||
| 465 | 467 | ||
| 466 | let vertical_scrollbar = ScrollbarProperties { | 468 | cx.subscribe_in( |
| 467 | axis: Axis::Vertical, | 469 | &git_store, |
| 468 | state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), | 470 | window, |
| 469 | show_scrollbar: false, | 471 | move |this, _git_store, event, window, cx| match event { |
| 470 | show_track: false, | 472 | GitStoreEvent::ActiveRepositoryChanged(_) => { |
| 471 | auto_hide: false, | 473 | this.active_repository = this.project.read(cx).active_repository(cx); |
| 472 | hide_task: None, | 474 | this.schedule_update(true, window, cx); |
| 473 | }; | 475 | } |
| 476 | GitStoreEvent::RepositoryUpdated( | ||
| 477 | _, | ||
| 478 | RepositoryEvent::Updated { full_scan }, | ||
| 479 | true, | ||
| 480 | ) => { | ||
| 481 | this.schedule_update(*full_scan, window, cx); | ||
| 482 | } | ||
| 474 | 483 | ||
| 475 | let horizontal_scrollbar = ScrollbarProperties { | 484 | GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => { |
| 476 | axis: Axis::Horizontal, | 485 | this.schedule_update(false, window, cx); |
| 477 | state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), | 486 | } |
| 478 | show_scrollbar: false, | 487 | GitStoreEvent::IndexWriteError(error) => { |
| 479 | show_track: false, | 488 | this.workspace |
| 480 | auto_hide: false, | 489 | .update(cx, |workspace, cx| { |
| 481 | hide_task: None, | 490 | workspace.show_error(error, cx); |
| 482 | }; | 491 | }) |
| 492 | .ok(); | ||
| 493 | } | ||
| 494 | GitStoreEvent::RepositoryUpdated(_, _, _) => {} | ||
| 495 | GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {} | ||
| 496 | }, | ||
| 497 | ) | ||
| 498 | .detach(); | ||
| 483 | 499 | ||
| 484 | let mut assistant_enabled = AgentSettings::get_global(cx).enabled; | 500 | let mut this = Self { |
| 485 | let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| { | 501 | active_repository, |
| 486 | if assistant_enabled != AgentSettings::get_global(cx).enabled { | 502 | commit_editor, |
| 487 | assistant_enabled = AgentSettings::get_global(cx).enabled; | 503 | conflicted_count: 0, |
| 488 | cx.notify(); | 504 | conflicted_staged_count: 0, |
| 489 | } | 505 | current_modifiers: window.modifiers(), |
| 506 | add_coauthors: true, | ||
| 507 | generate_commit_message_task: None, | ||
| 508 | entries: Vec::new(), | ||
| 509 | focus_handle: cx.focus_handle(), | ||
| 510 | fs, | ||
| 511 | new_count: 0, | ||
| 512 | new_staged_count: 0, | ||
| 513 | pending: Vec::new(), | ||
| 514 | pending_commit: None, | ||
| 515 | amend_pending: false, | ||
| 516 | pending_serialization: Task::ready(None), | ||
| 517 | single_staged_entry: None, | ||
| 518 | single_tracked_entry: None, | ||
| 519 | project, | ||
| 520 | scroll_handle, | ||
| 521 | max_width_item_index: None, | ||
| 522 | selected_entry: None, | ||
| 523 | marked_entries: Vec::new(), | ||
| 524 | tracked_count: 0, | ||
| 525 | tracked_staged_count: 0, | ||
| 526 | update_visible_entries_task: Task::ready(()), | ||
| 527 | width: None, | ||
| 528 | show_placeholders: false, | ||
| 529 | local_committer: None, | ||
| 530 | local_committer_task: None, | ||
| 531 | context_menu: None, | ||
| 532 | workspace: workspace.weak_handle(), | ||
| 533 | modal_open: false, | ||
| 534 | entry_count: 0, | ||
| 535 | horizontal_scrollbar, | ||
| 536 | vertical_scrollbar, | ||
| 537 | _settings_subscription, | ||
| 538 | }; | ||
| 539 | |||
| 540 | this.schedule_update(false, window, cx); | ||
| 541 | this | ||
| 490 | }); | 542 | }); |
| 491 | 543 | ||
| 492 | let mut git_panel = Self { | ||
| 493 | active_repository, | ||
| 494 | commit_editor, | ||
| 495 | conflicted_count: 0, | ||
| 496 | conflicted_staged_count: 0, | ||
| 497 | current_modifiers: window.modifiers(), | ||
| 498 | add_coauthors: true, | ||
| 499 | generate_commit_message_task: None, | ||
| 500 | entries: Vec::new(), | ||
| 501 | focus_handle: cx.focus_handle(), | ||
| 502 | fs, | ||
| 503 | new_count: 0, | ||
| 504 | new_staged_count: 0, | ||
| 505 | pending: Vec::new(), | ||
| 506 | pending_commit: None, | ||
| 507 | amend_pending: false, | ||
| 508 | pending_serialization: Task::ready(None), | ||
| 509 | single_staged_entry: None, | ||
| 510 | single_tracked_entry: None, | ||
| 511 | project, | ||
| 512 | scroll_handle, | ||
| 513 | max_width_item_index: None, | ||
| 514 | selected_entry: None, | ||
| 515 | marked_entries: Vec::new(), | ||
| 516 | tracked_count: 0, | ||
| 517 | tracked_staged_count: 0, | ||
| 518 | update_visible_entries_task: Task::ready(()), | ||
| 519 | width: None, | ||
| 520 | show_placeholders: false, | ||
| 521 | context_menu: None, | ||
| 522 | workspace, | ||
| 523 | modal_open: false, | ||
| 524 | entry_count: 0, | ||
| 525 | horizontal_scrollbar, | ||
| 526 | vertical_scrollbar, | ||
| 527 | _settings_subscription, | ||
| 528 | }; | ||
| 529 | git_panel.schedule_update(false, window, cx); | ||
| 530 | git_panel | 544 | git_panel |
| 531 | } | 545 | } |
| 532 | 546 | ||
| @@ -1774,7 +1788,19 @@ impl GitPanel { · 1241 unmodified lines | |||
| 1774 | this.generate_commit_message_task.take(); | 1788 | this.generate_commit_message_task.take(); |
| 1775 | }); | 1789 | }); |
| 1776 | 1790 | ||
| 1777 | let mut diff_text = diff.await??; | 1791 | let mut diff_text = match diff.await { |
| 1792 | Ok(result) => match result { | ||
| 1793 | Ok(text) => text, | ||
| 1794 | Err(e) => { | ||
| 1795 | Self::show_commit_message_error(&this, &e, cx); | ||
| 1796 | return anyhow::Ok(()); | ||
| 1797 | } | ||
| 1798 | }, | ||
| 1799 | Err(e) => { | ||
| 1800 | Self::show_commit_message_error(&this, &e, cx); | ||
| 1801 | return anyhow::Ok(()); | ||
| 1802 | } | ||
| 1803 | }; | ||
| 1778 | 1804 | ||
| 1779 | const ONE_MB: usize = 1_000_000; | 1805 | const ONE_MB: usize = 1_000_000; |
| 1780 | if diff_text.len() > ONE_MB { | 1806 | if diff_text.len() > ONE_MB { |
| @@ -1812,26 +1838,37 @@ impl GitPanel { · 31 unmodified lines | |||
| 1812 | }; | 1838 | }; |
| 1813 | 1839 | ||
| 1814 | let stream = model.stream_completion_text(request, &cx); | 1840 | let stream = model.stream_completion_text(request, &cx); |
| 1815 | let mut messages = stream.await?; | 1841 | match stream.await { |
| 1816 | 1842 | Ok(mut messages) => { | |
| 1817 | if !text_empty { | 1843 | if !text_empty { |
| 1818 | this.update(cx, |this, cx| { | 1844 | this.update(cx, |this, cx| { |
| 1819 | this.commit_message_buffer(cx).update(cx, |buffer, cx| { | 1845 | this.commit_message_buffer(cx).update(cx, |buffer, cx| { |
| 1820 | let insert_position = buffer.anchor_before(buffer.len()); | 1846 | let insert_position = buffer.anchor_before(buffer.len()); |
| 1821 | buffer.edit([(insert_position..insert_position, "\n")], None, cx) | 1847 | buffer.edit([(insert_position..insert_position, "\n")], None, cx) |
| 1822 | }); | 1848 | }); |
| 1823 | })?; | 1849 | })?; |
| 1824 | } | 1850 | } |
| 1825 | |||
| 1826 | while let Some(message) = messages.stream.next().await { | ||
| 1827 | let text = message?; | ||
| 1828 | 1851 | ||
| 1829 | this.update(cx, |this, cx| { | 1852 | while let Some(message) = messages.stream.next().await { |
| 1830 | this.commit_message_buffer(cx).update(cx, |buffer, cx| { | 1853 | match message { |
| 1831 | let insert_position = buffer.anchor_before(buffer.len()); | 1854 | Ok(text) => { |
| 1832 | buffer.edit([(insert_position..insert_position, text)], None, cx); | 1855 | this.update(cx, |this, cx| { |
| 1833 | }); | 1856 | this.commit_message_buffer(cx).update(cx, |buffer, cx| { |
| 1834 | })?; | 1857 | let insert_position = buffer.anchor_before(buffer.len()); |
| 1858 | buffer.edit([(insert_position..insert_position, text)], None, cx); | ||
| 1859 | }); | ||
| 1860 | })?; | ||
| 1861 | } | ||
| 1862 | Err(e) => { | ||
| 1863 | Self::show_commit_message_error(&this, &e, cx); | ||
| 1864 | break; | ||
| 1865 | } | ||
| 1866 | } | ||
| 1867 | } | ||
| 1868 | } | ||
| 1869 | Err(e) => { | ||
| 1870 | Self::show_commit_message_error(&this, &e, cx); | ||
| 1871 | } | ||
| 1835 | } | 1872 | } |
| 1836 | 1873 | ||
| 1837 | anyhow::Ok(()) | 1874 | anyhow::Ok(()) |
| @@ -2221,6 +2258,19 @@ impl GitPanel { · 383 unmodified lines | |||
| 2221 | } | 2258 | } |
| 2222 | } | 2259 | } |
| 2223 | 2260 | ||
| 2261 | pub fn load_local_committer(&mut self, cx: &Context<Self>) { | ||
| 2262 | if self.local_committer_task.is_none() { | ||
| 2263 | self.local_committer_task = Some(cx.spawn(async move |this, cx| { | ||
| 2264 | let committer = get_git_committer(cx).await; | ||
| 2265 | this.update(cx, |this, cx| { | ||
| 2266 | this.local_committer = Some(committer); | ||
| 2267 | cx.notify() | ||
| 2268 | }) | ||
| 2269 | .ok(); | ||
| 2270 | })); | ||
| 2271 | } | ||
| 2272 | } | ||
| 2273 | |||
| 2224 | fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> { | 2274 | fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> { |
| 2225 | let mut new_co_authors = Vec::new(); | 2275 | let mut new_co_authors = Vec::new(); |
| 2226 | let project = self.project.read(cx); | 2276 | let project = self.project.read(cx); |
| @@ -2243,34 +2293,38 @@ impl GitPanel { · 16 unmodified lines | |||
| 2243 | let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else { | 2293 | let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else { |
| 2244 | continue; | 2294 | continue; |
| 2245 | }; | 2295 | }; |
| 2246 | if participant.can_write() && participant.user.email.is_some() { | 2296 | if !participant.can_write() { |
| 2247 | let email = participant.user.email.clone().unwrap(); | 2297 | continue; |
| 2248 | 2298 | } | |
| 2249 | new_co_authors.push(( | 2299 | if let Some(email) = &collaborator.committer_email { |
| 2250 | participant | 2300 | let name = collaborator |
| 2251 | .user | 2301 | .committer_name |
| 2252 | .name | 2302 | .clone() |
| 2253 | .clone() | 2303 | .or_else(|| participant.user.name.clone()) |
| 2254 | .unwrap_or_else(|| participant.user.github_login.clone()), | 2304 | .unwrap_or_else(|| participant.user.github_login.clone()); |
| 2255 | email, | 2305 | new_co_authors.push((name.clone(), email.clone())) |
| 2256 | )) | ||
| 2257 | } | 2306 | } |
| 2258 | } | 2307 | } |
| 2259 | if !project.is_local() && !project.is_read_only(cx) { | 2308 | if !project.is_local() && !project.is_read_only(cx) { |
| 2260 | if let Some(user) = room.local_participant_user(cx) { | 2309 | if let Some(local_committer) = self.local_committer(room, cx) { |
| 2261 | if let Some(email) = user.email.clone() { | 2310 | new_co_authors.push(local_committer); |
| 2262 | new_co_authors.push(( | ||
| 2263 | user.name | ||
| 2264 | .clone() | ||
| 2265 | .unwrap_or_else(|| user.github_login.clone()), | ||
| 2266 | email.clone(), | ||
| 2267 | )) | ||
| 2268 | } | ||
| 2269 | } | 2311 | } |
| 2270 | } | 2312 | } |
| 2271 | new_co_authors | 2313 | new_co_authors |
| 2272 | } | 2314 | } |
| 2273 | 2315 | ||
| 2316 | fn local_committer(&self, room: &call::Room, cx: &App) -> Option<(String, String)> { | ||
| 2317 | let user = room.local_participant_user(cx)?; | ||
| 2318 | let committer = self.local_committer.as_ref()?; | ||
| 2319 | let email = committer.email.clone()?; | ||
| 2320 | let name = committer | ||
| 2321 | .name | ||
| 2322 | .clone() | ||
| 2323 | .or_else(|| user.name.clone()) | ||
| 2324 | .unwrap_or_else(|| user.github_login.clone()); | ||
| 2325 | Some((name, email)) | ||
| 2326 | } | ||
| 2327 | |||
| 2274 | fn toggle_fill_co_authors( | 2328 | fn toggle_fill_co_authors( |
| 2275 | &mut self, | 2329 | &mut self, |
| 2276 | _: &ToggleFillCoAuthors, | 2330 | _: &ToggleFillCoAuthors, |
| @@ -2689,6 +2743,26 @@ impl GitPanel { · 412 unmodified lines | |||
| 2689 | } | 2743 | } |
| 2690 | } | 2744 | } |
| 2691 | 2745 | ||
| 2746 | fn show_commit_message_error<E>(weak_this: &WeakEntity<Self>, err: &E, cx: &mut AsyncApp) | ||
| 2747 | where | ||
| 2748 | E: std::fmt::Debug + std::fmt::Display, | ||
| 2749 | { | ||
| 2750 | if let Ok(Some(workspace)) = weak_this.update(cx, |this, _cx| this.workspace.upgrade()) { | ||
| 2751 | let _ = workspace.update(cx, |workspace, cx| { | ||
| 2752 | struct CommitMessageError; | ||
| 2753 | let notification_id = NotificationId::unique::<CommitMessageError>(); | ||
| 2754 | workspace.show_notification(notification_id, cx, |cx| { | ||
| 2755 | cx.new(|cx| { | ||
| 2756 | ErrorMessagePrompt::new( | ||
| 2757 | format!("Failed to generate commit message: {err}"), | ||
| 2758 | cx, | ||
| 2759 | ) | ||
| 2760 | }) | ||
| 2761 | }); | ||
| 2762 | }); | ||
| 2763 | } | ||
| 2764 | } | ||
| 2765 | |||
| 2692 | fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) { | 2766 | fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) { |
| 2693 | let Some(workspace) = self.workspace.upgrade() else { | 2767 | let Some(workspace) = self.workspace.upgrade() else { |
| 2694 | return; | 2768 | return; |
| @@ -3662,8 +3736,10 @@ impl GitPanel { · 967 unmodified lines | |||
| 3662 | .relative() | 3736 | .relative() |
| 3663 | .overflow_hidden() | 3737 | .overflow_hidden() |
| 3664 | .child( | 3738 | .child( |
| 3665 | uniform_list(cx.entity().clone(), "entries", entry_count, { | 3739 | uniform_list( |
| 3666 | move |this, range, window, cx| { | 3740 | "entries", |
| 3741 | entry_count, | ||
| 3742 | cx.processor(move |this, range: Range<usize>, window, cx| { | ||
| 3667 | let mut items = Vec::with_capacity(range.end - range.start); | 3743 | let mut items = Vec::with_capacity(range.end - range.start); |
| 3668 | 3744 | ||
| 3669 | for ix in range { | 3745 | for ix in range { |
| @@ -3691,8 +3767,8 @@ impl GitPanel { · 21 unmodified lines | |||
| 3691 | } | 3767 | } |
| 3692 | 3768 | ||
| 3693 | items | 3769 | items |
| 3694 | } | 3770 | }), |
| 3695 | }) | 3771 | ) |
| 3696 | .when( | 3772 | .when( |
| 3697 | !self.horizontal_scrollbar.show_track | 3773 | !self.horizontal_scrollbar.show_track |
| 3698 | && self.horizontal_scrollbar.show_scrollbar, | 3774 | && self.horizontal_scrollbar.show_scrollbar, |
| @@ -4141,6 +4217,32 @@ impl GitPanel { · 442 unmodified lines | |||
| 4141 | self.amend_pending = value; | 4217 | self.amend_pending = value; |
| 4142 | cx.notify(); | 4218 | cx.notify(); |
| 4143 | } | 4219 | } |
| 4220 | |||
| 4221 | pub async fn load( | ||
| 4222 | workspace: WeakEntity<Workspace>, | ||
| 4223 | mut cx: AsyncWindowContext, | ||
| 4224 | ) -> anyhow::Result<Entity<Self>> { | ||
| 4225 | let serialized_panel = cx | ||
| 4226 | .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&GIT_PANEL_KEY) }) | ||
| 4227 | .await | ||
| 4228 | .context("loading git panel") | ||
| 4229 | .log_err() | ||
| 4230 | .flatten() | ||
| 4231 | .and_then(|panel| serde_json::from_str::<SerializedGitPanel>(&panel).log_err()); | ||
| 4232 | |||
| 4233 | workspace.update_in(&mut cx, |workspace, window, cx| { | ||
| 4234 | let panel = GitPanel::new(workspace, window, cx); | ||
| 4235 | |||
| 4236 | if let Some(serialized_panel) = serialized_panel { | ||
| 4237 | panel.update(cx, |panel, cx| { | ||
| 4238 | panel.width = serialized_panel.width; | ||
| 4239 | cx.notify(); | ||
| 4240 | }) | ||
| 4241 | } | ||
| 4242 | |||
| 4243 | panel | ||
| 4244 | }) | ||
| 4245 | } | ||
| 4144 | } | 4246 | } |
| 4145 | 4247 | ||
| 4146 | fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> { | 4248 | fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> { |
| @@ -4167,8 +4269,9 @@ impl Render for GitPanel { · 20 unmodified lines | |||
| 4167 | let has_write_access = self.has_write_access(cx); | 4269 | let has_write_access = self.has_write_access(cx); |
| 4168 | 4270 | ||
| 4169 | let has_co_authors = room.map_or(false, |room| { | 4271 | let has_co_authors = room.map_or(false, |room| { |
| 4170 | room.read(cx) | 4272 | self.load_local_committer(cx); |
| 4171 | .remote_participants() | 4273 | let room = room.read(cx); |
| 4274 | room.remote_participants() | ||
| 4172 | .values() | 4275 | .values() |
| 4173 | .any(|remote_participant| remote_participant.can_write()) | 4276 | .any(|remote_participant| remote_participant.can_write()) |
| 4174 | }); | 4277 | }); |
| @@ -4852,7 +4955,7 @@ impl Component for PanelRepoFooter { · 677 unmodified lines | |||
| 4852 | #[cfg(test)] | 4955 | #[cfg(test)] |
| 4853 | mod tests { | 4956 | mod tests { |
| 4854 | use git::status::StatusCode; | 4957 | use git::status::StatusCode; |
| 4855 | use gpui::TestAppContext; | 4958 | use gpui::{TestAppContext, VisualTestContext}; |
| 4856 | use project::{FakeFs, WorktreeSettings}; | 4959 | use project::{FakeFs, WorktreeSettings}; |
| 4857 | use serde_json::json; | 4960 | use serde_json::json; |
| 4858 | use settings::SettingsStore; | 4961 | use settings::SettingsStore; |
| @@ -4916,8 +5019,9 @@ mod tests { · 57 unmodified lines | |||
| 4916 | 5019 | ||
| 4917 | let project = | 5020 | let project = |
| 4918 | Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await; | 5021 | Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await; |
| 4919 | let (workspace, cx) = | 5022 | let workspace = |
| 4920 | cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); | 5023 | cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); |
| 5024 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 4921 | 5025 | ||
| 4922 | cx.read(|cx| { | 5026 | cx.read(|cx| { |
| 4923 | project | 5027 | project |
| @@ -4934,10 +5038,7 @@ mod tests { · 10 unmodified lines | |||
| 4934 | 5038 | ||
| 4935 | cx.executor().run_until_parked(); | 5039 | cx.executor().run_until_parked(); |
| 4936 | 5040 | ||
| 4937 | let app_state = workspace.read_with(cx, |workspace, _| workspace.app_state().clone()); | 5041 | let panel = workspace.update(cx, GitPanel::new).unwrap(); |
| 4938 | let panel = cx.new_window_entity(|window, cx| { | ||
| 4939 | GitPanel::new(workspace.clone(), project.clone(), app_state, window, cx) | ||
| 4940 | }); | ||
| 4941 | 5042 | ||
| 4942 | let handle = cx.update_window_entity(&panel, |panel, _, _| { | 5043 | let handle = cx.update_window_entity(&panel, |panel, _, _| { |
| 4943 | std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) | 5044 | std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) |
| @@ -10,7 +10,7 @@ use git::{ · 9 unmodified lines | |||
| 10 | status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode}, | 10 | status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode}, |
| 11 | }; | 11 | }; |
| 12 | use git_panel_settings::GitPanelSettings; | 12 | use git_panel_settings::GitPanelSettings; |
| 13 | use gpui::{Action, App, FocusHandle, actions}; | 13 | use gpui::{Action, App, Context, FocusHandle, Window, actions}; |
| 14 | use onboarding::GitOnboardingModal; | 14 | use onboarding::GitOnboardingModal; |
| 15 | use project_diff::ProjectDiff; | 15 | use project_diff::ProjectDiff; |
| 16 | use ui::prelude::*; | 16 | use ui::prelude::*; |
| @@ -142,10 +142,41 @@ pub fn init(cx: &mut App) { · 125 unmodified lines | |||
| 142 | panel.git_init(window, cx); | 142 | panel.git_init(window, cx); |
| 143 | }); | 143 | }); |
| 144 | }); | 144 | }); |
| 145 | workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| { | ||
| 146 | open_modified_files(workspace, window, cx); | ||
| 147 | }); | ||
| 145 | }) | 148 | }) |
| 146 | .detach(); | 149 | .detach(); |
| 147 | } | 150 | } |
| 148 | 151 | ||
| 152 | fn open_modified_files( | ||
| 153 | workspace: &mut Workspace, | ||
| 154 | window: &mut Window, | ||
| 155 | cx: &mut Context<Workspace>, | ||
| 156 | ) { | ||
| 157 | let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else { | ||
| 158 | return; | ||
| 159 | }; | ||
| 160 | let modified_paths: Vec<_> = panel.update(cx, |panel, cx| { | ||
| 161 | let Some(repo) = panel.active_repository.as_ref() else { | ||
| 162 | return Vec::new(); | ||
| 163 | }; | ||
| 164 | let repo = repo.read(cx); | ||
| 165 | repo.cached_status() | ||
| 166 | .filter_map(|entry| { | ||
| 167 | if entry.status.is_modified() { | ||
| 168 | repo.repo_path_to_project_path(&entry.repo_path, cx) | ||
| 169 | } else { | ||
| 170 | None | ||
| 171 | } | ||
| 172 | }) | ||
| 173 | .collect() | ||
| 174 | }); | ||
| 175 | for path in modified_paths { | ||
| 176 | workspace.open_path(path, None, true, window, cx).detach(); | ||
| 177 | } | ||
| 178 | } | ||
| 179 | |||
| 149 | pub fn git_status_icon(status: FileStatus) -> impl IntoElement { | 180 | pub fn git_status_icon(status: FileStatus) -> impl IntoElement { |
| 150 | GitStatusIcon::new(status) | 181 | GitStatusIcon::new(status) |
| 151 | } | 182 | } |
| @@ -37,7 +37,7 @@ use util::ResultExt as _; · 36 unmodified lines | |||
| 37 | use workspace::{ | 37 | use workspace::{ |
| 38 | CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, | 38 | CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, |
| 39 | ToolbarItemView, Workspace, | 39 | ToolbarItemView, Workspace, |
| 40 | item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, | 40 | item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, |
| 41 | searchable::SearchableItemHandle, | 41 | searchable::SearchableItemHandle, |
| 42 | }; | 42 | }; |
| 43 | 43 | ||
| @@ -632,12 +632,12 @@ impl Item for ProjectDiff { · 588 unmodified lines | |||
| 632 | 632 | ||
| 633 | fn save( | 633 | fn save( |
| 634 | &mut self, | 634 | &mut self, |
| 635 | format: bool, | 635 | options: SaveOptions, |
| 636 | project: Entity<Project>, | 636 | project: Entity<Project>, |
| 637 | window: &mut Window, | 637 | window: &mut Window, |
| 638 | cx: &mut Context<Self>, | 638 | cx: &mut Context<Self>, |
| 639 | ) -> Task<Result<()>> { | 639 | ) -> Task<Result<()>> { |
| 640 | self.editor.save(format, project, window, cx) | 640 | self.editor.save(options, project, window, cx) |
| 641 | } | 641 | } |
| 642 | 642 | ||
| 643 | fn save_as( | 643 | fn save_as( |
| @@ -1565,7 +1565,15 @@ mod tests { · 921 unmodified lines | |||
| 1565 | 1565 | ||
| 1566 | cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| { | 1566 | cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| { |
| 1567 | buffer_editor.set_text("different\n", window, cx); | 1567 | buffer_editor.set_text("different\n", window, cx); |
| 1568 | buffer_editor.save(false, project.clone(), window, cx) | 1568 | buffer_editor.save( |
| 1569 | SaveOptions { | ||
| 1570 | format: false, | ||
| 1571 | autosave: false, | ||
| 1572 | }, | ||
| 1573 | project.clone(), | ||
| 1574 | window, | ||
| 1575 | cx, | ||
| 1576 | ) | ||
| 1569 | }) | 1577 | }) |
| 1570 | .await | 1578 | .await |
| 1571 | .unwrap(); | 1579 | .unwrap(); |
| @@ -1,8 +1,4 @@ | |||
| 1 | use std::{ | 1 | use std::{ops::Range, rc::Rc, time::Duration}; |
| 2 | ops::Range, | ||
| 3 | rc::Rc, | ||
| 4 | time::{Duration, Instant}, | ||
| 5 | }; | ||
| 6 | 2 | ||
| 7 | use gpui::{ | 3 | use gpui::{ |
| 8 | App, Application, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, | 4 | App, Application, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, |
| @@ -22,7 +18,7 @@ pub struct Quote { · 13 unmodified lines | |||
| 22 | open: f64, | 18 | open: f64, |
| 23 | high: f64, | 19 | high: f64, |
| 24 | low: f64, | 20 | low: f64, |
| 25 | timestamp: Instant, | 21 | timestamp: Duration, |
| 26 | volume: i64, | 22 | volume: i64, |
| 27 | turnover: f64, | 23 | turnover: f64, |
| 28 | ttm: f64, | 24 | ttm: f64, |
| @@ -50,8 +46,7 @@ impl Quote { · 21 unmodified lines | |||
| 50 | let open = prev_close + rng.gen_range(-3.0..3.0); | 46 | let open = prev_close + rng.gen_range(-3.0..3.0); |
| 51 | let high = (prev_close + rng.gen_range::<f64, _>(0.0..10.0)).max(open); | 47 | let high = (prev_close + rng.gen_range::<f64, _>(0.0..10.0)).max(open); |
| 52 | let low = (prev_close - rng.gen_range::<f64, _>(0.0..10.0)).min(open); | 48 | let low = (prev_close - rng.gen_range::<f64, _>(0.0..10.0)).min(open); |
| 53 | // Randomize the timestamp in the past 24 hours | 49 | let timestamp = Duration::from_secs(rng.gen_range(0..86400)); |
| 54 | let timestamp = Instant::now() - Duration::from_secs(rng.gen_range(0..86400)); | ||
| 55 | let volume = rng.gen_range(1_000_000..100_000_000); | 50 | let volume = rng.gen_range(1_000_000..100_000_000); |
| 56 | let turnover = last_done * volume as f64; | 51 | let turnover = last_done * volume as f64; |
| 57 | let symbol = { | 52 | let symbol = { |
| @@ -170,7 +165,7 @@ impl TableRow { · 112 unmodified lines | |||
| 170 | .child(format!("{:.2}%", self.quote.change())), | 165 | .child(format!("{:.2}%", self.quote.change())), |
| 171 | "timestamp" => div() | 166 | "timestamp" => div() |
| 172 | .text_color(color) | 167 | .text_color(color) |
| 173 | .child(format!("{:?}", self.quote.timestamp.elapsed().as_secs())), | 168 | .child(format!("{:?}", self.quote.timestamp.as_secs())), |
| 174 | "open" => div() | 169 | "open" => div() |
| 175 | .text_color(color) | 170 | .text_color(color) |
| 176 | .child(format!("{:.2}", self.quote.open)), | 171 | .child(format!("{:.2}", self.quote.open)), |
| @@ -378,8 +373,6 @@ impl DataTable { · 201 unmodified lines | |||
| 378 | 373 | ||
| 379 | impl Render for DataTable { | 374 | impl Render for DataTable { |
| 380 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | 375 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| 381 | let entity = cx.entity(); | ||
| 382 | |||
| 383 | div() | 376 | div() |
| 384 | .font_family(".SystemUIFont") | 377 | .font_family(".SystemUIFont") |
| 385 | .bg(gpui::white()) | 378 | .bg(gpui::white()) |
| @@ -431,8 +424,10 @@ impl Render for DataTable { · 45 unmodified lines | |||
| 431 | .relative() | 424 | .relative() |
| 432 | .size_full() | 425 | .size_full() |
| 433 | .child( | 426 | .child( |
| 434 | uniform_list(entity, "items", self.quotes.len(), { | 427 | uniform_list( |
| 435 | move |this, range, _, _| { | 428 | "items", |
| 429 | self.quotes.len(), | ||
| 430 | cx.processor(move |this, range: Range<usize>, _, _| { | ||
| 436 | this.visible_range = range.clone(); | 431 | this.visible_range = range.clone(); |
| 437 | let mut items = Vec::with_capacity(range.end - range.start); | 432 | let mut items = Vec::with_capacity(range.end - range.start); |
| 438 | for i in range { | 433 | for i in range { |
| @@ -441,8 +436,8 @@ impl Render for DataTable { · 2 unmodified lines | |||
| 441 | } | 436 | } |
| 442 | } | 437 | } |
| 443 | items | 438 | items |
| 444 | } | 439 | }), |
| 445 | }) | 440 | ) |
| 446 | .size_full() | 441 | .size_full() |
| 447 | .track_scroll(self.scroll_handle.clone()), | 442 | .track_scroll(self.scroll_handle.clone()), |
| 448 | ) | 443 | ) |
| @@ -9,10 +9,9 @@ impl Render for UniformListExample { · 8 unmodified lines | |||
| 9 | fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | 9 | fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| 10 | div().size_full().bg(rgb(0xffffff)).child( | 10 | div().size_full().bg(rgb(0xffffff)).child( |
| 11 | uniform_list( | 11 | uniform_list( |
| 12 | cx.entity().clone(), | ||
| 13 | "entries", | 12 | "entries", |
| 14 | 50, | 13 | 50, |
| 15 | |_this, range, _window, _cx| { | 14 | cx.processor(|_this, range, _window, _cx| { |
| 16 | let mut items = Vec::new(); | 15 | let mut items = Vec::new(); |
| 17 | for ix in range { | 16 | for ix in range { |
| 18 | let item = ix + 1; | 17 | let item = ix + 1; |
| @@ -29,7 +28,7 @@ impl Render for UniformListExample { · 10 unmodified lines | |||
| 29 | ); | 28 | ); |
| 30 | } | 29 | } |
| 31 | items | 30 | items |
| 32 | }, | 31 | }), |
| 33 | ) | 32 | ) |
| 34 | .h_full(), | 33 | .h_full(), |
| 35 | ) | 34 | ) |
| @@ -64,7 +64,7 @@ pub struct AppCell { · 63 unmodified lines | |||
| 64 | impl AppCell { | 64 | impl AppCell { |
| 65 | #[doc(hidden)] | 65 | #[doc(hidden)] |
| 66 | #[track_caller] | 66 | #[track_caller] |
| 67 | pub fn borrow(&self) -> AppRef { | 67 | pub fn borrow(&self) -> AppRef<'_> { |
| 68 | if option_env!("TRACK_THREAD_BORROWS").is_some() { | 68 | if option_env!("TRACK_THREAD_BORROWS").is_some() { |
| 69 | let thread_id = std::thread::current().id(); | 69 | let thread_id = std::thread::current().id(); |
| 70 | eprintln!("borrowed {thread_id:?}"); | 70 | eprintln!("borrowed {thread_id:?}"); |
| @@ -74,7 +74,7 @@ impl AppCell { · 3 unmodified lines | |||
| 74 | 74 | ||
| 75 | #[doc(hidden)] | 75 | #[doc(hidden)] |
| 76 | #[track_caller] | 76 | #[track_caller] |
| 77 | pub fn borrow_mut(&self) -> AppRefMut { | 77 | pub fn borrow_mut(&self) -> AppRefMut<'_> { |
| 78 | if option_env!("TRACK_THREAD_BORROWS").is_some() { | 78 | if option_env!("TRACK_THREAD_BORROWS").is_some() { |
| 79 | let thread_id = std::thread::current().id(); | 79 | let thread_id = std::thread::current().id(); |
| 80 | eprintln!("borrowed {thread_id:?}"); | 80 | eprintln!("borrowed {thread_id:?}"); |
| @@ -84,7 +84,7 @@ impl AppCell { · 3 unmodified lines | |||
| 84 | 84 | ||
| 85 | #[doc(hidden)] | 85 | #[doc(hidden)] |
| 86 | #[track_caller] | 86 | #[track_caller] |
| 87 | pub fn try_borrow_mut(&self) -> Result<AppRefMut, BorrowMutError> { | 87 | pub fn try_borrow_mut(&self) -> Result<AppRefMut<'_>, BorrowMutError> { |
| 88 | if option_env!("TRACK_THREAD_BORROWS").is_some() { | 88 | if option_env!("TRACK_THREAD_BORROWS").is_some() { |
| 89 | let thread_id = std::thread::current().id(); | 89 | let thread_id = std::thread::current().id(); |
| 90 | eprintln!("borrowed {thread_id:?}"); | 90 | eprintln!("borrowed {thread_id:?}"); |
| @@ -225,6 +225,18 @@ impl<'a, T: 'static> Context<'a, T> { · 224 unmodified lines | |||
| 225 | } | 225 | } |
| 226 | } | 226 | } |
| 227 | 227 | ||
| 228 | /// Convenience method for producing view state in a closure. | ||
| 229 | /// See `listener` for more details. | ||
| 230 | pub fn processor<E, R>( | ||
| 231 | &self, | ||
| 232 | f: impl Fn(&mut T, E, &mut Window, &mut Context<T>) -> R + 'static, | ||
| 233 | ) -> impl Fn(E, &mut Window, &mut App) -> R + 'static { | ||
| 234 | let view = self.entity(); | ||
| 235 | move |e: E, window: &mut Window, cx: &mut App| { | ||
| 236 | view.update(cx, |view, cx| f(view, e, window, cx)) | ||
| 237 | } | ||
| 238 | } | ||
| 239 | |||
| 228 | /// Run something using this entity and cx, when the returned struct is dropped | 240 | /// Run something using this entity and cx, when the returned struct is dropped |
| 229 | pub fn on_drop( | 241 | pub fn on_drop( |
| 230 | &self, | 242 | &self, |
| @@ -5,10 +5,10 @@ · 4 unmodified lines | |||
| 5 | //! elements with uniform height. | 5 | //! elements with uniform height. |
| 6 | 6 | ||
| 7 | use crate::{ | 7 | use crate::{ |
| 8 | AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, Element, ElementId, Entity, | 8 | AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId, |
| 9 | GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, | 9 | Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId, |
| 10 | IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Render, ScrollHandle, Size, | 10 | ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window, |
| 11 | StyleRefinement, Styled, Window, point, size, | 11 | point, size, |
| 12 | }; | 12 | }; |
| 13 | use smallvec::SmallVec; | 13 | use smallvec::SmallVec; |
| 14 | use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; | 14 | use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; |
| @@ -19,28 +19,23 @@ use super::ListHorizontalSizingBehavior; · 4 unmodified lines | |||
| 19 | /// When rendered into a container with overflow-y: hidden and a fixed (or max) height, | 19 | /// When rendered into a container with overflow-y: hidden and a fixed (or max) height, |
| 20 | /// uniform_list will only render the visible subset of items. | 20 | /// uniform_list will only render the visible subset of items. |
| 21 | #[track_caller] | 21 | #[track_caller] |
| 22 | pub fn uniform_list<I, R, V>( | 22 | pub fn uniform_list<R>( |
| 23 | view: Entity<V>, | 23 | id: impl Into<ElementId>, |
| 24 | id: I, | ||
| 25 | item_count: usize, | 24 | item_count: usize, |
| 26 | f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>, | 25 | f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>, |
| 27 | ) -> UniformList | 26 | ) -> UniformList |
| 28 | where | 27 | where |
| 29 | I: Into<ElementId>, | ||
| 30 | R: IntoElement, | 28 | R: IntoElement, |
| 31 | V: Render, | ||
| 32 | { | 29 | { |
| 33 | let id = id.into(); | 30 | let id = id.into(); |
| 34 | let mut base_style = StyleRefinement::default(); | 31 | let mut base_style = StyleRefinement::default(); |
| 35 | base_style.overflow.y = Some(Overflow::Scroll); | 32 | base_style.overflow.y = Some(Overflow::Scroll); |
| 36 | 33 | ||
| 37 | let render_range = move |range, window: &mut Window, cx: &mut App| { | 34 | let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| { |
| 38 | view.update(cx, |this, cx| { | 35 | f(range, window, cx) |
| 39 | f(this, range, window, cx) | 36 | .into_iter() |
| 40 | .into_iter() | 37 | .map(|component| component.into_any_element()) |
| 41 | .map(|component| component.into_any_element()) | 38 | .collect() |
| 42 | .collect() | ||
| 43 | }) | ||
| 44 | }; | 39 | }; |
| 45 | 40 | ||
| 46 | UniformList { | 41 | UniformList { |
| @@ -255,7 +255,7 @@ pub trait VisualContext: AppContext { · 254 unmodified lines | |||
| 255 | update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R, | 255 | update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R, |
| 256 | ) -> Self::Result<R>; | 256 | ) -> Self::Result<R>; |
| 257 | 257 | ||
| 258 | /// Update a view with the given callback | 258 | /// Create a new entity, with access to `Window`. |
| 259 | fn new_window_entity<T: 'static>( | 259 | fn new_window_entity<T: 'static>( |
| 260 | &mut self, | 260 | &mut self, |
| 261 | build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T, | 261 | build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T, |
| @@ -142,7 +142,11 @@ pub fn guess_compositor() -> &'static str { · 141 unmodified lines | |||
| 142 | 142 | ||
| 143 | #[cfg(target_os = "windows")] | 143 | #[cfg(target_os = "windows")] |
| 144 | pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> { | 144 | pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> { |
| 145 | Rc::new(WindowsPlatform::new()) | 145 | Rc::new( |
| 146 | WindowsPlatform::new() | ||
| 147 | .inspect_err(|err| show_error("Error: Zed failed to launch", err.to_string())) | ||
| 148 | .unwrap(), | ||
| 149 | ) | ||
| 146 | } | 150 | } |
| 147 | 151 | ||
| 148 | pub(crate) trait Platform: 'static { | 152 | pub(crate) trait Platform: 'static { |
| @@ -718,7 +722,7 @@ impl<T> ops::Index<usize> for AtlasTextureList<T> { · 569 unmodified lines | |||
| 718 | 722 | ||
| 719 | impl<T> AtlasTextureList<T> { | 723 | impl<T> AtlasTextureList<T> { |
| 720 | #[allow(unused)] | 724 | #[allow(unused)] |
| 721 | fn drain(&mut self) -> std::vec::Drain<Option<T>> { | 725 | fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> { |
| 722 | self.free_list.clear(); | 726 | self.free_list.clear(); |
| 723 | self.textures.drain(..) | 727 | self.textures.drain(..) |
| 724 | } | 728 | } |
| @@ -342,7 +342,7 @@ impl BladeRenderer { · 341 unmodified lines | |||
| 342 | let surface = context | 342 | let surface = context |
| 343 | .gpu | 343 | .gpu |
| 344 | .create_surface_configured(window, surface_config) | 344 | .create_surface_configured(window, surface_config) |
| 345 | .unwrap(); | 345 | .map_err(|err| anyhow::anyhow!("Failed to create surface: {err:?}"))?; |
| 346 | 346 | ||
| 347 | let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc { | 347 | let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc { |
| 348 | name: "main", | 348 | name: "main", |
| @@ -52,7 +52,7 @@ impl LinuxClient for HeadlessClient { · 51 unmodified lines | |||
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> { | 54 | fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> { |
| 55 | Box::new(LinuxKeyboardLayout::new("unknown".to_string())) | 55 | Box::new(LinuxKeyboardLayout::new("unknown".into())) |
| 56 | } | 56 | } |
| 57 | 57 | ||
| 58 | fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> { | 58 | fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> { |
| @@ -1,21 +1,22 @@ | |||
| 1 | use crate::PlatformKeyboardLayout; | 1 | use crate::{PlatformKeyboardLayout, SharedString}; |
| 2 | 2 | ||
| 3 | #[derive(Clone)] | ||
| 3 | pub(crate) struct LinuxKeyboardLayout { | 4 | pub(crate) struct LinuxKeyboardLayout { |
| 4 | id: String, | 5 | name: SharedString, |
| 5 | } | 6 | } |
| 6 | 7 | ||
| 7 | impl PlatformKeyboardLayout for LinuxKeyboardLayout { | 8 | impl PlatformKeyboardLayout for LinuxKeyboardLayout { |
| 8 | fn id(&self) -> &str { | 9 | fn id(&self) -> &str { |
| 9 | &self.id | 10 | &self.name |
| 10 | } | 11 | } |
| 11 | 12 | ||
| 12 | fn name(&self) -> &str { | 13 | fn name(&self) -> &str { |
| 13 | &self.id | 14 | &self.name |
| 14 | } | 15 | } |
| 15 | } | 16 | } |
| 16 | 17 | ||
| 17 | impl LinuxKeyboardLayout { | 18 | impl LinuxKeyboardLayout { |
| 18 | pub(crate) fn new(id: String) -> Self { | 19 | pub(crate) fn new(name: SharedString) -> Self { |
| 19 | Self { id } | 20 | Self { name } |
| 20 | } | 21 | } |
| 21 | } | 22 | } |
| @@ -71,17 +71,6 @@ use super::{ · 70 unmodified lines | |||
| 71 | window::{ImeInput, WaylandWindowStatePtr}, | 71 | window::{ImeInput, WaylandWindowStatePtr}, |
| 72 | }; | 72 | }; |
| 73 | 73 | ||
| 74 | use crate::platform::linux::{ | ||
| 75 | LinuxClient, get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd, | ||
| 76 | reveal_path_internal, | ||
| 77 | wayland::{ | ||
| 78 | clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES}, | ||
| 79 | cursor::Cursor, | ||
| 80 | serial::{SerialKind, SerialTracker}, | ||
| 81 | window::WaylandWindow, | ||
| 82 | }, | ||
| 83 | xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, | ||
| 84 | }; | ||
| 85 | use crate::platform::{PlatformWindow, blade::BladeContext}; | 74 | use crate::platform::{PlatformWindow, blade::BladeContext}; |
| 86 | use crate::{ | 75 | use crate::{ |
| 87 | AnyWindowHandle, Bounds, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId, | 76 | AnyWindowHandle, Bounds, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId, |
| @@ -91,10 +80,26 @@ use crate::{ · 3 unmodified lines | |||
| 91 | PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScreenCaptureSource, | 80 | PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScreenCaptureSource, |
| 92 | ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size, | 81 | ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size, |
| 93 | }; | 82 | }; |
| 83 | use crate::{ | ||
| 84 | SharedString, | ||
| 85 | platform::linux::{ | ||
| 86 | LinuxClient, get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd, | ||
| 87 | reveal_path_internal, | ||
| 88 | wayland::{ | ||
| 89 | clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES}, | ||
| 90 | cursor::Cursor, | ||
| 91 | serial::{SerialKind, SerialTracker}, | ||
| 92 | window::WaylandWindow, | ||
| 93 | }, | ||
| 94 | xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, | ||
| 95 | }, | ||
| 96 | }; | ||
| 94 | 97 | ||
| 95 | /// Used to convert evdev scancode to xkb scancode | 98 | /// Used to convert evdev scancode to xkb scancode |
| 96 | const MIN_KEYCODE: u32 = 8; | 99 | const MIN_KEYCODE: u32 = 8; |
| 97 | 100 | ||
| 101 | const UNKNOWN_KEYBOARD_LAYOUT_NAME: SharedString = SharedString::new_static("unknown"); | ||
| 102 | |||
| 98 | #[derive(Clone)] | 103 | #[derive(Clone)] |
| 99 | pub struct Globals { | 104 | pub struct Globals { |
| 100 | pub qh: QueueHandle<WaylandClientStatePtr>, | 105 | pub qh: QueueHandle<WaylandClientStatePtr>, |
| @@ -205,6 +210,7 @@ pub(crate) struct WaylandClientState { · 104 unmodified lines | |||
| 205 | // Output to scale mapping | 210 | // Output to scale mapping |
| 206 | outputs: HashMap<ObjectId, Output>, | 211 | outputs: HashMap<ObjectId, Output>, |
| 207 | in_progress_outputs: HashMap<ObjectId, InProgressOutput>, | 212 | in_progress_outputs: HashMap<ObjectId, InProgressOutput>, |
| 213 | keyboard_layout: LinuxKeyboardLayout, | ||
| 208 | keymap_state: Option<xkb::State>, | 214 | keymap_state: Option<xkb::State>, |
| 209 | compose_state: Option<xkb::compose::State>, | 215 | compose_state: Option<xkb::compose::State>, |
| 210 | drag: DragState, | 216 | drag: DragState, |
| @@ -335,6 +341,35 @@ impl WaylandClientStatePtr { · 124 unmodified lines | |||
| 335 | text_input.commit(); | 341 | text_input.commit(); |
| 336 | } | 342 | } |
| 337 | 343 | ||
| 344 | pub fn handle_keyboard_layout_change(&self) { | ||
| 345 | let client = self.get_client(); | ||
| 346 | let mut state = client.borrow_mut(); | ||
| 347 | let changed = if let Some(keymap_state) = &state.keymap_state { | ||
| 348 | let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); | ||
| 349 | let keymap = keymap_state.get_keymap(); | ||
| 350 | let layout_name = keymap.layout_get_name(layout_idx); | ||
| 351 | let changed = layout_name != state.keyboard_layout.name(); | ||
| 352 | if changed { | ||
| 353 | state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); | ||
| 354 | } | ||
| 355 | changed | ||
| 356 | } else { | ||
| 357 | let changed = &UNKNOWN_KEYBOARD_LAYOUT_NAME != state.keyboard_layout.name(); | ||
| 358 | if changed { | ||
| 359 | state.keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME); | ||
| 360 | } | ||
| 361 | changed | ||
| 362 | }; | ||
| 363 | if changed { | ||
| 364 | if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { | ||
| 365 | drop(state); | ||
| 366 | callback(); | ||
| 367 | state = client.borrow_mut(); | ||
| 368 | state.common.callbacks.keyboard_layout_change = Some(callback); | ||
| 369 | } | ||
| 370 | } | ||
| 371 | } | ||
| 372 | |||
| 338 | pub fn drop_window(&self, surface_id: &ObjectId) { | 373 | pub fn drop_window(&self, surface_id: &ObjectId) { |
| 339 | let mut client = self.get_client(); | 374 | let mut client = self.get_client(); |
| 340 | let mut state = client.borrow_mut(); | 375 | let mut state = client.borrow_mut(); |
| @@ -533,6 +568,7 @@ impl WaylandClient { · 192 unmodified lines | |||
| 533 | in_progress_outputs, | 568 | in_progress_outputs, |
| 534 | windows: HashMap::default(), | 569 | windows: HashMap::default(), |
| 535 | common, | 570 | common, |
| 571 | keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME), | ||
| 536 | keymap_state: None, | 572 | keymap_state: None, |
| 537 | compose_state: None, | 573 | compose_state: None, |
| 538 | drag: DragState { | 574 | drag: DragState { |
| @@ -590,17 +626,7 @@ impl WaylandClient { · 51 unmodified lines | |||
| 590 | 626 | ||
| 591 | impl LinuxClient for WaylandClient { | 627 | impl LinuxClient for WaylandClient { |
| 592 | fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> { | 628 | fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> { |
| 593 | let state = self.0.borrow(); | 629 | Box::new(self.0.borrow().keyboard_layout.clone()) |
| 594 | let id = if let Some(keymap_state) = &state.keymap_state { | ||
| 595 | let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); | ||
| 596 | keymap_state | ||
| 597 | .get_keymap() | ||
| 598 | .layout_get_name(layout_idx) | ||
| 599 | .to_string() | ||
| 600 | } else { | ||
| 601 | "unknown".to_string() | ||
| 602 | }; | ||
| 603 | Box::new(LinuxKeyboardLayout::new(id)) | ||
| 604 | } | 630 | } |
| 605 | 631 | ||
| 606 | fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> { | 632 | fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> { |
| @@ -1181,13 +1207,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { · 574 unmodified lines | |||
| 1181 | }; | 1207 | }; |
| 1182 | state.keymap_state = Some(xkb::State::new(&keymap)); | 1208 | state.keymap_state = Some(xkb::State::new(&keymap)); |
| 1183 | state.compose_state = get_xkb_compose_state(&xkb_context); | 1209 | state.compose_state = get_xkb_compose_state(&xkb_context); |
| 1210 | drop(state); | ||
| 1184 | 1211 | ||
| 1185 | if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { | 1212 | this.handle_keyboard_layout_change(); |
| 1186 | drop(state); | ||
| 1187 | callback(); | ||
| 1188 | state = client.borrow_mut(); | ||
| 1189 | state.common.callbacks.keyboard_layout_change = Some(callback); | ||
| 1190 | } | ||
| 1191 | } | 1213 | } |
| 1192 | wl_keyboard::Event::Enter { surface, .. } => { | 1214 | wl_keyboard::Event::Enter { surface, .. } => { |
| 1193 | state.keyboard_focused_window = get_window(&mut state, &surface.id()); | 1215 | state.keyboard_focused_window = get_window(&mut state, &surface.id()); |
| @@ -1230,26 +1252,18 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { · 36 unmodified lines | |||
| 1230 | keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); | 1252 | keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); |
| 1231 | state.modifiers = Modifiers::from_xkb(keymap_state); | 1253 | state.modifiers = Modifiers::from_xkb(keymap_state); |
| 1232 | 1254 | ||
| 1233 | if group != old_layout { | 1255 | if let Some(focused_window) = focused_window { |
| 1234 | if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() | 1256 | let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent { |
| 1235 | { | 1257 | modifiers: state.modifiers, |
| 1236 | drop(state); | 1258 | }); |
| 1237 | callback(); | ||
| 1238 | state = client.borrow_mut(); | ||
| 1239 | state.common.callbacks.keyboard_layout_change = Some(callback); | ||
| 1240 | } | ||
| 1241 | } | ||
| 1242 | |||
| 1243 | let Some(focused_window) = focused_window else { | ||
| 1244 | return; | ||
| 1245 | }; | ||
| 1246 | 1259 | ||
| 1247 | let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent { | 1260 | drop(state); |
| 1248 | modifiers: state.modifiers, | 1261 | focused_window.handle_input(input); |
| 1249 | }); | 1262 | } |
| 1250 | 1263 | ||
| 1251 | drop(state); | 1264 | if group != old_layout { |
| 1252 | focused_window.handle_input(input); | 1265 | this.handle_keyboard_layout_change(); |
| 1266 | } | ||
| 1253 | } | 1267 | } |
| 1254 | wl_keyboard::Event::Key { | 1268 | wl_keyboard::Event::Key { |
| 1255 | serial, | 1269 | serial, |
| @@ -1374,6 +1388,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { · 118 unmodified lines | |||
| 1374 | } | 1388 | } |
| 1375 | } | 1389 | } |
| 1376 | } | 1390 | } |
| 1391 | |||
| 1377 | impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr { | 1392 | impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr { |
| 1378 | fn event( | 1393 | fn event( |
| 1379 | this: &mut Self, | 1394 | this: &mut Self, |
| @@ -28,7 +28,7 @@ use x11rb::{ · 27 unmodified lines | |||
| 28 | protocol::xkb::ConnectionExt as _, | 28 | protocol::xkb::ConnectionExt as _, |
| 29 | protocol::xproto::{ | 29 | protocol::xproto::{ |
| 30 | AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent, | 30 | AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent, |
| 31 | ConnectionExt as _, EventMask, KeyPressEvent, | 31 | ConnectionExt as _, EventMask, KeyPressEvent, Visibility, |
| 32 | }, | 32 | }, |
| 33 | protocol::{Event, randr, render, xinput, xkb, xproto}, | 33 | protocol::{Event, randr, render, xinput, xkb, xproto}, |
| 34 | resource_manager::Database, | 34 | resource_manager::Database, |
| @@ -78,7 +78,10 @@ pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1; · 43 unmodified lines | |||
| 78 | 78 | ||
| 79 | pub(crate) struct WindowRef { | 79 | pub(crate) struct WindowRef { |
| 80 | window: X11WindowStatePtr, | 80 | window: X11WindowStatePtr, |
| 81 | refresh_event_token: RegistrationToken, | 81 | refresh_state: Option<RefreshState>, |
| 82 | expose_event_received: bool, | ||
| 83 | last_visibility: Visibility, | ||
| 84 | is_mapped: bool, | ||
| 82 | } | 85 | } |
| 83 | 86 | ||
| 84 | impl WindowRef { | 87 | impl WindowRef { |
| @@ -95,6 +98,16 @@ impl Deref for WindowRef { · 10 unmodified lines | |||
| 95 | } | 98 | } |
| 96 | } | 99 | } |
| 97 | 100 | ||
| 101 | enum RefreshState { | ||
| 102 | Hidden { | ||
| 103 | refresh_rate: Duration, | ||
| 104 | }, | ||
| 105 | PeriodicRefresh { | ||
| 106 | refresh_rate: Duration, | ||
| 107 | event_loop_token: RegistrationToken, | ||
| 108 | }, | ||
| 109 | } | ||
| 110 | |||
| 98 | #[derive(Debug)] | 111 | #[derive(Debug)] |
| 99 | #[non_exhaustive] | 112 | #[non_exhaustive] |
| 100 | pub enum EventHandlerError { | 113 | pub enum EventHandlerError { |
| @@ -185,6 +198,7 @@ pub struct X11ClientState { · 84 unmodified lines | |||
| 185 | pub(crate) keyboard_focused_window: Option<xproto::Window>, | 198 | pub(crate) keyboard_focused_window: Option<xproto::Window>, |
| 186 | pub(crate) xkb: xkbc::State, | 199 | pub(crate) xkb: xkbc::State, |
| 187 | previous_xkb_state: XKBStateNotiy, | 200 | previous_xkb_state: XKBStateNotiy, |
| 201 | keyboard_layout: LinuxKeyboardLayout, | ||
| 188 | pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>, | 202 | pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>, |
| 189 | pub(crate) xim_handler: Option<XimHandler>, | 203 | pub(crate) xim_handler: Option<XimHandler>, |
| 190 | pub modifiers: Modifiers, | 204 | pub modifiers: Modifiers, |
| @@ -220,7 +234,14 @@ impl X11ClientStatePtr { · 29 unmodified lines | |||
| 220 | let mut state = client.0.borrow_mut(); | 234 | let mut state = client.0.borrow_mut(); |
| 221 | 235 | ||
| 222 | if let Some(window_ref) = state.windows.remove(&x_window) { | 236 | if let Some(window_ref) = state.windows.remove(&x_window) { |
| 223 | state.loop_handle.remove(window_ref.refresh_event_token); | 237 | match window_ref.refresh_state { |
| 238 | Some(RefreshState::PeriodicRefresh { | ||
| 239 | event_loop_token, .. | ||
| 240 | }) => { | ||
| 241 | state.loop_handle.remove(event_loop_token); | ||
| 242 | } | ||
| 243 | _ => {} | ||
| 244 | } | ||
| 224 | } | 245 | } |
| 225 | if state.mouse_focused_window == Some(x_window) { | 246 | if state.mouse_focused_window == Some(x_window) { |
| 226 | state.mouse_focused_window = None; | 247 | state.mouse_focused_window = None; |
| @@ -349,13 +370,21 @@ impl X11Client { · 122 unmodified lines | |||
| 349 | let events = xkb::EventType::STATE_NOTIFY | 370 | let events = xkb::EventType::STATE_NOTIFY |
| 350 | | xkb::EventType::MAP_NOTIFY | 371 | | xkb::EventType::MAP_NOTIFY |
| 351 | | xkb::EventType::NEW_KEYBOARD_NOTIFY; | 372 | | xkb::EventType::NEW_KEYBOARD_NOTIFY; |
| 373 | let map_notify_parts = xkb::MapPart::KEY_TYPES | ||
| 374 | | xkb::MapPart::KEY_SYMS | ||
| 375 | | xkb::MapPart::MODIFIER_MAP | ||
| 376 | | xkb::MapPart::EXPLICIT_COMPONENTS | ||
| 377 | | xkb::MapPart::KEY_ACTIONS | ||
| 378 | | xkb::MapPart::KEY_BEHAVIORS | ||
| 379 | | xkb::MapPart::VIRTUAL_MODS | ||
| 380 | | xkb::MapPart::VIRTUAL_MOD_MAP; | ||
| 352 | xcb_connection | 381 | xcb_connection |
| 353 | .xkb_select_events( | 382 | .xkb_select_events( |
| 354 | xkb::ID::USE_CORE_KBD.into(), | 383 | xkb::ID::USE_CORE_KBD.into(), |
| 355 | 0u8.into(), | 384 | 0u8.into(), |
| 356 | events, | 385 | events, |
| 357 | 0u8.into(), | 386 | map_notify_parts, |
| 358 | 0u8.into(), | 387 | map_notify_parts, |
| 359 | &xkb::SelectEventsAux::new(), | 388 | &xkb::SelectEventsAux::new(), |
| 360 | ) | 389 | ) |
| 361 | .unwrap(); | 390 | .unwrap(); |
| @@ -373,17 +402,22 @@ impl X11Client { · 11 unmodified lines | |||
| 373 | xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id) | 402 | xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id) |
| 374 | }; | 403 | }; |
| 375 | let compose_state = get_xkb_compose_state(&xkb_context); | 404 | let compose_state = get_xkb_compose_state(&xkb_context); |
| 376 | let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap(); | 405 | let layout_idx = xkb_state.serialize_layout(STATE_LAYOUT_EFFECTIVE); |
| 406 | let layout_name = xkb_state | ||
| 407 | .get_keymap() | ||
| 408 | .layout_get_name(layout_idx) | ||
| 409 | .to_string(); | ||
| 410 | let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into()); | ||
| 377 | 411 | ||
| 378 | let gpu_context = BladeContext::new().expect("Unable to init GPU context"); | 412 | let gpu_context = BladeContext::new().expect("Unable to init GPU context"); |
| 379 | 413 | ||
| 414 | let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap(); | ||
| 380 | let scale_factor = resource_database | 415 | let scale_factor = resource_database |
| 381 | .get_value("Xft.dpi", "Xft.dpi") | 416 | .get_value("Xft.dpi", "Xft.dpi") |
| 382 | .ok() | 417 | .ok() |
| 383 | .flatten() | 418 | .flatten() |
| 384 | .map(|dpi: f32| dpi / 96.0) | 419 | .map(|dpi: f32| dpi / 96.0) |
| 385 | .unwrap_or(1.0); | 420 | .unwrap_or(1.0); |
| 386 | |||
| 387 | let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database) | 421 | let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database) |
| 388 | .unwrap() | 422 | .unwrap() |
| 389 | .reply() | 423 | .reply() |
| @@ -461,6 +495,7 @@ impl X11Client { · 71 unmodified lines | |||
| 461 | keyboard_focused_window: None, | 495 | keyboard_focused_window: None, |
| 462 | xkb: xkb_state, | 496 | xkb: xkb_state, |
| 463 | previous_xkb_state: XKBStateNotiy::default(), | 497 | previous_xkb_state: XKBStateNotiy::default(), |
| 498 | keyboard_layout, | ||
| 464 | ximc, | 499 | ximc, |
| 465 | xim_handler, | 500 | xim_handler, |
| 466 | 501 | ||
| @@ -492,6 +527,10 @@ impl X11Client { · 25 unmodified lines | |||
| 492 | let mut last_key_release = None; | 527 | let mut last_key_release = None; |
| 493 | let mut last_key_press: Option<KeyPressEvent> = None; | 528 | let mut last_key_press: Option<KeyPressEvent> = None; |
| 494 | 529 | ||
| 530 | // event handlers for new keyboard / remapping refresh the state without using event | ||
| 531 | // details, this deduplicates them. | ||
| 532 | let mut last_keymap_change_event: Option<Event> = None; | ||
| 533 | |||
| 495 | loop { | 534 | loop { |
| 496 | match xcb_connection.poll_for_event() { | 535 | match xcb_connection.poll_for_event() { |
| 497 | Ok(Some(event)) => { | 536 | Ok(Some(event)) => { |
| @@ -500,9 +539,29 @@ impl X11Client { · 2 unmodified lines | |||
| 500 | windows_to_refresh.insert(expose_event.window); | 539 | windows_to_refresh.insert(expose_event.window); |
| 501 | } | 540 | } |
| 502 | Event::KeyRelease(_) => { | 541 | Event::KeyRelease(_) => { |
| 542 | if let Some(last_keymap_change_event) = | ||
| 543 | last_keymap_change_event.take() | ||
| 544 | { | ||
| 545 | if let Some(last_key_release) = last_key_release.take() { | ||
| 546 | events.push(last_key_release); | ||
| 547 | } | ||
| 548 | last_key_press = None; | ||
| 549 | events.push(last_keymap_change_event); | ||
| 550 | } | ||
| 551 | |||
| 503 | last_key_release = Some(event); | 552 | last_key_release = Some(event); |
| 504 | } | 553 | } |
| 505 | Event::KeyPress(key_press) => { | 554 | Event::KeyPress(key_press) => { |
| 555 | if let Some(last_keymap_change_event) = | ||
| 556 | last_keymap_change_event.take() | ||
| 557 | { | ||
| 558 | if let Some(last_key_release) = last_key_release.take() { | ||
| 559 | events.push(last_key_release); | ||
| 560 | } | ||
| 561 | last_key_press = None; | ||
| 562 | events.push(last_keymap_change_event); | ||
| 563 | } | ||
| 564 | |||
| 506 | if let Some(last_press) = last_key_press.as_ref() { | 565 | if let Some(last_press) = last_key_press.as_ref() { |
| 507 | if last_press.detail == key_press.detail { | 566 | if last_press.detail == key_press.detail { |
| 508 | continue; | 567 | continue; |
| @@ -523,6 +582,12 @@ impl X11Client { · 14 unmodified lines | |||
| 523 | events.push(Event::KeyPress(key_press)); | 582 | events.push(Event::KeyPress(key_press)); |
| 524 | last_key_press = Some(key_press); | 583 | last_key_press = Some(key_press); |
| 525 | } | 584 | } |
| 585 | Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { | ||
| 586 | if let Some(release_event) = last_key_release.take() { | ||
| 587 | events.push(release_event); | ||
| 588 | } | ||
| 589 | last_keymap_change_event = Some(event); | ||
| 590 | } | ||
| 526 | _ => { | 591 | _ => { |
| 527 | if let Some(release_event) = last_key_release.take() { | 592 | if let Some(release_event) = last_key_release.take() { |
| 528 | events.push(release_event); | 593 | events.push(release_event); |
| @@ -532,10 +597,6 @@ impl X11Client { · 3 unmodified lines | |||
| 532 | } | 597 | } |
| 533 | } | 598 | } |
| 534 | Ok(None) => { | 599 | Ok(None) => { |
| 535 | // Add any remaining stored KeyRelease event | ||
| 536 | if let Some(release_event) = last_key_release.take() { | ||
| 537 | events.push(release_event); | ||
| 538 | } | ||
| 539 | break; | 600 | break; |
| 540 | } | 601 | } |
| 541 | Err(e) => { | 602 | Err(e) => { |
| @@ -545,15 +606,21 @@ impl X11Client { · 3 unmodified lines | |||
| 545 | } | 606 | } |
| 546 | } | 607 | } |
| 547 | 608 | ||
| 609 | if let Some(release_event) = last_key_release.take() { | ||
| 610 | events.push(release_event); | ||
| 611 | } | ||
| 612 | if let Some(keymap_change_event) = last_keymap_change_event.take() { | ||
| 613 | events.push(keymap_change_event); | ||
| 614 | } | ||
| 615 | |||
| 548 | if events.is_empty() && windows_to_refresh.is_empty() { | 616 | if events.is_empty() && windows_to_refresh.is_empty() { |
| 549 | break; | 617 | break; |
| 550 | } | 618 | } |
| 551 | 619 | ||
| 552 | for window in windows_to_refresh.into_iter() { | 620 | for window in windows_to_refresh.into_iter() { |
| 553 | if let Some(window) = self.get_window(window) { | 621 | let mut state = self.0.borrow_mut(); |
| 554 | window.refresh(RequestFrameOptions { | 622 | if let Some(window) = state.windows.get_mut(&window) { |
| 555 | require_presentation: true, | 623 | window.expose_event_received = true; |
| 556 | }); | ||
| 557 | } | 624 | } |
| 558 | } | 625 | } |
| 559 | 626 | ||
| @@ -661,6 +728,27 @@ impl X11Client { · 101 unmodified lines | |||
| 661 | 728 | ||
| 662 | fn handle_event(&self, event: Event) -> Option<()> { | 729 | fn handle_event(&self, event: Event) -> Option<()> { |
| 663 | match event { | 730 | match event { |
| 731 | Event::UnmapNotify(event) => { | ||
| 732 | let mut state = self.0.borrow_mut(); | ||
| 733 | if let Some(window_ref) = state.windows.get_mut(&event.window) { | ||
| 734 | window_ref.is_mapped = false; | ||
| 735 | } | ||
| 736 | state.update_refresh_loop(event.window); | ||
| 737 | } | ||
| 738 | Event::MapNotify(event) => { | ||
| 739 | let mut state = self.0.borrow_mut(); | ||
| 740 | if let Some(window_ref) = state.windows.get_mut(&event.window) { | ||
| 741 | window_ref.is_mapped = true; | ||
| 742 | } | ||
| 743 | state.update_refresh_loop(event.window); | ||
| 744 | } | ||
| 745 | Event::VisibilityNotify(event) => { | ||
| 746 | let mut state = self.0.borrow_mut(); | ||
| 747 | if let Some(window_ref) = state.windows.get_mut(&event.window) { | ||
| 748 | window_ref.last_visibility = event.state; | ||
| 749 | } | ||
| 750 | state.update_refresh_loop(event.window); | ||
| 751 | } | ||
| 664 | Event::ClientMessage(event) => { | 752 | Event::ClientMessage(event) => { |
| 665 | let window = self.get_window(event.window)?; | 753 | let window = self.get_window(event.window)?; |
| 666 | let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32(); | 754 | let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32(); |
| @@ -826,7 +914,7 @@ impl X11Client { · 159 unmodified lines | |||
| 826 | self.reset_ime(); | 914 | self.reset_ime(); |
| 827 | window.handle_ime_delete(); | 915 | window.handle_ime_delete(); |
| 828 | } | 916 | } |
| 829 | Event::XkbNewKeyboardNotify(_) | Event::MapNotify(_) => { | 917 | Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { |
| 830 | let mut state = self.0.borrow_mut(); | 918 | let mut state = self.0.borrow_mut(); |
| 831 | let xkb_state = { | 919 | let xkb_state = { |
| 832 | let xkb_keymap = xkbc::x11::keymap_new_from_device( | 920 | let xkb_keymap = xkbc::x11::keymap_new_from_device( |
| @@ -850,6 +938,8 @@ impl X11Client { · 17 unmodified lines | |||
| 850 | locked_layout, | 938 | locked_layout, |
| 851 | }; | 939 | }; |
| 852 | state.xkb = xkb_state; | 940 | state.xkb = xkb_state; |
| 941 | drop(state); | ||
| 942 | self.handle_keyboard_layout_change(); | ||
| 853 | } | 943 | } |
| 854 | Event::XkbStateNotify(event) => { | 944 | Event::XkbStateNotify(event) => { |
| 855 | let mut state = self.0.borrow_mut(); | 945 | let mut state = self.0.borrow_mut(); |
| @@ -869,16 +959,6 @@ impl X11Client { · 13 unmodified lines | |||
| 869 | locked_layout: event.locked_group.into(), | 959 | locked_layout: event.locked_group.into(), |
| 870 | }; | 960 | }; |
| 871 | 961 | ||
| 872 | if new_layout != old_layout { | ||
| 873 | if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() | ||
| 874 | { | ||
| 875 | drop(state); | ||
| 876 | callback(); | ||
| 877 | state = self.0.borrow_mut(); | ||
| 878 | state.common.callbacks.keyboard_layout_change = Some(callback); | ||
| 879 | } | ||
| 880 | } | ||
| 881 | |||
| 882 | let modifiers = Modifiers::from_xkb(&state.xkb); | 962 | let modifiers = Modifiers::from_xkb(&state.xkb); |
| 883 | if state.last_modifiers_changed_event == modifiers { | 963 | if state.last_modifiers_changed_event == modifiers { |
| 884 | drop(state); | 964 | drop(state); |
| @@ -893,6 +973,10 @@ impl X11Client { · 8 unmodified lines | |||
| 893 | ModifiersChangedEvent { modifiers }, | 973 | ModifiersChangedEvent { modifiers }, |
| 894 | )); | 974 | )); |
| 895 | } | 975 | } |
| 976 | |||
| 977 | if new_layout != old_layout { | ||
| 978 | self.handle_keyboard_layout_change(); | ||
| 979 | } | ||
| 896 | } | 980 | } |
| 897 | Event::KeyPress(event) => { | 981 | Event::KeyPress(event) => { |
| 898 | let window = self.get_window(event.event)?; | 982 | let window = self.get_window(event.event)?; |
| @@ -1281,6 +1365,22 @@ impl X11Client { · 382 unmodified lines | |||
| 1281 | drop(state); | 1365 | drop(state); |
| 1282 | Some(()) | 1366 | Some(()) |
| 1283 | } | 1367 | } |
| 1368 | |||
| 1369 | fn handle_keyboard_layout_change(&self) { | ||
| 1370 | let mut state = self.0.borrow_mut(); | ||
| 1371 | let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); | ||
| 1372 | let keymap = state.xkb.get_keymap(); | ||
| 1373 | let layout_name = keymap.layout_get_name(layout_idx); | ||
| 1374 | if layout_name != state.keyboard_layout.name() { | ||
| 1375 | state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); | ||
| 1376 | if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { | ||
| 1377 | drop(state); | ||
| 1378 | callback(); | ||
| 1379 | state = self.0.borrow_mut(); | ||
| 1380 | state.common.callbacks.keyboard_layout_change = Some(callback); | ||
| 1381 | } | ||
| 1382 | } | ||
| 1383 | } | ||
| 1284 | } | 1384 | } |
| 1285 | 1385 | ||
| 1286 | impl LinuxClient for X11Client { | 1386 | impl LinuxClient for X11Client { |
| @@ -1294,14 +1394,7 @@ impl LinuxClient for X11Client { · 7 unmodified lines | |||
| 1294 | 1394 | ||
| 1295 | fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> { | 1395 | fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> { |
| 1296 | let state = self.0.borrow(); | 1396 | let state = self.0.borrow(); |
| 1297 | let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); | 1397 | Box::new(state.keyboard_layout.clone()) |
| 1298 | Box::new(LinuxKeyboardLayout::new( | ||
| 1299 | state | ||
| 1300 | .xkb | ||
| 1301 | .get_keymap() | ||
| 1302 | .layout_get_name(layout_idx) | ||
| 1303 | .to_string(), | ||
| 1304 | )) | ||
| 1305 | } | 1398 | } |
| 1306 | 1399 | ||
| 1307 | fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> { | 1400 | fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> { |
| @@ -1383,61 +1476,12 @@ impl LinuxClient for X11Client { · 75 unmodified lines | |||
| 1383 | ) | 1476 | ) |
| 1384 | .unwrap(); | 1477 | .unwrap(); |
| 1385 | 1478 | ||
| 1386 | let screen_resources = state | ||
| 1387 | .xcb_connection | ||
| 1388 | .randr_get_screen_resources(x_window) | ||
| 1389 | .unwrap() | ||
| 1390 | .reply() | ||
| 1391 | .expect("Could not find available screens"); | ||
| 1392 | |||
| 1393 | let mode = screen_resources | ||
| 1394 | .crtcs | ||
| 1395 | .iter() | ||
| 1396 | .find_map(|crtc| { | ||
| 1397 | let crtc_info = state | ||
| 1398 | .xcb_connection | ||
| 1399 | .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) | ||
| 1400 | .ok()? | ||
| 1401 | .reply() | ||
| 1402 | .ok()?; | ||
| 1403 | |||
| 1404 | screen_resources | ||
| 1405 | .modes | ||
| 1406 | .iter() | ||
| 1407 | .find(|m| m.id == crtc_info.mode) | ||
| 1408 | }) | ||
| 1409 | .expect("Unable to find screen refresh rate"); | ||
158 more lines aren’t shown here.
This diff is large: its 64 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 68 lines aren’t shown here.
This diff is large: its 46 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 23 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 21 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 87 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 74 lines aren’t shown here.
This diff is large: its 11 lines aren’t shown here.
This diff is large: its 110 lines aren’t shown here.
This diff is large: its 19 lines aren’t shown here.
This diff is large: its 182 lines aren’t shown here.
This diff is large: its 102 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 34 lines aren’t shown here.
This diff is large: its 44 lines aren’t shown here.
This diff is large: its 80 lines aren’t shown here.
This diff is large: its 61 lines aren’t shown here.
This diff is large: its 36 lines aren’t shown here.
This diff is large: its 47 lines aren’t shown here.
This diff is large: its 33 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 17 lines aren’t shown here.
This diff is large: its 26 lines aren’t shown here.
This diff is large: its 144 lines aren’t shown here.
This diff is large: its 183 lines aren’t shown here.
This diff is large: its 21 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 31 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 21 lines aren’t shown here.
This diff is large: its 29 lines aren’t shown here.
This diff is large: its 7 lines aren’t shown here.
This diff is large: its 32 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 34 lines aren’t shown here.
This diff is large: its 981 lines aren’t shown here.
This diff is large: its 7 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 30 lines aren’t shown here.
This diff is large: its 130 lines aren’t shown here.
This diff is large: its 371 lines aren’t shown here.
This diff is large: its 183 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 112 lines aren’t shown here.
This diff is large: its 26 lines aren’t shown here.
This diff is large: its 17 lines aren’t shown here.
This diff is large: its 154 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 144 lines aren’t shown here.
This diff is large: its 62 lines aren’t shown here.
This diff is large: its 703 lines aren’t shown here.
This diff is large: its 83 lines aren’t shown here.
This diff is large: its 26 lines aren’t shown here.
This diff is large: its 511 lines aren’t shown here.
This diff is large: its 62 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 107 lines aren’t shown here.
This diff is large: its 490 lines aren’t shown here.
This diff is large: its 99 lines aren’t shown here.
This diff is large: its 63 lines aren’t shown here.
This diff is large: its 441 lines aren’t shown here.
This diff is large: its 944 lines aren’t shown here.
This diff is large: its 34 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 229 lines aren’t shown here.
This diff is large: its 19 lines aren’t shown here.
This diff is large: its 946 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 19 lines aren’t shown here.
This diff is large: its 13 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 22 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 12 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 38 lines aren’t shown here.
This diff is large: its 48 lines aren’t shown here.
This diff is large: its 30 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 184 lines aren’t shown here.
This diff is large: its 77 lines aren’t shown here.
This diff is large: its 16 lines aren’t shown here.
This diff is large: its 76 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 22 lines aren’t shown here.
This diff is large: its 128 lines aren’t shown here.
This diff is large: its 46 lines aren’t shown here.
This diff is large: its 103 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 32 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 289 lines aren’t shown here.
This diff is large: its 124 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 28 lines aren’t shown here.
This diff is large: its 26 lines aren’t shown here.
This diff is large: its 69 lines aren’t shown here.
This diff is large: its 15 lines aren’t shown here.
This diff is large: its 178 lines aren’t shown here.
This diff is large: its 100 lines aren’t shown here.
This diff is large: its 16 lines aren’t shown here.
This diff is large: its 166 lines aren’t shown here.
This diff is large: its 304 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 341 lines aren’t shown here.
This diff is large: its 186 lines aren’t shown here.
This diff is large: its 42 lines aren’t shown here.
This diff is large: its 204 lines aren’t shown here.
This diff is large: its 13 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 73 lines aren’t shown here.
This diff is large: its 423 lines aren’t shown here.
This diff is large: its 79 lines aren’t shown here.
This diff is large: its 33 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 54 lines aren’t shown here.
This diff is large: its 94 lines aren’t shown here.
This diff is large: its 31 lines aren’t shown here.
This diff is large: its 29 lines aren’t shown here.
This diff is large: its 55 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 12 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 44 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 10 lines aren’t shown here.
This diff is large: its 194 lines aren’t shown here.
This diff is large: its 70 lines aren’t shown here.
This diff is large: its 5 lines aren’t shown here.
This diff is large: its 40 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 45 lines aren’t shown here.
This diff is large: its 39 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 114 lines aren’t shown here.
This diff is large: its 33 lines aren’t shown here.
This diff is large: its 32 lines aren’t shown here.