Unified · Split
| @@ -1,4 +1,4 @@ | ||
| 1 | −name: Bug Report (AI Related) | |
| 1 | +name: Bug Report (AI) | |
| 2 | 2 | description: Zed Agent Panel Bugs |
| 3 | 3 | type: "Bug" |
| 4 | 4 | labels: ["ai"] |
| @@ -19,15 +19,14 @@ body: · 14 unmodified lines | ||
| 19 | 19 | 2. |
| 20 | 20 | 3. |
| 21 | 21 | |
| 22 | − Actual Behavior: | |
| 23 | − Expected Behavior: | |
| 22 | + **Expected Behavior**: | |
| 23 | + **Actual Behavior**: | |
| 24 | 24 | |
| 25 | 25 | ### Model Provider Details |
| 26 | 26 | - Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc) |
| 27 | 27 | - Model Name: |
| 28 | 28 | - Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads) |
| 29 | − - MCP Servers in-use: | |
| 30 | − - Other Details: | |
| 29 | + - Other Details (MCPs, other settings, etc): | |
| 31 | 30 | validations: |
| 32 | 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 | 19 | 2. |
| 20 | 20 | 3. |
| 21 | 21 | |
| 22 | − Actual Behavior: | |
| 23 | − Expected Behavior: | |
| 22 | + **Expected Behavior**: | |
| 23 | + **Actual Behavior**: | |
| 24 | 24 | |
| 25 | 25 | validations: |
| 26 | 26 | required: true |
| @@ -18,14 +18,16 @@ body: · 17 unmodified lines | ||
| 18 | 18 | - Issues with insufficient detail may be summarily closed. |
| 19 | 19 | --> |
| 20 | 20 | |
| 21 | + DESCRIPTION_HERE | |
| 22 | + | |
| 21 | 23 | Steps to reproduce: |
| 22 | 24 | 1. |
| 23 | 25 | 2. |
| 24 | 26 | 3. |
| 25 | 27 | 4. |
| 26 | 28 | |
| 27 | − Expected Behavior: | |
| 28 | − Actual Behavior: | |
| 29 | + **Expected Behavior**: | |
| 30 | + **Actual Behavior**: | |
| 29 | 31 | |
| 30 | 32 | <!-- Before Submitting, did you: |
| 31 | 33 | 1. Include settings.json, keymap.json, .editorconfig if relevant? |
| @@ -22,7 +22,7 @@ runs: · 21 unmodified lines | ||
| 22 | 22 | - name: Check for broken links |
| 23 | 23 | uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1 |
| 24 | 24 | with: |
| 25 | − args: --no-progress './docs/src/**/*' | |
| 25 | + args: --no-progress --exclude '^http' './docs/src/**/*' | |
| 26 | 26 | fail: true |
| 27 | 27 | |
| 28 | 28 | - name: Build book |
| @@ -498,6 +498,7 @@ jobs: · 497 unmodified lines | ||
| 498 | 498 | needs: |
| 499 | 499 | - job_spec |
| 500 | 500 | - style |
| 501 | + - check_docs | |
| 501 | 502 | - migration_checks |
| 502 | 503 | # run_tests: If adding required tests, add them here and to script below. |
| 503 | 504 | - workspace_hack |
| @@ -515,7 +516,8 @@ jobs: · 11 unmodified lines | ||
| 515 | 516 | # Check dependent jobs... |
| 516 | 517 | RET_CODE=0 |
| 517 | 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 | 522 | # Only check test jobs if they were supposed to run |
| 521 | 523 | if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then |
| @@ -801,6 +803,7 @@ jobs: · 279 unmodified lines | ||
| 801 | 803 | name: Build with Nix |
| 802 | 804 | uses: ./.github/workflows/nix.yml |
| 803 | 805 | if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix') |
| 806 | + secrets: inherit | |
| 804 | 807 | with: |
| 805 | 808 | flake-output: debug |
| 806 | 809 | # excludes the final package to only cache dependencies |
| @@ -30,6 +30,7 @@ jobs: · 29 unmodified lines | ||
| 30 | 30 | noop: |
| 31 | 31 | name: No-op |
| 32 | 32 | runs-on: ubuntu-latest |
| 33 | + if: github.repository_owner == 'zed-industries' | |
| 33 | 34 | steps: |
| 34 | 35 | - name: No-op |
| 35 | 36 | run: echo "Nothing to do" |
| @@ -214,6 +214,7 @@ jobs: · 213 unmodified lines | ||
| 214 | 214 | bundle-nix: |
| 215 | 215 | name: Build and cache Nix package |
| 216 | 216 | needs: tests |
| 217 | + secrets: inherit | |
| 217 | 218 | uses: ./.github/workflows/nix.yml |
| 218 | 219 | |
| 219 | 220 | update-nightly-tag: |
| @@ -19,6 +19,7 @@ env: · 18 unmodified lines | ||
| 19 | 19 | |
| 20 | 20 | jobs: |
| 21 | 21 | unit_evals: |
| 22 | + if: github.repository_owner == 'zed-industries' | |
| 22 | 23 | timeout-minutes: 60 |
| 23 | 24 | name: Run unit evals |
| 24 | 25 | runs-on: |
| @@ -62,7 +63,7 @@ jobs: · 37 unmodified lines | ||
| 62 | 63 | |
| 63 | 64 | - name: Run unit evals |
| 64 | 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 | 67 | env: |
| 67 | 68 | ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} |
| 68 | 69 | |
| @@ -99,6 +99,7 @@ dependencies = [ · 98 unmodified lines | ||
| 99 | 99 | "paths", |
| 100 | 100 | "picker", |
| 101 | 101 | "postage", |
| 102 | + "pretty_assertions", | |
| 102 | 103 | "project", |
| 103 | 104 | "prompt_store", |
| 104 | 105 | "proto", |
| @@ -490,7 +491,6 @@ dependencies = [ · 385 unmodified lines | ||
| 490 | 491 | "anyhow", |
| 491 | 492 | "futures 0.3.31", |
| 492 | 493 | "gpui", |
| 493 | − "shlex", | |
| 494 | 494 | "smol", |
| 495 | 495 | "tempfile", |
| 496 | 496 | "util", |
| @@ -704,6 +704,7 @@ dependencies = [ · 207 unmodified lines | ||
| 704 | 704 | "serde_json", |
| 705 | 705 | "settings", |
| 706 | 706 | "smallvec", |
| 707 | + "smol", | |
| 707 | 708 | "streaming_diff", |
| 708 | 709 | "strsim", |
| 709 | 710 | "task", |
| @@ -2821,9 +2822,11 @@ dependencies = [ · 2111 unmodified lines | ||
| 2821 | 2822 | "collections", |
| 2822 | 2823 | "credentials_provider", |
| 2823 | 2824 | "feature_flags", |
| 2825 | + "fs", | |
| 2824 | 2826 | "futures 0.3.31", |
| 2825 | 2827 | "gpui", |
| 2826 | 2828 | "gpui_tokio", |
| 2829 | + "hickory-resolver", | |
| 2827 | 2830 | "http_client", |
| 2828 | 2831 | "http_client_tls", |
| 2829 | 2832 | "httparse", |
| @@ -2832,6 +2835,7 @@ dependencies = [ · 2 unmodified lines | ||
| 2832 | 2835 | "paths", |
| 2833 | 2836 | "postage", |
| 2834 | 2837 | "rand 0.8.5", |
| 2838 | + "regex", | |
| 2835 | 2839 | "release_channel", |
| 2836 | 2840 | "rpc", |
| 2837 | 2841 | "rustls-pki-types", |
| @@ -3159,6 +3163,16 @@ dependencies = [ · 321 unmodified lines | ||
| 3159 | 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 | 3176 | [[package]] |
| 3163 | 3177 | name = "command_palette" |
| 3164 | 3178 | version = "0.1.0" |
| @@ -3528,6 +3542,20 @@ dependencies = [ · 363 unmodified lines | ||
| 3528 | 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 | 3559 | [[package]] |
| 3532 | 3560 | name = "coreaudio-sys" |
| 3533 | 3561 | version = "0.2.16" |
| @@ -3563,7 +3591,8 @@ dependencies = [ · 29 unmodified lines | ||
| 3563 | 3591 | [[package]] |
| 3564 | 3592 | name = "cpal" |
| 3565 | 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 | 3596 | dependencies = [ |
| 3568 | 3597 | "alsa", |
| 3569 | 3598 | "core-foundation-sys", |
| @@ -3573,7 +3602,7 @@ dependencies = [ · 3 unmodified lines | ||
| 3573 | 3602 | "js-sys", |
| 3574 | 3603 | "libc", |
| 3575 | 3604 | "mach2", |
| 3576 | − "ndk", | |
| 3605 | + "ndk 0.8.0", | |
| 3577 | 3606 | "ndk-context", |
| 3578 | 3607 | "oboe", |
| 3579 | 3608 | "wasm-bindgen", |
| @@ -3582,6 +3611,32 @@ dependencies = [ · 2 unmodified lines | ||
| 3582 | 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 | 3640 | [[package]] |
| 3586 | 3641 | name = "cpp_demangle" |
| 3587 | 3642 | version = "0.4.4" |
| @@ -4015,6 +4070,7 @@ dependencies = [ · 427 unmodified lines | ||
| 4015 | 4070 | "gpui", |
| 4016 | 4071 | "http_client", |
| 4017 | 4072 | "language", |
| 4073 | + "libc", | |
| 4018 | 4074 | "log", |
| 4019 | 4075 | "node_runtime", |
| 4020 | 4076 | "parking_lot", |
| @@ -4038,7 +4094,7 @@ dependencies = [ · 17 unmodified lines | ||
| 4038 | 4094 | [[package]] |
| 4039 | 4095 | name = "dap-types" |
| 4040 | 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 | 4098 | dependencies = [ |
| 4043 | 4099 | "schemars", |
| 4044 | 4100 | "serde", |
| @@ -4051,6 +4107,7 @@ version = "0.1.0" · 6 unmodified lines | ||
| 4051 | 4107 | dependencies = [ |
| 4052 | 4108 | "anyhow", |
| 4053 | 4109 | "async-trait", |
| 4110 | + "collections", | |
| 4054 | 4111 | "dap", |
| 4055 | 4112 | "futures 0.3.31", |
| 4056 | 4113 | "gpui", |
| @@ -4682,7 +4739,6 @@ dependencies = [ · 625 unmodified lines | ||
| 4682 | 4739 | "client", |
| 4683 | 4740 | "clock", |
| 4684 | 4741 | "collections", |
| 4685 | − "command_palette_hooks", | |
| 4686 | 4742 | "convert_case 0.8.0", |
| 4687 | 4743 | "ctor", |
| 4688 | 4744 | "dap", |
| @@ -4851,6 +4907,18 @@ version = "1.1.0" · 162 unmodified lines | ||
| 4851 | 4907 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 4852 | 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 | 4922 | [[package]] |
| 4855 | 4923 | name = "enumflags2" |
| 4856 | 4924 | version = "0.7.11" |
| @@ -6108,6 +6176,7 @@ dependencies = [ · 1251 unmodified lines | ||
| 6108 | 6176 | "anyhow", |
| 6109 | 6177 | "askpass", |
| 6110 | 6178 | "buffer_diff", |
| 6179 | + "call", | |
| 6111 | 6180 | "chrono", |
| 6112 | 6181 | "collections", |
| 6113 | 6182 | "command_palette_hooks", |
| @@ -7428,6 +7497,51 @@ version = "0.2.1" · 1314 unmodified lines | ||
| 7428 | 7497 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 7429 | 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 | 7545 | [[package]] |
| 7432 | 7546 | name = "hidden-trait" |
| 7433 | 7547 | version = "0.1.2" |
| @@ -8297,6 +8411,18 @@ dependencies = [ · 863 unmodified lines | ||
| 8297 | 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 | 8426 | [[package]] |
| 8301 | 8427 | name = "ipnet" |
| 8302 | 8428 | version = "2.11.0" |
| @@ -8990,7 +9116,6 @@ dependencies = [ · 687 unmodified lines | ||
| 8990 | 9116 | "tree-sitter-yaml", |
| 8991 | 9117 | "unindent", |
| 8992 | 9118 | "util", |
| 8993 | − "which 6.0.3", | |
| 8994 | 9119 | "workspace", |
| 8995 | 9120 | "workspace-hack", |
| 8996 | 9121 | ] |
| @@ -9182,6 +9307,12 @@ dependencies = [ · 185 unmodified lines | ||
| 9182 | 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 | 9316 | [[package]] |
| 9186 | 9317 | name = "linkify" |
| 9187 | 9318 | version = "0.10.0" |
| @@ -9308,7 +9439,7 @@ dependencies = [ · 120 unmodified lines | ||
| 9308 | 9439 | "core-foundation 0.10.0", |
| 9309 | 9440 | "core-video", |
| 9310 | 9441 | "coreaudio-rs 0.12.1", |
| 9311 | − "cpal", | |
| 9442 | + "cpal 0.16.0", | |
| 9312 | 9443 | "futures 0.3.31", |
| 9313 | 9444 | "gpui", |
| 9314 | 9445 | "gpui_tokio", |
| @@ -9442,6 +9573,15 @@ dependencies = [ · 127 unmodified lines | ||
| 9442 | 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 | 9585 | [[package]] |
| 9446 | 9586 | name = "lsp" |
| 9447 | 9587 | version = "0.1.0" |
| @@ -10078,7 +10218,21 @@ dependencies = [ · 630 unmodified lines | ||
| 10078 | 10218 | "bitflags 2.9.0", |
| 10079 | 10219 | "jni-sys", |
| 10080 | 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 | 10236 | "num_enum", |
| 10083 | 10237 | "thiserror 1.0.69", |
| 10084 | 10238 | ] |
| @@ -10098,6 +10252,15 @@ dependencies = [ · 13 unmodified lines | ||
| 10098 | 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 | 10264 | [[package]] |
| 10102 | 10265 | name = "new_debug_unreachable" |
| 10103 | 10266 | version = "1.0.6" |
| @@ -10129,6 +10292,18 @@ dependencies = [ · 25 unmodified lines | ||
| 10129 | 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 | 10307 | [[package]] |
| 10133 | 10308 | name = "node_runtime" |
| 10134 | 10309 | version = "0.1.0" |
| @@ -10499,6 +10674,43 @@ dependencies = [ · 364 unmodified lines | ||
| 10499 | 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 | 10714 | [[package]] |
| 10503 | 10715 | name = "objc2-core-foundation" |
| 10504 | 10716 | version = "0.3.1" |
| @@ -10604,7 +10816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" · 99 unmodified lines | ||
| 10604 | 10816 | checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" |
| 10605 | 10817 | dependencies = [ |
| 10606 | 10818 | "jni", |
| 10607 | − "ndk", | |
| 10819 | + "ndk 0.8.0", | |
| 10608 | 10820 | "ndk-context", |
| 10609 | 10821 | "num-derive", |
| 10610 | 10822 | "num-traits", |
| @@ -12109,7 +12321,6 @@ dependencies = [ · 1498 unmodified lines | ||
| 12109 | 12321 | "unindent", |
| 12110 | 12322 | "url", |
| 12111 | 12323 | "util", |
| 12112 | − "uuid", | |
| 12113 | 12324 | "which 6.0.3", |
| 12114 | 12325 | "workspace-hack", |
| 12115 | 12326 | "worktree", |
| @@ -13202,6 +13413,7 @@ dependencies = [ · 1086 unmodified lines | ||
| 13202 | 13413 | "futures-core", |
| 13203 | 13414 | "futures-util", |
| 13204 | 13415 | "h2 0.4.9", |
| 13416 | + "hickory-resolver", | |
| 13205 | 13417 | "http 1.3.1", |
| 13206 | 13418 | "http-body 1.0.1", |
| 13207 | 13419 | "http-body-util", |
| @@ -13258,6 +13470,12 @@ dependencies = [ · 50 unmodified lines | ||
| 13258 | 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 | 13479 | [[package]] |
| 13262 | 13480 | name = "resvg" |
| 13263 | 13481 | version = "0.45.1" |
| @@ -13377,7 +13595,7 @@ version = "0.20.1" · 113 unmodified lines | ||
| 13377 | 13595 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 13378 | 13596 | checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" |
| 13379 | 13597 | dependencies = [ |
| 13380 | − "cpal", | |
| 13598 | + "cpal 0.15.3", | |
| 13381 | 13599 | "hound", |
| 13382 | 13600 | ] |
| 13383 | 13601 | |
| @@ -14389,12 +14607,12 @@ dependencies = [ · 1005 unmodified lines | ||
| 14389 | 14607 | "fs", |
| 14390 | 14608 | "gpui", |
| 14391 | 14609 | "log", |
| 14392 | − "paths", | |
| 14393 | 14610 | "schemars", |
| 14394 | 14611 | "serde", |
| 14395 | 14612 | "settings", |
| 14396 | 14613 | "theme", |
| 14397 | 14614 | "ui", |
| 14615 | + "util", | |
| 14398 | 14616 | "workspace", |
| 14399 | 14617 | "workspace-hack", |
| 14400 | 14618 | ] |
| @@ -15725,6 +15943,7 @@ dependencies = [ · 1324 unmodified lines | ||
| 15725 | 15943 | "theme", |
| 15726 | 15944 | "thiserror 2.0.12", |
| 15727 | 15945 | "url", |
| 15946 | + "urlencoding", | |
| 15728 | 15947 | "util", |
| 15729 | 15948 | "windows 0.61.1", |
| 15730 | 15949 | "workspace-hack", |
| @@ -17121,6 +17340,7 @@ dependencies = [ · 1390 unmodified lines | ||
| 17121 | 17340 | "async-fs", |
| 17122 | 17341 | "async_zip", |
| 17123 | 17342 | "collections", |
| 17343 | + "command-fds", | |
| 17124 | 17344 | "dirs 4.0.0", |
| 17125 | 17345 | "dunce", |
| 17126 | 17346 | "futures 0.3.31", |
| @@ -17131,12 +17351,14 @@ dependencies = [ · 4 unmodified lines | ||
| 17131 | 17351 | "itertools 0.14.0", |
| 17132 | 17352 | "libc", |
| 17133 | 17353 | "log", |
| 17354 | + "nix 0.29.0", | |
| 17134 | 17355 | "rand 0.8.5", |
| 17135 | 17356 | "regex", |
| 17136 | 17357 | "rust-embed", |
| 17137 | 17358 | "serde", |
| 17138 | 17359 | "serde_json", |
| 17139 | 17360 | "serde_json_lenient", |
| 17361 | + "shlex", | |
| 17140 | 17362 | "smol", |
| 17141 | 17363 | "take-until", |
| 17142 | 17364 | "tempfile", |
| @@ -18178,6 +18400,12 @@ dependencies = [ · 1035 unmodified lines | ||
| 18178 | 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 | 18409 | [[package]] |
| 18182 | 18410 | name = "wiggle" |
| 18183 | 18411 | version = "29.0.1" |
| @@ -19298,6 +19526,7 @@ dependencies = [ · 1114 unmodified lines | ||
| 19298 | 19526 | "num-rational", |
| 19299 | 19527 | "num-traits", |
| 19300 | 19528 | "objc2", |
| 19529 | + "objc2-core-foundation", | |
| 19301 | 19530 | "objc2-foundation", |
| 19302 | 19531 | "objc2-metal", |
| 19303 | 19532 | "object", |
| @@ -19718,7 +19947,7 @@ dependencies = [ · 414 unmodified lines | ||
| 19718 | 19947 | |
| 19719 | 19948 | [[package]] |
| 19720 | 19949 | name = "zed" |
| 19721 | −version = "0.191.0" | |
| 19950 | +version = "0.192.0" | |
| 19722 | 19951 | dependencies = [ |
| 19723 | 19952 | "activity_indicator", |
| 19724 | 19953 | "agent", |
| @@ -433,9 +433,10 @@ convert_case = "0.8.0" · 432 unmodified lines | ||
| 433 | 433 | core-foundation = "0.10.0" |
| 434 | 434 | core-foundation-sys = "0.8.6" |
| 435 | 435 | core-video = { version = "0.4.3", features = ["metal"] } |
| 436 | +cpal = "0.16" | |
| 436 | 437 | criterion = { version = "0.5", features = ["html_reports"] } |
| 437 | 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 | 440 | dashmap = "6.0" |
| 440 | 441 | derive_more = "0.99.17" |
| 441 | 442 | dirs = "4.0" |
| @@ -523,6 +524,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 · 81 unmodified lines | ||
| 523 | 524 | "rustls-tls-native-roots", |
| 524 | 525 | "socks", |
| 525 | 526 | "stream", |
| 527 | + "hickory-dns", | |
| 526 | 528 | ] } |
| 527 | 529 | rsa = "0.9.6" |
| 528 | 530 | runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [ |
| @@ -682,9 +684,7 @@ features = [ · 153 unmodified lines | ||
| 682 | 684 | "Win32_UI_WindowsAndMessaging", |
| 683 | 685 | ] |
| 684 | 686 | |
| 685 | −# TODO livekit https://github.com/RustAudio/cpal/pull/891 | |
| 686 | 687 | [patch.crates-io] |
| 687 | −cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" } | |
| 688 | 688 | notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } |
| 689 | 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 | 698 | [profile.dev.package] |
| 699 | 699 | taffy = { opt-level = 3 } |
| 700 | 700 | cranelift-codegen = { opt-level = 3 } |
| 701 | +cranelift-codegen-meta = { opt-level = 3 } | |
| 702 | +cranelift-codegen-shared = { opt-level = 3 } | |
| 701 | 703 | resvg = { opt-level = 3 } |
| 702 | 704 | rustybuzz = { opt-level = 3 } |
| 703 | 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 | 115 | "ctrl-\"": "editor::ExpandAllDiffHunks", |
| 116 | 116 | "ctrl-i": "editor::ShowSignatureHelp", |
| 117 | 117 | "alt-g b": "git::Blame", |
| 118 | + "alt-g m": "git::OpenModifiedFiles", | |
| 118 | 119 | "menu": "editor::OpenContextMenu", |
| 119 | 120 | "shift-f10": "editor::OpenContextMenu", |
| 120 | 121 | "ctrl-shift-e": "editor::ToggleEditPrediction", |
| @@ -139,6 +139,7 @@ · 138 unmodified lines | ||
| 139 | 139 | "cmd-'": "editor::ToggleSelectedDiffHunks", |
| 140 | 140 | "cmd-\"": "editor::ExpandAllDiffHunks", |
| 141 | 141 | "cmd-alt-g b": "git::Blame", |
| 142 | + "cmd-alt-g m": "git::OpenModifiedFiles", | |
| 142 | 143 | "cmd-i": "editor::ShowSignatureHelp", |
| 143 | 144 | "f9": "editor::ToggleBreakpoint", |
| 144 | 145 | "shift-f9": "editor::EditLogBreakpoint", |
| @@ -184,6 +184,8 @@ · 183 unmodified lines | ||
| 184 | 184 | "z f": "editor::FoldSelectedRanges", |
| 185 | 185 | "z shift-m": "editor::FoldAll", |
| 186 | 186 | "z shift-r": "editor::UnfoldAll", |
| 187 | + "z l": "vim::ColumnRight", | |
| 188 | + "z h": "vim::ColumnLeft", | |
| 187 | 189 | "shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }], |
| 188 | 190 | "shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }], |
| 189 | 191 | // Count support |
| @@ -395,6 +397,8 @@ · 205 unmodified lines | ||
| 395 | 397 | "ctrl-pagedown": "pane::ActivateNextItem", |
| 396 | 398 | "ctrl-pageup": "pane::ActivatePreviousItem", |
| 397 | 399 | "insert": "vim::InsertBefore", |
| 400 | + ".": "vim::Repeat", | |
| 401 | + "alt-.": "vim::RepeatFind", | |
| 398 | 402 | // tree-sitter related commands |
| 399 | 403 | "[ x": "editor::SelectLargerSyntaxNode", |
| 400 | 404 | "] x": "editor::SelectSmallerSyntaxNode", |
| @@ -421,6 +425,7 @@ · 20 unmodified lines | ||
| 421 | 425 | |
| 422 | 426 | "x": "editor::SelectLine", |
| 423 | 427 | "shift-x": "editor::SelectLine", |
| 428 | + "%": "editor::SelectAll", | |
| 424 | 429 | // Window mode |
| 425 | 430 | "space w h": "workspace::ActivatePaneLeft", |
| 426 | 431 | "space w l": "workspace::ActivatePaneRight", |
| @@ -450,7 +455,8 @@ · 23 unmodified lines | ||
| 450 | 455 | "ctrl-c": "editor::ToggleComments", |
| 451 | 456 | "d": "vim::HelixDelete", |
| 452 | 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 | 27 | If appropriate, use tool calls to explore the current project, which contains the following root directories: |
| 28 | 28 | |
| 29 | 29 | {{#each worktrees}} |
| 30 | −- `{{root_name}}` | |
| 30 | +- `{{abs_path}}` | |
| 31 | 31 | {{/each}} |
| 32 | 32 | |
| 33 | 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 | 35 | - Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path! |
| 36 | 36 | {{# if (has_tool 'grep') }} |
| 37 | 37 | - When looking for symbols in the project, prefer the `grep` tool. |
| @@ -307,6 +307,8 @@ · 306 unmodified lines | ||
| 307 | 307 | // "all" |
| 308 | 308 | // 4. Draw whitespaces at boundaries only: |
| 309 | 309 | // "boundary" |
| 310 | + // 5. Draw whitespaces only after non-whitespace characters: | |
| 311 | + // "trailing" | |
| 310 | 312 | // For a whitespace to be on a boundary, any of the following conditions need to be met: |
| 311 | 313 | // - It is a tab |
| 312 | 314 | // - It is adjacent to an edge (start or end) |
| @@ -445,7 +447,9 @@ · 132 unmodified lines | ||
| 445 | 447 | // Whether to show breakpoints in the gutter. |
| 446 | 448 | "breakpoints": true, |
| 447 | 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 | 454 | "indent_guides": { |
| 451 | 455 | // Whether to show indent guides in the editor. |
| @@ -604,7 +608,9 @@ · 152 unmodified lines | ||
| 604 | 608 | // 2. Never show indent guides: |
| 605 | 609 | // "never" |
| 606 | 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 | 615 | "outline_panel": { |
| 610 | 616 | // Whether to show the outline panel button in the status bar |
| @@ -1476,7 +1482,8 @@ · 865 unmodified lines | ||
| 1476 | 1482 | "Go": { |
| 1477 | 1483 | "code_actions_on_format": { |
| 1478 | 1484 | "source.organizeImports": true |
| 1479 | − } | |
| 1485 | + }, | |
| 1486 | + "debuggers": ["Delve"] | |
| 1480 | 1487 | }, |
| 1481 | 1488 | "GraphQL": { |
| 1482 | 1489 | "prettier": { |
| @@ -1541,9 +1548,15 @@ · 58 unmodified lines | ||
| 1541 | 1548 | "Plain Text": { |
| 1542 | 1549 | "allow_rewrap": "anywhere" |
| 1543 | 1550 | }, |
| 1551 | + "Python": { | |
| 1552 | + "debuggers": ["Debugpy"] | |
| 1553 | + }, | |
| 1544 | 1554 | "Ruby": { |
| 1545 | 1555 | "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."] |
| 1546 | 1556 | }, |
| 1557 | + "Rust": { | |
| 1558 | + "debuggers": ["CodeLLDB"] | |
| 1559 | + }, | |
| 1547 | 1560 | "SCSS": { |
| 1548 | 1561 | "prettier": { |
| 1549 | 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 | 3 | // Example: |
| 4 | 4 | [ |
| @@ -99,6 +99,8 @@ · 98 unmodified lines | ||
| 99 | 99 | "version_control.added": "#27a657ff", |
| 100 | 100 | "version_control.modified": "#d3b020ff", |
| 101 | 101 | "version_control.deleted": "#e06c76ff", |
| 102 | + "version_control.conflict_marker.ours": "#a1c1811a", | |
| 103 | + "version_control.conflict_marker.theirs": "#74ade81a", | |
| 102 | 104 | "conflict": "#dec184ff", |
| 103 | 105 | "conflict.background": "#dec1841a", |
| 104 | 106 | "conflict.border": "#5d4c2fff", |
| @@ -7,7 +7,10 @@ use gpui::{ · 6 unmodified lines | ||
| 7 | 7 | InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement, |
| 8 | 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 | 14 | use project::{ |
| 12 | 15 | EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project, |
| 13 | 16 | ProjectEnvironmentEvent, |
| @@ -16,6 +19,7 @@ use project::{ · 2 unmodified lines | ||
| 16 | 19 | use smallvec::SmallVec; |
| 17 | 20 | use std::{ |
| 18 | 21 | cmp::Reverse, |
| 22 | + collections::HashSet, | |
| 19 | 23 | fmt::Write, |
| 20 | 24 | path::Path, |
| 21 | 25 | sync::Arc, |
| @@ -30,9 +34,9 @@ const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0); · 8 unmodified lines | ||
| 30 | 34 | actions!(activity_indicator, [ShowErrorMessage]); |
| 31 | 35 | |
| 32 | 36 | pub enum Event { |
| 33 | − ShowError { | |
| 34 | − server_name: SharedString, | |
| 35 | − error: String, | |
| 37 | + ShowStatus { | |
| 38 | + server_name: LanguageServerName, | |
| 39 | + status: SharedString, | |
| 36 | 40 | }, |
| 37 | 41 | } |
| 38 | 42 | |
| @@ -45,8 +49,8 @@ pub struct ActivityIndicator { · 6 unmodified lines | ||
| 45 | 49 | |
| 46 | 50 | #[derive(Debug)] |
| 47 | 51 | struct ServerStatus { |
| 48 | − name: SharedString, | |
| 49 | − status: BinaryStatus, | |
| 52 | + name: LanguageServerName, | |
| 53 | + status: LanguageServerStatusUpdate, | |
| 50 | 54 | } |
| 51 | 55 | |
| 52 | 56 | struct PendingWork<'a> { |
| @@ -145,19 +149,19 @@ impl ActivityIndicator { · 92 unmodified lines | ||
| 145 | 149 | }); |
| 146 | 150 | |
| 147 | 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 | 156 | let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx)); |
| 150 | 157 | let project = project.clone(); |
| 151 | − let error = error.clone(); | |
| 158 | + let status = status.clone(); | |
| 152 | 159 | let server_name = server_name.clone(); |
| 153 | 160 | cx.spawn_in(window, async move |workspace, cx| { |
| 154 | 161 | let buffer = create_buffer.await?; |
| 155 | 162 | buffer.update(cx, |buffer, cx| { |
| 156 | 163 | buffer.edit( |
| 157 | − [( | |
| 158 | − 0..0, | |
| 159 | − format!("Language server error: {}\n\n{}", server_name, error), | |
| 160 | − )], | |
| 164 | + [(0..0, format!("Language server {server_name}:\n\n{status}"))], | |
| 161 | 165 | None, |
| 162 | 166 | cx, |
| 163 | 167 | ); |
| @@ -166,7 +170,10 @@ impl ActivityIndicator { · 2 unmodified lines | ||
| 166 | 170 | workspace.update_in(cx, |workspace, window, cx| { |
| 167 | 171 | workspace.add_item_to_active_pane( |
| 168 | 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 | 178 | None, |
| 172 | 179 | true, |
| @@ -185,19 +192,34 @@ impl ActivityIndicator { · 12 unmodified lines | ||
| 185 | 192 | } |
| 186 | 193 | |
| 187 | 194 | fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) { |
| 188 | − self.statuses.retain(|status| { | |
| 189 | − if let BinaryStatus::Failed { error } = &status.status { | |
| 190 | − cx.emit(Event::ShowError { | |
| 195 | + let mut status_message_shown = false; | |
| 196 | + self.statuses.retain(|status| match &status.status { | |
| 197 | + LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error }) | |
| 198 | + if !status_message_shown => | |
| 199 | + { | |
| 200 | + cx.emit(Event::ShowStatus { | |
| 191 | 201 | server_name: status.name.clone(), |
| 192 | − error: error.clone(), | |
| 202 | + status: SharedString::from(error), | |
| 193 | 203 | }); |
| 204 | + status_message_shown = true; | |
| 194 | 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 | 225 | fn dismiss_error_message( |
| @@ -267,48 +289,52 @@ impl ActivityIndicator { · 63 unmodified lines | ||
| 267 | 289 | }); |
| 268 | 290 | } |
| 269 | 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 | |
| 278 | − .title | |
| 279 | − .as_deref() | |
| 280 | − .unwrap_or(progress_token) | |
| 281 | − .to_string(); | |
| 282 | − | |
| 283 | − if let Some(percentage) = progress.percentage { | |
| 284 | − write!(&mut message, " ({}%)", percentage).unwrap(); | |
| 285 | − } | |
| 293 | + let mut pending_work = self.pending_language_server_work(cx); | |
| 294 | + if let Some(PendingWork { | |
| 295 | + progress_token, | |
| 296 | + progress, | |
| 297 | + .. | |
| 298 | + }) = pending_work.next() | |
| 299 | + { | |
| 300 | + let mut message = progress | |
| 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() { | |
| 288 | − message.push_str(": "); | |
| 289 | − message.push_str(progress_message); | |
| 290 | − } | |
| 310 | + if let Some(progress_message) = progress.message.as_ref() { | |
| 311 | + message.push_str(": "); | |
| 312 | + message.push_str(progress_message); | |
| 313 | + } | |
| 291 | 314 | |
| 292 | − let additional_work_count = pending_work.count(); | |
| 293 | − if additional_work_count > 0 { | |
| 294 | − write!(&mut message, " + {} more", additional_work_count).unwrap(); | |
| 295 | − } | |
| 315 | + let additional_work_count = pending_work.count(); | |
| 316 | + if additional_work_count > 0 { | |
| 317 | + write!(&mut message, " + {} more", additional_work_count).unwrap(); | |
| 318 | + } | |
| 296 | 319 | |
| 297 | − return Some(Content { | |
| 298 | − icon: Some( | |
| 299 | − Icon::new(IconName::ArrowCircle) | |
| 300 | − .size(IconSize::Small) | |
| 301 | − .with_animation( | |
| 302 | − "arrow-circle", | |
| 303 | − Animation::new(Duration::from_secs(2)).repeat(), | |
| 304 | − |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), | |
| 305 | − ) | |
| 306 | − .into_any_element(), | |
| 307 | − ), | |
| 308 | − message, | |
| 309 | − on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)), | |
| 310 | − tooltip_message: None, | |
| 311 | − }); | |
| 320 | + return Some(Content { | |
| 321 | + icon: Some( | |
| 322 | + Icon::new(IconName::ArrowCircle) | |
| 323 | + .size(IconSize::Small) | |
| 324 | + .with_animation( | |
| 325 | + "arrow-circle", | |
| 326 | + Animation::new(Duration::from_secs(2)).repeat(), | |
| 327 | + |icon, delta| { | |
| 328 | + icon.transform(Transformation::rotate(percentage(delta))) | |
| 329 | + }, | |
| 330 | + ) | |
| 331 | + .into_any_element(), | |
| 332 | + ), | |
| 333 | + message, | |
| 334 | + on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)), | |
| 335 | + tooltip_message: None, | |
| 336 | + }); | |
| 337 | + } | |
| 312 | 338 | } |
| 313 | 339 | |
| 314 | 340 | if let Some(session) = self |
| @@ -369,14 +395,38 @@ impl ActivityIndicator { · 54 unmodified lines | ||
| 369 | 395 | let mut downloading = SmallVec::<[_; 3]>::new(); |
| 370 | 396 | let mut checking_for_update = SmallVec::<[_; 3]>::new(); |
| 371 | 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 | 400 | for status in &self.statuses { |
| 373 | − match status.status { | |
| 374 | − BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()), | |
| 375 | − BinaryStatus::Downloading => downloading.push(status.name.clone()), | |
| 376 | − BinaryStatus::Failed { .. } => failed.push(status.name.clone()), | |
| 377 | − BinaryStatus::None => {} | |
| 401 | + match &status.status { | |
| 402 | + LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => { | |
| 403 | + checking_for_update.push(status.name.clone()); | |
| 404 | + } | |
| 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 | 431 | if !downloading.is_empty() { |
| 382 | 432 | return Some(Content { |
| @@ -457,7 +507,7 @@ impl ActivityIndicator { · 74 unmodified lines | ||
| 457 | 507 | }), |
| 458 | 508 | ), |
| 459 | 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 | 512 | tooltip_message: None, |
| 463 | 513 | }); |
| @@ -471,7 +521,7 @@ impl ActivityIndicator { · 7 unmodified lines | ||
| 471 | 521 | .size(IconSize::Small) |
| 472 | 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 | 525 | on_click: Some(Arc::new(|indicator, window, cx| { |
| 476 | 526 | indicator.project.update(cx, |project, cx| { |
| 477 | 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 | 585 | // Show any application auto-update info. |
| 486 | 586 | if let Some(updater) = &self.auto_updater { |
| 487 | 587 | return match &updater.read(cx).status() { |
| @@ -109,5 +109,6 @@ gpui = { workspace = true, "features" = ["test-support"] } · 108 unmodified lines | ||
| 109 | 109 | indoc.workspace = true |
| 110 | 110 | language = { workspace = true, "features" = ["test-support"] } |
| 111 | 111 | language_model = { workspace = true, "features" = ["test-support"] } |
| 112 | +pretty_assertions.workspace = true | |
| 112 | 113 | project = { workspace = true, features = ["test-support"] } |
| 113 | 114 | rand.workspace = true |
| @@ -1605,6 +1605,7 @@ impl ActiveThread { · 1604 unmodified lines | ||
| 1605 | 1605 | |
| 1606 | 1606 | this.thread.update(cx, |thread, cx| { |
| 1607 | 1607 | thread.advance_prompt_id(); |
| 1608 | + thread.cancel_last_completion(Some(window.window_handle()), cx); | |
| 1608 | 1609 | thread.send_to_model( |
| 1609 | 1610 | model.model, |
| 1610 | 1611 | CompletionIntent::UserPrompt, |
| @@ -1680,7 +1681,10 @@ impl ActiveThread { · 69 unmodified lines | ||
| 1680 | 1681 | |
| 1681 | 1682 | let editor = cx.new(|cx| { |
| 1682 | 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 | 1688 | buffer, |
| 1685 | 1689 | None, |
| 1686 | 1690 | window, |
| @@ -1788,12 +1792,31 @@ impl ActiveThread { · 101 unmodified lines | ||
| 1788 | 1792 | |
| 1789 | 1793 | fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { |
| 1790 | 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 | 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 | 1818 | if message.is_hidden { |
| 1796 | − return Empty.into_any(); | |
| 1819 | + return div().children(loading_dots).into_any(); | |
| 1797 | 1820 | } |
| 1798 | 1821 | |
| 1799 | 1822 | let message_creases = message.creases.clone(); |
| @@ -1802,9 +1825,6 @@ impl ActiveThread { · 2 unmodified lines | ||
| 1802 | 1825 | return Empty.into_any(); |
| 1803 | 1826 | }; |
| 1804 | 1827 | |
| 1805 | − let workspace = self.workspace.clone(); | |
| 1806 | − let thread = self.thread.read(cx); | |
| 1807 | − | |
| 1808 | 1828 | // Get all the data we need from thread before we start using it in closures |
| 1809 | 1829 | let checkpoint = thread.checkpoint_for_message(message_id); |
| 1810 | 1830 | let configured_model = thread.configured_model().map(|m| m.model); |
| @@ -1815,14 +1835,6 @@ impl ActiveThread { · 4 unmodified lines | ||
| 1815 | 1835 | |
| 1816 | 1836 | let tool_uses = thread.tool_uses_for_message(message_id, cx); |
| 1817 | 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 | 1839 | let editing_message_state = self |
| 1828 | 1840 | .editing_message |
| @@ -2238,17 +2250,7 @@ impl ActiveThread { · 409 unmodified lines | ||
| 2238 | 2250 | parent.child(self.render_rules_item(cx)) |
| 2239 | 2251 | }) |
| 2240 | 2252 | .child(styled_message) |
| 2241 | − .when(is_generating && is_last_message, |this| { | |
| 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 | − }) | |
| 2253 | + .children(loading_dots) | |
| 2252 | 2254 | .when(show_feedback, move |parent| { |
| 2253 | 2255 | parent.child(feedback_items).when_some( |
| 2254 | 2256 | self.open_feedback_editors.get(&message_id), |
| @@ -3708,7 +3710,7 @@ mod tests { · 1453 unmodified lines | ||
| 3708 | 3710 | use util::path; |
| 3709 | 3711 | use workspace::CollaboratorId; |
| 3710 | 3712 | |
| 3711 | − use crate::{ContextLoadResult, thread_store}; | |
| 3713 | + use crate::{ContextLoadResult, thread::MessageSegment, thread_store}; | |
| 3712 | 3714 | |
| 3713 | 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 | 3955 | fn init_test_settings(cx: &mut TestAppContext) { |
| 3846 | 3956 | cx.update(|cx| { |
| 3847 | 3957 | let settings_store = SettingsStore::test(cx); |
| @@ -162,7 +162,7 @@ pub fn init( · 161 unmodified lines | ||
| 162 | 162 | assistant_slash_command::init(cx); |
| 163 | 163 | thread_store::init(cx); |
| 164 | 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 | 167 | register_slash_commands(cx); |
| 168 | 168 | inline_assistant::init( |
| @@ -12,7 +12,7 @@ use context_server::ContextServerId; · 11 unmodified lines | ||
| 12 | 12 | use fs::Fs; |
| 13 | 13 | use gpui::{ |
| 14 | 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 | 17 | use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; |
| 18 | 18 | use project::context_server_store::{ContextServerStatus, ContextServerStore}; |
| @@ -475,7 +475,6 @@ impl AgentConfiguration { · 456 unmodified lines | ||
| 475 | 475 | .get(&context_server_id) |
| 476 | 476 | .copied() |
| 477 | 477 | .unwrap_or_default(); |
| 478 | − | |
| 479 | 478 | let tools = tools_by_source |
| 480 | 479 | .get(&ToolSource::ContextServer { |
| 481 | 480 | id: context_server_id.0.clone().into(), |
| @@ -484,25 +483,23 @@ impl AgentConfiguration { · 2 unmodified lines | ||
| 484 | 483 | let tool_count = tools.len(); |
| 485 | 484 | |
| 486 | 485 | let border_color = cx.theme().colors().border.opacity(0.6); |
| 487 | − let success_color = Color::Success.color(cx); | |
| 488 | 486 | |
| 489 | 487 | let (status_indicator, tooltip_text) = match server_status { |
| 490 | 488 | ContextServerStatus::Starting => ( |
| 491 | − Indicator::dot() | |
| 492 | − .color(Color::Success) | |
| 489 | + Icon::new(IconName::LoadCircle) | |
| 490 | + .size(IconSize::XSmall) | |
| 491 | + .color(Color::Accent) | |
| 493 | 492 | .with_animation( |
| 494 | 493 | SharedString::from(format!("{}-starting", context_server_id.0.clone(),)), |
| 495 | − Animation::new(Duration::from_secs(2)) | |
| 496 | − .repeat() | |
| 497 | − .with_easing(pulsating_between(0.4, 1.)), | |
| 498 | − move |this, delta| this.color(success_color.alpha(delta).into()), | |
| 494 | + Animation::new(Duration::from_secs(3)).repeat(), | |
| 495 | + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), | |
| 499 | 496 | ) |
| 500 | 497 | .into_any_element(), |
| 501 | 498 | "Server is starting.", |
| 502 | 499 | ), |
| 503 | 500 | ContextServerStatus::Running => ( |
| 504 | 501 | Indicator::dot().color(Color::Success).into_any_element(), |
| 505 | − "Server is running.", | |
| 502 | + "Server is active.", | |
| 506 | 503 | ), |
| 507 | 504 | ContextServerStatus::Error(_) => ( |
| 508 | 505 | Indicator::dot().color(Color::Error).into_any_element(), |
| @@ -526,12 +523,11 @@ impl AgentConfiguration { · 17 unmodified lines | ||
| 526 | 523 | .p_1() |
| 527 | 524 | .justify_between() |
| 528 | 525 | .when( |
| 529 | − error.is_some() || are_tools_expanded && tool_count > 1, | |
| 526 | + error.is_some() || are_tools_expanded && tool_count >= 1, | |
| 530 | 527 | |element| element.border_b_1().border_color(border_color), |
| 531 | 528 | ) |
| 532 | 529 | .child( |
| 533 | 530 | h_flex() |
| 534 | − .gap_1p5() | |
| 535 | 531 | .child( |
| 536 | 532 | Disclosure::new( |
| 537 | 533 | "tool-list-disclosure", |
| @@ -551,12 +547,16 @@ impl AgentConfiguration { · 13 unmodified lines | ||
| 551 | 547 | })), |
| 552 | 548 | ) |
| 553 | 549 | .child( |
| 554 | − div() | |
| 555 | − .id(item_id.clone()) | |
| 550 | + h_flex() | |
| 551 | + .id(SharedString::from(format!("tooltip-{}", item_id))) | |
| 552 | + .h_full() | |
| 553 | + .w_3() | |
| 554 | + .mx_1() | |
| 555 | + .justify_center() | |
| 556 | 556 | .tooltip(Tooltip::text(tooltip_text)) |
| 557 | 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 | 560 | .when(is_running, |this| { |
| 561 | 561 | this.child( |
| 562 | 562 | Label::new(if tool_count == 1 { |
| @@ -89,7 +89,7 @@ impl ConfigureContextServerModal { · 88 unmodified lines | ||
| 89 | 89 | }), |
| 90 | 90 | settings_validator, |
| 91 | 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 | 93 | editor.set_text(config.default_settings.trim(), window, cx); |
| 94 | 94 | editor.set_show_gutter(false, cx); |
| 95 | 95 | editor.set_soft_wrap_mode( |
| @@ -31,7 +31,7 @@ use util::ResultExt; · 30 unmodified lines | ||
| 31 | 31 | use workspace::{ |
| 32 | 32 | Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, |
| 33 | 33 | Workspace, |
| 34 | − item::{BreadcrumbText, ItemEvent, TabContentParams}, | |
| 34 | + item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams}, | |
| 35 | 35 | searchable::SearchableItemHandle, |
| 36 | 36 | }; |
| 37 | 37 | use zed_actions::assistant::ToggleFocus; |
| @@ -532,12 +532,12 @@ impl Item for AgentDiffPane { · 494 unmodified lines | ||
| 532 | 532 | |
| 533 | 533 | fn save( |
| 534 | 534 | &mut self, |
| 535 | − format: bool, | |
| 535 | + options: SaveOptions, | |
| 536 | 536 | project: Entity<Project>, |
| 537 | 537 | window: &mut Window, |
| 538 | 538 | cx: &mut Context<Self>, |
| 539 | 539 | ) -> Task<Result<()>> { |
| 540 | − self.editor.save(format, project, window, cx) | |
| 540 | + self.editor.save(options, project, window, cx) | |
| 541 | 541 | } |
| 542 | 542 | |
| 543 | 543 | fn save_as( |
| @@ -1513,7 +1513,7 @@ impl AgentDiff { · 969 unmodified lines | ||
| 1513 | 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 | 1517 | EditorState::Generating |
| 1518 | 1518 | } else { |
| 1519 | 1519 | EditorState::Reviewing |
| @@ -91,12 +91,13 @@ impl AgentModelSelector { · 90 unmodified lines | ||
| 91 | 91 | |
| 92 | 92 | impl Render for AgentModelSelector { |
| 93 | 93 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| 94 | − let focus_handle = self.focus_handle.clone(); | |
| 95 | − | |
| 96 | 94 | let model = self.selector.read(cx).delegate.active_model(cx); |
| 97 | 95 | let model_name = model |
| 98 | 96 | .map(|model| model.model.name().0) |
| 99 | 97 | .unwrap_or_else(|| SharedString::from("No model selected")); |
| 98 | + | |
| 99 | + let focus_handle = self.focus_handle.clone(); | |
| 100 | + | |
| 100 | 101 | PickerPopoverMenu::new( |
| 101 | 102 | self.selector.clone(), |
| 102 | 103 | Button::new("active-model", model_name) |
| @@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize}; · 9 unmodified lines | ||
| 10 | 10 | use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView}; |
| 11 | 11 | use anyhow::{Result, anyhow}; |
| 12 | 12 | use assistant_context_editor::{ |
| 13 | − AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent, | |
| 14 | − ContextSummary, SlashCommandCompletionProvider, humanize_token_count, | |
| 15 | − make_lsp_adapter_delegate, render_remaining_tokens, | |
| 13 | + AgentPanelDelegate, AssistantContext, ContextEditor, ContextEvent, ContextSummary, | |
| 14 | + SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate, | |
| 15 | + render_remaining_tokens, | |
| 16 | 16 | }; |
| 17 | 17 | use assistant_slash_command::SlashCommandWorkingSet; |
| 18 | 18 | use assistant_tool::ToolWorkingSet; |
| @@ -29,7 +29,8 @@ use gpui::{ · 10 unmodified lines | ||
| 29 | 29 | }; |
| 30 | 30 | use language::LanguageRegistry; |
| 31 | 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 | 35 | use project::{Project, ProjectPath, Worktree}; |
| 35 | 36 | use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; |
| @@ -2353,24 +2354,6 @@ impl AgentPanel { · 2317 unmodified lines | ||
| 2353 | 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 | 2357 | fn render_thread_empty_state( |
| 2375 | 2358 | &self, |
| 2376 | 2359 | window: &mut Window, |
| @@ -2380,7 +2363,9 @@ impl AgentPanel { · 3 unmodified lines | ||
| 2380 | 2363 | .history_store |
| 2381 | 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 | 2369 | let no_error = configuration_error.is_none(); |
| 2385 | 2370 | let focus_handle = self.focus_handle(cx); |
| 2386 | 2371 | |
| @@ -2397,11 +2382,7 @@ impl AgentPanel { · 10 unmodified lines | ||
| 2397 | 2382 | .justify_center() |
| 2398 | 2383 | .items_center() |
| 2399 | 2384 | .gap_1() |
| 2400 | − .child( | |
| 2401 | − h_flex().child( | |
| 2402 | − Headline::new("Welcome to the Agent Panel") | |
| 2403 | − ), | |
| 2404 | − ) | |
| 2385 | + .child(h_flex().child(Headline::new("Welcome to the Agent Panel"))) | |
| 2405 | 2386 | .when(no_error, |parent| { |
| 2406 | 2387 | parent |
| 2407 | 2388 | .child( |
| @@ -2425,7 +2406,10 @@ impl AgentPanel { · 17 unmodified lines | ||
| 2425 | 2406 | cx, |
| 2426 | 2407 | )) |
| 2427 | 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 | 2415 | .child( |
| @@ -2442,7 +2426,10 @@ impl AgentPanel { · 10 unmodified lines | ||
| 2442 | 2426 | cx, |
| 2443 | 2427 | )) |
| 2444 | 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 | 2435 | .child( |
| @@ -2459,7 +2446,10 @@ impl AgentPanel { · 10 unmodified lines | ||
| 2459 | 2446 | cx, |
| 2460 | 2447 | )) |
| 2461 | 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 | 2455 | .child( |
| @@ -2476,51 +2466,50 @@ impl AgentPanel { · 10 unmodified lines | ||
| 2476 | 2466 | cx, |
| 2477 | 2467 | )) |
| 2478 | 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| { | |
| 2484 | − match configuration_error_ref { | |
| 2485 | − Some(ConfigurationError::ProviderNotAuthenticated) | |
| 2486 | − | Some(ConfigurationError::NoProvider) => { | |
| 2487 | − parent | |
| 2488 | − .child( | |
| 2489 | − h_flex().child( | |
| 2490 | − Label::new("To start using the agent, configure at least one LLM provider.") | |
| 2491 | − .color(Color::Muted) | |
| 2492 | − .mb_2p5() | |
| 2493 | − ) | |
| 2494 | − ) | |
| 2495 | − .child( | |
| 2496 | − Button::new("settings", "Configure a Provider") | |
| 2497 | − .icon(IconName::Settings) | |
| 2498 | − .icon_position(IconPosition::Start) | |
| 2499 | − .icon_size(IconSize::Small) | |
| 2500 | − .icon_color(Color::Muted) | |
| 2501 | − .full_width() | |
| 2502 | − .key_binding(KeyBinding::for_action_in( | |
| 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, | |
| 2476 | + .map(|parent| match configuration_error_ref { | |
| 2477 | + Some( | |
| 2478 | + err @ (ConfigurationError::ModelNotFound | |
| 2479 | + | ConfigurationError::ProviderNotAuthenticated(_) | |
| 2480 | + | ConfigurationError::NoProvider), | |
| 2481 | + ) => parent | |
| 2482 | + .child(h_flex().child( | |
| 2483 | + Label::new(err.to_string()).color(Color::Muted).mb_2p5(), | |
| 2484 | + )) | |
| 2485 | + .child( | |
| 2486 | + Button::new("settings", "Configure a Provider") | |
| 2487 | + .icon(IconName::Settings) | |
| 2488 | + .icon_position(IconPosition::Start) | |
| 2489 | + .icon_size(IconSize::Small) | |
| 2490 | + .icon_color(Color::Muted) | |
| 2491 | + .full_width() | |
| 2492 | + .key_binding(KeyBinding::for_action_in( | |
| 2493 | + &OpenConfiguration, | |
| 2494 | + &focus_handle, | |
| 2495 | + window, | |
| 2517 | 2496 | cx, |
| 2518 | − ), | |
| 2519 | − ) | |
| 2520 | − } | |
| 2521 | − None => parent, | |
| 2497 | + )) | |
| 2498 | + .on_click(|_event, window, cx| { | |
| 2499 | + window.dispatch_action( | |
| 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 | 2515 | .when(!recent_history.is_empty(), |parent| { |
| @@ -2555,7 +2544,8 @@ impl AgentPanel { · 28 unmodified lines | ||
| 2555 | 2544 | &self.focus_handle(cx), |
| 2556 | 2545 | window, |
| 2557 | 2546 | cx, |
| 2558 | − ).map(|kb| kb.size(rems_from_px(12.))), | |
| 2547 | + ) | |
| 2548 | + .map(|kb| kb.size(rems_from_px(12.))), | |
| 2559 | 2549 | ) |
| 2560 | 2550 | .on_click(move |_event, window, cx| { |
| 2561 | 2551 | window.dispatch_action(OpenHistory.boxed_clone(), cx); |
| @@ -2565,79 +2555,68 @@ impl AgentPanel { · 3 unmodified lines | ||
| 2565 | 2555 | .child( |
| 2566 | 2556 | v_flex() |
| 2567 | 2557 | .gap_1() |
| 2568 | − .children( | |
| 2569 | − recent_history.into_iter().enumerate().map(|(index, entry)| { | |
| 2558 | + .children(recent_history.into_iter().enumerate().map( | |
| 2559 | + |(index, entry)| { | |
| 2570 | 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 | 2563 | HistoryEntryElement::new(entry.clone(), cx.entity().downgrade()) |
| 2573 | 2564 | .hovered(is_hovered) |
| 2574 | − .on_hover(cx.listener(move |this, is_hovered, _window, cx| { | |
| 2575 | − if *is_hovered { | |
| 2576 | − this.hovered_recent_history_item = Some(index); | |
| 2577 | − } else if this.hovered_recent_history_item == Some(index) { | |
| 2578 | − this.hovered_recent_history_item = None; | |
| 2579 | − } | |
| 2580 | − cx.notify(); | |
| 2581 | − })) | |
| 2565 | + .on_hover(cx.listener( | |
| 2566 | + move |this, is_hovered, _window, cx| { | |
| 2567 | + if *is_hovered { | |
| 2568 | + this.hovered_recent_history_item = Some(index); | |
| 2569 | + } else if this.hovered_recent_history_item | |
| 2570 | + == Some(index) | |
| 2571 | + { | |
| 2572 | + this.hovered_recent_history_item = None; | |
| 2573 | + } | |
| 2574 | + cx.notify(); | |
| 2575 | + }, | |
| 2576 | + )) | |
| 2582 | 2577 | .into_any_element() |
| 2583 | − }), | |
| 2584 | − ) | |
| 2578 | + }, | |
| 2579 | + )), | |
| 2585 | 2580 | ) |
| 2586 | − .map(|parent| { | |
| 2587 | − match configuration_error_ref { | |
| 2588 | − Some(ConfigurationError::ProviderNotAuthenticated) | |
| 2589 | − | Some(ConfigurationError::NoProvider) => { | |
| 2590 | − parent | |
| 2591 | − .child( | |
| 2592 | − Banner::new() | |
| 2593 | − .severity(ui::Severity::Warning) | |
| 2594 | − .child( | |
| 2595 | − Label::new( | |
| 2596 | − "Configure at least one LLM provider to start using the panel.", | |
| 2597 | − ) | |
| 2598 | − .size(LabelSize::Small), | |
| 2581 | + .map(|parent| match configuration_error_ref { | |
| 2582 | + Some( | |
| 2583 | + err @ (ConfigurationError::ModelNotFound | |
| 2584 | + | ConfigurationError::ProviderNotAuthenticated(_) | |
| 2585 | + | ConfigurationError::NoProvider), | |
| 2586 | + ) => parent.child( | |
| 2587 | + Banner::new() | |
| 2588 | + .severity(ui::Severity::Warning) | |
| 2589 | + .child(Label::new(err.to_string()).size(LabelSize::Small)) | |
| 2590 | + .action_slot( | |
| 2591 | + Button::new("settings", "Configure Provider") | |
| 2592 | + .style(ButtonStyle::Tinted(ui::TintColor::Warning)) | |
| 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 | − Button::new("settings", "Configure Provider") | |
| 2602 | − .style(ButtonStyle::Tinted(ui::TintColor::Warning)) | |
| 2603 | − .label_size(LabelSize::Small) | |
| 2604 | − .key_binding( | |
| 2605 | − KeyBinding::for_action_in( | |
| 2606 | − &OpenConfiguration, | |
| 2607 | − &focus_handle, | |
| 2608 | − window, | |
| 2609 | − cx, | |
| 2610 | − ) | |
| 2611 | − .map(|kb| kb.size(rems_from_px(12.))), | |
| 2612 | − ) | |
| 2613 | − .on_click(|_event, window, cx| { | |
| 2614 | − window.dispatch_action( | |
| 2615 | − OpenConfiguration.boxed_clone(), | |
| 2616 | − cx, | |
| 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, | |
| 2601 | + .map(|kb| kb.size(rems_from_px(12.))), | |
| 2602 | + ) | |
| 2603 | + .on_click(|_event, window, cx| { | |
| 2604 | + window.dispatch_action( | |
| 2605 | + OpenConfiguration.boxed_clone(), | |
| 2606 | + cx, | |
| 2607 | + ) | |
| 2608 | + }), | |
| 2609 | + ), | |
| 2610 | + ), | |
| 2611 | + Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { | |
| 2612 | + parent.child(Banner::new().severity(ui::Severity::Warning).child( | |
| 2613 | + h_flex().w_full().children(provider.render_accept_terms( | |
| 2614 | + LanguageModelProviderTosView::ThreadtEmptyState, | |
| 2615 | + cx, | |
| 2616 | + )), | |
| 2617 | + )) | |
| 2640 | 2618 | } |
| 2619 | + None => parent, | |
| 2641 | 2620 | }) |
| 2642 | 2621 | }) |
| 2643 | 2622 | } |
| @@ -386,8 +386,10 @@ impl CodegenAlternative { · 385 unmodified lines | ||
| 386 | 386 | async { Ok(LanguageModelTextStream::default()) }.boxed_local() |
| 387 | 387 | } else { |
| 388 | 388 | let request = self.build_request(&model, user_prompt, cx)?; |
| 389 | − cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await) | |
| 390 | − .boxed_local() | |
| 389 | + cx.spawn(async move |_, cx| { | |
| 390 | + Ok(model.stream_completion_text(request.await, &cx).await?) | |
| 391 | + }) | |
| 392 | + .boxed_local() | |
| 391 | 393 | }; |
| 392 | 394 | self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx); |
| 393 | 395 | Ok(()) |
| @@ -1066,7 +1066,7 @@ mod tests { · 1065 unmodified lines | ||
| 1066 | 1066 | use serde_json::json; |
| 1067 | 1067 | use settings::SettingsStore; |
| 1068 | 1068 | use std::{ops::Deref, rc::Rc}; |
| 1069 | − use util::{path, separator}; | |
| 1069 | + use util::path; | |
| 1070 | 1070 | use workspace::{AppState, Item}; |
| 1071 | 1071 | |
| 1072 | 1072 | #[test] |
| @@ -1217,14 +1217,14 @@ mod tests { · 144 unmodified lines | ||
| 1217 | 1217 | let mut cx = VisualTestContext::from_window(*window.deref(), cx); |
| 1218 | 1218 | |
| 1219 | 1219 | let paths = vec![ |
| 1220 | − separator!("a/one.txt"), | |
| 1221 | − separator!("a/two.txt"), | |
| 1222 | − separator!("a/three.txt"), | |
| 1223 | − separator!("a/four.txt"), | |
| 1224 | − separator!("b/five.txt"), | |
| 1225 | − separator!("b/six.txt"), | |
| 1226 | − separator!("b/seven.txt"), | |
| 1227 | − separator!("b/eight.txt"), | |
| 1220 | + path!("a/one.txt"), | |
| 1221 | + path!("a/two.txt"), | |
| 1222 | + path!("a/three.txt"), | |
| 1223 | + path!("a/four.txt"), | |
| 1224 | + path!("b/five.txt"), | |
| 1225 | + path!("b/six.txt"), | |
| 1226 | + path!("b/seven.txt"), | |
| 1227 | + path!("b/eight.txt"), | |
| 1228 | 1228 | ]; |
| 1229 | 1229 | |
| 1230 | 1230 | let mut opened_editors = Vec::new(); |
| @@ -3,16 +3,21 @@ use std::sync::Arc; · 2 unmodified lines | ||
| 3 | 3 | use anyhow::Context as _; |
| 4 | 4 | use context_server::ContextServerId; |
| 5 | 5 | use extension::{ContextServerConfiguration, ExtensionManifest}; |
| 6 | +use fs::Fs; | |
| 6 | 7 | use gpui::Task; |
| 7 | 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 | 14 | use ui::prelude::*; |
| 10 | 15 | use util::ResultExt; |
| 11 | 16 | use workspace::Workspace; |
| 12 | 17 | |
| 13 | 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 | 21 | cx.observe_new(move |_: &mut Workspace, window, cx| { |
| 17 | 22 | let Some(window) = window else { |
| 18 | 23 | return; |
| @@ -21,6 +26,7 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 2 unmodified lines | ||
| 21 | 26 | if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() { |
| 22 | 27 | cx.subscribe_in(extension_events, window, { |
| 23 | 28 | let language_registry = language_registry.clone(); |
| 29 | + let fs = fs.clone(); | |
| 24 | 30 | move |workspace, _, event, window, cx| match event { |
| 25 | 31 | extension::Event::ExtensionInstalled(manifest) => { |
| 26 | 32 | show_configure_mcp_modal( |
| @@ -31,6 +37,13 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 4 unmodified lines | ||
| 31 | 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 | 47 | extension::Event::ConfigureExtensionRequested(manifest) => { |
| 35 | 48 | if !manifest.context_servers.is_empty() { |
| 36 | 49 | show_configure_mcp_modal( |
| @@ -55,6 +68,18 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 18 unmodified lines | ||
| 55 | 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 | 83 | pub enum Configuration { |
| 59 | 84 | NotAvailable(ContextServerId, Option<SharedString>), |
| 60 | 85 | Required( |
| @@ -71,6 +96,10 @@ fn show_configure_mcp_modal( · 10 unmodified lines | ||
| 71 | 96 | window: &mut Window, |
| 72 | 97 | cx: &mut Context<'_, Workspace>, |
| 73 | 98 | ) { |
| 99 | + if !window.is_window_active() { | |
| 100 | + return; | |
| 101 | + } | |
| 102 | + | |
| 74 | 103 | let context_server_store = workspace.project().read(cx).context_server_store(); |
| 75 | 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 | 105 | arguments |
| 106 | 106 | ); |
| 107 | 107 | let response = protocol |
| 108 | − .request::<context_server::types::request::CallTool>( | |
| 108 | + .request::<context_server::types::requests::CallTool>( | |
| 109 | 109 | context_server::types::CallToolParams { |
| 110 | 110 | name: tool_name, |
| 111 | 111 | arguments, |
| @@ -123,6 +123,9 @@ impl Tool for ContextServerTool { · 11 unmodified lines | ||
| 123 | 123 | types::ToolResponseContent::Image { .. } => { |
| 124 | 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 | 129 | types::ToolResponseContent::Resource { .. } => { |
| 127 | 130 | log::warn!("Ignoring resource content from tool response"); |
| 128 | 131 | } |
| @@ -24,6 +24,7 @@ use gpui::{ · 23 unmodified lines | ||
| 24 | 24 | WeakEntity, Window, point, |
| 25 | 25 | }; |
| 26 | 26 | use language::{Buffer, Point, Selection, TransactionId}; |
| 27 | +use language_model::ConfigurationError; | |
| 27 | 28 | use language_model::ConfiguredModel; |
| 28 | 29 | use language_model::{LanguageModelRegistry, report_assistant_event}; |
| 29 | 30 | use multi_buffer::MultiBufferRow; |
| @@ -38,8 +39,7 @@ use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; · 8 unmodified lines | ||
| 38 | 39 | use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; |
| 39 | 40 | use text::{OffsetRangeExt, ToPoint as _}; |
| 40 | 41 | use ui::prelude::*; |
| 41 | −use util::RangeExt; | |
| 42 | −use util::ResultExt; | |
| 42 | +use util::{RangeExt, ResultExt, maybe}; | |
| 43 | 43 | use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId}; |
| 44 | 44 | use zed_actions::agent::OpenConfiguration; |
| 45 | 45 | |
| @@ -233,10 +233,9 @@ impl InlineAssistant { · 187 unmodified lines | ||
| 233 | 233 | return; |
| 234 | 234 | }; |
| 235 | 235 | |
| 236 | − let is_authenticated = || { | |
| 237 | − LanguageModelRegistry::read_global(cx) | |
| 238 | − .inline_assistant_model() | |
| 239 | − .map_or(false, |model| model.provider.is_authenticated(cx)) | |
| 236 | + let configuration_error = || { | |
| 237 | + let model_registry = LanguageModelRegistry::read_global(cx); | |
| 238 | + model_registry.configuration_error(model_registry.inline_assistant_model(), cx) | |
| 240 | 239 | }; |
| 241 | 240 | |
| 242 | 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() { | |
| 288 | − handle_assist(window, cx); | |
| 289 | − } else { | |
| 290 | − cx.spawn_in(window, async move |_workspace, cx| { | |
| 291 | − let Some(task) = cx.update(|_, cx| { | |
| 292 | − LanguageModelRegistry::read_global(cx) | |
| 293 | − .inline_assistant_model() | |
| 294 | − .map_or(None, |model| Some(model.provider.authenticate(cx))) | |
| 295 | − })? | |
| 296 | − else { | |
| 286 | + if let Some(error) = configuration_error() { | |
| 287 | + if let ConfigurationError::ProviderNotAuthenticated(provider) = error { | |
| 288 | + cx.spawn(async move |_, cx| { | |
| 289 | + cx.update(|cx| provider.authenticate(cx))?.await?; | |
| 290 | + anyhow::Ok(()) | |
| 291 | + }) | |
| 292 | + .detach_and_log_err(cx); | |
| 293 | + | |
| 294 | + if configuration_error().is_none() { | |
| 295 | + handle_assist(window, cx); | |
| 296 | + } | |
| 297 | + } else { | |
| 298 | + cx.spawn_in(window, async move |_, cx| { | |
| 297 | 299 | let answer = cx |
| 298 | 300 | .prompt( |
| 299 | 301 | gpui::PromptLevel::Warning, |
| 300 | − "No language model provider configured", | |
| 302 | + &error.to_string(), | |
| 301 | 303 | None, |
| 302 | 304 | &["Configure", "Cancel"], |
| 303 | 305 | ) |
| @@ -311,17 +313,12 @@ impl InlineAssistant { · 7 unmodified lines | ||
| 311 | 313 | .ok(); |
| 312 | 314 | } |
| 313 | 315 | } |
| 314 | − return Ok(()); | |
| 315 | − }; | |
| 316 | − task.await?; | |
| 317 | − | |
| 318 | − anyhow::Ok(()) | |
| 319 | − }) | |
| 320 | − .detach_and_log_err(cx); | |
| 321 | − | |
| 322 | − if is_authenticated() { | |
| 323 | − handle_assist(window, cx); | |
| 316 | + anyhow::Ok(()) | |
| 317 | + }) | |
| 318 | + .detach_and_log_err(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 | 1168 | selections.select_anchor_ranges([position..position]) |
| 1172 | 1169 | }); |
| 1173 | 1170 | |
| 1174 | − let mut scroll_target_top; | |
| 1175 | − let mut scroll_target_bottom; | |
| 1171 | + let mut scroll_target_range = None; | |
| 1176 | 1172 | if let Some(decorations) = assist.decorations.as_ref() { |
| 1177 | − scroll_target_top = editor | |
| 1178 | − .row_for_block(decorations.prompt_block_id, cx) | |
| 1179 | − .unwrap() | |
| 1180 | − .0 as f32; | |
| 1181 | − scroll_target_bottom = editor | |
| 1182 | − .row_for_block(decorations.end_block_id, cx) | |
| 1183 | − .unwrap() | |
| 1184 | − .0 as f32; | |
| 1185 | − } else { | |
| 1173 | + scroll_target_range = maybe!({ | |
| 1174 | + let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32; | |
| 1175 | + let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f32; | |
| 1176 | + Some((top, bottom)) | |
| 1177 | + }); | |
| 1178 | + if scroll_target_range.is_none() { | |
| 1179 | + log::error!("bug: failed to find blocks for scrolling to inline assist"); | |
| 1180 | + } | |
| 1181 | + } | |
| 1182 | + let scroll_target_range = scroll_target_range.unwrap_or_else(|| { | |
| 1186 | 1183 | let snapshot = editor.snapshot(window, cx); |
| 1187 | 1184 | let start_row = assist |
| 1188 | 1185 | .range |
| 1189 | 1186 | .start |
| 1190 | 1187 | .to_display_point(&snapshot.display_snapshot) |
| 1191 | 1188 | .row(); |
| 1192 | − scroll_target_top = start_row.0 as f32; | |
| 1193 | − scroll_target_bottom = scroll_target_top + 1.; | |
| 1194 | − } | |
| 1189 | + let top = start_row.0 as f32; | |
| 1190 | + let bottom = top + 1.0; | |
| 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 | 1196 | scroll_target_top -= editor.vertical_scroll_margin() as f32; |
| 1196 | 1197 | scroll_target_bottom += editor.vertical_scroll_margin() as f32; |
| 1197 | 1198 | |
| @@ -1331,7 +1332,7 @@ impl InlineAssistant { · 133 unmodified lines | ||
| 1331 | 1332 | editor.clear_gutter_highlights::<GutterPendingRange>(cx); |
| 1332 | 1333 | } else { |
| 1333 | 1334 | editor.highlight_gutter::<GutterPendingRange>( |
| 1334 | − &gutter_pending_ranges, | |
| 1335 | + gutter_pending_ranges, | |
| 1335 | 1336 | |cx| cx.theme().status().info_background, |
| 1336 | 1337 | cx, |
| 1337 | 1338 | ) |
| @@ -1342,7 +1343,7 @@ impl InlineAssistant { · 4 unmodified lines | ||
| 1342 | 1343 | editor.clear_gutter_highlights::<GutterTransformedRange>(cx); |
| 1343 | 1344 | } else { |
| 1344 | 1345 | editor.highlight_gutter::<GutterTransformedRange>( |
| 1345 | − &gutter_transformed_ranges, | |
| 1346 | + gutter_transformed_ranges, | |
| 1346 | 1347 | |cx| cx.theme().status().info, |
| 1347 | 1348 | cx, |
| 1348 | 1349 | ) |
| @@ -261,7 +261,7 @@ impl<T: 'static> PromptEditor<T> { · 260 unmodified lines | ||
| 261 | 261 | |
| 262 | 262 | let focus = self.editor.focus_handle(cx).contains_focused(window, cx); |
| 263 | 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 | 265 | editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); |
| 266 | 266 | editor.set_placeholder_text("Add a prompt…", cx); |
| 267 | 267 | editor.set_text(prompt, window, cx); |
| @@ -872,6 +872,7 @@ impl PromptEditor<BufferCodegen> { · 604 unmodified lines | ||
| 872 | 872 | let prompt_editor = cx.new(|cx| { |
| 873 | 873 | let mut editor = Editor::new( |
| 874 | 874 | EditorMode::AutoHeight { |
| 875 | + min_lines: 1, | |
| 875 | 876 | max_lines: Self::MAX_LINES as usize, |
| 876 | 877 | }, |
| 877 | 878 | prompt_buffer, |
| @@ -1050,6 +1051,7 @@ impl PromptEditor<TerminalCodegen> { · 172 unmodified lines | ||
| 1050 | 1051 | let prompt_editor = cx.new(|cx| { |
| 1051 | 1052 | let mut editor = Editor::new( |
| 1052 | 1053 | EditorMode::AutoHeight { |
| 1054 | + min_lines: 1, | |
| 1053 | 1055 | max_lines: Self::MAX_LINES as usize, |
| 1054 | 1056 | }, |
| 1055 | 1057 | prompt_buffer, |
| @@ -39,7 +39,7 @@ use proto::Plan; · 38 unmodified lines | ||
| 39 | 39 | use settings::Settings; |
| 40 | 40 | use std::time::Duration; |
| 41 | 41 | use theme::ThemeSettings; |
| 42 | −use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*}; | |
| 42 | +use ui::{Callout, Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*}; | |
| 43 | 43 | use util::{ResultExt as _, maybe}; |
| 44 | 44 | use workspace::{CollaboratorId, Workspace}; |
| 45 | 45 | use zed_llm_client::CompletionIntent; |
| @@ -79,6 +79,7 @@ pub struct MessageEditor { · 33 unmodified lines | ||
| 79 | 79 | _subscriptions: Vec<Subscription>, |
| 80 | 80 | } |
| 81 | 81 | |
| 82 | +const MIN_EDITOR_LINES: usize = 4; | |
| 82 | 83 | const MAX_EDITOR_LINES: usize = 8; |
| 83 | 84 | |
| 84 | 85 | pub(crate) fn create_editor( |
| @@ -102,6 +103,7 @@ pub(crate) fn create_editor( · 17 unmodified lines | ||
| 102 | 103 | let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); |
| 103 | 104 | let mut editor = Editor::new( |
| 104 | 105 | editor::EditorMode::AutoHeight { |
| 106 | + min_lines: MIN_EDITOR_LINES, | |
| 105 | 107 | max_lines: MAX_EDITOR_LINES, |
| 106 | 108 | }, |
| 107 | 109 | buffer, |
| @@ -253,6 +255,7 @@ impl MessageEditor { · 145 unmodified lines | ||
| 253 | 255 | }) |
| 254 | 256 | } else { |
| 255 | 257 | editor.set_mode(EditorMode::AutoHeight { |
| 258 | + min_lines: MIN_EDITOR_LINES, | |
| 256 | 259 | max_lines: MAX_EDITOR_LINES, |
| 257 | 260 | }) |
| 258 | 261 | } |
| @@ -428,10 +431,6 @@ impl MessageEditor { · 169 unmodified lines | ||
| 428 | 431 | } |
| 429 | 432 | |
| 430 | 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 | 434 | self.edits_expanded = true; |
| 436 | 435 | AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err(); |
| 437 | 436 | cx.notify(); |
| @@ -506,7 +505,7 @@ impl MessageEditor { · 68 unmodified lines | ||
| 506 | 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 | 509 | let thread = self.thread.read(cx); |
| 511 | 510 | let model = thread.configured_model(); |
| 512 | 511 | if !model?.model.supports_max_mode() { |
| @@ -641,96 +640,87 @@ impl MessageEditor { · 128 unmodified lines | ||
| 641 | 640 | .border_color(cx.theme().colors().border) |
| 642 | 641 | .child( |
| 643 | 642 | h_flex() |
| 644 | − .items_start() | |
| 645 | 643 | .justify_between() |
| 646 | 644 | .child(self.context_strip.clone()) |
| 647 | − .child( | |
| 648 | − h_flex() | |
| 649 | − .gap_1() | |
| 650 | − .when(focus_handle.is_focused(window), |this| { | |
| 651 | − this.child( | |
| 652 | − IconButton::new("toggle-height", expand_icon) | |
| 653 | − .icon_size(IconSize::XSmall) | |
| 654 | − .icon_color(Color::Muted) | |
| 655 | − .tooltip({ | |
| 656 | − let focus_handle = focus_handle.clone(); | |
| 657 | − move |window, cx| { | |
| 658 | − let expand_label = if is_editor_expanded { | |
| 659 | − "Minimize Message Editor".to_string() | |
| 660 | − } else { | |
| 661 | − "Expand Message Editor".to_string() | |
| 662 | − }; | |
| 663 | − | |
| 664 | − Tooltip::for_action_in( | |
| 665 | − expand_label, | |
| 666 | − &ExpandMessageEditor, | |
| 667 | − &focus_handle, | |
| 668 | − window, | |
| 669 | − cx, | |
| 670 | − ) | |
| 671 | − } | |
| 672 | − }) | |
| 673 | − .on_click(cx.listener(|_, _, window, cx| { | |
| 674 | − window | |
| 675 | − .dispatch_action(Box::new(ExpandMessageEditor), cx); | |
| 676 | − })), | |
| 677 | − ) | |
| 678 | − }), | |
| 679 | − ), | |
| 645 | + .when(focus_handle.is_focused(window), |this| { | |
| 646 | + this.child( | |
| 647 | + IconButton::new("toggle-height", expand_icon) | |
| 648 | + .icon_size(IconSize::XSmall) | |
| 649 | + .icon_color(Color::Muted) | |
| 650 | + .tooltip({ | |
| 651 | + let focus_handle = focus_handle.clone(); | |
| 652 | + move |window, cx| { | |
| 653 | + let expand_label = if is_editor_expanded { | |
| 654 | + "Minimize Message Editor".to_string() | |
| 655 | + } else { | |
| 656 | + "Expand Message Editor".to_string() | |
| 657 | + }; | |
| 658 | + | |
| 659 | + Tooltip::for_action_in( | |
| 660 | + expand_label, | |
| 661 | + &ExpandMessageEditor, | |
| 662 | + &focus_handle, | |
| 663 | + window, | |
| 664 | + cx, | |
| 665 | + ) | |
| 666 | + } | |
| 667 | + }) | |
| 668 | + .on_click(cx.listener(|_, _, window, cx| { | |
| 669 | + window.dispatch_action(Box::new(ExpandMessageEditor), cx); | |
| 670 | + })), | |
| 671 | + ) | |
| 672 | + }), | |
| 680 | 673 | ) |
| 681 | 674 | .child( |
| 682 | 675 | v_flex() |
| 683 | 676 | .size_full() |
| 684 | − .gap_4() | |
| 677 | + .gap_1() | |
| 685 | 678 | .when(is_editor_expanded, |this| { |
| 686 | 679 | this.h(vh(0.8, window)).justify_between() |
| 687 | 680 | }) |
| 688 | − .child( | |
| 689 | − v_flex() | |
| 690 | − .min_h_16() | |
| 691 | − .when(is_editor_expanded, |this| this.h_full()) | |
| 692 | − .child({ | |
| 693 | − let settings = ThemeSettings::get_global(cx); | |
| 694 | − let font_size = TextSize::Small | |
| 695 | − .rems(cx) | |
| 696 | − .to_pixels(settings.agent_font_size(cx)); | |
| 697 | − let line_height = settings.buffer_line_height.value() * font_size; | |
| 698 | − | |
| 699 | − let text_style = TextStyle { | |
| 700 | − color: cx.theme().colors().text, | |
| 701 | − font_family: settings.buffer_font.family.clone(), | |
| 702 | − font_fallbacks: settings.buffer_font.fallbacks.clone(), | |
| 703 | − font_features: settings.buffer_font.features.clone(), | |
| 704 | − font_size: font_size.into(), | |
| 705 | − line_height: line_height.into(), | |
| 706 | − ..Default::default() | |
| 707 | − }; | |
| 708 | − | |
| 709 | − EditorElement::new( | |
| 710 | − &self.editor, | |
| 711 | − EditorStyle { | |
| 712 | − background: editor_bg_color, | |
| 713 | − local_player: cx.theme().players().local(), | |
| 714 | − text: text_style, | |
| 715 | − syntax: cx.theme().syntax().clone(), | |
| 716 | − ..Default::default() | |
| 717 | − }, | |
| 718 | − ) | |
| 719 | − .into_any() | |
| 720 | − }), | |
| 721 | − ) | |
| 681 | + .child({ | |
| 682 | + let settings = ThemeSettings::get_global(cx); | |
| 683 | + let font_size = TextSize::Small | |
| 684 | + .rems(cx) | |
| 685 | + .to_pixels(settings.agent_font_size(cx)); | |
| 686 | + let line_height = settings.buffer_line_height.value() * font_size; | |
| 687 | + | |
| 688 | + let text_style = TextStyle { | |
| 689 | + color: cx.theme().colors().text, | |
| 690 | + font_family: settings.buffer_font.family.clone(), | |
| 691 | + font_fallbacks: settings.buffer_font.fallbacks.clone(), | |
| 692 | + font_features: settings.buffer_font.features.clone(), | |
| 693 | + font_size: font_size.into(), | |
| 694 | + line_height: line_height.into(), | |
| 695 | + ..Default::default() | |
| 696 | + }; | |
| 697 | + | |
| 698 | + EditorElement::new( | |
| 699 | + &self.editor, | |
| 700 | + EditorStyle { | |
| 701 | + background: editor_bg_color, | |
| 702 | + local_player: cx.theme().players().local(), | |
| 703 | + text: text_style, | |
| 704 | + syntax: cx.theme().syntax().clone(), | |
| 705 | + ..Default::default() | |
| 706 | + }, | |
| 707 | + ) | |
| 708 | + .into_any() | |
| 709 | + }) | |
| 722 | 710 | .child( |
| 723 | 711 | h_flex() |
| 724 | 712 | .flex_none() |
| 713 | + .flex_wrap() | |
| 725 | 714 | .justify_between() |
| 726 | 715 | .child( |
| 727 | 716 | h_flex() |
| 728 | 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 | 720 | .child( |
| 732 | 721 | h_flex() |
| 733 | 722 | .gap_1() |
| 723 | + .flex_wrap() | |
| 734 | 724 | .when(!incompatible_tools.is_empty(), |this| { |
| 735 | 725 | this.child( |
| 736 | 726 | IconButton::new( |
| @@ -1183,6 +1173,7 @@ impl MessageEditor { · 446 unmodified lines | ||
| 1183 | 1173 | .map_or(false, |model| { |
| 1184 | 1174 | model.provider.id().0 == ZED_CLOUD_PROVIDER_ID |
| 1185 | 1175 | }); |
| 1176 | + | |
| 1186 | 1177 | if !is_using_zed_provider { |
| 1187 | 1178 | return None; |
| 1188 | 1179 | } |
| @@ -1237,14 +1228,6 @@ impl MessageEditor { · 48 unmodified lines | ||
| 1237 | 1228 | token_usage_ratio: TokenUsageRatio, |
| 1238 | 1229 | cx: &mut Context<Self>, |
| 1239 | 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 | 1231 | let icon = if token_usage_ratio == TokenUsageRatio::Exceeded { |
| 1249 | 1232 | Icon::new(IconName::X) |
| 1250 | 1233 | .color(Color::Error) |
| @@ -1255,19 +1238,43 @@ impl MessageEditor { · 4 unmodified lines | ||
| 1255 | 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 | 1247 | Some( |
| 1259 | 1248 | div() |
| 1260 | − .child(ui::Callout::multi_line( | |
| 1261 | − title, | |
| 1262 | − message, | |
| 1263 | − icon, | |
| 1264 | − "Start New Thread", | |
| 1265 | − Box::new(cx.listener(|this, _, window, cx| { | |
| 1266 | − let from_thread_id = Some(this.thread.read(cx).id().clone()); | |
| 1267 | − window.dispatch_action(Box::new(NewThread { from_thread_id }), cx); | |
| 1268 | − })), | |
| 1269 | − )) | |
| 1270 | − .line_height(line_height), | |
| 1249 | + .border_t_1() | |
| 1250 | + .border_color(cx.theme().colors().border) | |
| 1251 | + .child( | |
| 1252 | + Callout::new() | |
| 1253 | + .line_height(line_height) | |
| 1254 | + .icon(icon) | |
| 1255 | + .title(title) | |
| 1256 | + .description( | |
| 1257 | + "To continue, start a new thread from a summary or turn burn mode on.", | |
| 1258 | + ) | |
| 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 | 1471 | total_token_usage.ratio() |
| 1465 | 1472 | }); |
| 1466 | 1473 | |
| 1474 | + let burn_mode_enabled = thread.completion_mode() == CompletionMode::Burn; | |
| 1475 | + | |
| 1467 | 1476 | let action_log = self.thread.read(cx).action_log(); |
| 1468 | 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 | 1490 | if usage_callout.is_some() { |
| 1482 | 1491 | usage_callout |
| 1483 | − } else if token_usage_ratio != TokenUsageRatio::Normal { | |
| 1492 | + } else if token_usage_ratio != TokenUsageRatio::Normal && !burn_mode_enabled { | |
| 1484 | 1493 | self.render_token_limit_callout(line_height, token_usage_ratio, cx) |
| 1485 | 1494 | } else { |
| 1486 | 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 | 199 | pub struct ProjectSnapshot { |
| 200 | 200 | pub worktree_snapshots: Vec<WorktreeSnapshot>, |
| 201 | 201 | pub unsaved_buffer_paths: Vec<String>, |
| 202 | 202 | pub timestamp: DateTime<Utc>, |
| 203 | 203 | } |
| 204 | 204 | |
| 205 | −#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 205 | +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | |
| 206 | 206 | pub struct WorktreeSnapshot { |
| 207 | 207 | pub worktree_path: String, |
| 208 | 208 | pub git_state: Option<GitState>, |
| 209 | 209 | } |
| 210 | 210 | |
| 211 | −#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 211 | +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | |
| 212 | 212 | pub struct GitState { |
| 213 | 213 | pub remote_url: Option<String>, |
| 214 | 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 | 251 | pub enum DetailedSummaryState { |
| 252 | 252 | #[default] |
| 253 | 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 | 395 | pub struct ExceededWindowError { |
| 396 | 396 | /// Model used when last message exceeded context window |
| 397 | 397 | model_id: LanguageModelId, |
| @@ -1563,6 +1563,9 @@ impl Thread { · 1165 unmodified lines | ||
| 1563 | 1563 | Err(LanguageModelCompletionError::Other(error)) => { |
| 1564 | 1564 | return Err(error); |
| 1565 | 1565 | } |
| 1566 | + Err(err @ LanguageModelCompletionError::RateLimit(..)) => { | |
| 1567 | + return Err(err.into()); | |
| 1568 | + } | |
| 1566 | 1569 | }; |
| 1567 | 1570 | |
| 1568 | 1571 | match event { |
| @@ -594,10 +594,11 @@ impl Render for ThreadHistory { · 593 unmodified lines | ||
| 594 | 594 | view.pr_5() |
| 595 | 595 | .child( |
| 596 | 596 | uniform_list( |
| 597 | − cx.entity().clone(), | |
| 598 | 597 | "thread-history", |
| 599 | 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 | 603 | .p_1() |
| 603 | 604 | .track_scroll(self.scroll_handle.clone()) |
| @@ -89,7 +89,7 @@ pub fn init(cx: &mut App) { · 88 unmodified lines | ||
| 89 | 89 | pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>); |
| 90 | 90 | |
| 91 | 91 | impl SharedProjectContext { |
| 92 | − pub fn borrow(&self) -> Ref<Option<ProjectContext>> { | |
| 92 | + pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> { | |
| 93 | 93 | self.0.borrow() |
| 94 | 94 | } |
| 95 | 95 | } |
| @@ -305,17 +305,19 @@ impl ThreadStore { · 209 unmodified lines | ||
| 305 | 305 | project: Entity<Project>, |
| 306 | 306 | cx: &mut App, |
| 307 | 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 | 318 | let rules_task = Self::load_worktree_rules_file(worktree, project, cx); |
| 311 | 319 | let Some(rules_task) = rules_task else { |
| 312 | − return Task::ready(( | |
| 313 | − WorktreeContext { | |
| 314 | − root_name, | |
| 315 | − rules_file: None, | |
| 316 | − }, | |
| 317 | − None, | |
| 318 | − )); | |
| 320 | + return Task::ready((context, None)); | |
| 319 | 321 | }; |
| 320 | 322 | |
| 321 | 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 { | |
| 332 | − root_name, | |
| 333 | − rules_file, | |
| 334 | − }; | |
| 335 | − (worktree_info, rules_file_error) | |
| 333 | + context.rules_file = rules_file; | |
| 334 | + (context, rules_file_error) | |
| 336 | 335 | }) |
| 337 | 336 | } |
| 338 | 337 | |
| @@ -341,12 +340,12 @@ impl ThreadStore { · 2 unmodified lines | ||
| 341 | 340 | project: Entity<Project>, |
| 342 | 341 | cx: &mut App, |
| 343 | 342 | ) -> Option<Task<Result<RulesFileContext>>> { |
| 344 | − let worktree_ref = worktree.read(cx); | |
| 345 | − let worktree_id = worktree_ref.id(); | |
| 343 | + let worktree = worktree.read(cx); | |
| 344 | + let worktree_id = worktree.id(); | |
| 346 | 345 | let selected_rules_file = RULES_FILE_NAMES |
| 347 | 346 | .into_iter() |
| 348 | 347 | .filter_map(|name| { |
| 349 | − worktree_ref | |
| 348 | + worktree | |
| 350 | 349 | .entry_for_path(name) |
| 351 | 350 | .filter(|entry| entry.is_file()) |
| 352 | 351 | .map(|entry| entry.path.clone()) |
| @@ -562,7 +561,7 @@ impl ThreadStore { · 209 unmodified lines | ||
| 562 | 561 | |
| 563 | 562 | if protocol.capable(context_server::protocol::ServerCapability::Tools) { |
| 564 | 563 | if let Some(response) = protocol |
| 565 | − .request::<context_server::types::request::ListTools>(()) | |
| 564 | + .request::<context_server::types::requests::ListTools>(()) | |
| 566 | 565 | .await |
| 567 | 566 | .log_err() |
| 568 | 567 | { |
| @@ -603,7 +602,7 @@ pub struct SerializedThreadMetadata { · 34 unmodified lines | ||
| 603 | 602 | pub updated_at: DateTime<Utc>, |
| 604 | 603 | } |
| 605 | 604 | |
| 606 | −#[derive(Serialize, Deserialize, Debug)] | |
| 605 | +#[derive(Serialize, Deserialize, Debug, PartialEq)] | |
| 607 | 606 | pub struct SerializedThread { |
| 608 | 607 | pub version: String, |
| 609 | 608 | pub summary: SharedString, |
| @@ -629,7 +628,7 @@ pub struct SerializedThread { · 19 unmodified lines | ||
| 629 | 628 | pub profile: Option<AgentProfileId>, |
| 630 | 629 | } |
| 631 | 630 | |
| 632 | −#[derive(Serialize, Deserialize, Debug)] | |
| 631 | +#[derive(Serialize, Deserialize, Debug, PartialEq)] | |
| 633 | 632 | pub struct SerializedLanguageModel { |
| 634 | 633 | pub provider: String, |
| 635 | 634 | pub model: String, |
| @@ -690,11 +689,15 @@ impl SerializedThreadV0_1_0 { · 54 unmodified lines | ||
| 690 | 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 | 701 | pub struct SerializedMessage { |
| 699 | 702 | pub id: MessageId, |
| 700 | 703 | pub role: Role, |
| @@ -712,7 +715,7 @@ pub struct SerializedMessage { · 11 unmodified lines | ||
| 712 | 715 | pub is_hidden: bool, |
| 713 | 716 | } |
| 714 | 717 | |
| 715 | −#[derive(Debug, Serialize, Deserialize)] | |
| 718 | +#[derive(Debug, Serialize, Deserialize, PartialEq)] | |
| 716 | 719 | #[serde(tag = "type")] |
| 717 | 720 | pub enum SerializedMessageSegment { |
| 718 | 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 | 737 | pub struct SerializedToolUse { |
| 735 | 738 | pub id: LanguageModelToolUseId, |
| 736 | 739 | pub name: SharedString, |
| 737 | 740 | pub input: serde_json::Value, |
| 738 | 741 | } |
| 739 | 742 | |
| 740 | −#[derive(Debug, Serialize, Deserialize)] | |
| 743 | +#[derive(Debug, Serialize, Deserialize, PartialEq)] | |
| 741 | 744 | pub struct SerializedToolResult { |
| 742 | 745 | pub tool_use_id: LanguageModelToolUseId, |
| 743 | 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 | 807 | pub struct SerializedCrease { |
| 805 | 808 | pub start: usize, |
| 806 | 809 | pub end: usize, |
| @@ -919,7 +922,7 @@ impl ThreadsDatabase { · 112 unmodified lines | ||
| 919 | 922 | |
| 920 | 923 | fn bytes_encode( |
| 921 | 924 | item: &Self::EItem, |
| 922 | − ) -> Result<std::borrow::Cow<[u8]>, heed::BoxedError> { | |
| 925 | + ) -> Result<std::borrow::Cow<'_, [u8]>, heed::BoxedError> { | |
| 923 | 926 | serde_json::to_vec(&item.0) |
| 924 | 927 | .map(std::borrow::Cow::Owned) |
| 925 | 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 | 2 | use component::{empty_example, example_group_with_title, single_example}; |
| 3 | 3 | use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; |
| 4 | 4 | use language_model::RequestUsage; |
| 5 | −use ui::{Callout, Color, Icon, IconName, IconSize, prelude::*}; | |
| 5 | +use ui::{Callout, prelude::*}; | |
| 6 | 6 | use zed_llm_client::{Plan, UsageLimit}; |
| 7 | 7 | |
| 8 | 8 | #[derive(IntoElement, RegisterComponent)] |
| @@ -91,16 +91,23 @@ impl RenderOnce for UsageCallout { · 82 unmodified lines | ||
| 91 | 91 | .size(IconSize::XSmall) |
| 92 | 92 | }; |
| 93 | 93 | |
| 94 | − Callout::multi_line( | |
| 95 | − title, | |
| 96 | − message, | |
| 97 | − icon, | |
| 98 | − button_text, | |
| 99 | − Box::new(move |_, _, cx| { | |
| 100 | − cx.open_url(&url); | |
| 101 | − }), | |
| 102 | − ) | |
| 103 | − .into_any_element() | |
| 94 | + div() | |
| 95 | + .border_t_1() | |
| 96 | + .border_color(cx.theme().colors().border) | |
| 97 | + .child( | |
| 98 | + Callout::new() | |
| 99 | + .icon(icon) | |
| 100 | + .title(title) | |
| 101 | + .description(message) | |
| 102 | + .primary_action( | |
| 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 | 198 | Some( |
| 192 | − div() | |
| 199 | + v_flex() | |
| 193 | 200 | .p_4() |
| 194 | − .flex() | |
| 195 | − .flex_col() | |
| 196 | 201 | .gap_4() |
| 197 | 202 | .child(free_examples) |
| 198 | 203 | .child(trial_examples) |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | use std::str::FromStr; |
| 2 | +use std::time::Duration; | |
| 2 | 3 | |
| 3 | 4 | use anyhow::{Context as _, Result, anyhow}; |
| 4 | 5 | use chrono::{DateTime, Utc}; |
| @@ -32,15 +33,6 @@ pub enum AnthropicModelMode { · 27 unmodified lines | ||
| 32 | 33 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] |
| 33 | 34 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] |
| 34 | 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 | 36 | #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")] |
| 45 | 37 | ClaudeOpus4, |
| 46 | 38 | #[serde( |
| @@ -56,6 +48,15 @@ pub enum Model { · 9 unmodified lines | ||
| 56 | 48 | alias = "claude-sonnet-4-thinking-latest" |
| 57 | 49 | )] |
| 58 | 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 | 60 | #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")] |
| 60 | 61 | Claude3_5Haiku, |
| 61 | 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 | 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 | 117 | if id.starts_with("claude-3-5-sonnet") { |
| 93 | − Ok(Self::Claude3_5Sonnet) | |
| 94 | − } else if id.starts_with("claude-3-7-sonnet-thinking") { | |
| 95 | − Ok(Self::Claude3_7SonnetThinking) | |
| 96 | − } else if id.starts_with("claude-3-7-sonnet") { | |
| 97 | − Ok(Self::Claude3_7Sonnet) | |
| 98 | − } else if id.starts_with("claude-3-5-haiku") { | |
| 99 | − Ok(Self::Claude3_5Haiku) | |
| 100 | − } else if id.starts_with("claude-3-opus") { | |
| 101 | − 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}"); | |
| 118 | + return Ok(Self::Claude3_5Sonnet); | |
| 119 | + } | |
| 120 | + | |
| 121 | + if id.starts_with("claude-3-5-haiku") { | |
| 122 | + return Ok(Self::Claude3_5Haiku); | |
| 123 | + } | |
| 124 | + | |
| 125 | + if id.starts_with("claude-3-opus") { | |
| 126 | + return Ok(Self::Claude3Opus); | |
| 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 | 140 | pub fn id(&self) -> &str { |
| 120 | 141 | match self { |
| 121 | − Model::ClaudeOpus4 => "claude-opus-4-latest", | |
| 122 | − Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest", | |
| 123 | − Model::ClaudeSonnet4 => "claude-sonnet-4-latest", | |
| 124 | − Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest", | |
| 125 | − Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest", | |
| 126 | − Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest", | |
| 127 | − Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest", | |
| 128 | − Model::Claude3_5Haiku => "claude-3-5-haiku-latest", | |
| 129 | − Model::Claude3Opus => "claude-3-opus-latest", | |
| 130 | − Model::Claude3Sonnet => "claude-3-sonnet-20240229", | |
| 131 | − Model::Claude3Haiku => "claude-3-haiku-20240307", | |
| 142 | + Self::ClaudeOpus4 => "claude-opus-4-latest", | |
| 143 | + Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest", | |
| 144 | + Self::ClaudeSonnet4 => "claude-sonnet-4-latest", | |
| 145 | + Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest", | |
| 146 | + Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", | |
| 147 | + Self::Claude3_7Sonnet => "claude-3-7-sonnet-latest", | |
| 148 | + Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest", | |
| 149 | + Self::Claude3_5Haiku => "claude-3-5-haiku-latest", | |
| 150 | + Self::Claude3Opus => "claude-3-opus-latest", | |
| 151 | + Self::Claude3Sonnet => "claude-3-sonnet-20240229", | |
| 152 | + Self::Claude3Haiku => "claude-3-haiku-20240307", | |
| 132 | 153 | Self::Custom { name, .. } => name, |
| 133 | 154 | } |
| 134 | 155 | } |
| @@ -136,24 +157,24 @@ impl Model { · 1 unmodified line | ||
| 136 | 157 | /// The id of the model that should be used for making API requests |
| 137 | 158 | pub fn request_id(&self) -> &str { |
| 138 | 159 | match self { |
| 139 | − Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514", | |
| 140 | − Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514", | |
| 141 | − Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest", | |
| 142 | − Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest", | |
| 143 | − Model::Claude3_5Haiku => "claude-3-5-haiku-latest", | |
| 144 | − Model::Claude3Opus => "claude-3-opus-latest", | |
| 145 | − Model::Claude3Sonnet => "claude-3-sonnet-20240229", | |
| 146 | − Model::Claude3Haiku => "claude-3-haiku-20240307", | |
| 160 | + Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514", | |
| 161 | + Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514", | |
| 162 | + Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", | |
| 163 | + Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest", | |
| 164 | + Self::Claude3_5Haiku => "claude-3-5-haiku-latest", | |
| 165 | + Self::Claude3Opus => "claude-3-opus-latest", | |
| 166 | + Self::Claude3Sonnet => "claude-3-sonnet-20240229", | |
| 167 | + Self::Claude3Haiku => "claude-3-haiku-20240307", | |
| 147 | 168 | Self::Custom { name, .. } => name, |
| 148 | 169 | } |
| 149 | 170 | } |
| 150 | 171 | |
| 151 | 172 | pub fn display_name(&self) -> &str { |
| 152 | 173 | match self { |
| 153 | − Model::ClaudeOpus4 => "Claude Opus 4", | |
| 154 | − Model::ClaudeOpus4Thinking => "Claude Opus 4 Thinking", | |
| 155 | − Model::ClaudeSonnet4 => "Claude Sonnet 4", | |
| 156 | − Model::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking", | |
| 174 | + Self::ClaudeOpus4 => "Claude Opus 4", | |
| 175 | + Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking", | |
| 176 | + Self::ClaudeSonnet4 => "Claude Sonnet 4", | |
| 177 | + Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking", | |
| 157 | 178 | Self::Claude3_7Sonnet => "Claude 3.7 Sonnet", |
| 158 | 179 | Self::Claude3_5Sonnet => "Claude 3.5 Sonnet", |
| 159 | 180 | Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking", |
| @@ -209,15 +230,15 @@ impl Model { · 49 unmodified lines | ||
| 209 | 230 | |
| 210 | 231 | pub fn max_output_tokens(&self) -> u32 { |
| 211 | 232 | match self { |
| 212 | − Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096, | |
| 213 | − Self::Claude3_5Sonnet | |
| 214 | − | Self::Claude3_7Sonnet | |
| 215 | − | Self::Claude3_7SonnetThinking | |
| 216 | − | Self::Claude3_5Haiku | |
| 217 | − | Self::ClaudeOpus4 | |
| 233 | + Self::ClaudeOpus4 | |
| 218 | 234 | | Self::ClaudeOpus4Thinking |
| 219 | 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 | 242 | Self::Custom { |
| 222 | 243 | max_output_tokens, .. |
| 223 | 244 | } => max_output_tokens.unwrap_or(4_096), |
| @@ -246,17 +267,17 @@ impl Model { · 22 unmodified lines | ||
| 246 | 267 | |
| 247 | 268 | pub fn mode(&self) -> AnthropicModelMode { |
| 248 | 269 | match self { |
| 249 | − Self::Claude3_5Sonnet | |
| 270 | + Self::ClaudeOpus4 | |
| 271 | + | Self::ClaudeSonnet4 | |
| 272 | + | Self::Claude3_5Sonnet | |
| 250 | 273 | | Self::Claude3_7Sonnet |
| 251 | 274 | | Self::Claude3_5Haiku |
| 252 | − | Self::ClaudeOpus4 | |
| 253 | − | Self::ClaudeSonnet4 | |
| 254 | 275 | | Self::Claude3Opus |
| 255 | 276 | | Self::Claude3Sonnet |
| 256 | 277 | | Self::Claude3Haiku => AnthropicModelMode::Default, |
| 257 | − Self::Claude3_7SonnetThinking | |
| 258 | − | Self::ClaudeOpus4Thinking | |
| 259 | − | Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking { | |
| 278 | + Self::ClaudeOpus4Thinking | |
| 279 | + | Self::ClaudeSonnet4Thinking | |
| 280 | + | Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking { | |
| 260 | 281 | budget_tokens: Some(4_096), |
| 261 | 282 | }, |
| 262 | 283 | Self::Custom { mode, .. } => mode.clone(), |
| @@ -267,7 +288,7 @@ impl Model { · 4 unmodified lines | ||
| 267 | 288 | |
| 268 | 289 | pub fn beta_headers(&self) -> String { |
| 269 | 290 | let mut headers = Self::DEFAULT_BETA_HEADERS |
| 270 | − .into_iter() | |
| 291 | + .iter() | |
| 271 | 292 | .map(|header| header.to_string()) |
| 272 | 293 | .collect::<Vec<_>>(); |
| 273 | 294 | |
| @@ -406,6 +427,7 @@ impl RateLimit { · 132 unmodified lines | ||
| 406 | 427 | /// <https://docs.anthropic.com/en/api/rate-limits#response-headers> |
| 407 | 428 | #[derive(Debug)] |
| 408 | 429 | pub struct RateLimitInfo { |
| 430 | + pub retry_after: Option<Duration>, | |
| 409 | 431 | pub requests: Option<RateLimit>, |
| 410 | 432 | pub tokens: Option<RateLimit>, |
| 411 | 433 | pub input_tokens: Option<RateLimit>, |
| @@ -417,10 +439,11 @@ impl RateLimitInfo { · 5 unmodified lines | ||
| 417 | 439 | // Check if any rate limit headers exist |
| 418 | 440 | let has_rate_limit_headers = headers |
| 419 | 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 | 444 | if !has_rate_limit_headers { |
| 423 | 445 | return Self { |
| 446 | + retry_after: None, | |
| 424 | 447 | requests: None, |
| 425 | 448 | tokens: None, |
| 426 | 449 | input_tokens: None, |
| @@ -429,6 +452,11 @@ impl RateLimitInfo { · 2 unmodified lines | ||
| 429 | 452 | } |
| 430 | 453 | |
| 431 | 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 | 460 | requests: RateLimit::from_headers("requests", headers).ok(), |
| 433 | 461 | tokens: RateLimit::from_headers("tokens", headers).ok(), |
| 434 | 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 | 509 | .send(request) |
| 482 | 510 | .await |
| 483 | 511 | .context("failed to send request to Anthropic")?; |
| 512 | + let rate_limits = RateLimitInfo::from_headers(response.headers()); | |
| 484 | 513 | if response.status().is_success() { |
| 485 | − let rate_limits = RateLimitInfo::from_headers(response.headers()); | |
| 486 | 514 | let reader = BufReader::new(response.into_body()); |
| 487 | 515 | let stream = reader |
| 488 | 516 | .lines() |
| @@ -500,6 +528,8 @@ pub async fn stream_completion_with_rate_limit_info( · 11 unmodified lines | ||
| 500 | 528 | }) |
| 501 | 529 | .boxed(); |
| 502 | 530 | Ok((stream, Some(rate_limits))) |
| 531 | + } else if let Some(retry_after) = rate_limits.retry_after { | |
| 532 | + Err(AnthropicError::RateLimit(retry_after)) | |
| 503 | 533 | } else { |
| 504 | 534 | let mut body = Vec::new(); |
| 505 | 535 | response |
| @@ -769,6 +799,8 @@ pub struct MessageDelta { · 263 unmodified lines | ||
| 769 | 799 | |
| 770 | 800 | #[derive(Error, Debug)] |
| 771 | 801 | pub enum AnthropicError { |
| 802 | + #[error("rate limit exceeded, retry after {0:?}")] | |
| 803 | + RateLimit(Duration), | |
| 772 | 804 | #[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)] |
| 773 | 805 | ApiError(ApiError), |
| 774 | 806 | #[error("{0}")] |
| @@ -15,7 +15,6 @@ path = "src/askpass.rs" · 14 unmodified lines | ||
| 15 | 15 | anyhow.workspace = true |
| 16 | 16 | futures.workspace = true |
| 17 | 17 | gpui.workspace = true |
| 18 | −shlex.workspace = true | |
| 19 | 18 | smol.workspace = true |
| 20 | 19 | tempfile.workspace = true |
| 21 | 20 | util.workspace = true |
| @@ -16,6 +16,8 @@ use smol::fs; · 15 unmodified lines | ||
| 16 | 16 | use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener}; |
| 17 | 17 | #[cfg(unix)] |
| 18 | 18 | use util::ResultExt as _; |
| 19 | +#[cfg(unix)] | |
| 20 | +use util::get_shell_safe_zed_path; | |
| 19 | 21 | |
| 20 | 22 | #[derive(PartialEq, Eq)] |
| 21 | 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 | 165 | /// The main function for when Zed is running in netcat mode for use in askpass. |
| 196 | 166 | /// Called from both the remote server binary and the zed binary in their respective main functions. |
| 197 | 167 | #[cfg(unix)] |
| @@ -39,7 +39,7 @@ use language::{ · 38 unmodified lines | ||
| 39 | 39 | language_settings::{SoftWrap, all_language_settings}, |
| 40 | 40 | }; |
| 41 | 41 | use language_model::{ |
| 42 | − LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry, | |
| 42 | + ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry, | |
| 43 | 43 | Role, |
| 44 | 44 | }; |
| 45 | 45 | use multi_buffer::MultiBufferRow; |
| @@ -1887,6 +1887,8 @@ impl ContextEditor { · 1841 unmodified lines | ||
| 1887 | 1887 | // value to not show the nudge. |
| 1888 | 1888 | let nudge = Some(false); |
| 1889 | 1889 | |
| 1890 | + let model_registry = LanguageModelRegistry::read_global(cx); | |
| 1891 | + | |
| 1890 | 1892 | if nudge.map_or(false, |value| value) { |
| 1891 | 1893 | Some( |
| 1892 | 1894 | h_flex() |
| @@ -1935,14 +1937,9 @@ impl ContextEditor { · 42 unmodified lines | ||
| 1935 | 1937 | ) |
| 1936 | 1938 | .into_any_element(), |
| 1937 | 1939 | ) |
| 1938 | − } else if let Some(configuration_error) = configuration_error(cx) { | |
| 1939 | − let label = match configuration_error { | |
| 1940 | − ConfigurationError::NoProvider => "No LLM provider selected.", | |
| 1941 | − ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.", | |
| 1942 | − ConfigurationError::ProviderPendingTermsAcceptance(_) => { | |
| 1943 | − "LLM provider requires accepting the Terms of Service." | |
| 1944 | − } | |
| 1945 | − }; | |
| 1940 | + } else if let Some(configuration_error) = | |
| 1941 | + model_registry.configuration_error(model_registry.default_model(), cx) | |
| 1942 | + { | |
| 1946 | 1943 | Some( |
| 1947 | 1944 | h_flex() |
| 1948 | 1945 | .px_3() |
| @@ -1959,7 +1956,7 @@ impl ContextEditor { · 10 unmodified lines | ||
| 1959 | 1956 | .size(IconSize::Small) |
| 1960 | 1957 | .color(Color::Warning), |
| 1961 | 1958 | ) |
| 1962 | − .child(Label::new(label)), | |
| 1959 | + .child(Label::new(configuration_error.to_string())), | |
| 1963 | 1960 | ) |
| 1964 | 1961 | .child( |
| 1965 | 1962 | Button::new("open-configuration", "Configure Providers") |
| @@ -2034,14 +2031,19 @@ impl ContextEditor { · 68 unmodified lines | ||
| 2034 | 2031 | /// Will return false if the selected provided has a configuration error or |
| 2035 | 2032 | /// if the user has not accepted the terms of service for this provider. |
| 2036 | 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(); | |
| 2040 | − let needs_to_accept_terms = self.show_accept_terms | |
| 2041 | − && model | |
| 2042 | − .as_ref() | |
| 2043 | − .map_or(false, |model| model.provider.must_accept_terms(cx)); | |
| 2044 | − has_configuration_error || needs_to_accept_terms | |
| 2041 | + match configuration_error { | |
| 2042 | + ConfigurationError::NoProvider | |
| 2043 | + | ConfigurationError::ModelNotFound | |
| 2044 | + | ConfigurationError::ProviderNotAuthenticated(_) => true, | |
| 2045 | + ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms, | |
| 2046 | + } | |
| 2045 | 2047 | } |
| 2046 | 2048 | |
| 2047 | 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 | 3185 | pub fn humanize_token_count(count: usize) -> String { |
| 3211 | 3186 | match count { |
| 3212 | 3187 | 0..=999 => count.to_string(), |
| @@ -869,7 +869,7 @@ impl ContextStore { · 868 unmodified lines | ||
| 869 | 869 | |
| 870 | 870 | if protocol.capable(context_server::protocol::ServerCapability::Prompts) { |
| 871 | 871 | if let Some(response) = protocol |
| 872 | − .request::<context_server::types::request::PromptsList>(()) | |
| 872 | + .request::<context_server::types::requests::PromptsList>(()) | |
| 873 | 873 | .await |
| 874 | 874 | .log_err() |
| 875 | 875 | { |
| @@ -682,11 +682,12 @@ mod tests { · 681 unmodified lines | ||
| 682 | 682 | _: &AsyncApp, |
| 683 | 683 | ) -> BoxFuture< |
| 684 | 684 | 'static, |
| 685 | − http_client::Result< | |
| 685 | + Result< | |
| 686 | 686 | BoxStream< |
| 687 | 687 | 'static, |
| 688 | − http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>, | |
| 688 | + Result<LanguageModelCompletionEvent, LanguageModelCompletionError>, | |
| 689 | 689 | >, |
| 690 | + LanguageModelCompletionError, | |
| 690 | 691 | >, |
| 691 | 692 | > { |
| 692 | 693 | unimplemented!() |
| @@ -87,7 +87,7 @@ impl SlashCommand for ContextServerSlashCommand { · 86 unmodified lines | ||
| 87 | 87 | let protocol = server.client().context("Context server not initialized")?; |
| 88 | 88 | |
| 89 | 89 | let response = protocol |
| 90 | − .request::<context_server::types::request::CompletionComplete>( | |
| 90 | + .request::<context_server::types::requests::CompletionComplete>( | |
| 91 | 91 | context_server::types::CompletionCompleteParams { |
| 92 | 92 | reference: context_server::types::CompletionReference::Prompt( |
| 93 | 93 | context_server::types::PromptReference { |
| @@ -145,7 +145,7 @@ impl SlashCommand for ContextServerSlashCommand { · 51 unmodified lines | ||
| 145 | 145 | cx.foreground_executor().spawn(async move { |
| 146 | 146 | let protocol = server.client().context("Context server not initialized")?; |
| 147 | 147 | let response = protocol |
| 148 | − .request::<context_server::types::request::PromptsGet>( | |
| 148 | + .request::<context_server::types::requests::PromptsGet>( | |
| 149 | 149 | context_server::types::PromptsGetParams { |
| 150 | 150 | name: prompt_name.clone(), |
| 151 | 151 | arguments: Some(prompt_args), |
| @@ -582,7 +582,7 @@ mod test { · 581 unmodified lines | ||
| 582 | 582 | use serde_json::json; |
| 583 | 583 | use settings::SettingsStore; |
| 584 | 584 | use smol::stream::StreamExt; |
| 585 | − use util::{path, separator}; | |
| 585 | + use util::path; | |
| 586 | 586 | |
| 587 | 587 | use super::collect_files; |
| 588 | 588 | |
| @@ -627,7 +627,7 @@ mod test { · 38 unmodified lines | ||
| 627 | 627 | .await |
| 628 | 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 | 631 | // 4 files + 2 directories |
| 632 | 632 | assert_eq!(result_1.sections.len(), 6); |
| 633 | 633 | |
| @@ -643,7 +643,7 @@ mod test { · 9 unmodified lines | ||
| 643 | 643 | cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed()); |
| 644 | 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 | 647 | // 5 files + 2 directories |
| 648 | 648 | assert_eq!(result.sections.len(), 7); |
| 649 | 649 | |
| @@ -691,24 +691,20 @@ mod test { · 41 unmodified lines | ||
| 691 | 691 | .unwrap(); |
| 692 | 692 | |
| 693 | 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 | 695 | assert_eq!(result.sections.len(), 7); |
| 696 | 696 | |
| 697 | 697 | // Ensure that full file paths are included in the real output |
| 698 | 698 | assert!( |
| 699 | 699 | result |
| 700 | 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 | 704 | assert!( |
| 704 | 705 | result |
| 705 | 706 | .text |
| 706 | − .contains(separator!("zed/assets/themes/ayu/LICENSE")) | |
| 707 | − ); | |
| 708 | − assert!( | |
| 709 | − result | |
| 710 | − .text | |
| 711 | − .contains(separator!("zed/assets/themes/summercamp/LICENSE")) | |
| 707 | + .contains(path!("zed/assets/themes/summercamp/LICENSE")) | |
| 712 | 708 | ); |
| 713 | 709 | |
| 714 | 710 | assert_eq!(result.sections[5].label, "summercamp"); |
| @@ -716,17 +712,17 @@ mod test { · 1 unmodified line | ||
| 716 | 712 | // Ensure that things are in descending order, with properly relativized paths |
| 717 | 713 | assert_eq!( |
| 718 | 714 | result.sections[0].label, |
| 719 | − separator!("zed/assets/themes/andromeda/LICENSE") | |
| 715 | + path!("zed/assets/themes/andromeda/LICENSE") | |
| 720 | 716 | ); |
| 721 | 717 | assert_eq!(result.sections[1].label, "andromeda"); |
| 722 | 718 | assert_eq!( |
| 723 | 719 | result.sections[2].label, |
| 724 | − separator!("zed/assets/themes/ayu/LICENSE") | |
| 720 | + path!("zed/assets/themes/ayu/LICENSE") | |
| 725 | 721 | ); |
| 726 | 722 | assert_eq!(result.sections[3].label, "ayu"); |
| 727 | 723 | assert_eq!( |
| 728 | 724 | result.sections[4].label, |
| 729 | − separator!("zed/assets/themes/summercamp/LICENSE") | |
| 725 | + path!("zed/assets/themes/summercamp/LICENSE") | |
| 730 | 726 | ); |
| 731 | 727 | |
| 732 | 728 | // Ensure that the project lasts until after the last await |
| @@ -767,31 +763,28 @@ mod test { · 34 unmodified lines | ||
| 767 | 763 | .await |
| 768 | 764 | .unwrap(); |
| 769 | 765 | |
| 770 | − assert!(result.text.starts_with(separator!("zed/assets/themes\n"))); | |
| 771 | − assert_eq!( | |
| 772 | − result.sections[0].label, | |
| 773 | − separator!("zed/assets/themes/LICENSE") | |
| 774 | − ); | |
| 766 | + assert!(result.text.starts_with(path!("zed/assets/themes\n"))); | |
| 767 | + assert_eq!(result.sections[0].label, path!("zed/assets/themes/LICENSE")); | |
| 775 | 768 | assert_eq!( |
| 776 | 769 | result.sections[1].label, |
| 777 | − separator!("zed/assets/themes/summercamp/LICENSE") | |
| 770 | + path!("zed/assets/themes/summercamp/LICENSE") | |
| 778 | 771 | ); |
| 779 | 772 | assert_eq!( |
| 780 | 773 | result.sections[2].label, |
| 781 | − separator!("zed/assets/themes/summercamp/subdir/LICENSE") | |
| 774 | + path!("zed/assets/themes/summercamp/subdir/LICENSE") | |
| 782 | 775 | ); |
| 783 | 776 | assert_eq!( |
| 784 | 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 | 780 | assert_eq!(result.sections[4].label, "subsubdir"); |
| 788 | 781 | assert_eq!(result.sections[5].label, "subdir"); |
| 789 | 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 | 785 | assert_eq!( |
| 793 | 786 | result.text, |
| 794 | − separator!( | |
| 787 | + path!( | |
| 795 | 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 | 80 | pretty_assertions.workspace = true |
| 81 | 81 | reqwest_client.workspace = true |
| 82 | 82 | settings = { workspace = true, features = ["test-support"] } |
| 83 | +smol.workspace = true | |
| 83 | 84 | task = { workspace = true, features = ["test-support"]} |
| 84 | 85 | tempfile.workspace = true |
| 85 | 86 | theme.workspace = true |
| @@ -286,7 +286,13 @@ impl EditAgent { · 285 unmodified lines | ||
| 286 | 286 | _ => { |
| 287 | 287 | let ranges = resolved_old_text |
| 288 | 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 | 296 | .collect(); |
| 291 | 297 | output_events |
| 292 | 298 | .unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges)) |
| @@ -429,25 +435,25 @@ impl EditAgent { · 136 unmodified lines | ||
| 429 | 435 | let task = cx.background_spawn(async move { |
| 430 | 436 | let mut matcher = StreamingFuzzyMatcher::new(snapshot); |
| 431 | 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 | 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 | 448 | if done { |
| 438 | 449 | break; |
| 439 | 450 | } |
| 440 | 451 | } |
| 441 | 452 | |
| 442 | 453 | let matches = matcher.finish(); |
| 454 | + let best_match = matcher.select_best_match(); | |
| 443 | 455 | |
| 444 | − let old_range = if matches.len() == 1 { | |
| 445 | − matches.first() | |
| 446 | − } else { | |
| 447 | − // No matches or multiple ambiguous matches | |
| 448 | − None | |
| 449 | − }; | |
| 450 | − old_range_tx.send(old_range.cloned())?; | |
| 456 | + old_range_tx.send(best_match.clone())?; | |
| 451 | 457 | |
| 452 | 458 | let indent = LineIndent::from_iter( |
| 453 | 459 | matcher |
| @@ -456,10 +462,18 @@ impl EditAgent { · 2 unmodified lines | ||
| 456 | 462 | .unwrap_or(&String::new()) |
| 457 | 463 | .chars(), |
| 458 | 464 | ); |
| 459 | − let resolved_old_texts = matches | |
| 460 | − .into_iter() | |
| 461 | − .map(|range| ResolvedOldText { range, indent }) | |
| 462 | − .collect::<Vec<_>>(); | |
| 465 | + | |
| 466 | + let resolved_old_texts = if let Some(best_match) = best_match { | |
| 467 | + vec![ResolvedOldText { | |
| 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 | 478 | Ok((edit_events, resolved_old_texts)) |
| 465 | 479 | }); |
| @@ -1374,10 +1388,12 @@ mod tests { · 908 unmodified lines | ||
| 1374 | 1388 | &agent, |
| 1375 | 1389 | indoc! {" |
| 1376 | 1390 | <old_text> |
| 1377 | − return 42; | |
| 1391 | + return 42; | |
| 1392 | + } | |
| 1378 | 1393 | </old_text> |
| 1379 | 1394 | <new_text> |
| 1380 | − return 100; | |
| 1395 | + return 100; | |
| 1396 | + } | |
| 1381 | 1397 | </new_text> |
| 1382 | 1398 | "}, |
| 1383 | 1399 | &mut rng, |
| @@ -1407,7 +1423,7 @@ mod tests { · 23 unmodified lines | ||
| 1407 | 1423 | |
| 1408 | 1424 | // And AmbiguousEditRange even should be emitted |
| 1409 | 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 | 1427 | assert!( |
| 1412 | 1428 | events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)), |
| 1413 | 1429 | "Should emit AmbiguousEditRange for non-unique text" |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | use derive_more::{Add, AddAssign}; |
| 2 | +use regex::Regex; | |
| 2 | 3 | use schemars::JsonSchema; |
| 3 | 4 | use serde::{Deserialize, Serialize}; |
| 4 | 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 | 13 | #[derive(Debug)] |
| 13 | 14 | pub enum EditParserEvent { |
| 14 | − OldTextChunk { chunk: String, done: bool }, | |
| 15 | − NewTextChunk { chunk: String, done: bool }, | |
| 15 | + OldTextChunk { | |
| 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 | 26 | #[derive( |
| @@ -33,7 +41,7 @@ pub struct EditParser { · 14 unmodified lines | ||
| 33 | 41 | #[derive(Debug, PartialEq)] |
| 34 | 42 | enum EditParserState { |
| 35 | 43 | Pending, |
| 36 | − WithinOldText { start: bool }, | |
| 44 | + WithinOldText { start: bool, line_hint: Option<u32> }, | |
| 37 | 45 | AfterOldText, |
| 38 | 46 | WithinNewText { start: bool }, |
| 39 | 47 | } |
| @@ -54,14 +62,24 @@ impl EditParser { · 14 unmodified lines | ||
| 54 | 62 | loop { |
| 55 | 63 | match &mut self.state { |
| 56 | 64 | EditParserState::Pending => { |
| 57 | − if let Some(start) = self.buffer.find("<old_text>") { | |
| 58 | − self.buffer.drain(..start + "<old_text>".len()); | |
| 59 | − self.state = EditParserState::WithinOldText { start: true }; | |
| 65 | + if let Some(start) = self.buffer.find("<old_text") { | |
| 66 | + if let Some(tag_end) = self.buffer[start..].find('>') { | |
| 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 | 78 | } else { |
| 61 | 79 | break; |
| 62 | 80 | } |
| 63 | 81 | } |
| 64 | − EditParserState::WithinOldText { start } => { | |
| 82 | + EditParserState::WithinOldText { start, line_hint } => { | |
| 65 | 83 | if !self.buffer.is_empty() { |
| 66 | 84 | if *start && self.buffer.starts_with('\n') { |
| 67 | 85 | self.buffer.remove(0); |
| @@ -69,6 +87,7 @@ impl EditParser { · 1 unmodified line | ||
| 69 | 87 | *start = false; |
| 70 | 88 | } |
| 71 | 89 | |
| 90 | + let line_hint = *line_hint; | |
| 72 | 91 | if let Some(tag_range) = self.find_end_tag() { |
| 73 | 92 | let mut chunk = self.buffer[..tag_range.start].to_string(); |
| 74 | 93 | if chunk.ends_with('\n') { |
| @@ -82,12 +101,17 @@ impl EditParser { · 7 unmodified lines | ||
| 82 | 101 | |
| 83 | 102 | self.buffer.drain(..tag_range.end); |
| 84 | 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 | 109 | } else { |
| 87 | 110 | if !self.ends_with_tag_prefix() { |
| 88 | 111 | edit_events.push(EditParserEvent::OldTextChunk { |
| 89 | 112 | chunk: mem::take(&mut self.buffer), |
| 90 | 113 | done: false, |
| 114 | + line_hint, | |
| 91 | 115 | }); |
| 92 | 116 | } |
| 93 | 117 | break; |
| @@ -154,6 +178,16 @@ impl EditParser { · 60 unmodified lines | ||
| 154 | 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 | 191 | pub fn finish(self) -> EditParserMetrics { |
| 158 | 192 | self.metrics |
| 159 | 193 | } |
| @@ -178,6 +212,7 @@ mod tests { · 18 unmodified lines | ||
| 178 | 212 | vec![Edit { |
| 179 | 213 | old_text: "original".to_string(), |
| 180 | 214 | new_text: "updated".to_string(), |
| 215 | + line_hint: None, | |
| 181 | 216 | }] |
| 182 | 217 | ); |
| 183 | 218 | assert_eq!( |
| @@ -209,10 +244,12 @@ mod tests { · 25 unmodified lines | ||
| 209 | 244 | Edit { |
| 210 | 245 | old_text: "first old".to_string(), |
| 211 | 246 | new_text: "first new".to_string(), |
| 247 | + line_hint: None, | |
| 212 | 248 | }, |
| 213 | 249 | Edit { |
| 214 | 250 | old_text: "second old".to_string(), |
| 215 | 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 | 281 | Edit { |
| 245 | 282 | old_text: "content".to_string(), |
| 246 | 283 | new_text: "updated content".to_string(), |
| 284 | + line_hint: None, | |
| 247 | 285 | }, |
| 248 | 286 | Edit { |
| 249 | 287 | old_text: "second item".to_string(), |
| 250 | 288 | new_text: "modified second item".to_string(), |
| 289 | + line_hint: None, | |
| 251 | 290 | }, |
| 252 | 291 | Edit { |
| 253 | 292 | old_text: "third case".to_string(), |
| 254 | 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 | 316 | vec![Edit { |
| 277 | 317 | old_text: "code with <tag>nested</tag> elements".to_string(), |
| 278 | 318 | new_text: "new <code>content</code>".to_string(), |
| 319 | + line_hint: None, | |
| 279 | 320 | }] |
| 280 | 321 | ); |
| 281 | 322 | assert_eq!( |
| @@ -299,6 +340,7 @@ mod tests { · 17 unmodified lines | ||
| 299 | 340 | vec![Edit { |
| 300 | 341 | old_text: "".to_string(), |
| 301 | 342 | new_text: "".to_string(), |
| 343 | + line_hint: None, | |
| 302 | 344 | }] |
| 303 | 345 | ); |
| 304 | 346 | assert_eq!( |
| @@ -322,6 +364,7 @@ mod tests { · 17 unmodified lines | ||
| 322 | 364 | vec![Edit { |
| 323 | 365 | old_text: "line1\nline2\nline3".to_string(), |
| 324 | 366 | new_text: "line1\nmodified line2\nline3".to_string(), |
| 367 | + line_hint: None, | |
| 325 | 368 | }] |
| 326 | 369 | ); |
| 327 | 370 | assert_eq!( |
| @@ -368,10 +411,12 @@ mod tests { · 40 unmodified lines | ||
| 368 | 411 | Edit { |
| 369 | 412 | old_text: "a\nb\nc".to_string(), |
| 370 | 413 | new_text: "a\nB\nc".to_string(), |
| 414 | + line_hint: None, | |
| 371 | 415 | }, |
| 372 | 416 | Edit { |
| 373 | 417 | old_text: "d\ne\nf".to_string(), |
| 374 | 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 | 447 | vec![Edit { |
| 403 | 448 | old_text: "Lorem".to_string(), |
| 404 | 449 | new_text: "LOREM".to_string(), |
| 450 | + line_hint: None, | |
| 405 | 451 | },] |
| 406 | 452 | ); |
| 407 | 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 | 528 | #[derive(Default, Debug, PartialEq, Eq)] |
| 417 | 529 | struct Edit { |
| 418 | 530 | old_text: String, |
| 419 | 531 | new_text: String, |
| 532 | + line_hint: Option<u32>, | |
| 420 | 533 | } |
| 421 | 534 | |
| 422 | 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 | 546 | for chunk_ix in chunk_indices { |
| 434 | 547 | for event in parser.push(&input[last_ix..chunk_ix]) { |
| 435 | 548 | match event { |
| 436 | − EditParserEvent::OldTextChunk { chunk, done } => { | |
| 549 | + EditParserEvent::OldTextChunk { | |
| 550 | + chunk, | |
| 551 | + done, | |
| 552 | + line_hint, | |
| 553 | + } => { | |
| 437 | 554 | old_text.as_mut().unwrap().push_str(&chunk); |
| 438 | 555 | if done { |
| 439 | 556 | pending_edit.old_text = old_text.take().unwrap(); |
| 557 | + pending_edit.line_hint = line_hint; | |
| 440 | 558 | new_text = Some(String::new()); |
| 441 | 559 | } |
| 442 | 560 | } |
| @@ -11,7 +11,7 @@ use client::{Client, UserStore}; · 10 unmodified lines | ||
| 11 | 11 | use collections::HashMap; |
| 12 | 12 | use fs::FakeFs; |
| 13 | 13 | use futures::{FutureExt, future::LocalBoxFuture}; |
| 14 | −use gpui::{AppContext, TestAppContext}; | |
| 14 | +use gpui::{AppContext, TestAppContext, Timer}; | |
| 15 | 15 | use indoc::{formatdoc, indoc}; |
| 16 | 16 | use language_model::{ |
| 17 | 17 | LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, |
| @@ -26,6 +26,7 @@ use std::{ · 8 unmodified lines | ||
| 26 | 26 | cmp::Reverse, |
| 27 | 27 | fmt::{self, Display}, |
| 28 | 28 | io::Write as _, |
| 29 | + path::Path, | |
| 29 | 30 | str::FromStr, |
| 30 | 31 | sync::mpsc, |
| 31 | 32 | }; |
| @@ -38,10 +39,11 @@ fn eval_extract_handle_command_output() { · 6 unmodified lines | ||
| 38 | 39 | // |
| 39 | 40 | // Model | Pass rate |
| 40 | 41 | // ----------------------------|---------- |
| 41 | − // claude-3.7-sonnet | 0.98 | |
| 42 | − // gemini-2.5-pro-06-05 | 0.77 | |
| 43 | − // gemini-2.5-flash | 0.11 | |
| 44 | − // gpt-4.1 | 1.00 | |
| 42 | + // claude-3.7-sonnet | 0.99 (2025-06-14) | |
| 43 | + // claude-sonnet-4 | 0.97 (2025-06-14) | |
| 44 | + // gemini-2.5-pro-06-05 | 0.77 (2025-05-22) | |
| 45 | + // gemini-2.5-flash | 0.11 (2025-05-22) | |
| 46 | + // gpt-4.1 | 1.00 (2025-05-22) | |
| 45 | 47 | |
| 46 | 48 | let input_file_path = "root/blame.rs"; |
| 47 | 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 | 112 | #[test] |
| 111 | 113 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 112 | 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 | 122 | let input_file_path = "root/blame.rs"; |
| 114 | 123 | let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs"); |
| 115 | 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 | 174 | #[test] |
| 166 | 175 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 167 | 176 | fn eval_translate_doc_comments() { |
| 168 | − // Results for 2025-05-22 | |
| 169 | − // | |
| 170 | 177 | // Model | Pass rate |
| 171 | 178 | // ============================================ |
| 172 | 179 | // |
| 173 | − // claude-3.7-sonnet | | |
| 174 | − // gemini-2.5-pro-preview-03-25 | 1.0 | |
| 180 | + // claude-3.7-sonnet | 1.0 (2025-06-14) | |
| 181 | + // claude-sonnet-4 | 1.0 (2025-06-14) | |
| 182 | + // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) | |
| 175 | 183 | // gemini-2.5-flash-preview-04-17 | |
| 176 | 184 | // gpt-4.1 | |
| 177 | 185 | let input_file_path = "root/canvas.rs"; |
| @@ -228,13 +236,12 @@ fn eval_translate_doc_comments() { · 50 unmodified lines | ||
| 228 | 236 | #[test] |
| 229 | 237 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 230 | 238 | fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { |
| 231 | − // Results for 2025-05-22 | |
| 232 | − // | |
| 233 | 239 | // Model | Pass rate |
| 234 | 240 | // ============================================ |
| 235 | 241 | // |
| 236 | − // claude-3.7-sonnet | 0.98 | |
| 237 | − // gemini-2.5-pro-preview-03-25 | 0.99 | |
| 242 | + // claude-3.7-sonnet | 0.96 (2025-06-14) | |
| 243 | + // claude-sonnet-4 | 0.11 (2025-06-14) | |
| 244 | + // gemini-2.5-pro-preview-03-25 | 0.99 (2025-05-22) | |
| 238 | 245 | // gemini-2.5-flash-preview-04-17 | |
| 239 | 246 | // gpt-4.1 | |
| 240 | 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 | 361 | #[test] |
| 355 | 362 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 356 | 363 | fn eval_disable_cursor_blinking() { |
| 357 | − // Results for 2025-05-22 | |
| 358 | − // | |
| 359 | 364 | // Model | Pass rate |
| 360 | 365 | // ============================================ |
| 361 | 366 | // |
| 362 | − // claude-3.7-sonnet | | |
| 363 | − // gemini-2.5-pro-preview-03-25 | 1.0 | |
| 367 | + // claude-3.7-sonnet | 0.99 (2025-06-14) | |
| 368 | + // claude-sonnet-4 | 0.85 (2025-06-14) | |
| 369 | + // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) | |
| 364 | 370 | // gemini-2.5-flash-preview-04-17 | |
| 365 | 371 | // gpt-4.1 | |
| 366 | 372 | let input_file_path = "root/editor.rs"; |
| @@ -438,14 +444,21 @@ fn eval_disable_cursor_blinking() { · 71 unmodified lines | ||
| 438 | 444 | #[test] |
| 439 | 445 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 440 | 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 | 456 | // Model | Pass rate |
| 444 | 457 | // ============================================ |
| 445 | 458 | // |
| 446 | − // claude-3.7-sonnet | | |
| 447 | − // gemini-2.5-pro-preview-03-25 | 0.94 | |
| 448 | − // gemini-2.5-flash-preview-04-17 | | |
| 459 | + // claude-4.0-sonnet | 0.99 | |
| 460 | + // claude-3.7-sonnet | 0.88 | |
| 461 | + // gemini-2.5-pro-preview-03-25 | 0.96 | |
| 449 | 462 | // gpt-4.1 | |
| 450 | 463 | let input_file_path = "root/canvas.rs"; |
| 451 | 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 | 468 | 0.95, |
| 456 | 469 | // For whatever reason, this eval produces more mismatched tags. |
| 457 | 470 | // Increasing for now, let's see if we can bring this down. |
| 458 | − 0.2, | |
| 471 | + 0.25, | |
| 459 | 472 | EvalInput::from_conversation( |
| 460 | 473 | vec![ |
| 461 | 474 | message( |
| @@ -641,15 +654,14 @@ fn eval_from_pixels_constructor() { · 179 unmodified lines | ||
| 641 | 654 | #[test] |
| 642 | 655 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 643 | 656 | fn eval_zode() { |
| 644 | − // Results for 2025-05-22 | |
| 645 | − // | |
| 646 | 657 | // Model | Pass rate |
| 647 | 658 | // ============================================ |
| 648 | 659 | // |
| 649 | − // claude-3.7-sonnet | 1.0 | |
| 650 | − // gemini-2.5-pro-preview-03-25 | 1.0 | |
| 651 | − // gemini-2.5-flash-preview-04-17 | 1.0 | |
| 652 | − // gpt-4.1 | 1.0 | |
| 660 | + // claude-3.7-sonnet | 1.0 (2025-06-14) | |
| 661 | + // claude-sonnet-4 | 1.0 (2025-06-14) | |
| 662 | + // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) | |
| 663 | + // gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22) | |
| 664 | + // gpt-4.1 | 1.0 (2025-05-22) | |
| 653 | 665 | let input_file_path = "root/zode.py"; |
| 654 | 666 | let input_content = None; |
| 655 | 667 | let edit_description = "Create the main Zode CLI script"; |
| @@ -748,13 +760,12 @@ fn eval_zode() { · 92 unmodified lines | ||
| 748 | 760 | #[test] |
| 749 | 761 | #[cfg_attr(not(feature = "eval"), ignore)] |
| 750 | 762 | fn eval_add_overwrite_test() { |
| 751 | − // Results for 2025-05-22 | |
| 752 | − // | |
| 753 | 763 | // Model | Pass rate |
| 754 | 764 | // ============================================ |
| 755 | 765 | // |
| 756 | − // claude-3.7-sonnet | 0.16 | |
| 757 | − // gemini-2.5-pro-preview-03-25 | 0.35 | |
| 766 | + // claude-3.7-sonnet | 0.65 (2025-06-14) | |
| 767 | + // claude-sonnet-4 | 0.07 (2025-06-14) | |
| 768 | + // gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22) | |
| 758 | 769 | // gemini-2.5-flash-preview-04-17 | |
| 759 | 770 | // gpt-4.1 | |
| 760 | 771 | let input_file_path = "root/action_log.rs"; |
| @@ -984,15 +995,14 @@ fn eval_create_empty_file() { · 223 unmodified lines | ||
| 984 | 995 | // thoughts into it. This issue is not specific to empty files, but |
| 985 | 996 | // it's easier to reproduce with them. |
| 986 | 997 | // |
| 987 | − // Results for 2025-05-21: | |
| 988 | − // | |
| 989 | 998 | // Model | Pass rate |
| 990 | 999 | // ============================================ |
| 991 | 1000 | // |
| 992 | − // claude-3.7-sonnet | 1.00 | |
| 993 | − // gemini-2.5-pro-preview-03-25 | 1.00 | |
| 994 | − // gemini-2.5-flash-preview-04-17 | 1.00 | |
| 995 | − // gpt-4.1 | 1.00 | |
| 1001 | + // claude-3.7-sonnet | 1.00 (2025-06-14) | |
| 1002 | + // claude-sonnet-4 | 1.00 (2025-06-14) | |
| 1003 | + // gemini-2.5-pro-preview-03-25 | 1.00 (2025-05-21) | |
| 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 | 1008 | // TODO: gpt-4.1-mini errored 38 times: |
| @@ -1255,9 +1265,12 @@ impl EvalAssertion { · 256 unmodified lines | ||
| 1255 | 1265 | }], |
| 1256 | 1266 | ..Default::default() |
| 1257 | 1267 | }; |
| 1258 | − let mut response = judge | |
| 1259 | − .stream_completion_text(request, &cx.to_async()) | |
| 1260 | − .await?; | |
| 1268 | + let mut response = retry_on_rate_limit(async || { | |
| 1269 | + Ok(judge | |
| 1270 | + .stream_completion_text(request.clone(), &cx.to_async()) | |
| 1271 | + .await?) | |
| 1272 | + }) | |
| 1273 | + .await?; | |
| 1261 | 1274 | let mut output = String::new(); |
| 1262 | 1275 | while let Some(chunk) = response.stream.next().await { |
| 1263 | 1276 | let chunk = chunk?; |
| @@ -1308,10 +1321,17 @@ fn eval( · 44 unmodified lines | ||
| 1308 | 1321 | run_eval(eval.clone(), tx.clone()); |
| 1309 | 1322 | |
| 1310 | 1323 | let executor = gpui::background_executor(); |
| 1324 | + let semaphore = Arc::new(smol::lock::Semaphore::new(32)); | |
| 1311 | 1325 | for _ in 1..iterations { |
| 1312 | 1326 | let eval = eval.clone(); |
| 1313 | 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 | 1336 | drop(tx); |
| 1317 | 1337 | |
| @@ -1539,6 +1559,7 @@ impl EditAgentTest { · 221 unmodified lines | ||
| 1539 | 1559 | .collect::<Vec<_>>(); |
| 1540 | 1560 | let worktrees = vec![WorktreeContext { |
| 1541 | 1561 | root_name: "root".to_string(), |
| 1562 | + abs_path: Path::new("/path/to/root").into(), | |
| 1542 | 1563 | rules_file: None, |
| 1543 | 1564 | }]; |
| 1544 | 1565 | let prompt_builder = PromptBuilder::new(None)?; |
| @@ -1577,21 +1598,31 @@ impl EditAgentTest { · 32 unmodified lines | ||
| 1577 | 1598 | if let Some(input_content) = eval.input_content.as_deref() { |
| 1578 | 1599 | buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx)); |
| 1579 | 1600 | } |
| 1580 | − let (edit_output, _) = self.agent.edit( | |
| 1581 | − buffer.clone(), | |
| 1582 | − eval.edit_file_input.display_description, | |
| 1583 | − &conversation, | |
| 1584 | − &mut cx.to_async(), | |
| 1585 | − ); | |
| 1586 | − edit_output.await? | |
| 1601 | + retry_on_rate_limit(async || { | |
| 1602 | + self.agent | |
| 1603 | + .edit( | |
| 1604 | + buffer.clone(), | |
| 1605 | + eval.edit_file_input.display_description.clone(), | |
| 1606 | + &conversation, | |
| 1607 | + &mut cx.to_async(), | |
| 1608 | + ) | |
| 1609 | + .0 | |
| 1610 | + .await | |
| 1611 | + }) | |
| 1612 | + .await? | |
| 1587 | 1613 | } else { |
| 1588 | − let (edit_output, _) = self.agent.overwrite( | |
| 1589 | − buffer.clone(), | |
| 1590 | − eval.edit_file_input.display_description, | |
| 1591 | − &conversation, | |
| 1592 | − &mut cx.to_async(), | |
| 1593 | − ); | |
| 1594 | − edit_output.await? | |
| 1614 | + retry_on_rate_limit(async || { | |
| 1615 | + self.agent | |
| 1616 | + .overwrite( | |
| 1617 | + buffer.clone(), | |
| 1618 | + eval.edit_file_input.display_description.clone(), | |
| 1619 | + &conversation, | |
| 1620 | + &mut cx.to_async(), | |
| 1621 | + ) | |
| 1622 | + .0 | |
| 1623 | + .await | |
| 1624 | + }) | |
| 1625 | + .await? | |
| 1595 | 1626 | }; |
| 1596 | 1627 | |
| 1597 | 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 | 1672 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] |
| 1617 | 1673 | struct EvalAssertionOutcome { |
| 1618 | 1674 | score: usize, |
| @@ -10,8 +10,9 @@ const DELETION_COST: u32 = 10; · 9 unmodified lines | ||
| 10 | 10 | pub struct StreamingFuzzyMatcher { |
| 11 | 11 | snapshot: TextBufferSnapshot, |
| 12 | 12 | query_lines: Vec<String>, |
| 13 | + line_hint: Option<u32>, | |
| 13 | 14 | incomplete_line: String, |
| 14 | − best_matches: Vec<Range<usize>>, | |
| 15 | + matches: Vec<Range<usize>>, | |
| 15 | 16 | matrix: SearchMatrix, |
| 16 | 17 | } |
| 17 | 18 | |
| @@ -21,8 +22,9 @@ impl StreamingFuzzyMatcher { · 3 unmodified lines | ||
| 21 | 22 | Self { |
| 22 | 23 | snapshot, |
| 23 | 24 | query_lines: Vec::new(), |
| 25 | + line_hint: None, | |
| 24 | 26 | incomplete_line: String::new(), |
| 25 | − best_matches: Vec::new(), | |
| 27 | + matches: Vec::new(), | |
| 26 | 28 | matrix: SearchMatrix::new(buffer_line_count + 1), |
| 27 | 29 | } |
| 28 | 30 | } |
| @@ -41,9 +43,10 @@ impl StreamingFuzzyMatcher { · 12 unmodified lines | ||
| 41 | 43 | /// |
| 42 | 44 | /// Returns `Some(range)` if a match has been found with the accumulated |
| 43 | 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 | 47 | // Add the chunk to our incomplete line buffer |
| 46 | 48 | self.incomplete_line.push_str(chunk); |
| 49 | + self.line_hint = line_hint; | |
| 47 | 50 | |
| 48 | 51 | if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() { |
| 49 | 52 | let complete_part = &self.incomplete_line[..=last_pos]; |
| @@ -55,20 +58,11 @@ impl StreamingFuzzyMatcher { · 5 unmodified lines | ||
| 55 | 58 | |
| 56 | 59 | self.incomplete_line.replace_range(..last_pos + 1, ""); |
| 57 | 60 | |
| 58 | − self.best_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 | − } | |
| 61 | + self.matches = self.resolve_location_fuzzy(); | |
| 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 | 68 | /// Finish processing and return the final best match(es). |
| @@ -80,9 +74,9 @@ impl StreamingFuzzyMatcher { · 5 unmodified lines | ||
| 80 | 74 | if !self.incomplete_line.is_empty() { |
| 81 | 75 | self.query_lines.push(self.incomplete_line.clone()); |
| 82 | 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 | 82 | fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> { |
| @@ -198,6 +192,43 @@ impl StreamingFuzzyMatcher { · 109 unmodified lines | ||
| 198 | 192 | |
| 199 | 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 | 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 | 720 | #[track_caller] |
| 644 | 721 | fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) { |
| 645 | 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 | 731 | // Push chunks incrementally |
| 655 | 732 | for chunk in &chunks { |
| 656 | − matcher.push(chunk); | |
| 733 | + matcher.push(chunk, None); | |
| 657 | 734 | } |
| 658 | 735 | |
| 659 | 736 | let actual_ranges = matcher.finish(); |
| @@ -706,7 +783,7 @@ mod tests { · 46 unmodified lines | ||
| 706 | 783 | |
| 707 | 784 | fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> { |
| 708 | 785 | finder |
| 709 | − .push(chunk) | |
| 786 | + .push(chunk, None) | |
| 710 | 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 | 10 | ToolUseStatus, |
| 11 | 11 | }; |
| 12 | 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 | 14 | use futures::StreamExt; |
| 15 | 15 | use gpui::{ |
| 16 | 16 | Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, |
| @@ -69,13 +69,13 @@ pub struct EditFileToolInput { · 52 unmodified lines | ||
| 69 | 69 | /// start each path with one of the project's root directories. |
| 70 | 70 | /// |
| 71 | 71 | /// The following examples assume we have two root directories in the project: |
| 72 | − /// - backend | |
| 73 | − /// - frontend | |
| 72 | + /// - /a/b/backend | |
| 73 | + /// - /c/d/frontend | |
| 74 | 74 | /// |
| 75 | 75 | /// <example> |
| 76 | 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 | 79 | /// would be ambiguous and the call would fail! |
| 80 | 80 | /// </example> |
| 81 | 81 | /// |
| @@ -333,14 +333,18 @@ impl Tool for EditFileTool { · 251 unmodified lines | ||
| 333 | 333 | ); |
| 334 | 334 | anyhow::ensure!( |
| 335 | 335 | ambiguous_ranges.is_empty(), |
| 336 | − // TODO: Include ambiguous_ranges, converted to line numbers. | |
| 337 | − // This would work best if we add `line_hint` parameter | |
| 338 | − // to edit_file_tool | |
| 339 | − formatdoc! {" | |
| 340 | − <old_text> matches more than one position in the file. Read the | |
| 341 | − relevant sections of {input_path} again and extend <old_text> so | |
| 342 | − that I can perform the requested edits. | |
| 343 | − "} | |
| 336 | + { | |
| 337 | + let line_numbers = ambiguous_ranges | |
| 338 | + .iter() | |
| 339 | + .map(|range| range.start.to_string()) | |
| 340 | + .collect::<Vec<_>>() | |
| 341 | + .join(", "); | |
| 342 | + formatdoc! {" | |
| 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 | 349 | Ok(ToolResultOutput { |
| 346 | 350 | content: ToolResultContent::Text("No edits were made.".into()), |
| @@ -800,11 +804,30 @@ impl ToolCard for EditFileToolCard { · 453 unmodified lines | ||
| 800 | 804 | if let Some(active_editor) = item.downcast::<Editor>() { |
| 801 | 805 | active_editor |
| 802 | 806 | .update_in(cx, |editor, window, cx| { |
| 803 | − editor.go_to_singleton_buffer_point( | |
| 804 | − language::Point::new(0, 0), | |
| 805 | − window, | |
| 806 | − cx, | |
| 807 | − ); | |
| 807 | + let snapshot = | |
| 808 | + editor.buffer().read(cx).snapshot(cx); | |
| 809 | + let first_hunk = editor | |
| 810 | + .diff_hunks_in_ranges( | |
| 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 | 832 | .log_err(); |
| 810 | 833 | } |
| @@ -31,8 +31,8 @@ pub struct ReadFileToolInput { · 30 unmodified lines | ||
| 31 | 31 | /// <example> |
| 32 | 32 | /// If the project has the following root directories: |
| 33 | 33 | /// |
| 34 | − /// - directory1 | |
| 35 | − /// - directory2 | |
| 34 | + /// - /a/b/directory1 | |
| 35 | + /// - /c/d/directory2 | |
| 36 | 36 | /// |
| 37 | 37 | /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`. |
| 38 | 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 | 4 | <edits> |
| 5 | 5 | |
| 6 | −<old_text> | |
| 6 | +<old_text line=10> | |
| 7 | 7 | OLD TEXT 1 HERE |
| 8 | 8 | </old_text> |
| 9 | 9 | <new_text> |
| 10 | 10 | NEW TEXT 1 HERE |
| 11 | 11 | </new_text> |
| 12 | 12 | |
| 13 | −<old_text> | |
| 13 | +<old_text line=456> | |
| 14 | 14 | OLD TEXT 2 HERE |
| 15 | 15 | </old_text> |
| 16 | 16 | <new_text> |
| 17 | 17 | NEW TEXT 2 HERE |
| 18 | 18 | </new_text> |
| 19 | 19 | |
| 20 | −<old_text> | |
| 20 | +<old_text line=42> | |
| 21 | 21 | OLD TEXT 3 HERE |
| 22 | 22 | </old_text> |
| 23 | 23 | <new_text> |
| @@ -33,6 +33,7 @@ NEW TEXT 3 HERE · 9 unmodified lines | ||
| 33 | 33 | - `<old_text>` must exactly match existing file content, including indentation |
| 34 | 34 | - `<old_text>` must come from the actual file, not an outline |
| 35 | 35 | - `<old_text>` cannot be empty |
| 36 | +- `line` should be a starting line number for the text to be replaced | |
| 36 | 37 | - Be minimal with replacements: |
| 37 | 38 | - For unique lines, include only those lines |
| 38 | 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 | 49 | <example> |
| 49 | 50 | <edits> |
| 50 | 51 | |
| 51 | −<old_text> | |
| 52 | +<old_text line=3> | |
| 52 | 53 | struct User { |
| 53 | 54 | name: String, |
| 54 | 55 | email: String, |
| @@ -62,7 +63,7 @@ struct User { · 7 unmodified lines | ||
| 62 | 63 | } |
| 63 | 64 | </new_text> |
| 64 | 65 | |
| 65 | −<old_text> | |
| 66 | +<old_text line=25> | |
| 66 | 67 | let user = User { |
| 67 | 68 | name: String::from("John"), |
| 68 | 69 | email: String::from("john@example.com"), |
| @@ -638,29 +638,36 @@ impl ToolCard for TerminalToolCard { · 637 unmodified lines | ||
| 638 | 638 | .bg(cx.theme().colors().editor_background) |
| 639 | 639 | .rounded_b_md() |
| 640 | 640 | .text_ui_sm(cx) |
| 641 | − .child( | |
| 642 | − ToolOutputPreview::new( | |
| 643 | − terminal.clone().into_any_element(), | |
| 644 | − terminal.entity_id(), | |
| 645 | − ) | |
| 646 | − .with_total_lines(self.content_line_count) | |
| 647 | − .toggle_state(!terminal.read(cx).is_content_limited(window)) | |
| 648 | − .on_toggle({ | |
| 649 | − let terminal = terminal.clone(); | |
| 650 | − move |is_expanded, _, cx| { | |
| 651 | − terminal.update(cx, |terminal, cx| { | |
| 652 | − terminal.set_embedded_mode( | |
| 653 | − if is_expanded { | |
| 654 | − None | |
| 655 | − } else { | |
| 656 | − Some(COLLAPSED_LINES) | |
| 657 | − }, | |
| 658 | − cx, | |
| 659 | − ); | |
| 660 | − }); | |
| 661 | − } | |
| 662 | − }), | |
| 663 | − ), | |
| 641 | + .child({ | |
| 642 | + let content_mode = terminal.read(cx).content_mode(window, cx); | |
| 643 | + | |
| 644 | + if content_mode.is_scrollable() { | |
| 645 | + div().h_72().child(terminal.clone()).into_any_element() | |
| 646 | + } else { | |
| 647 | + ToolOutputPreview::new( | |
| 648 | + terminal.clone().into_any_element(), | |
| 649 | + terminal.entity_id(), | |
| 650 | + ) | |
| 651 | + .with_total_lines(self.content_line_count) | |
| 652 | + .toggle_state(!content_mode.is_limited()) | |
| 653 | + .on_toggle({ | |
| 654 | + let terminal = terminal.clone(); | |
| 655 | + move |is_expanded, _, cx| { | |
| 656 | + terminal.update(cx, |terminal, cx| { | |
| 657 | + terminal.set_embedded_mode( | |
| 658 | + if is_expanded { | |
| 659 | + None | |
| 660 | + } else { | |
| 661 | + Some(COLLAPSED_LINES) | |
| 662 | + }, | |
| 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 | 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 | 225 | } else { |
| 226 | 226 | drop(window.prompt( |
| 227 | 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 | 304 | impl AutoUpdater { |
| 300 | 305 | pub fn get(cx: &mut App) -> Option<Entity<Self>> { |
| 301 | 306 | cx.default_global::<GlobalAutoUpdate>().0.clone() |
| @@ -313,13 +318,13 @@ impl AutoUpdater { · 11 unmodified lines | ||
| 313 | 318 | pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> { |
| 314 | 319 | cx.spawn(async move |this, cx| { |
| 315 | 320 | loop { |
| 316 | − this.update(cx, |this, cx| this.poll(cx))?; | |
| 321 | + this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?; | |
| 317 | 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 | 328 | if self.pending_poll.is_some() { |
| 324 | 329 | return; |
| 325 | 330 | } |
| @@ -331,8 +336,18 @@ impl AutoUpdater { · 5 unmodified lines | ||
| 331 | 336 | this.update(cx, |this, cx| { |
| 332 | 337 | this.pending_poll = None; |
| 333 | 338 | if let Err(error) = result { |
| 334 | − log::error!("auto-update failed: error:{:?}", error); | |
| 335 | − this.status = AutoUpdateStatus::Errored; | |
| 339 | + this.status = match check_type { | |
| 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 | 351 | cx.notify(); |
| 337 | 352 | } |
| 338 | 353 | }) |
| @@ -452,6 +452,10 @@ impl Model { · 451 unmodified lines | ||
| 452 | 452 | | Model::Claude3_5SonnetV2 |
| 453 | 453 | | Model::Claude3_7Sonnet |
| 454 | 454 | | Model::Claude3_7SonnetThinking |
| 455 | + | Model::ClaudeSonnet4 | |
| 456 | + | Model::ClaudeSonnet4Thinking | |
| 457 | + | Model::ClaudeOpus4 | |
| 458 | + | Model::ClaudeOpus4Thinking | |
| 455 | 459 | | Model::Claude3Haiku |
| 456 | 460 | | Model::Claude3Opus |
| 457 | 461 | | Model::Claude3Sonnet |
| @@ -111,7 +111,7 @@ pub struct ChannelMembership { · 110 unmodified lines | ||
| 111 | 111 | pub role: proto::ChannelRole, |
| 112 | 112 | } |
| 113 | 113 | impl ChannelMembership { |
| 114 | − pub fn sort_key(&self) -> MembershipSortKey { | |
| 114 | + pub fn sort_key(&self) -> MembershipSortKey<'_> { | |
| 115 | 115 | MembershipSortKey { |
| 116 | 116 | role_order: match self.role { |
| 117 | 117 | proto::ChannelRole::Admin => 0, |
| @@ -32,7 +32,7 @@ impl ChannelIndex { · 31 unmodified lines | ||
| 32 | 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 | 36 | ChannelPathsInsertGuard { |
| 37 | 37 | channels_ordered: &mut self.channels_ordered, |
| 38 | 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 | 269 | github_login: "nathansobo".into(), |
| 270 | 270 | avatar_url: "http://avatar.com/nathansobo".into(), |
| 271 | 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 | 322 | github_login: "maxbrunsfeld".into(), |
| 324 | 323 | avatar_url: "http://avatar.com/maxbrunsfeld".into(), |
| 325 | 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 | 366 | github_login: "as-cii".into(), |
| 369 | 367 | avatar_url: "http://avatar.com/as-cii".into(), |
| 370 | 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 | 129 | fn main() -> Result<()> { |
| 130 | + #[cfg(unix)] | |
| 131 | + util::prevent_root_execution(); | |
| 132 | + | |
| 130 | 133 | // Exit flatpak sandbox if needed |
| 131 | 134 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] |
| 132 | 135 | { |
| @@ -28,6 +28,9 @@ feature_flags.workspace = true · 27 unmodified lines | ||
| 28 | 28 | futures.workspace = true |
| 29 | 29 | gpui.workspace = true |
| 30 | 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 | 34 | http_client.workspace = true |
| 32 | 35 | http_client_tls.workspace = true |
| 33 | 36 | httparse = "1.10" |
| @@ -36,6 +39,7 @@ paths.workspace = true · 2 unmodified lines | ||
| 36 | 39 | parking_lot.workspace = true |
| 37 | 40 | postage.workspace = true |
| 38 | 41 | rand.workspace = true |
| 42 | +regex.workspace = true | |
| 39 | 43 | release_channel.workspace = true |
| 40 | 44 | rpc = { workspace = true, features = ["gpui"] } |
| 41 | 45 | schemars.workspace = true |
| @@ -48,7 +52,7 @@ telemetry_events.workspace = true · 6 unmodified lines | ||
| 48 | 52 | text.workspace = true |
| 49 | 53 | thiserror.workspace = true |
| 50 | 54 | time.workspace = true |
| 51 | −tiny_http = "0.8" | |
| 55 | +tiny_http.workspace = true | |
| 52 | 56 | tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] } |
| 53 | 57 | url.workspace = true |
| 54 | 58 | util.workspace = true |
| @@ -60,11 +64,12 @@ workspace-hack.workspace = true · 5 unmodified lines | ||
| 60 | 64 | [dev-dependencies] |
| 61 | 65 | clock = { workspace = true, features = ["test-support"] } |
| 62 | 66 | collections = { workspace = true, features = ["test-support"] } |
| 67 | +fs.workspace = true | |
| 63 | 68 | gpui = { workspace = true, features = ["test-support"] } |
| 69 | +http_client = { workspace = true, features = ["test-support"] } | |
| 64 | 70 | rpc = { workspace = true, features = ["test-support"] } |
| 65 | 71 | settings = { workspace = true, features = ["test-support"] } |
| 66 | 72 | util = { workspace = true, features = ["test-support"] } |
| 67 | −http_client = { workspace = true, features = ["test-support"] } | |
| 68 | 73 | |
| 69 | 74 | [target.'cfg(target_os = "windows")'.dependencies] |
| 70 | 75 | windows.workspace = true |
| @@ -1887,8 +1887,16 @@ mod tests { · 1886 unmodified lines | ||
| 1887 | 1887 | .set_entity(&entity3, &mut cx.to_async()); |
| 1888 | 1888 | drop(subscription3); |
| 1889 | 1889 | |
| 1890 | − server.send(proto::JoinProject { project_id: 1 }); | |
| 1891 | − server.send(proto::JoinProject { project_id: 2 }); | |
| 1890 | + server.send(proto::JoinProject { | |
| 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 | 1900 | done_rx1.recv().await.unwrap(); |
| 1893 | 1901 | done_rx2.recv().await.unwrap(); |
| 1894 | 1902 | } |
| @@ -3,20 +3,30 @@ · 2 unmodified lines | ||
| 3 | 3 | mod http_proxy; |
| 4 | 4 | mod socks_proxy; |
| 5 | 5 | |
| 6 | +use std::sync::LazyLock; | |
| 7 | + | |
| 6 | 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 | 15 | use http_client::Url; |
| 8 | 16 | use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy}; |
| 9 | 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 | 21 | pub(crate) async fn connect_proxy_stream( |
| 12 | 22 | proxy: &Url, |
| 13 | 23 | rpc_host: (&str, u16), |
| 14 | 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 | 26 | // If parsing the proxy URL fails, we must avoid falling back to an insecure connection. |
| 17 | 27 | // SOCKS proxies are often used in contexts where security and privacy are critical, |
| 18 | 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 | 32 | // Connect to proxy and wrap protocol later |
| @@ -39,10 +49,8 @@ enum ProxyType<'t> { · 16 unmodified lines | ||
| 39 | 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 | 53 | let scheme = proxy.scheme(); |
| 44 | − let host = proxy.host()?.to_string(); | |
| 45 | − let port = proxy.port_or_known_default()?; | |
| 46 | 54 | let proxy_type = match scheme { |
| 47 | 55 | scheme if scheme.starts_with("socks") => { |
| 48 | 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 | 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 | 97 | pub(crate) trait AsyncReadWrite: |
| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | //! socks proxy |
| 2 | 2 | |
| 3 | +use std::net::SocketAddr; | |
| 4 | + | |
| 3 | 5 | use anyhow::{Context as _, Result}; |
| 4 | 6 | use http_client::Url; |
| 5 | 7 | use tokio::net::TcpStream; |
| @@ -8,6 +10,8 @@ use tokio_socks::{ · 2 unmodified lines | ||
| 8 | 10 | tcp::{Socks4Stream, Socks5Stream}, |
| 9 | 11 | }; |
| 10 | 12 | |
| 13 | +use crate::proxy::SYSTEM_DNS_RESOLVER; | |
| 14 | + | |
| 11 | 15 | use super::AsyncReadWrite; |
| 12 | 16 | |
| 13 | 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 | 78 | let rpc_host = match (rpc_host, local_dns) { |
| 75 | 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 | 82 | .await |
| 78 | 83 | .with_context(|| format!("Failed to lookup domain {}", domain))? |
| 84 | + .into_iter() | |
| 79 | 85 | .next() |
| 80 | 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 | 89 | (rpc_host, _) => rpc_host, |
| 84 | 90 | }; |
| @@ -8,10 +8,11 @@ use futures::{Future, FutureExt, StreamExt}; · 7 unmodified lines | ||
| 8 | 8 | use gpui::{App, AppContext as _, BackgroundExecutor, Task}; |
| 9 | 9 | use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request}; |
| 10 | 10 | use parking_lot::Mutex; |
| 11 | +use regex::Regex; | |
| 11 | 12 | use release_channel::ReleaseChannel; |
| 12 | 13 | use settings::{Settings, SettingsStore}; |
| 13 | 14 | use sha2::{Digest, Sha256}; |
| 14 | −use std::collections::{HashMap, HashSet}; | |
| 15 | +use std::collections::HashSet; | |
| 15 | 16 | use std::fs::File; |
| 16 | 17 | use std::io::Write; |
| 17 | 18 | use std::sync::LazyLock; |
| @@ -45,31 +46,13 @@ struct TelemetryState { · 27 unmodified lines | ||
| 45 | 46 | first_event_date_time: Option<Instant>, |
| 46 | 47 | event_coalescer: EventCoalescer, |
| 47 | 48 | max_queue_size: usize, |
| 48 | − worktree_id_map: WorktreeIdMap, | |
| 49 | + worktrees_with_project_type_events_sent: HashSet<WorktreeId>, | |
| 49 | 50 | |
| 50 | 51 | os_name: String, |
| 51 | 52 | app_version: String, |
| 52 | 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 | 56 | #[cfg(debug_assertions)] |
| 74 | 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 | 81 | pub fn os_name() -> String { |
| 95 | 82 | #[cfg(target_os = "macos")] |
| 96 | 83 | { |
| @@ -194,20 +181,7 @@ impl Telemetry { · 97 unmodified lines | ||
| 194 | 181 | first_event_date_time: None, |
| 195 | 182 | event_coalescer: EventCoalescer::new(clock.clone()), |
| 196 | 183 | max_queue_size: MAX_QUEUE_LEN, |
| 197 | − worktree_id_map: WorktreeIdMap(HashMap::from_iter([ | |
| 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 | − ])), | |
| 184 | + worktrees_with_project_type_events_sent: HashSet::new(), | |
| 211 | 185 | |
| 212 | 186 | os_version: None, |
| 213 | 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 | 349 | self: &Arc<Self>, |
| 376 | 350 | worktree_id: WorktreeId, |
| 377 | 351 | updated_entries_set: &UpdatedEntriesSet, |
| 378 | 352 | ) { |
| 379 | − let project_type_names: Vec<String> = { | |
| 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 | − } | |
| 353 | + let project_type_names = self.detect_project_types(worktree_id, updated_entries_set); | |
| 392 | 354 | |
| 393 | − let project_file_found = updated_entries_set.iter().any(|(path, _, _)| { | |
| 394 | − path.as_ref() | |
| 395 | − .file_name() | |
| 396 | − .and_then(|name| name.to_str()) | |
| 397 | − .map(|name_str| name_str == project_file_name) | |
| 398 | − .unwrap_or(false) | |
| 399 | − }); | |
| 355 | + for project_type_name in project_type_names { | |
| 356 | + telemetry::event!("Project Opened", project_type = project_type_name); | |
| 357 | + } | |
| 358 | + } | |
| 400 | 359 | |
| 401 | − if !project_file_found { | |
| 402 | − return None; | |
| 403 | − } | |
| 360 | + fn detect_project_types( | |
| 361 | + self: &Arc<Self>, | |
| 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 | |
| 406 | − .worktree_ids_reported | |
| 407 | − .insert(worktree_id); | |
| 368 | + if state | |
| 369 | + .worktrees_with_project_type_events_sent | |
| 370 | + .contains(&worktree_id) | |
| 371 | + { | |
| 372 | + return project_names.into_iter().collect(); | |
| 373 | + } | |
| 408 | 374 | |
| 409 | − Some(project_type_telemetry.name.clone()) | |
| 410 | − }) | |
| 411 | − .collect() | |
| 412 | − }; | |
| 375 | + for (path, _, _) in updated_entries_set.iter() { | |
| 376 | + let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else { | |
| 377 | + continue; | |
| 378 | + }; | |
| 413 | 379 | |
| 414 | − for project_type_name in project_type_names { | |
| 415 | − telemetry::event!("Project Opened", project_type = project_type_name); | |
| 380 | + if file_name == "pnpm-lock.yaml" { | |
| 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 | 402 | fn report_event(self: &Arc<Self>, event: Event) { |
| @@ -578,7 +561,9 @@ mod tests { · 158 unmodified lines | ||
| 578 | 561 | use clock::FakeSystemClock; |
| 579 | 562 | use gpui::TestAppContext; |
| 580 | 563 | use http_client::FakeHttpClient; |
| 564 | + use std::collections::HashMap; | |
| 581 | 565 | use telemetry_events::FlexibleEvent; |
| 566 | + use worktree::{PathChange, ProjectEntryId, WorktreeId}; | |
| 582 | 567 | |
| 583 | 568 | #[gpui::test] |
| 584 | 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 | 775 | // TODO: |
| 700 | 776 | // Test settings |
| 701 | 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 | 788 | && telemetry.state.lock().flush_events_task.is_none() |
| 713 | 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 | 49 | pub github_login: String, |
| 50 | 50 | pub avatar_uri: SharedUri, |
| 51 | 51 | pub name: Option<String>, |
| 52 | − pub email: Option<String>, | |
| 53 | 52 | } |
| 54 | 53 | |
| 55 | 54 | #[derive(Clone, Debug, PartialEq, Eq)] |
| @@ -58,6 +57,8 @@ pub struct Collaborator { · 2 unmodified lines | ||
| 58 | 57 | pub replica_id: ReplicaId, |
| 59 | 58 | pub user_id: UserId, |
| 60 | 59 | pub is_host: bool, |
| 60 | + pub committer_name: Option<String>, | |
| 61 | + pub committer_email: Option<String>, | |
| 61 | 62 | } |
| 62 | 63 | |
| 63 | 64 | impl PartialOrd for User { |
| @@ -881,7 +882,6 @@ impl User { · 817 unmodified lines | ||
| 881 | 882 | github_login: message.github_login, |
| 882 | 883 | avatar_uri: message.avatar_url.into(), |
| 883 | 884 | name: message.name, |
| 884 | − email: message.email, | |
| 885 | 885 | }) |
| 886 | 886 | } |
| 887 | 887 | } |
| @@ -912,6 +912,8 @@ impl Collaborator { · 24 unmodified lines | ||
| 912 | 912 | replica_id: message.replica_id as ReplicaId, |
| 913 | 913 | user_id: message.user_id as UserId, |
| 914 | 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 | 185 | "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, |
| 186 | 186 | "user_id" INTEGER NOT NULL, |
| 187 | 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 | 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 | 98 | pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> { |
| 99 | 99 | Router::new() |
| 100 | − .route("/user", get(get_authenticated_user)) | |
| 100 | + .route("/user", get(update_or_create_authenticated_user)) | |
| 101 | 101 | .route("/users/look_up", get(look_up_user)) |
| 102 | 102 | .route("/users/:id/access_tokens", post(create_access_token)) |
| 103 | 103 | .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) |
| @@ -157,7 +157,7 @@ struct AuthenticatedUserResponse { · 53 unmodified lines | ||
| 157 | 157 | feature_flags: Vec<String>, |
| 158 | 158 | } |
| 159 | 159 | |
| 160 | −async fn get_authenticated_user( | |
| 160 | +async fn update_or_create_authenticated_user( | |
| 161 | 161 | Query(params): Query<AuthenticatedUserParams>, |
| 162 | 162 | Extension(app): Extension<Arc<AppState>>, |
| 163 | 163 | ) -> Result<Json<AuthenticatedUserResponse>> { |
| @@ -165,7 +165,7 @@ async fn get_authenticated_user( · 1 unmodified line | ||
| 165 | 165 | |
| 166 | 166 | let user = app |
| 167 | 167 | .db |
| 168 | − .get_or_create_user_by_github_account( | |
| 168 | + .update_or_create_user_by_github_account( | |
| 169 | 169 | ¶ms.github_login, |
| 170 | 170 | params.github_user_id, |
| 171 | 171 | params.github_email.as_deref(), |
| @@ -751,6 +751,8 @@ pub struct ProjectCollaborator { · 750 unmodified lines | ||
| 751 | 751 | pub user_id: UserId, |
| 752 | 752 | pub replica_id: ReplicaId, |
| 753 | 753 | pub is_host: bool, |
| 754 | + pub committer_name: Option<String>, | |
| 755 | + pub committer_email: Option<String>, | |
| 754 | 756 | } |
| 755 | 757 | |
| 756 | 758 | impl ProjectCollaborator { |
| @@ -760,6 +762,8 @@ impl ProjectCollaborator { · 3 unmodified lines | ||
| 760 | 762 | replica_id: self.replica_id.0 as u32, |
| 761 | 763 | user_id: self.user_id.to_proto(), |
| 762 | 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 | 118 | user_id: collaborator.user_id.to_proto(), |
| 119 | 119 | replica_id: collaborator.replica_id.0 as u32, |
| 120 | 120 | is_host: false, |
| 121 | + committer_name: None, | |
| 122 | + committer_email: None, | |
| 121 | 123 | }) |
| 122 | 124 | .collect(), |
| 123 | 125 | }) |
| @@ -225,6 +227,8 @@ impl Database { · 101 unmodified lines | ||
| 225 | 227 | user_id: collaborator.user_id.to_proto(), |
| 226 | 228 | replica_id: collaborator.replica_id.0 as u32, |
| 227 | 229 | is_host: false, |
| 230 | + committer_name: None, | |
| 231 | + committer_email: None, | |
| 228 | 232 | }) |
| 229 | 233 | .collect(), |
| 230 | 234 | }, |
| @@ -261,6 +265,8 @@ impl Database { · 30 unmodified lines | ||
| 261 | 265 | replica_id: db_collaborator.replica_id.0 as u32, |
| 262 | 266 | user_id: db_collaborator.user_id.to_proto(), |
| 263 | 267 | is_host: false, |
| 268 | + committer_name: None, | |
| 269 | + committer_email: None, | |
| 264 | 270 | }) |
| 265 | 271 | } else { |
| 266 | 272 | collaborator_ids_to_remove.push(db_collaborator.id); |
| @@ -390,6 +396,8 @@ impl Database { · 123 unmodified lines | ||
| 390 | 396 | replica_id: row.replica_id.0 as u32, |
| 391 | 397 | user_id: row.user_id.to_proto(), |
| 392 | 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 | 502 | /// Returns all channels for the user with the given ID. |
| 503 | 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 }) | |
| 505 | − .await | |
| 504 | + self.weak_transaction( | |
| 505 | + |tx| async move { self.get_user_channels(user_id, None, true, &tx).await }, | |
| 506 | + ) | |
| 507 | + .await | |
| 506 | 508 | } |
| 507 | 509 | |
| 508 | 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 | 740 | github_login: user.github_login, |
| 739 | 741 | name: user.name, |
| 740 | − email: user.email_address, | |
| 741 | 742 | }) |
| 742 | 743 | } |
| 743 | 744 | proto::ChannelMember { |
| @@ -15,7 +15,7 @@ impl Database { · 14 unmodified lines | ||
| 15 | 15 | user_b_busy: bool, |
| 16 | 16 | } |
| 17 | 17 | |
| 18 | − self.transaction(|tx| async move { | |
| 18 | + self.weak_transaction(|tx| async move { | |
| 19 | 19 | let user_a_participant = Alias::new("user_a_participant"); |
| 20 | 20 | let user_b_participant = Alias::new("user_b_participant"); |
| 21 | 21 | let mut db_contacts = contact::Entity::find() |
| @@ -91,7 +91,7 @@ impl Database { · 69 unmodified lines | ||
| 91 | 91 | |
| 92 | 92 | /// Returns whether the given user is a busy (on a call). |
| 93 | 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 | 95 | let participant = room_participant::Entity::find() |
| 96 | 96 | .filter(room_participant::Column::UserId.eq(user_id)) |
| 97 | 97 | .one(&*tx) |
| @@ -71,7 +71,7 @@ impl Database { · 70 unmodified lines | ||
| 71 | 71 | ) -> Result<()> { |
| 72 | 72 | self.weak_transaction(|tx| async move { |
| 73 | 73 | let user = self |
| 74 | − .get_or_create_user_by_github_account_tx( | |
| 74 | + .update_or_create_user_by_github_account_tx( | |
| 75 | 75 | github_login, |
| 76 | 76 | github_user_id, |
| 77 | 77 | github_email, |
| @@ -98,7 +98,9 @@ impl Database { · 97 unmodified lines | ||
| 98 | 98 | user_id: ActiveValue::set(participant.user_id), |
| 99 | 99 | replica_id: ActiveValue::set(ReplicaId(replica_id)), |
| 100 | 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 | 105 | .insert(&*tx) |
| 104 | 106 | .await?; |
| @@ -784,13 +786,27 @@ impl Database { · 679 unmodified lines | ||
| 784 | 786 | project_id: ProjectId, |
| 785 | 787 | connection: ConnectionId, |
| 786 | 788 | user_id: UserId, |
| 789 | + committer_name: Option<String>, | |
| 790 | + committer_email: Option<String>, | |
| 787 | 791 | ) -> Result<TransactionGuard<(Project, ReplicaId)>> { |
| 788 | − self.project_transaction(project_id, |tx| async move { | |
| 789 | − let (project, role) = self | |
| 790 | − .access_project(project_id, connection, Capability::ReadOnly, &tx) | |
| 791 | − .await?; | |
| 792 | − self.join_project_internal(project, user_id, connection, role, &tx) | |
| 792 | + self.project_transaction(project_id, move |tx| { | |
| 793 | + let committer_name = committer_name.clone(); | |
| 794 | + let committer_email = committer_email.clone(); | |
| 795 | + async move { | |
| 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 | 808 | .await |
| 809 | + } | |
| 794 | 810 | }) |
| 795 | 811 | .await |
| 796 | 812 | } |
| @@ -799,6 +815,8 @@ impl Database { · 2 unmodified lines | ||
| 799 | 815 | &self, |
| 800 | 816 | project: project::Model, |
| 801 | 817 | user_id: UserId, |
| 818 | + committer_name: Option<String>, | |
| 819 | + committer_email: Option<String>, | |
| 802 | 820 | connection: ConnectionId, |
| 803 | 821 | role: ChannelRole, |
| 804 | 822 | tx: &DatabaseTransaction, |
| @@ -822,7 +840,9 @@ impl Database { · 17 unmodified lines | ||
| 822 | 840 | user_id: ActiveValue::set(user_id), |
| 823 | 841 | replica_id: ActiveValue::set(replica_id), |
| 824 | 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 | 847 | .insert(tx) |
| 828 | 848 | .await?; |
| @@ -1026,6 +1046,8 @@ impl Database { · 197 unmodified lines | ||
| 1026 | 1046 | user_id: collaborator.user_id, |
| 1027 | 1047 | replica_id: collaborator.replica_id, |
| 1028 | 1048 | is_host: collaborator.is_host, |
| 1049 | + committer_name: collaborator.committer_name, | |
| 1050 | + committer_email: collaborator.committer_email, | |
| 1029 | 1051 | }) |
| 1030 | 1052 | .collect(), |
| 1031 | 1053 | worktrees, |
| @@ -80,7 +80,7 @@ impl Database { · 79 unmodified lines | ||
| 80 | 80 | &self, |
| 81 | 81 | user_id: UserId, |
| 82 | 82 | ) -> Result<Option<proto::IncomingCall>> { |
| 83 | − self.transaction(|tx| async move { | |
| 83 | + self.weak_transaction(|tx| async move { | |
| 84 | 84 | let pending_participant = room_participant::Entity::find() |
| 85 | 85 | .filter( |
| 86 | 86 | room_participant::Column::UserId |
| @@ -553,6 +553,8 @@ impl Database { · 466 unmodified lines | ||
| 553 | 553 | user_id: collaborator.user_id, |
| 554 | 554 | replica_id: collaborator.replica_id, |
| 555 | 555 | is_host: collaborator.is_host, |
| 556 | + committer_name: collaborator.committer_name.clone(), | |
| 557 | + committer_email: collaborator.committer_email.clone(), | |
| 556 | 558 | }) |
| 557 | 559 | .collect(), |
| 558 | 560 | worktrees: reshared_project.worktrees.clone(), |
| @@ -857,6 +859,8 @@ impl Database { · 298 unmodified lines | ||
| 857 | 859 | user_id: collaborator.user_id, |
| 858 | 860 | replica_id: collaborator.replica_id, |
| 859 | 861 | is_host: collaborator.is_host, |
| 862 | + committer_name: collaborator.committer_name, | |
| 863 | + committer_email: collaborator.committer_email, | |
| 860 | 864 | }) |
| 861 | 865 | .collect::<Vec<_>>(); |
| 862 | 866 | |
| @@ -111,7 +111,7 @@ impl Database { · 110 unmodified lines | ||
| 111 | 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 | 115 | &self, |
| 116 | 116 | github_login: &str, |
| 117 | 117 | github_user_id: i32, |
| @@ -121,7 +121,7 @@ impl Database { · 3 unmodified lines | ||
| 121 | 121 | initial_channel_id: Option<ChannelId>, |
| 122 | 122 | ) -> Result<User> { |
| 123 | 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 | 125 | github_login, |
| 126 | 126 | github_user_id, |
| 127 | 127 | github_email, |
| @@ -135,7 +135,7 @@ impl Database { · 7 unmodified lines | ||
| 135 | 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 | 139 | &self, |
| 140 | 140 | github_login: &str, |
| 141 | 141 | github_user_id: i32, |
| @@ -13,6 +13,8 @@ pub struct Model { · 12 unmodified lines | ||
| 13 | 13 | pub user_id: UserId, |
| 14 | 14 | pub replica_id: ReplicaId, |
| 15 | 15 | pub is_host: bool, |
| 16 | + pub committer_name: Option<String>, | |
| 17 | + pub committer_email: Option<String>, | |
| 16 | 18 | } |
| 17 | 19 | |
| 18 | 20 | impl Model { |
| @@ -126,12 +126,16 @@ async fn test_channel_buffers(db: &Arc<Database>) { · 125 unmodified lines | ||
| 126 | 126 | peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }), |
| 127 | 127 | replica_id: 0, |
| 128 | 128 | is_host: false, |
| 129 | + committer_name: None, | |
| 130 | + committer_email: None, | |
| 129 | 131 | }, |
| 130 | 132 | rpc::proto::Collaborator { |
| 131 | 133 | user_id: b_id.to_proto(), |
| 132 | 134 | peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }), |
| 133 | 135 | replica_id: 1, |
| 134 | 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 | 74 | test_both_dbs!( |
| 75 | − test_get_or_create_user_by_github_account, | |
| 76 | − test_get_or_create_user_by_github_account_postgres, | |
| 77 | − test_get_or_create_user_by_github_account_sqlite | |
| 75 | + test_update_or_create_user_by_github_account, | |
| 76 | + test_update_or_create_user_by_github_account_postgres, | |
| 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 | 81 | db.create_user( |
| 82 | 82 | "user1@example.com", |
| 83 | 83 | None, |
| @@ -104,7 +104,14 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) { · 20 unmodified lines | ||
| 104 | 104 | .user_id; |
| 105 | 105 | |
| 106 | 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 | 115 | .await |
| 109 | 116 | .unwrap(); |
| 110 | 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 | 119 | assert_eq!(user.github_user_id, 102); |
| 113 | 120 | |
| 114 | 121 | let user = db |
| 115 | − .get_or_create_user_by_github_account( | |
| 122 | + .update_or_create_user_by_github_account( | |
| 116 | 123 | "login3", |
| 117 | 124 | 103, |
| 118 | 125 | Some("user3@example.com"), |
| @@ -7,6 +7,12 @@ pub use token::*; · 6 unmodified lines | ||
| 7 | 7 | |
| 8 | 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 | 16 | /// The default value to use for maximum spend per month if the user did not |
| 11 | 17 | /// explicitly set a maximum spend. |
| 12 | 18 | /// |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | use crate::db::billing_subscription::SubscriptionKind; |
| 2 | 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 | 4 | use crate::{Config, db::billing_preference}; |
| 5 | 5 | use anyhow::{Context as _, Result}; |
| 6 | 6 | use chrono::{NaiveDateTime, Utc}; |
| @@ -84,7 +84,7 @@ impl LlmTokenClaims { · 77 unmodified lines | ||
| 84 | 84 | .any(|flag| flag == "llm-closed-beta"), |
| 85 | 85 | bypass_account_age_check: feature_flags |
| 86 | 86 | .iter() |
| 87 | − .any(|flag| flag == "bypass-account-age-check"), | |
| 87 | + .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG), | |
| 88 | 88 | can_use_web_search_tool: true, |
| 89 | 89 | use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"), |
| 90 | 90 | plan, |
| @@ -4,14 +4,17 @@ use crate::api::billing::find_or_create_billing_customer; · 3 unmodified lines | ||
| 4 | 4 | use crate::api::{CloudflareIpCountryHeader, SystemIdHeader}; |
| 5 | 5 | use crate::db::billing_subscription::SubscriptionKind; |
| 6 | 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 | 11 | use crate::stripe_client::StripeCustomerId; |
| 9 | 12 | use crate::{ |
| 10 | 13 | AppState, Error, Result, auth, |
| 11 | 14 | db::{ |
| 12 | 15 | self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser, |
| 13 | 16 | CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId, |
| 14 | − NotificationId, Project, ProjectId, RejoinedProject, RemoveChannelMemberResult, ReplicaId, | |
| 17 | + NotificationId, ProjectId, RejoinedProject, RemoveChannelMemberResult, | |
| 15 | 18 | RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId, |
| 16 | 19 | }, |
| 17 | 20 | executor::Executor, |
| @@ -65,7 +68,7 @@ use std::{ · 47 unmodified lines | ||
| 65 | 68 | rc::Rc, |
| 66 | 69 | sync::{ |
| 67 | 70 | Arc, OnceLock, |
| 68 | − atomic::{AtomicBool, Ordering::SeqCst}, | |
| 71 | + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, | |
| 69 | 72 | }, |
| 70 | 73 | time::{Duration, Instant}, |
| 71 | 74 | }; |
| @@ -86,10 +89,36 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15); · 14 unmodified lines | ||
| 86 | 89 | const MESSAGE_COUNT_PER_PAGE: usize = 100; |
| 87 | 90 | const MAX_MESSAGE_LEN: usize = 1024; |
| 88 | 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 | 96 | type MessageHandler = |
| 91 | 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 | 122 | struct Response<R> { |
| 94 | 123 | peer: Arc<Peer>, |
| 95 | 124 | receipt: Receipt<R>, |
| @@ -722,6 +751,7 @@ impl Server { · 626 unmodified lines | ||
| 722 | 751 | system_id: Option<String>, |
| 723 | 752 | send_connection_id: Option<oneshot::Sender<ConnectionId>>, |
| 724 | 753 | executor: Executor, |
| 754 | + connection_guard: Option<ConnectionGuard>, | |
| 725 | 755 | ) -> impl Future<Output = ()> + use<> { |
| 726 | 756 | let this = self.clone(); |
| 727 | 757 | let span = info_span!("handle connection", %address, |
| @@ -742,6 +772,7 @@ impl Server { · 14 unmodified lines | ||
| 742 | 772 | tracing::error!("server is tearing down"); |
| 743 | 773 | return |
| 744 | 774 | } |
| 775 | + | |
| 745 | 776 | let (connection_id, handle_io, mut incoming_rx) = this |
| 746 | 777 | .peer |
| 747 | 778 | .add_connection(connection, { |
| @@ -783,6 +814,7 @@ impl Server { · 35 unmodified lines | ||
| 783 | 814 | tracing::error!(?error, "failed to send initial client update"); |
| 784 | 815 | return; |
| 785 | 816 | } |
| 817 | + drop(connection_guard); | |
| 786 | 818 | |
| 787 | 819 | let handle_io = handle_io.fuse(); |
| 788 | 820 | futures::pin_mut!(handle_io); |
| @@ -1154,6 +1186,19 @@ pub async fn handle_websocket_request( · 365 unmodified lines | ||
| 1154 | 1186 | } |
| 1155 | 1187 | |
| 1156 | 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 | 1202 | ws.on_upgrade(move |socket| { |
| 1158 | 1203 | let socket = socket |
| 1159 | 1204 | .map_ok(to_tungstenite_message) |
| @@ -1171,6 +1216,7 @@ pub async fn handle_websocket_request( · 11 unmodified lines | ||
| 1171 | 1216 | system_id_header.map(|header| header.to_string()), |
| 1172 | 1217 | None, |
| 1173 | 1218 | Executor::Production, |
| 1219 | + Some(connection_guard), | |
| 1174 | 1220 | ) |
| 1175 | 1221 | .await; |
| 1176 | 1222 | } |
| @@ -1844,28 +1890,16 @@ async fn join_project( · 667 unmodified lines | ||
| 1844 | 1890 | |
| 1845 | 1891 | let db = session.db().await; |
| 1846 | 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 | 1900 | .await?; |
| 1849 | 1901 | drop(db); |
| 1850 | 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 | 1903 | let collaborators = project |
| 1870 | 1904 | .collaborators |
| 1871 | 1905 | .iter() |
| @@ -1893,6 +1927,8 @@ fn join_project_internal( · 21 unmodified lines | ||
| 1893 | 1927 | replica_id: replica_id.0 as u32, |
| 1894 | 1928 | user_id: guest_user_id.to_proto(), |
| 1895 | 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 | 2557 | id: user.id.to_proto(), |
| 2522 | 2558 | avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), |
| 2523 | 2559 | github_login: user.github_login, |
| 2524 | − email: user.email_address, | |
| 2525 | 2560 | name: user.name, |
| 2526 | 2561 | }) |
| 2527 | 2562 | .collect(); |
| @@ -2555,7 +2590,6 @@ async fn fuzzy_search_users( · 27 unmodified lines | ||
| 2555 | 2590 | avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), |
| 2556 | 2591 | github_login: user.github_login, |
| 2557 | 2592 | name: user.name, |
| 2558 | − email: user.email_address, | |
| 2559 | 2593 | }) |
| 2560 | 2594 | .collect(); |
| 2561 | 2595 | response.send(proto::UsersResponse { users })?; |
| @@ -2773,8 +2807,12 @@ async fn make_update_user_plan_message( · 211 unmodified lines | ||
| 2773 | 2807 | (None, None) |
| 2774 | 2808 | }; |
| 2775 | 2809 | |
| 2776 | − let account_too_young = | |
| 2777 | − !matches!(plan, proto::Plan::ZedPro) && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE; | |
| 2810 | + let bypass_account_age_check = feature_flags | |
| 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 | 2817 | Ok(proto::UpdateUserPlan { |
| 2780 | 2818 | plan: plan.into(), |
| @@ -4075,9 +4113,6 @@ async fn accept_terms_of_service( · 1294 unmodified lines | ||
| 4075 | 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 | 4116 | async fn get_llm_api_token( |
| 4082 | 4117 | _request: proto::GetLlmToken, |
| 4083 | 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 | 127 | log::info!("Seeding {:?} from GitHub", github_user.login); |
| 128 | 128 | |
| 129 | 129 | let user = db |
| 130 | − .get_or_create_user_by_github_account( | |
| 130 | + .update_or_create_user_by_github_account( | |
| 131 | 131 | &github_user.login, |
| 132 | 132 | github_user.id, |
| 133 | 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 | 180 | server |
| 181 | 181 | .app_state |
| 182 | 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 | 184 | .await |
| 185 | 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 | 1610 | .root(cx_a) |
| 1611 | 1611 | .unwrap(); |
| 1612 | 1612 | |
| 1613 | + executor.run_until_parked(); | |
| 1614 | + | |
| 1613 | 1615 | workspace_a_project_b.update(cx_a2, |workspace, cx| { |
| 1614 | 1616 | assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); |
| 1615 | 1617 | assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); |
| @@ -51,7 +51,7 @@ use std::{ · 50 unmodified lines | ||
| 51 | 51 | time::Duration, |
| 52 | 52 | }; |
| 53 | 53 | use unindent::Unindent as _; |
| 54 | −use util::{path, separator, uri}; | |
| 54 | +use util::{path, uri}; | |
| 55 | 55 | use workspace::Pane; |
| 56 | 56 | |
| 57 | 57 | #[ctor::ctor] |
| @@ -1676,13 +1676,13 @@ async fn test_project_reconnect( · 1618 unmodified lines | ||
| 1676 | 1676 | .map(|p| p.to_str().unwrap()) |
| 1677 | 1677 | .collect::<Vec<_>>(), |
| 1678 | 1678 | vec![ |
| 1679 | − separator!("a.txt"), | |
| 1680 | − separator!("b.txt"), | |
| 1681 | − separator!("subdir2"), | |
| 1682 | − separator!("subdir2/f.txt"), | |
| 1683 | − separator!("subdir2/g.txt"), | |
| 1684 | − separator!("subdir2/h.txt"), | |
| 1685 | − separator!("subdir2/i.txt") | |
| 1679 | + path!("a.txt"), | |
| 1680 | + path!("b.txt"), | |
| 1681 | + path!("subdir2"), | |
| 1682 | + path!("subdir2/f.txt"), | |
| 1683 | + path!("subdir2/g.txt"), | |
| 1684 | + path!("subdir2/h.txt"), | |
| 1685 | + path!("subdir2/i.txt") | |
| 1686 | 1686 | ] |
| 1687 | 1687 | ); |
| 1688 | 1688 | assert!(worktree_a3.read(cx).has_update_observer()); |
| @@ -1709,13 +1709,13 @@ async fn test_project_reconnect( · 20 unmodified lines | ||
| 1709 | 1709 | .map(|p| p.to_str().unwrap()) |
| 1710 | 1710 | .collect::<Vec<_>>(), |
| 1711 | 1711 | vec![ |
| 1712 | − separator!("a.txt"), | |
| 1713 | − separator!("b.txt"), | |
| 1714 | − separator!("subdir2"), | |
| 1715 | − separator!("subdir2/f.txt"), | |
| 1716 | − separator!("subdir2/g.txt"), | |
| 1717 | − separator!("subdir2/h.txt"), | |
| 1718 | − separator!("subdir2/i.txt") | |
| 1712 | + path!("a.txt"), | |
| 1713 | + path!("b.txt"), | |
| 1714 | + path!("subdir2"), | |
| 1715 | + path!("subdir2/f.txt"), | |
| 1716 | + path!("subdir2/g.txt"), | |
| 1717 | + path!("subdir2/h.txt"), | |
| 1718 | + path!("subdir2/i.txt") | |
| 1719 | 1719 | ] |
| 1720 | 1720 | ); |
| 1721 | 1721 | assert!(project.worktree_for_id(worktree2_id, cx).is_none()); |
| @@ -1806,13 +1806,13 @@ async fn test_project_reconnect( · 84 unmodified lines | ||
| 1806 | 1806 | .map(|p| p.to_str().unwrap()) |
| 1807 | 1807 | .collect::<Vec<_>>(), |
| 1808 | 1808 | vec![ |
| 1809 | − separator!("a.txt"), | |
| 1810 | − separator!("b.txt"), | |
| 1811 | − separator!("subdir2"), | |
| 1812 | − separator!("subdir2/f.txt"), | |
| 1813 | − separator!("subdir2/g.txt"), | |
| 1814 | − separator!("subdir2/h.txt"), | |
| 1815 | − separator!("subdir2/j.txt") | |
| 1809 | + path!("a.txt"), | |
| 1810 | + path!("b.txt"), | |
| 1811 | + path!("subdir2"), | |
| 1812 | + path!("subdir2/f.txt"), | |
| 1813 | + path!("subdir2/g.txt"), | |
| 1814 | + path!("subdir2/h.txt"), | |
| 1815 | + path!("subdir2/j.txt") | |
| 1816 | 1816 | ] |
| 1817 | 1817 | ); |
| 1818 | 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 | 1876 | github_login: "user_a".to_string(), |
| 1877 | 1877 | avatar_uri: "avatar_a".into(), |
| 1878 | 1878 | name: None, |
| 1879 | − email: None, | |
| 1880 | 1879 | }), |
| 1881 | 1880 | project_id: project_a_id, |
| 1882 | 1881 | worktree_root_names: vec!["a".to_string()], |
| @@ -1896,7 +1895,6 @@ async fn test_active_call_events( · 13 unmodified lines | ||
| 1896 | 1895 | github_login: "user_b".to_string(), |
| 1897 | 1896 | avatar_uri: "avatar_b".into(), |
| 1898 | 1897 | name: None, |
| 1899 | − email: None, | |
| 1900 | 1898 | }), |
| 1901 | 1899 | project_id: project_b_id, |
| 1902 | 1900 | worktree_root_names: vec!["b".to_string()] |
| @@ -3317,13 +3315,13 @@ async fn test_fs_operations( · 1414 unmodified lines | ||
| 3317 | 3315 | .map(|p| p.to_string_lossy()) |
| 3318 | 3316 | .collect::<Vec<_>>(), |
| 3319 | 3317 | [ |
| 3320 | − separator!("DIR"), | |
| 3321 | − separator!("DIR/SUBDIR"), | |
| 3322 | − separator!("DIR/SUBDIR/f.txt"), | |
| 3323 | − separator!("DIR/e.txt"), | |
| 3324 | − separator!("a.txt"), | |
| 3325 | − separator!("b.txt"), | |
| 3326 | − separator!("d.txt") | |
| 3318 | + path!("DIR"), | |
| 3319 | + path!("DIR/SUBDIR"), | |
| 3320 | + path!("DIR/SUBDIR/f.txt"), | |
| 3321 | + path!("DIR/e.txt"), | |
| 3322 | + path!("a.txt"), | |
| 3323 | + path!("b.txt"), | |
| 3324 | + path!("d.txt") | |
| 3327 | 3325 | ] |
| 3328 | 3326 | ); |
| 3329 | 3327 | }); |
| @@ -3335,13 +3333,13 @@ async fn test_fs_operations( · 5 unmodified lines | ||
| 3335 | 3333 | .map(|p| p.to_string_lossy()) |
| 3336 | 3334 | .collect::<Vec<_>>(), |
| 3337 | 3335 | [ |
| 3338 | − separator!("DIR"), | |
| 3339 | − separator!("DIR/SUBDIR"), | |
| 3340 | − separator!("DIR/SUBDIR/f.txt"), | |
| 3341 | − separator!("DIR/e.txt"), | |
| 3342 | − separator!("a.txt"), | |
| 3343 | − separator!("b.txt"), | |
| 3344 | − separator!("d.txt") | |
| 3336 | + path!("DIR"), | |
| 3337 | + path!("DIR/SUBDIR"), | |
| 3338 | + path!("DIR/SUBDIR/f.txt"), | |
| 3339 | + path!("DIR/e.txt"), | |
| 3340 | + path!("a.txt"), | |
| 3341 | + path!("b.txt"), | |
| 3342 | + path!("d.txt") | |
| 3345 | 3343 | ] |
| 3346 | 3344 | ); |
| 3347 | 3345 | }); |
| @@ -3361,14 +3359,14 @@ async fn test_fs_operations( · 13 unmodified lines | ||
| 3361 | 3359 | .map(|p| p.to_string_lossy()) |
| 3362 | 3360 | .collect::<Vec<_>>(), |
| 3363 | 3361 | [ |
| 3364 | − separator!("DIR"), | |
| 3365 | − separator!("DIR/SUBDIR"), | |
| 3366 | − separator!("DIR/SUBDIR/f.txt"), | |
| 3367 | − separator!("DIR/e.txt"), | |
| 3368 | − separator!("a.txt"), | |
| 3369 | − separator!("b.txt"), | |
| 3370 | − separator!("d.txt"), | |
| 3371 | − separator!("f.txt") | |
| 3362 | + path!("DIR"), | |
| 3363 | + path!("DIR/SUBDIR"), | |
| 3364 | + path!("DIR/SUBDIR/f.txt"), | |
| 3365 | + path!("DIR/e.txt"), | |
| 3366 | + path!("a.txt"), | |
| 3367 | + path!("b.txt"), | |
| 3368 | + path!("d.txt"), | |
| 3369 | + path!("f.txt") | |
| 3372 | 3370 | ] |
| 3373 | 3371 | ); |
| 3374 | 3372 | }); |
| @@ -3380,14 +3378,14 @@ async fn test_fs_operations( · 5 unmodified lines | ||
| 3380 | 3378 | .map(|p| p.to_string_lossy()) |
| 3381 | 3379 | .collect::<Vec<_>>(), |
| 3382 | 3380 | [ |
| 3383 | − separator!("DIR"), | |
| 3384 | − separator!("DIR/SUBDIR"), | |
| 3385 | − separator!("DIR/SUBDIR/f.txt"), | |
| 3386 | − separator!("DIR/e.txt"), | |
| 3387 | − separator!("a.txt"), | |
| 3388 | − separator!("b.txt"), | |
| 3389 | − separator!("d.txt"), | |
| 3390 | − separator!("f.txt") | |
| 3381 | + path!("DIR"), | |
| 3382 | + path!("DIR/SUBDIR"), | |
| 3383 | + path!("DIR/SUBDIR/f.txt"), | |
| 3384 | + path!("DIR/e.txt"), | |
| 3385 | + path!("a.txt"), | |
| 3386 | + path!("b.txt"), | |
| 3387 | + path!("d.txt"), | |
| 3388 | + path!("f.txt") | |
| 3391 | 3389 | ] |
| 3392 | 3390 | ); |
| 3393 | 3391 | }); |
| @@ -30,7 +30,7 @@ use rpc::proto; · 29 unmodified lines | ||
| 30 | 30 | use serde_json::json; |
| 31 | 31 | use settings::SettingsStore; |
| 32 | 32 | use std::{path::Path, sync::Arc}; |
| 33 | −use util::{path, separator}; | |
| 33 | +use util::path; | |
| 34 | 34 | |
| 35 | 35 | #[gpui::test(iterations = 10)] |
| 36 | 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 | 198 | .path() |
| 199 | 199 | .to_string_lossy() |
| 200 | 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 | 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 | 677 | let shutdown_session = workspace.update(cx_a, |workspace, cx| { |
| @@ -258,6 +258,7 @@ impl TestServer { · 257 unmodified lines | ||
| 258 | 258 | None, |
| 259 | 259 | Some(connection_id_tx), |
| 260 | 260 | Executor::Deterministic(cx.background_executor().clone()), |
| 261 | + None, | |
| 261 | 262 | )) |
| 262 | 263 | .detach(); |
| 263 | 264 | let connection_id = connection_id_rx.await.map_err(|e| { |
| @@ -82,7 +82,7 @@ impl UserBackfiller { · 81 unmodified lines | ||
| 82 | 82 | { |
| 83 | 83 | Ok(github_user) => { |
| 84 | 84 | self.db |
| 85 | − .get_or_create_user_by_github_account( | |
| 85 | + .update_or_create_user_by_github_account( | |
| 86 | 86 | &user.github_login, |
| 87 | 87 | github_user.id, |
| 88 | 88 | user.email_address.as_deref(), |
| @@ -90,7 +90,7 @@ impl ChatPanel { · 89 unmodified lines | ||
| 90 | 90 | languages.clone(), |
| 91 | 91 | user_store.clone(), |
| 92 | 92 | None, |
| 93 | − cx.new(|cx| Editor::auto_height(4, window, cx)), | |
| 93 | + cx.new(|cx| Editor::auto_height(1, 4, window, cx)), | |
| 94 | 94 | window, |
| 95 | 95 | cx, |
| 96 | 96 | ) |
| @@ -1218,7 +1218,6 @@ mod tests { · 1121 unmodified lines | ||
| 1218 | 1218 | avatar_uri: "avatar_fgh".into(), |
| 1219 | 1219 | id: 103, |
| 1220 | 1220 | name: None, |
| 1221 | − email: None, | |
| 1222 | 1221 | }), |
| 1223 | 1222 | nonce: 5, |
| 1224 | 1223 | mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)], |
| @@ -1274,7 +1273,6 @@ mod tests { · 49 unmodified lines | ||
| 1274 | 1273 | avatar_uri: "avatar_fgh".into(), |
| 1275 | 1274 | id: 103, |
| 1276 | 1275 | name: None, |
| 1277 | − email: None, | |
| 1278 | 1276 | }), |
| 1279 | 1277 | nonce: 5, |
| 1280 | 1278 | mentions: Vec::new(), |
| @@ -1323,7 +1321,6 @@ mod tests { · 42 unmodified lines | ||
| 1323 | 1321 | avatar_uri: "avatar_fgh".into(), |
| 1324 | 1322 | id: 103, |
| 1325 | 1323 | name: None, |
| 1326 | − email: None, | |
| 1327 | 1324 | }), |
| 1328 | 1325 | nonce: 5, |
| 1329 | 1326 | mentions: Vec::new(), |
| @@ -8,7 +8,7 @@ · 7 unmodified lines | ||
| 8 | 8 | use anyhow::Result; |
| 9 | 9 | |
| 10 | 10 | use crate::client::Client; |
| 11 | −use crate::types::{self, Request}; | |
| 11 | +use crate::types::{self, Notification, Request}; | |
| 12 | 12 | |
| 13 | 13 | pub struct ModelContextProtocol { |
| 14 | 14 | inner: Client, |
| @@ -20,9 +20,10 @@ impl ModelContextProtocol { · 5 unmodified lines | ||
| 20 | 20 | } |
| 21 | 21 | |
| 22 | 22 | fn supported_protocols() -> Vec<types::ProtocolVersion> { |
| 23 | − vec![types::ProtocolVersion( | |
| 24 | − types::LATEST_PROTOCOL_VERSION.to_string(), | |
| 25 | − )] | |
| 23 | + vec![ | |
| 24 | + types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()), | |
| 25 | + types::ProtocolVersion(types::VERSION_2024_11_05.to_string()), | |
| 26 | + ] | |
| 26 | 27 | } |
| 27 | 28 | |
| 28 | 29 | pub async fn initialize( |
| @@ -42,7 +43,7 @@ impl ModelContextProtocol { · 13 unmodified lines | ||
| 42 | 43 | |
| 43 | 44 | let response: types::InitializeResponse = self |
| 44 | 45 | .inner |
| 45 | − .request(types::request::Initialize::METHOD, params) | |
| 46 | + .request(types::requests::Initialize::METHOD, params) | |
| 46 | 47 | .await?; |
| 47 | 48 | |
| 48 | 49 | anyhow::ensure!( |
| @@ -53,16 +54,13 @@ impl ModelContextProtocol { · 4 unmodified lines | ||
| 53 | 54 | |
| 54 | 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 | 57 | let initialized_protocol = InitializedContextServerProtocol { |
| 62 | 58 | inner: self.inner, |
| 63 | 59 | initialize: response, |
| 64 | 60 | }; |
| 65 | 61 | |
| 62 | + initialized_protocol.notify::<types::notifications::Initialized>(())?; | |
| 63 | + | |
| 66 | 64 | Ok(initialized_protocol) |
| 67 | 65 | } |
| 68 | 66 | } |
| @@ -96,4 +94,8 @@ impl InitializedContextServerProtocol { · 27 unmodified lines | ||
| 96 | 94 | pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> { |
| 97 | 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 | 14 | executor: BackgroundExecutor, |
| 15 | 15 | ) -> FakeTransport { |
| 16 | 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 | 18 | create_initialize_response(name.clone()) |
| 19 | 19 | }) |
| 20 | 20 | } |
| @@ -3,9 +3,10 @@ use serde::de::DeserializeOwned; · 2 unmodified lines | ||
| 3 | 3 | use serde::{Deserialize, Serialize}; |
| 4 | 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 | 10 | use super::*; |
| 10 | 11 | |
| 11 | 12 | macro_rules! request { |
| @@ -82,6 +83,57 @@ pub trait Request { · 70 unmodified lines | ||
| 82 | 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 | 137 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 86 | 138 | #[serde(transparent)] |
| 87 | 139 | pub struct ProtocolVersion(pub String); |
| @@ -291,13 +343,20 @@ pub enum MessageContent { · 203 unmodified lines | ||
| 291 | 343 | #[serde(skip_serializing_if = "Option::is_none")] |
| 292 | 344 | annotations: Option<MessageAnnotations>, |
| 293 | 345 | }, |
| 294 | − #[serde(rename = "image")] | |
| 346 | + #[serde(rename = "image", rename_all = "camelCase")] | |
| 295 | 347 | Image { |
| 296 | 348 | data: String, |
| 297 | 349 | mime_type: String, |
| 298 | 350 | #[serde(skip_serializing_if = "Option::is_none")] |
| 299 | 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 | 360 | #[serde(rename = "resource")] |
| 302 | 361 | Resource { |
| 303 | 362 | resource: ResourceContents, |
| @@ -394,6 +453,8 @@ pub struct ServerCapabilities { · 90 unmodified lines | ||
| 394 | 453 | #[serde(skip_serializing_if = "Option::is_none")] |
| 395 | 454 | pub logging: Option<serde_json::Value>, |
| 396 | 455 | #[serde(skip_serializing_if = "Option::is_none")] |
| 456 | + pub completions: Option<serde_json::Value>, | |
| 457 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 397 | 458 | pub prompts: Option<PromptsCapabilities>, |
| 398 | 459 | #[serde(skip_serializing_if = "Option::is_none")] |
| 399 | 460 | pub resources: Option<ResourcesCapabilities>, |
| @@ -438,6 +499,28 @@ pub struct Tool { · 38 unmodified lines | ||
| 438 | 499 | #[serde(skip_serializing_if = "Option::is_none")] |
| 439 | 500 | pub description: Option<String>, |
| 440 | 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 | 526 | #[derive(Debug, Serialize, Deserialize)] |
| @@ -528,34 +611,6 @@ pub struct ModelHint { · 84 unmodified lines | ||
| 528 | 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 | 614 | #[derive(Debug, Serialize)] |
| 560 | 615 | #[serde(untagged)] |
| 561 | 616 | pub enum ClientNotification { |
| @@ -576,12 +631,14 @@ pub enum ProgressToken { · 14 unmodified lines | ||
| 576 | 631 | Number(f64), |
| 577 | 632 | } |
| 578 | 633 | |
| 579 | −#[derive(Debug, Serialize)] | |
| 634 | +#[derive(Debug, Serialize, Deserialize)] | |
| 580 | 635 | #[serde(rename_all = "camelCase")] |
| 581 | 636 | pub struct ProgressParams { |
| 582 | 637 | pub progress_token: ProgressToken, |
| 583 | 638 | pub progress: f64, |
| 584 | 639 | #[serde(skip_serializing_if = "Option::is_none")] |
| 640 | + pub message: Option<String>, | |
| 641 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 585 | 642 | pub total: Option<f64>, |
| 586 | 643 | #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] |
| 587 | 644 | pub meta: Option<HashMap<String, serde_json::Value>>, |
| @@ -625,6 +682,8 @@ pub enum ToolResponseContent { · 37 unmodified lines | ||
| 625 | 682 | Text { text: String }, |
| 626 | 683 | #[serde(rename = "image", rename_all = "camelCase")] |
| 627 | 684 | Image { data: String, mime_type: String }, |
| 685 | + #[serde(rename = "audio", rename_all = "camelCase")] | |
| 686 | + Audio { data: String, mime_type: String }, | |
| 628 | 687 | #[serde(rename = "resource")] |
| 629 | 688 | Resource { resource: ResourceContents }, |
| 630 | 689 | } |
| @@ -26,6 +26,7 @@ use parking_lot::Mutex; · 25 unmodified lines | ||
| 26 | 26 | use request::StatusNotification; |
| 27 | 27 | use settings::SettingsStore; |
| 28 | 28 | use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace}; |
| 29 | +use std::collections::hash_map::Entry; | |
| 29 | 30 | use std::{ |
| 30 | 31 | any::TypeId, |
| 31 | 32 | env, |
| @@ -731,42 +732,43 @@ impl Copilot { · 699 unmodified lines | ||
| 731 | 732 | return; |
| 732 | 733 | } |
| 733 | 734 | |
| 734 | − registered_buffers | |
| 735 | − .entry(buffer.entity_id()) | |
| 736 | − .or_insert_with(|| { | |
| 737 | − let uri: lsp::Url = uri_for_buffer(buffer, cx); | |
| 738 | − let language_id = id_for_language(buffer.read(cx).language()); | |
| 739 | − let snapshot = buffer.read(cx).snapshot(); | |
| 740 | − server | |
| 741 | − .notify::<lsp::notification::DidOpenTextDocument>( | |
| 742 | − &lsp::DidOpenTextDocumentParams { | |
| 743 | − text_document: lsp::TextDocumentItem { | |
| 744 | − uri: uri.clone(), | |
| 745 | − language_id: language_id.clone(), | |
| 746 | − version: 0, | |
| 747 | − text: snapshot.text(), | |
| 748 | − }, | |
| 735 | + let entry = registered_buffers.entry(buffer.entity_id()); | |
| 736 | + if let Entry::Vacant(e) = entry { | |
| 737 | + let Ok(uri) = uri_for_buffer(buffer, cx) else { | |
| 738 | + return; | |
| 739 | + }; | |
| 740 | + let language_id = id_for_language(buffer.read(cx).language()); | |
| 741 | + let snapshot = buffer.read(cx).snapshot(); | |
| 742 | + server | |
| 743 | + .notify::<lsp::notification::DidOpenTextDocument>( | |
| 744 | + &lsp::DidOpenTextDocumentParams { | |
| 745 | + text_document: lsp::TextDocumentItem { | |
| 746 | + uri: uri.clone(), | |
| 747 | + language_id: language_id.clone(), | |
| 748 | + version: 0, | |
| 749 | + text: snapshot.text(), | |
| 749 | 750 | }, |
| 750 | − ) | |
| 751 | − .ok(); | |
| 751 | + }, | |
| 752 | + ) | |
| 753 | + .ok(); | |
| 752 | 754 | |
| 753 | − RegisteredBuffer { | |
| 754 | − uri, | |
| 755 | − language_id, | |
| 756 | − snapshot, | |
| 757 | − snapshot_version: 0, | |
| 758 | − pending_buffer_change: Task::ready(Some(())), | |
| 759 | − _subscriptions: [ | |
| 760 | − cx.subscribe(buffer, |this, buffer, event, cx| { | |
| 761 | − this.handle_buffer_event(buffer, event, cx).log_err(); | |
| 762 | − }), | |
| 763 | − cx.observe_release(buffer, move |this, _buffer, _cx| { | |
| 764 | − this.buffers.remove(&weak_buffer); | |
| 765 | − this.unregister_buffer(&weak_buffer); | |
| 766 | − }), | |
| 767 | − ], | |
| 768 | − } | |
| 755 | + e.insert(RegisteredBuffer { | |
| 756 | + uri, | |
| 757 | + language_id, | |
| 758 | + snapshot, | |
| 759 | + snapshot_version: 0, | |
| 760 | + pending_buffer_change: Task::ready(Some(())), | |
| 761 | + _subscriptions: [ | |
| 762 | + cx.subscribe(buffer, |this, buffer, event, cx| { | |
| 763 | + this.handle_buffer_event(buffer, event, cx).log_err(); | |
| 764 | + }), | |
| 765 | + cx.observe_release(buffer, move |this, _buffer, _cx| { | |
| 766 | + this.buffers.remove(&weak_buffer); | |
| 767 | + this.unregister_buffer(&weak_buffer); | |
| 768 | + }), | |
| 769 | + ], | |
| 769 | 770 | }); |
| 771 | + } | |
| 770 | 772 | } |
| 771 | 773 | } |
| 772 | 774 | |
| @@ -798,7 +800,9 @@ impl Copilot { · 25 unmodified lines | ||
| 798 | 800 | language::BufferEvent::FileHandleChanged |
| 799 | 801 | | language::BufferEvent::LanguageChanged => { |
| 800 | 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 | 806 | if new_uri != registered_buffer.uri |
| 803 | 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 | 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 | 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 | 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 | 51 | util.workspace = true |
| 52 | 52 | workspace-hack.workspace = true |
| 53 | 53 | |
| 54 | +[target.'cfg(not(windows))'.dependencies] | |
| 55 | +libc.workspace = true | |
| 56 | + | |
| 54 | 57 | [dev-dependencies] |
| 55 | 58 | async-pipe.workspace = true |
| 56 | 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 | 97 | pub struct TcpArguments { |
| 98 | 98 | pub host: Ipv4Addr, |
| 99 | 99 | pub port: u16, |
| @@ -179,9 +179,9 @@ impl DebugTaskDefinition { · 79 unmodified lines | ||
| 179 | 179 | } |
| 180 | 180 | |
| 181 | 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 | 183 | pub struct DebugAdapterBinary { |
| 184 | − pub command: String, | |
| 184 | + pub command: Option<String>, | |
| 185 | 185 | pub arguments: Vec<String>, |
| 186 | 186 | pub envs: HashMap<String, String>, |
| 187 | 187 | pub cwd: Option<PathBuf>, |
| @@ -369,6 +369,10 @@ pub trait DebugAdapter: 'static + Send + Sync { · 181 unmodified lines | ||
| 369 | 369 | } |
| 370 | 370 | |
| 371 | 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 | 378 | #[cfg(any(test, feature = "test-support"))] |
| @@ -433,7 +437,7 @@ impl DebugAdapter for FakeAdapter { · 58 unmodified lines | ||
| 433 | 437 | _: &mut AsyncApp, |
| 434 | 438 | ) -> Result<DebugAdapterBinary> { |
| 435 | 439 | Ok(DebugAdapterBinary { |
| 436 | − command: "command".into(), | |
| 440 | + command: Some("command".into()), | |
| 437 | 441 | arguments: vec![], |
| 438 | 442 | connection: None, |
| 439 | 443 | envs: HashMap::default(), |
| @@ -218,7 +218,7 @@ impl DebugAdapterClient { · 217 unmodified lines | ||
| 218 | 218 | |
| 219 | 219 | pub fn add_log_handler<F>(&self, f: F, kind: LogKind) |
| 220 | 220 | where |
| 221 | − F: 'static + Send + FnMut(IoKind, &str), | |
| 221 | + F: 'static + Send + FnMut(IoKind, Option<&str>, &str), | |
| 222 | 222 | { |
| 223 | 223 | self.transport_delegate.add_log_handler(f, kind); |
| 224 | 224 | } |
| @@ -297,7 +297,7 @@ mod tests { · 72 unmodified lines | ||
| 297 | 297 | let client = DebugAdapterClient::start( |
| 298 | 298 | crate::client::SessionId(1), |
| 299 | 299 | DebugAdapterBinary { |
| 300 | − command: "command".into(), | |
| 300 | + command: Some("command".into()), | |
| 301 | 301 | arguments: Default::default(), |
| 302 | 302 | envs: Default::default(), |
| 303 | 303 | connection: None, |
| @@ -367,7 +367,7 @@ mod tests { · 63 unmodified lines | ||
| 367 | 367 | let client = DebugAdapterClient::start( |
| 368 | 368 | crate::client::SessionId(1), |
| 369 | 369 | DebugAdapterBinary { |
| 370 | − command: "command".into(), | |
| 370 | + command: Some("command".into()), | |
| 371 | 371 | arguments: Default::default(), |
| 372 | 372 | envs: Default::default(), |
| 373 | 373 | connection: None, |
| @@ -420,7 +420,7 @@ mod tests { · 46 unmodified lines | ||
| 420 | 420 | let client = DebugAdapterClient::start( |
| 421 | 421 | crate::client::SessionId(1), |
| 422 | 422 | DebugAdapterBinary { |
| 423 | − command: "command".into(), | |
| 423 | + command: Some("command".into()), | |
| 424 | 424 | arguments: Default::default(), |
| 425 | 425 | envs: Default::default(), |
| 426 | 426 | connection: None, |
| @@ -19,11 +19,11 @@ use std::{collections::BTreeMap, sync::Arc}; · 18 unmodified lines | ||
| 19 | 19 | pub trait DapLocator: Send + Sync { |
| 20 | 20 | fn name(&self) -> SharedString; |
| 21 | 21 | /// Determines whether this locator can generate debug target for given task. |
| 22 | − fn create_scenario( | |
| 22 | + async fn create_scenario( | |
| 23 | 23 | &self, |
| 24 | 24 | build_config: &TaskTemplate, |
| 25 | 25 | resolved_label: &str, |
| 26 | − adapter: DebugAdapterName, | |
| 26 | + adapter: &DebugAdapterName, | |
| 27 | 27 | ) -> Option<DebugScenario>; |
| 28 | 28 | |
| 29 | 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 | 2 | use dap_types::{ |
| 3 | 3 | ErrorResponse, |
| 4 | 4 | messages::{Message, Response}, |
| @@ -12,7 +12,6 @@ use smol::{ · 7 unmodified lines | ||
| 12 | 12 | io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader}, |
| 13 | 13 | lock::Mutex, |
| 14 | 14 | net::{TcpListener, TcpStream}, |
| 15 | − process::Child, | |
| 16 | 15 | }; |
| 17 | 16 | use std::{ |
| 18 | 17 | collections::HashMap, |
| @@ -22,11 +21,13 @@ use std::{ · 3 unmodified lines | ||
| 22 | 21 | time::Duration, |
| 23 | 22 | }; |
| 24 | 23 | use task::TcpArgumentsTemplate; |
| 25 | −use util::{ConnectionResult, ResultExt as _}; | |
| 24 | +use util::ConnectionResult; | |
| 26 | 25 | |
| 27 | 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 | 32 | #[derive(PartialEq, Eq, Clone, Copy)] |
| 32 | 33 | pub enum LogKind { |
| @@ -86,10 +87,12 @@ impl Transport { · 53 unmodified lines | ||
| 86 | 87 | TcpTransport::start(binary, cx) |
| 87 | 88 | .await |
| 88 | 89 | .map(|(transports, tcp)| (transports, Self::Tcp(tcp))) |
| 90 | + .context("Tried to connect to a debug adapter via TCP transport layer") | |
| 89 | 91 | } else { |
| 90 | 92 | StdioTransport::start(binary, cx) |
| 91 | 93 | .await |
| 92 | 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 | 109 | match self { |
| 107 | 110 | Transport::Stdio(stdio_transport) => stdio_transport.kill().await, |
| 108 | 111 | Transport::Tcp(tcp_transport) => tcp_transport.kill().await, |
| @@ -191,7 +194,7 @@ impl TransportDelegate { · 82 unmodified lines | ||
| 191 | 194 | match Self::handle_output( |
| 192 | 195 | params.output, |
| 193 | 196 | client_tx, |
| 194 | − pending_requests, | |
| 197 | + pending_requests.clone(), | |
| 195 | 198 | output_log_handler, |
| 196 | 199 | ) |
| 197 | 200 | .await |
| @@ -199,6 +202,12 @@ impl TransportDelegate { · 1 unmodified line | ||
| 199 | 202 | Ok(()) => {} |
| 200 | 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 | 213 | if let Some(stderr) = params.stderr.take() { |
| @@ -289,7 +298,7 @@ impl TransportDelegate { · 84 unmodified lines | ||
| 289 | 298 | if let Some(log_handlers) = log_handlers.as_ref() { |
| 290 | 299 | for (kind, handler) in log_handlers.lock().iter_mut() { |
| 291 | 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 | 341 | let message = match serde_json::to_string(&message) { |
| 327 | 342 | Ok(message) => message, |
| 328 | 343 | Err(e) => break Err(e.into()), |
| @@ -331,7 +346,7 @@ impl TransportDelegate { · 2 unmodified lines | ||
| 331 | 346 | if let Some(log_handlers) = log_handlers.as_ref() { |
| 332 | 347 | for (kind, log_handler) in log_handlers.lock().iter_mut() { |
| 333 | 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 | 431 | Ok(_) => { |
| 417 | 432 | for (kind, log_handler) in log_handlers.lock().iter_mut() { |
| 418 | 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 | 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 | 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 | 533 | for (kind, log_handler) in log_handlers.lock().iter_mut() { |
| 510 | 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( | |
| 517 | − serde_json::from_str::<Message>(message_str).context("deserializing server message"), | |
| 518 | − ) | |
| 540 | + ConnectionResult::Result(message) | |
| 519 | 541 | } |
| 520 | 542 | |
| 521 | 543 | pub async fn shutdown(&self) -> Result<()> { |
| @@ -531,7 +553,7 @@ impl TransportDelegate { · 9 unmodified lines | ||
| 531 | 553 | current_requests.clear(); |
| 532 | 554 | pending_requests.clear(); |
| 533 | 555 | |
| 534 | − let _ = self.transport.kill().await.log_err(); | |
| 556 | + self.transport.kill().await; | |
| 535 | 557 | |
| 536 | 558 | drop(current_requests); |
| 537 | 559 | drop(pending_requests); |
| @@ -551,7 +573,7 @@ impl TransportDelegate { · 13 unmodified lines | ||
| 551 | 573 | |
| 552 | 574 | pub fn add_log_handler<F>(&self, f: F, kind: LogKind) |
| 553 | 575 | where |
| 554 | − F: 'static + Send + FnMut(IoKind, &str), | |
| 576 | + F: 'static + Send + FnMut(IoKind, Option<&Command>, &IoMessage), | |
| 555 | 577 | { |
| 556 | 578 | let mut log_handlers = self.log_handlers.lock(); |
| 557 | 579 | log_handlers.push((kind, Box::new(f))); |
| @@ -562,7 +584,7 @@ pub struct TcpTransport { · 4 unmodified lines | ||
| 562 | 584 | pub port: u16, |
| 563 | 585 | pub host: Ipv4Addr, |
| 564 | 586 | pub timeout: u64, |
| 565 | − process: Mutex<Child>, | |
| 587 | + process: Option<Mutex<Child>>, | |
| 566 | 588 | } |
| 567 | 589 | |
| 568 | 590 | impl TcpTransport { |
| @@ -591,26 +613,23 @@ impl TcpTransport { · 22 unmodified lines | ||
| 591 | 613 | let host = connection_args.host; |
| 592 | 614 | let port = connection_args.port; |
| 593 | 615 | |
| 594 | − let mut command = util::command::new_std_command(&binary.command); | |
| 595 | − util::set_pre_exec_to_start_new_session(&mut command); | |
| 596 | − let mut command = smol::process::Command::from(command); | |
| 616 | + let mut process = if let Some(command) = &binary.command { | |
| 617 | + let mut command = util::command::new_std_command(&command); | |
| 597 | 618 | |
| 598 | − if let Some(cwd) = &binary.cwd { | |
| 599 | − command.current_dir(cwd); | |
| 600 | − } | |
| 601 | − | |
| 602 | − command.args(&binary.arguments); | |
| 603 | − command.envs(&binary.envs); | |
| 619 | + if let Some(cwd) = &binary.cwd { | |
| 620 | + command.current_dir(cwd); | |
| 621 | + } | |
| 604 | 622 | |
| 605 | − command | |
| 606 | − .stdin(Stdio::null()) | |
| 607 | − .stdout(Stdio::piped()) | |
| 608 | − .stderr(Stdio::piped()) | |
| 609 | − .kill_on_drop(true); | |
| 623 | + command.args(&binary.arguments); | |
| 624 | + command.envs(&binary.envs); | |
| 610 | 625 | |
| 611 | − let mut process = command | |
| 612 | − .spawn() | |
| 613 | − .with_context(|| "failed to start debug adapter.")?; | |
| 626 | + Some( | |
| 627 | + Child::spawn(command, Stdio::null()) | |
| 628 | + .with_context(|| "failed to start debug adapter.")?, | |
| 629 | + ) | |
| 630 | + } else { | |
| 631 | + None | |
| 632 | + }; | |
| 614 | 633 | |
| 615 | 634 | let address = SocketAddrV4::new(host, port); |
| 616 | 635 | |
| @@ -628,15 +647,18 @@ impl TcpTransport { · 11 unmodified lines | ||
| 628 | 647 | match TcpStream::connect(address).await { |
| 629 | 648 | Ok(stream) => return Ok((process, stream.split())), |
| 630 | 649 | Err(_) => { |
| 631 | − if let Ok(Some(_)) = process.try_status() { | |
| 632 | − let output = process.output().await?; | |
| 633 | − let output = if output.stderr.is_empty() { | |
| 634 | − String::from_utf8_lossy(&output.stdout).to_string() | |
| 635 | − } else { | |
| 636 | − String::from_utf8_lossy(&output.stderr).to_string() | |
| 637 | − }; | |
| 638 | − anyhow::bail!("{output}\nerror: process exited before debugger attached."); | |
| 650 | + if let Some(p) = &mut process { | |
| 651 | + if let Ok(Some(_)) = p.try_status() { | |
| 652 | + let output = process.take().unwrap().into_inner().output().await?; | |
| 653 | + let output = if output.stderr.is_empty() { | |
| 654 | + String::from_utf8_lossy(&output.stdout).to_string() | |
| 655 | + } else { | |
| 656 | + String::from_utf8_lossy(&output.stderr).to_string() | |
| 657 | + }; | |
| 658 | + anyhow::bail!("{output}\nerror: process exited before debugger attached."); | |
| 659 | + } | |
| 639 | 660 | } |
| 661 | + | |
| 640 | 662 | cx.background_executor().timer(Duration::from_millis(100)).await; |
| 641 | 663 | } |
| 642 | 664 | } |
| @@ -649,13 +671,13 @@ impl TcpTransport { · 6 unmodified lines | ||
| 649 | 671 | host, |
| 650 | 672 | port |
| 651 | 673 | ); |
| 652 | − let stdout = process.stdout.take(); | |
| 653 | − let stderr = process.stderr.take(); | |
| 674 | + let stdout = process.as_mut().and_then(|p| p.stdout.take()); | |
| 675 | + let stderr = process.as_mut().and_then(|p| p.stderr.take()); | |
| 654 | 676 | |
| 655 | 677 | let this = Self { |
| 656 | 678 | port, |
| 657 | 679 | host, |
| 658 | − process: Mutex::new(process), | |
| 680 | + process: process.map(Mutex::new), | |
| 659 | 681 | timeout, |
| 660 | 682 | }; |
| 661 | 683 | |
| @@ -673,10 +695,19 @@ impl TcpTransport { · 11 unmodified lines | ||
| 673 | 695 | true |
| 674 | 696 | } |
| 675 | 697 | |
| 676 | − async fn kill(&self) -> Result<()> { | |
| 677 | − self.process.lock().await.kill()?; | |
| 698 | + async fn kill(&self) { | |
| 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 | 718 | impl StdioTransport { |
| 688 | 719 | #[allow(dead_code, reason = "This is used in non test builds of Zed")] |
| 689 | 720 | async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> { |
| 690 | − let mut command = util::command::new_std_command(&binary.command); | |
| 691 | − util::set_pre_exec_to_start_new_session(&mut command); | |
| 692 | − let mut command = smol::process::Command::from(command); | |
| 721 | + let Some(binary_command) = &binary.command else { | |
| 722 | + bail!( | |
| 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 | 728 | if let Some(cwd) = &binary.cwd { |
| 695 | 729 | command.current_dir(cwd); |
| @@ -698,16 +732,10 @@ impl StdioTransport { · 2 unmodified lines | ||
| 698 | 732 | command.args(&binary.arguments); |
| 699 | 733 | command.envs(&binary.envs); |
| 700 | 734 | |
| 701 | − command | |
| 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(|| { | |
| 735 | + let mut process = Child::spawn(command, Stdio::piped()).with_context(|| { | |
| 708 | 736 | format!( |
| 709 | 737 | "failed to spawn command `{} {}`.", |
| 710 | − binary.command, | |
| 738 | + binary_command, | |
| 711 | 739 | binary.arguments.join(" ") |
| 712 | 740 | ) |
| 713 | 741 | })?; |
| @@ -722,7 +750,7 @@ impl StdioTransport { · 8 unmodified lines | ||
| 722 | 750 | if stderr.is_none() { |
| 723 | 751 | bail!( |
| 724 | 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 | 773 | false |
| 746 | 774 | } |
| 747 | 775 | |
| 748 | − async fn kill(&self) -> Result<()> { | |
| 749 | − self.process.lock().await.kill()?; | |
| 750 | − Ok(()) | |
| 776 | + async fn kill(&self) { | |
| 777 | + let mut process = self.process.lock().await; | |
| 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 | 955 | false |
| 922 | 956 | } |
| 923 | 957 | |
| 924 | − async fn kill(&self) -> Result<()> { | |
| 925 | − Ok(()) | |
| 958 | + async fn kill(&self) {} | |
| 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 | 23 | [dependencies] |
| 24 | 24 | anyhow.workspace = true |
| 25 | 25 | async-trait.workspace = true |
| 26 | +collections.workspace = true | |
| 26 | 27 | dap.workspace = true |
| 27 | 28 | futures.workspace = true |
| 28 | 29 | gpui.workspace = true |
| @@ -21,18 +21,21 @@ impl CodeLldbDebugAdapter { · 20 unmodified lines | ||
| 21 | 21 | |
| 22 | 22 | fn request_args( |
| 23 | 23 | &self, |
| 24 | + delegate: &Arc<dyn DapDelegate>, | |
| 24 | 25 | task_definition: &DebugTaskDefinition, |
| 25 | 26 | ) -> Result<dap::StartDebuggingRequestArguments> { |
| 26 | 27 | // CodeLLDB uses `name` for a terminal label. |
| 27 | 28 | let mut configuration = task_definition.config.clone(); |
| 28 | 29 | |
| 29 | − configuration | |
| 30 | + let obj = configuration | |
| 30 | 31 | .as_object_mut() |
| 31 | − .context("CodeLLDB is not a valid json object")? | |
| 32 | − .insert( | |
| 33 | − "name".into(), | |
| 34 | − Value::String(String::from(task_definition.label.as_ref())), | |
| 35 | − ); | |
| 32 | + .context("CodeLLDB is not a valid json object")?; | |
| 33 | + | |
| 34 | + obj.entry("name") | |
| 35 | + .or_insert(Value::String(String::from(task_definition.label.as_ref()))); | |
| 36 | + | |
| 37 | + obj.entry("cwd") | |
| 38 | + .or_insert(delegate.worktree_root_path().to_string_lossy().into()); | |
| 36 | 39 | |
| 37 | 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 | 364 | Ok(DebugAdapterBinary { |
| 362 | − command: command.unwrap(), | |
| 365 | + command: Some(command.unwrap()), | |
| 363 | 366 | cwd: Some(delegate.worktree_root_path().to_path_buf()), |
| 364 | 367 | arguments: vec![ |
| 365 | 368 | "--settings".into(), |
| 366 | 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 | 372 | envs: HashMap::default(), |
| 370 | 373 | connection: None, |
| 371 | 374 | }) |
| @@ -177,18 +177,23 @@ impl DebugAdapter for GdbDebugAdapter { · 176 unmodified lines | ||
| 177 | 177 | |
| 178 | 178 | let gdb_path = user_setting_path.unwrap_or(gdb_path?); |
| 179 | 179 | |
| 180 | − let request_args = StartDebuggingRequestArguments { | |
| 181 | − request: self.request_kind(&config.config)?, | |
| 182 | − configuration: config.config.clone(), | |
| 183 | − }; | |
| 180 | + let mut configuration = config.config.clone(); | |
| 181 | + if let Some(configuration) = configuration.as_object_mut() { | |
| 182 | + configuration | |
| 183 | + .entry("cwd") | |
| 184 | + .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into()); | |
| 185 | + } | |
| 184 | 186 | |
| 185 | 187 | Ok(DebugAdapterBinary { |
| 186 | − command: gdb_path, | |
| 188 | + command: Some(gdb_path), | |
| 187 | 189 | arguments: vec!["-i=dap".into()], |
| 188 | 190 | envs: HashMap::default(), |
| 189 | 191 | cwd: Some(delegate.worktree_root_path().to_path_buf()), |
| 190 | 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 | 2 | use dap::{ |
| 3 | 3 | StartDebuggingRequestArguments, |
| 4 | 4 | adapters::{ |
| 5 | − DebugTaskDefinition, DownloadedFileType, download_adapter_from_github, | |
| 5 | + DebugTaskDefinition, DownloadedFileType, TcpArguments, download_adapter_from_github, | |
| 6 | 6 | latest_github_release, |
| 7 | 7 | }, |
| 8 | 8 | }; |
| @@ -10,6 +10,7 @@ use dap::{ · 1 unmodified line | ||
| 10 | 10 | use gpui::{AsyncApp, SharedString}; |
| 11 | 11 | use language::LanguageName; |
| 12 | 12 | use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock}; |
| 13 | +use task::TcpArgumentsTemplate; | |
| 13 | 14 | use util; |
| 14 | 15 | |
| 15 | 16 | use crate::*; |
| @@ -433,10 +434,6 @@ impl DebugAdapter for GoDebugAdapter { · 417 unmodified lines | ||
| 433 | 434 | |
| 434 | 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 | 438 | let cwd = task_definition |
| 442 | 439 | .config |
| @@ -445,31 +442,58 @@ impl DebugAdapter for GoDebugAdapter { · 2 unmodified lines | ||
| 445 | 442 | .map(PathBuf::from) |
| 446 | 443 | .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()); |
| 447 | 444 | |
| 448 | − let arguments = if cfg!(windows) { | |
| 449 | − vec![ | |
| 450 | − delve_path, | |
| 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 | − }; | |
| 445 | + let arguments; | |
| 446 | + let command; | |
| 447 | + let connection; | |
| 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 | 489 | Ok(DebugAdapterBinary { |
| 466 | − command: minidelve_path.to_string_lossy().into_owned(), | |
| 490 | + command, | |
| 467 | 491 | arguments, |
| 468 | 492 | cwd: Some(cwd), |
| 469 | 493 | envs: HashMap::default(), |
| 470 | − connection: None, | |
| 494 | + connection, | |
| 471 | 495 | request_args: StartDebuggingRequestArguments { |
| 472 | − configuration: task_definition.config.clone(), | |
| 496 | + configuration, | |
| 473 | 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 | 2 | use anyhow::Context as _; |
| 3 | 3 | use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; |
| 4 | 4 | use gpui::AsyncApp; |
| 5 | +use serde_json::Value; | |
| 5 | 6 | use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; |
| 6 | 7 | use task::DebugRequest; |
| 7 | 8 | use util::ResultExt; |
| @@ -68,13 +69,44 @@ impl JsDebugAdapter { · 60 unmodified lines | ||
| 68 | 69 | let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); |
| 69 | 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 | 101 | Ok(DebugAdapterBinary { |
| 72 | − command: delegate | |
| 73 | − .node_runtime() | |
| 74 | − .binary_path() | |
| 75 | − .await? | |
| 76 | − .to_string_lossy() | |
| 77 | − .into_owned(), | |
| 102 | + command: Some( | |
| 103 | + delegate | |
| 104 | + .node_runtime() | |
| 105 | + .binary_path() | |
| 106 | + .await? | |
| 107 | + .to_string_lossy() | |
| 108 | + .into_owned(), | |
| 109 | + ), | |
| 78 | 110 | arguments: vec![ |
| 79 | 111 | adapter_path |
| 80 | 112 | .join(Self::ADAPTER_PATH) |
| @@ -91,7 +123,7 @@ impl JsDebugAdapter { · 10 unmodified lines | ||
| 91 | 123 | timeout, |
| 92 | 124 | }), |
| 93 | 125 | request_args: StartDebuggingRequestArguments { |
| 94 | − configuration: task_definition.config.clone(), | |
| 126 | + configuration, | |
| 95 | 127 | request: self.request_kind(&task_definition.config)?, |
| 96 | 128 | }, |
| 97 | 129 | }) |
| @@ -171,7 +203,7 @@ impl DebugAdapter for JsDebugAdapter { · 73 unmodified lines | ||
| 171 | 203 | "properties": { |
| 172 | 204 | "type": { |
| 173 | 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 | 207 | "description": "The type of debug session", |
| 176 | 208 | "default": "pwa-node" |
| 177 | 209 | }, |
| @@ -431,4 +463,25 @@ impl DebugAdapter for JsDebugAdapter { · 253 unmodified lines | ||
| 431 | 463 | self.get_installed_binary(delegate, &config, user_installed_path, cx) |
| 432 | 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 | 71 | let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); |
| 72 | 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 | 80 | Ok(DebugAdapterBinary { |
| 75 | − command: delegate | |
| 76 | − .node_runtime() | |
| 77 | − .binary_path() | |
| 78 | − .await? | |
| 79 | − .to_string_lossy() | |
| 80 | − .into_owned(), | |
| 81 | + command: Some( | |
| 82 | + delegate | |
| 83 | + .node_runtime() | |
| 84 | + .binary_path() | |
| 85 | + .await? | |
| 86 | + .to_string_lossy() | |
| 87 | + .into_owned(), | |
| 88 | + ), | |
| 81 | 89 | arguments: vec![ |
| 82 | 90 | adapter_path |
| 83 | 91 | .join(Self::ADAPTER_PATH) |
| @@ -93,7 +101,7 @@ impl PhpDebugAdapter { · 9 unmodified lines | ||
| 93 | 101 | cwd: Some(delegate.worktree_root_path().to_path_buf()), |
| 94 | 102 | envs: HashMap::default(), |
| 95 | 103 | request_args: StartDebuggingRequestArguments { |
| 96 | − configuration: task_definition.config.clone(), | |
| 104 | + configuration, | |
| 97 | 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 | 84 | fn request_args( |
| 85 | 85 | &self, |
| 86 | + delegate: &Arc<dyn DapDelegate>, | |
| 86 | 87 | task_definition: &DebugTaskDefinition, |
| 87 | 88 | ) -> Result<StartDebuggingRequestArguments> { |
| 88 | 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 | 104 | Ok(StartDebuggingRequestArguments { |
| 99 | 105 | configuration, |
| 100 | 106 | request, |
| @@ -187,7 +193,7 @@ impl PythonDebugAdapter { · 86 unmodified lines | ||
| 187 | 193 | ); |
| 188 | 194 | |
| 189 | 195 | Ok(DebugAdapterBinary { |
| 190 | − command: python_command, | |
| 196 | + command: Some(python_command), | |
| 191 | 197 | arguments, |
| 192 | 198 | connection: Some(adapters::TcpArguments { |
| 193 | 199 | host, |
| @@ -196,7 +202,7 @@ impl PythonDebugAdapter { · 2 unmodified lines | ||
| 196 | 202 | }), |
| 197 | 203 | cwd: Some(delegate.worktree_root_path().to_path_buf()), |
| 198 | 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 | 2 | use async_trait::async_trait; |
| 3 | +use collections::FxHashMap; | |
| 3 | 4 | use dap::{ |
| 4 | − DebugRequest, StartDebuggingRequestArguments, | |
| 5 | + DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, | |
| 5 | 6 | adapters::{ |
| 6 | 7 | DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, |
| 7 | 8 | }, |
| 8 | 9 | }; |
| 9 | 10 | use gpui::{AsyncApp, SharedString}; |
| 10 | 11 | use language::LanguageName; |
| 12 | +use serde::{Deserialize, Serialize}; | |
| 11 | 13 | use serde_json::json; |
| 12 | 14 | use std::path::PathBuf; |
| 13 | −use std::sync::Arc; | |
| 15 | +use std::{ffi::OsStr, sync::Arc}; | |
| 14 | 16 | use task::{DebugScenario, ZedDebugConfig}; |
| 15 | 17 | use util::command::new_smol_command; |
| 16 | 18 | |
| @@ -21,6 +23,18 @@ impl RubyDebugAdapter { · 4 unmodified lines | ||
| 21 | 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 | 38 | #[async_trait(?Send)] |
| 25 | 39 | impl DebugAdapter for RubyDebugAdapter { |
| 26 | 40 | fn name(&self) -> DebugAdapterName { |
| @@ -31,185 +45,70 @@ impl DebugAdapter for RubyDebugAdapter { · 4 unmodified lines | ||
| 31 | 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 | 52 | async fn dap_schema(&self) -> serde_json::Value { |
| 35 | 53 | json!({ |
| 36 | − "oneOf": [ | |
| 37 | − { | |
| 38 | − "allOf": [ | |
| 39 | − { | |
| 40 | − "type": "object", | |
| 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 | − ] | |
| 54 | + "type": "object", | |
| 55 | + "properties": { | |
| 56 | + "command": { | |
| 57 | + "type": "string", | |
| 58 | + "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)", | |
| 124 | 59 | }, |
| 125 | − { | |
| 126 | − "allOf": [ | |
| 127 | − { | |
| 128 | − "type": "object", | |
| 129 | − "required": ["request"], | |
| 130 | − "properties": { | |
| 131 | − "request": { | |
| 132 | − "type": "string", | |
| 133 | − "enum": ["attach"], | |
| 134 | − "description": "Request to attach to an existing process" | |
| 135 | − } | |
| 136 | − } | |
| 137 | − }, | |
| 138 | − { | |
| 139 | − "type": "object", | |
| 140 | − "properties": { | |
| 141 | − "rdbgPath": { | |
| 142 | − "type": "string", | |
| 143 | − "description": "Location of the rdbg executable" | |
| 144 | − }, | |
| 145 | − "debugPort": { | |
| 146 | − "type": "string", | |
| 147 | − "description": "UNIX domain socket name or TPC/IP host:port" | |
| 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 | − ] | |
| 60 | + "script": { | |
| 61 | + "type": "string", | |
| 62 | + "description": "Absolute path to a Ruby file." | |
| 63 | + }, | |
| 64 | + "cwd": { | |
| 65 | + "type": "string", | |
| 66 | + "description": "Directory to execute the program in", | |
| 67 | + "default": "${ZED_WORKTREE_ROOT}" | |
| 68 | + }, | |
| 69 | + "args": { | |
| 70 | + "type": "array", | |
| 71 | + "description": "Command line arguments passed to the program", | |
| 72 | + "items": { | |
| 73 | + "type": "string" | |
| 74 | + }, | |
| 75 | + "default": [] | |
| 76 | + }, | |
| 77 | + "env": { | |
| 78 | + "type": "object", | |
| 79 | + "description": "Additional environment variables to pass to the debugging (and debugged) process", | |
| 80 | + "default": {} | |
| 81 | + }, | |
| 82 | + } | |
| 173 | 83 | }) |
| 174 | 84 | } |
| 175 | 85 | |
| 176 | 86 | fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> { |
| 177 | − let mut config = serde_json::Map::new(); | |
| 178 | − | |
| 179 | − match &zed_scenario.request { | |
| 87 | + match zed_scenario.request { | |
| 180 | 88 | DebugRequest::Launch(launch) => { |
| 181 | − config.insert("request".to_string(), json!("launch")); | |
| 182 | − config.insert("script".to_string(), json!(launch.program)); | |
| 183 | − config.insert("command".to_string(), json!("ruby")); | |
| 184 | − | |
| 185 | − if !launch.args.is_empty() { | |
| 186 | − config.insert("args".to_string(), json!(launch.args)); | |
| 187 | − } | |
| 188 | − | |
| 189 | − if !launch.env.is_empty() { | |
| 190 | − config.insert("env".to_string(), json!(launch.env)); | |
| 191 | − } | |
| 192 | − | |
| 193 | − if let Some(cwd) = &launch.cwd { | |
| 194 | − config.insert("cwd".to_string(), json!(cwd)); | |
| 195 | − } | |
| 196 | − | |
| 197 | − // Ruby stops on entry so there's no need to handle that case | |
| 89 | + let config = RubyDebugConfig { | |
| 90 | + script_or_command: Some(launch.program), | |
| 91 | + script: None, | |
| 92 | + command: None, | |
| 93 | + args: launch.args, | |
| 94 | + env: launch.env, | |
| 95 | + cwd: launch.cwd.clone(), | |
| 96 | + }; | |
| 97 | + | |
| 98 | + let config = serde_json::to_value(config)?; | |
| 99 | + | |
| 100 | + Ok(DebugScenario { | |
| 101 | + adapter: zed_scenario.adapter, | |
| 102 | + label: zed_scenario.label, | |
| 103 | + config, | |
| 104 | + tcp_connection: None, | |
| 105 | + build: None, | |
| 106 | + }) | |
| 198 | 107 | } |
| 199 | − DebugRequest::Attach(attach) => { | |
| 200 | − config.insert("request".to_string(), json!("attach")); | |
| 201 | − | |
| 202 | − config.insert("processId".to_string(), json!(attach.process_id)); | |
| 108 | + DebugRequest::Attach(_) => { | |
| 109 | + anyhow::bail!("Attach requests are unsupported"); | |
| 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 | 114 | async fn get_binary( |
| @@ -247,26 +146,58 @@ impl DebugAdapter for RubyDebugAdapter { · 31 unmodified lines | ||
| 247 | 146 | |
| 248 | 147 | let tcp_connection = definition.tcp_connection.clone().unwrap_or_default(); |
| 249 | 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 | 152 | "--open".to_string(), |
| 253 | 153 | format!("--port={}", port), |
| 254 | 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 | 184 | Ok(DebugAdapterBinary { |
| 258 | − command: rdbg_path.to_string_lossy().to_string(), | |
| 185 | + command: Some(rdbg_path.to_string_lossy().to_string()), | |
| 259 | 186 | arguments, |
| 260 | 187 | connection: Some(dap::adapters::TcpArguments { |
| 261 | 188 | host, |
| 262 | 189 | port, |
| 263 | 190 | timeout, |
| 264 | 191 | }), |
| 265 | − cwd: None, | |
| 266 | − envs: std::collections::HashMap::default(), | |
| 192 | + cwd: Some( | |
| 193 | + ruby_config | |
| 194 | + .cwd | |
| 195 | + .unwrap_or(delegate.worktree_root_path().to_owned()), | |
| 196 | + ), | |
| 197 | + envs: ruby_config.env.into_iter().collect(), | |
| 267 | 198 | request_args: StartDebuggingRequestArguments { |
| 268 | 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 | 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 | 78 | ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true) |
| 79 | 79 | .with_db_initialization_query(DB_INITIALIZE_QUERY) |
| 80 | 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 | 86 | async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection { |
| 87 | − log::info!("Opening fallback db"); | |
| 87 | + log::warn!("Opening fallback in-memory database"); | |
| 88 | 88 | ThreadSafeConnection::builder::<M>(FALLBACK_DB_NAME, false) |
| 89 | 89 | .with_db_initialization_query(DB_INITIALIZE_QUERY) |
| 90 | 90 | .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY) |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | use dap::{ |
| 2 | + adapters::DebugAdapterName, | |
| 2 | 3 | client::SessionId, |
| 3 | 4 | debugger_settings::DebuggerSettings, |
| 4 | 5 | transport::{IoKind, LogKind}, |
| @@ -31,6 +32,13 @@ use workspace::{ · 26 unmodified lines | ||
| 31 | 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 | 42 | struct DapLogView { |
| 35 | 43 | editor: Entity<Editor>, |
| 36 | 44 | focus_handle: FocusHandle, |
| @@ -43,9 +51,9 @@ struct DapLogView { · 6 unmodified lines | ||
| 43 | 51 | |
| 44 | 52 | pub struct LogStore { |
| 45 | 53 | projects: HashMap<WeakEntity<Project>, ProjectState>, |
| 46 | − debug_clients: HashMap<SessionId, DebugAdapterState>, | |
| 47 | − rpc_tx: UnboundedSender<(SessionId, IoKind, String)>, | |
| 48 | − adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>, | |
| 54 | + debug_sessions: VecDeque<DebugAdapterState>, | |
| 55 | + rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>, | |
| 56 | + adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>, | |
| 49 | 57 | } |
| 50 | 58 | |
| 51 | 59 | struct ProjectState { |
| @@ -53,13 +61,19 @@ struct ProjectState { · 1 unmodified line | ||
| 53 | 61 | } |
| 54 | 62 | |
| 55 | 63 | struct DebugAdapterState { |
| 56 | − log_messages: VecDeque<String>, | |
| 64 | + id: SessionId, | |
| 65 | + log_messages: VecDeque<SharedString>, | |
| 57 | 66 | rpc_messages: RpcMessages, |
| 67 | + adapter_name: DebugAdapterName, | |
| 68 | + has_adapter_logs: bool, | |
| 69 | + is_terminated: bool, | |
| 58 | 70 | } |
| 59 | 71 | |
| 60 | 72 | struct RpcMessages { |
| 61 | − messages: VecDeque<String>, | |
| 73 | + messages: VecDeque<SharedString>, | |
| 62 | 74 | last_message_kind: Option<MessageKind>, |
| 75 | + initialization_sequence: Vec<SharedString>, | |
| 76 | + last_init_message_kind: Option<MessageKind>, | |
| 63 | 77 | } |
| 64 | 78 | |
| 65 | 79 | impl RpcMessages { |
| @@ -68,7 +82,9 @@ impl RpcMessages { · 2 unmodified lines | ||
| 68 | 82 | fn new() -> Self { |
| 69 | 83 | Self { |
| 70 | 84 | last_message_kind: None, |
| 85 | + last_init_message_kind: None, | |
| 71 | 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 | 110 | impl DebugAdapterState { |
| 95 | − fn new() -> Self { | |
| 111 | + fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self { | |
| 96 | 112 | Self { |
| 113 | + id, | |
| 97 | 114 | log_messages: VecDeque::new(), |
| 98 | 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 | 123 | impl LogStore { |
| 104 | 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 | 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 | 129 | if let Some(this) = this.upgrade() { |
| 109 | 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 | 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 | 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 | 145 | if let Some(this) = this.upgrade() { |
| 124 | 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 | 157 | rpc_tx, |
| 136 | 158 | adapter_log_tx, |
| 137 | 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 | 164 | pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) { |
| 163 | 165 | let weak_project = project.downgrade(); |
| 164 | 166 | self.projects.insert( |
| @@ -174,13 +176,15 @@ impl LogStore { · 9 unmodified lines | ||
| 174 | 176 | dap_store::DapStoreEvent::DebugClientStarted(session_id) => { |
| 175 | 177 | let session = dap_store.read(cx).session_by_id(session_id); |
| 176 | 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 | 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 | 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 | 203 | &mut self, |
| 198 | 204 | id: SessionId, |
| 199 | 205 | io_kind: IoKind, |
| 200 | − message: String, | |
| 206 | + command: Option<SharedString>, | |
| 207 | + message: SharedString, | |
| 201 | 208 | cx: &mut Context<Self>, |
| 202 | 209 | ) { |
| 203 | 210 | let Some(debug_client_state) = self.get_debug_adapter_state(id) else { |
| 204 | 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 | 221 | let kind = match io_kind { |
| 208 | 222 | IoKind::StdOut | IoKind::StdErr => MessageKind::Receive, |
| 209 | 223 | IoKind::StdIn => MessageKind::Send, |
| 210 | 224 | }; |
| 211 | 225 | |
| 212 | 226 | let rpc_messages = &mut debug_client_state.rpc_messages; |
| 227 | + | |
| 228 | + // Push a separator if the kind has changed | |
| 213 | 229 | if rpc_messages.last_message_kind != Some(kind) { |
| 214 | − Self::add_debug_client_entry( | |
| 230 | + Self::get_debug_adapter_entry( | |
| 215 | 231 | &mut rpc_messages.messages, |
| 216 | 232 | id, |
| 217 | − kind.label().to_string(), | |
| 233 | + kind.label().into(), | |
| 218 | 234 | LogKind::Rpc, |
| 219 | 235 | cx, |
| 220 | 236 | ); |
| 221 | 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 | 258 | cx.notify(); |
| 226 | 259 | } |
| 227 | 260 | |
| 228 | − fn add_debug_client_log( | |
| 261 | + fn add_debug_adapter_log( | |
| 229 | 262 | &mut self, |
| 230 | 263 | id: SessionId, |
| 231 | 264 | io_kind: IoKind, |
| 232 | − message: String, | |
| 265 | + message: SharedString, | |
| 233 | 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 | 269 | return; |
| 237 | 270 | }; |
| 238 | 271 | |
| 239 | 272 | let message = match io_kind { |
| 240 | − IoKind::StdErr => { | |
| 241 | − let mut message = message.clone(); | |
| 242 | − message.insert_str(0, "stderr: "); | |
| 243 | − message | |
| 244 | − } | |
| 273 | + IoKind::StdErr => format!("stderr: {message}").into(), | |
| 245 | 274 | _ => message, |
| 246 | 275 | }; |
| 247 | 276 | |
| 248 | − Self::add_debug_client_entry( | |
| 249 | − &mut debug_client_state.log_messages, | |
| 277 | + Self::get_debug_adapter_entry( | |
| 278 | + &mut debug_adapter_state.log_messages, | |
| 250 | 279 | id, |
| 251 | 280 | message, |
| 252 | 281 | LogKind::Adapter, |
| @@ -255,13 +284,13 @@ impl LogStore { · 2 unmodified lines | ||
| 255 | 284 | cx.notify(); |
| 256 | 285 | } |
| 257 | 286 | |
| 258 | − fn add_debug_client_entry( | |
| 259 | − log_lines: &mut VecDeque<String>, | |
| 287 | + fn get_debug_adapter_entry( | |
| 288 | + log_lines: &mut VecDeque<SharedString>, | |
| 260 | 289 | id: SessionId, |
| 261 | − message: String, | |
| 290 | + message: SharedString, | |
| 262 | 291 | kind: LogKind, |
| 263 | 292 | cx: &mut Context<Self>, |
| 264 | − ) { | |
| 293 | + ) -> SharedString { | |
| 265 | 294 | while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { |
| 266 | 295 | log_lines.pop_front(); |
| 267 | 296 | } |
| @@ -275,33 +304,69 @@ impl LogStore { · 7 unmodified lines | ||
| 275 | 304 | ) |
| 276 | 305 | .ok() |
| 277 | 306 | }) |
| 307 | + .map(SharedString::from) | |
| 278 | 308 | .unwrap_or(message) |
| 279 | 309 | } else { |
| 280 | 310 | message |
| 281 | 311 | }; |
| 282 | 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 | 324 | &mut self, |
| 289 | − client_id: SessionId, | |
| 290 | − client: Entity<Session>, | |
| 291 | − cx: &App, | |
| 292 | − ) -> Option<&mut DebugAdapterState> { | |
| 293 | − let client_state = self | |
| 294 | − .debug_clients | |
| 295 | − .entry(client_id) | |
| 296 | − .or_insert_with(DebugAdapterState::new); | |
| 325 | + session_id: SessionId, | |
| 326 | + session: Entity<Session>, | |
| 327 | + cx: &mut Context<Self>, | |
| 328 | + ) { | |
| 329 | + if self | |
| 330 | + .debug_sessions | |
| 331 | + .iter_mut() | |
| 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 | 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 | 361 | client.add_log_handler( |
| 302 | − move |io_kind, message| { | |
| 362 | + move |io_kind, command, message| { | |
| 303 | 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 | 370 | .ok(); |
| 306 | 371 | }, |
| 307 | 372 | LogKind::Rpc, |
| @@ -309,34 +374,66 @@ impl LogStore { · 1 unmodified line | ||
| 309 | 374 | |
| 310 | 375 | let log_io_tx = self.adapter_log_tx.clone(); |
| 311 | 376 | client.add_log_handler( |
| 312 | − move |io_kind, message| { | |
| 377 | + move |io_kind, command, message| { | |
| 313 | 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 | 385 | .ok(); |
| 316 | 386 | }, |
| 317 | 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>) { | |
| 324 | − self.debug_clients.remove(&client_id); | |
| 391 | + fn clean_sessions(&mut self, cx: &mut Context<Self>) { | |
| 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 | 400 | cx.notify(); |
| 326 | 401 | } |
| 327 | 402 | |
| 328 | − fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> { | |
| 329 | − Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages) | |
| 403 | + fn log_messages_for_session( | |
| 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>> { | |
| 333 | − Some( | |
| 334 | − &mut self | |
| 335 | − .debug_clients | |
| 336 | − .get_mut(&client_id)? | |
| 337 | − .rpc_messages | |
| 338 | − .messages, | |
| 339 | − ) | |
| 426 | + fn initialization_sequence_for_session( | |
| 427 | + &mut self, | |
| 428 | + session_id: SessionId, | |
| 429 | + ) -> Option<&mut Vec<SharedString>> { | |
| 430 | + self.debug_sessions.iter_mut().find_map(|state| { | |
| 431 | + if state.id == session_id { | |
| 432 | + Some(&mut state.rpc_messages.initialization_sequence) | |
| 433 | + } else { | |
| 434 | + None | |
| 435 | + } | |
| 436 | + }) | |
| 340 | 437 | } |
| 341 | 438 | } |
| 342 | 439 | |
| @@ -356,18 +453,15 @@ impl Render for DapLogToolbarItemView { · 13 unmodified lines | ||
| 356 | 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(), | |
| 362 | − log_view.current_view.map(|(client_id, _)| client_id), | |
| 458 | + log_view.menu_items(cx), | |
| 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| { | |
| 367 | − menu_rows | |
| 368 | − .iter() | |
| 369 | − .find(|row| row.client_id == current_client_id) | |
| 370 | − }); | |
| 463 | + let current_client = current_session_id | |
| 464 | + .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id)); | |
| 371 | 465 | |
| 372 | 466 | let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") |
| 373 | 467 | .anchor(gpui::Corner::TopLeft) |
| @@ -377,8 +471,8 @@ impl Render for DapLogToolbarItemView { · 3 unmodified lines | ||
| 377 | 471 | .map(|sub_item| { |
| 378 | 472 | Cow::Owned(format!( |
| 379 | 473 | "{} ({}) - {}", |
| 380 | − sub_item.client_name, | |
| 381 | − sub_item.client_id.0, | |
| 474 | + sub_item.adapter_name, | |
| 475 | + sub_item.session_id.0, | |
| 382 | 476 | match sub_item.selected_entry { |
| 383 | 477 | LogKind::Adapter => ADAPTER_LOGS, |
| 384 | 478 | LogKind::Rpc => RPC_MESSAGES, |
| @@ -397,9 +491,10 @@ impl Render for DapLogToolbarItemView { · 12 unmodified lines | ||
| 397 | 491 | .w_full() |
| 398 | 492 | .pl_2() |
| 399 | 493 | .child( |
| 400 | − Label::new( | |
| 401 | − format!("{}. {}", row.client_id.0, row.client_name,), | |
| 402 | − ) | |
| 494 | + Label::new(format!( | |
| 495 | + "{}. {}", | |
| 496 | + row.session_id.0, row.adapter_name, | |
| 497 | + )) | |
| 403 | 498 | .color(workspace::ui::Color::Muted), |
| 404 | 499 | ) |
| 405 | 500 | .into_any_element() |
| @@ -415,23 +510,40 @@ impl Render for DapLogToolbarItemView { · 9 unmodified lines | ||
| 415 | 510 | .into_any_element() |
| 416 | 511 | }, |
| 417 | 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( | |
| 424 | − move |_window, _cx| { | |
| 425 | − div() | |
| 426 | − .w_full() | |
| 427 | − .pl_4() | |
| 428 | − .child(Label::new(RPC_MESSAGES)) | |
| 429 | − .into_any_element() | |
| 430 | − }, | |
| 431 | − window.handler_for(&log_view, move |view, window, cx| { | |
| 432 | − view.show_rpc_trace_for_server(row.client_id, window, cx); | |
| 433 | − }), | |
| 434 | − ); | |
| 518 | + menu = menu | |
| 519 | + .custom_entry( | |
| 520 | + move |_window, _cx| { | |
| 521 | + div() | |
| 522 | + .w_full() | |
| 523 | + .pl_4() | |
| 524 | + .child(Label::new(RPC_MESSAGES)) | |
| 525 | + .into_any_element() | |
| 526 | + }, | |
| 527 | + window.handler_for(&log_view, move |view, window, cx| { | |
| 528 | + view.show_rpc_trace_for_server(row.session_id, window, cx); | |
| 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 | 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 | 640 | editor, |
| 523 | 641 | focus_handle, |
| 524 | 642 | project, |
| @@ -526,7 +644,17 @@ impl DapLogView { · 1 unmodified line | ||
| 526 | 644 | editor_subscriptions, |
| 527 | 645 | current_view: None, |
| 528 | 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 | 660 | fn editor_for_logs( |
| @@ -559,42 +687,34 @@ impl DapLogView { · 26 unmodified lines | ||
| 559 | 687 | (editor, vec![editor_subscription, search_subscription]) |
| 560 | 688 | } |
| 561 | 689 | |
| 562 | − fn menu_items(&self, cx: &App) -> Option<Vec<DapMenuItem>> { | |
| 563 | − let mut menu_items = self | |
| 564 | − .project | |
| 690 | + fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> { | |
| 691 | + self.log_store | |
| 565 | 692 | .read(cx) |
| 566 | − .dap_store() | |
| 567 | − .read(cx) | |
| 568 | − .sessions() | |
| 569 | − .filter_map(|session| { | |
| 570 | − let session = session.read(cx); | |
| 571 | − session.adapter(); | |
| 572 | − let client = session.adapter_client()?; | |
| 573 | − Some(DapMenuItem { | |
| 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 | − }) | |
| 693 | + .debug_sessions | |
| 694 | + .iter() | |
| 695 | + .rev() | |
| 696 | + .map(|state| DapMenuItem { | |
| 697 | + session_id: state.id, | |
| 698 | + adapter_name: state.adapter_name.clone(), | |
| 699 | + has_adapter_logs: state.has_adapter_logs, | |
| 700 | + selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), | |
| 579 | 701 | }) |
| 580 | − .collect::<Vec<_>>(); | |
| 581 | − menu_items.sort_by_key(|item| item.client_id.0); | |
| 582 | − Some(menu_items) | |
| 702 | + .collect::<Vec<_>>() | |
| 583 | 703 | } |
| 584 | 704 | |
| 585 | 705 | fn show_rpc_trace_for_server( |
| 586 | 706 | &mut self, |
| 587 | − client_id: SessionId, | |
| 707 | + session_id: SessionId, | |
| 588 | 708 | window: &mut Window, |
| 589 | 709 | cx: &mut Context<Self>, |
| 590 | 710 | ) { |
| 591 | 711 | let rpc_log = self.log_store.update(cx, |log_store, _| { |
| 592 | 712 | log_store |
| 593 | − .rpc_messages_for_client(client_id) | |
| 594 | − .map(|state| log_contents(&state)) | |
| 713 | + .rpc_messages_for_session(session_id) | |
| 714 | + .map(|state| log_contents(state.iter().cloned())) | |
| 595 | 715 | }); |
| 596 | 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 | 718 | let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); |
| 599 | 719 | let language = self.project.read(cx).languages().language_for_name("JSON"); |
| 600 | 720 | editor |
| @@ -626,17 +746,17 @@ impl DapLogView { · 25 unmodified lines | ||
| 626 | 746 | |
| 627 | 747 | fn show_log_messages_for_adapter( |
| 628 | 748 | &mut self, |
| 629 | − client_id: SessionId, | |
| 749 | + session_id: SessionId, | |
| 630 | 750 | window: &mut Window, |
| 631 | 751 | cx: &mut Context<Self>, |
| 632 | 752 | ) { |
| 633 | 753 | let message_log = self.log_store.update(cx, |log_store, _| { |
| 634 | 754 | log_store |
| 635 | − .log_messages_for_client(client_id) | |
| 636 | − .map(|state| log_contents(&state)) | |
| 755 | + .log_messages_for_session(session_id) | |
| 756 | + .map(|state| log_contents(state.iter().cloned())) | |
| 637 | 757 | }); |
| 638 | 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 | 760 | let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx); |
| 641 | 761 | editor |
| 642 | 762 | .read(cx) |
| @@ -652,14 +772,53 @@ impl DapLogView { · 9 unmodified lines | ||
| 652 | 772 | |
| 653 | 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 { | |
| 658 | − let (a, b) = lines.as_slices(); | |
| 659 | − let a = a.iter().map(move |v| v.as_ref()); | |
| 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); | |
| 819 | +fn log_contents(lines: impl Iterator<Item = SharedString>) -> String { | |
| 820 | + lines.fold(String::new(), |mut acc, el| { | |
| 821 | + acc.push_str(&el); | |
| 663 | 822 | acc.push('\n'); |
| 664 | 823 | acc |
| 665 | 824 | }) |
| @@ -667,14 +826,15 @@ fn log_contents(lines: &VecDeque<String>) -> String { · 1 unmodified line | ||
| 667 | 826 | |
| 668 | 827 | #[derive(Clone, PartialEq)] |
| 669 | 828 | pub(crate) struct DapMenuItem { |
| 670 | − pub client_id: SessionId, | |
| 671 | − pub client_name: String, | |
| 829 | + pub session_id: SessionId, | |
| 830 | + pub adapter_name: DebugAdapterName, | |
| 672 | 831 | pub has_adapter_logs: bool, |
| 673 | 832 | pub selected_entry: LogKind, |
| 674 | 833 | } |
| 675 | 834 | |
| 676 | 835 | const ADAPTER_LOGS: &str = "Adapter Logs"; |
| 677 | 836 | const RPC_MESSAGES: &str = "RPC Messages"; |
| 837 | +const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence"; | |
| 678 | 838 | |
| 679 | 839 | impl Render for DapLogView { |
| 680 | 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 | 996 | pub enum Event { |
| 837 | 997 | NewLogEntry { |
| 838 | 998 | id: SessionId, |
| 839 | − entry: String, | |
| 999 | + entry: SharedString, | |
| 840 | 1000 | kind: LogKind, |
| 841 | 1001 | }, |
| 842 | 1002 | } |
| @@ -849,12 +1009,16 @@ impl EventEmitter<SearchEvent> for DapLogView {} · 6 unmodified lines | ||
| 849 | 1009 | #[cfg(any(test, feature = "test-support"))] |
| 850 | 1010 | impl LogStore { |
| 851 | 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> { | |
| 856 | − self.debug_clients | |
| 857 | − .get(&session_id) | |
| 1018 | + pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> { | |
| 1019 | + self.debug_sessions | |
| 1020 | + .iter() | |
| 1021 | + .find(|adapter_state| adapter_state.id == session_id) | |
| 858 | 1022 | .expect("This session should exist if a test is calling") |
| 859 | 1023 | .rpc_messages |
| 860 | 1024 | .messages |
| @@ -862,9 +1026,10 @@ impl LogStore { · 1 unmodified line | ||
| 862 | 1026 | .into() |
| 863 | 1027 | } |
| 864 | 1028 | |
| 865 | − pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> { | |
| 866 | − self.debug_clients | |
| 867 | − .get(&session_id) | |
| 1029 | + pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> { | |
| 1030 | + self.debug_sessions | |
| 1031 | + .iter() | |
| 1032 | + .find(|adapter_state| adapter_state.id == session_id) | |
| 868 | 1033 | .expect("This session should exist if a test is calling") |
| 869 | 1034 | .log_messages |
| 870 | 1035 | .clone() |
| @@ -2,25 +2,23 @@ use crate::persistence::DebuggerPaneItem; · 1 unmodified line | ||
| 2 | 2 | use crate::session::DebugSession; |
| 3 | 3 | use crate::session::running::RunningState; |
| 4 | 4 | use crate::{ |
| 5 | − ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames, | |
| 6 | − FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal, | |
| 7 | − NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, | |
| 8 | − ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker, | |
| 9 | − persistence, spawn_task_or_modal, | |
| 5 | + ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList, | |
| 6 | + FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, | |
| 7 | + NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop, | |
| 8 | + ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal, | |
| 10 | 9 | }; |
| 11 | 10 | use anyhow::Result; |
| 12 | −use command_palette_hooks::CommandPaletteFilter; | |
| 13 | −use dap::StartDebuggingRequestArguments; | |
| 14 | 11 | use dap::adapters::DebugAdapterName; |
| 15 | 12 | use dap::debugger_settings::DebugPanelDockPosition; |
| 16 | 13 | use dap::{ |
| 17 | 14 | ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, |
| 18 | 15 | client::SessionId, debugger_settings::DebuggerSettings, |
| 19 | 16 | }; |
| 17 | +use dap::{DapRegistry, StartDebuggingRequestArguments}; | |
| 20 | 18 | use gpui::{ |
| 21 | − Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter, | |
| 22 | − FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity, | |
| 23 | − actions, anchored, deferred, | |
| 19 | + Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId, | |
| 20 | + EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, | |
| 21 | + WeakEntity, actions, anchored, deferred, | |
| 24 | 22 | }; |
| 25 | 23 | |
| 26 | 24 | use language::Buffer; |
| @@ -29,10 +27,10 @@ use project::{Fs, WorktreeId}; · 2 unmodified lines | ||
| 29 | 27 | use project::{Project, debugger::session::ThreadStatus}; |
| 30 | 28 | use rpc::proto::{self}; |
| 31 | 29 | use settings::Settings; |
| 32 | −use std::any::TypeId; | |
| 33 | 30 | use std::sync::Arc; |
| 34 | 31 | use task::{DebugScenario, TaskContext}; |
| 35 | 32 | use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*}; |
| 33 | +use util::maybe; | |
| 36 | 34 | use workspace::SplitDirection; |
| 37 | 35 | use workspace::{ |
| 38 | 36 | Pane, Workspace, |
| @@ -140,82 +138,6 @@ impl DebugPanel { · 101 unmodified lines | ||
| 140 | 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 | 141 | pub fn load( |
| 220 | 142 | workspace: WeakEntity<Workspace>, |
| 221 | 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 | 158 | workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone())); |
| 248 | 159 | |
| 249 | 160 | debug_panel |
| @@ -269,6 +180,19 @@ impl DebugPanel { · 19 unmodified lines | ||
| 269 | 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 | 196 | self.debug_scenario_scheduled_last = true; |
| 273 | 197 | if let Some(inventory) = self |
| 274 | 198 | .project |
| @@ -303,7 +227,7 @@ impl DebugPanel { · 28 unmodified lines | ||
| 303 | 227 | .await?; |
| 304 | 228 | dap_store |
| 305 | 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 | 232 | .await |
| 309 | 233 | } |
| @@ -412,7 +336,7 @@ impl DebugPanel { · 102 unmodified lines | ||
| 412 | 336 | let dap_store_handle = self.project.read(cx).dap_store().clone(); |
| 413 | 337 | let label = curr_session.read(cx).label().clone(); |
| 414 | 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 | 340 | let task = curr_session.update(cx, |session, cx| session.shutdown(cx)); |
| 417 | 341 | |
| 418 | 342 | cx.spawn_in(window, async move |this, cx| { |
| @@ -426,8 +350,26 @@ impl DebugPanel { · 7 unmodified lines | ||
| 426 | 350 | }); |
| 427 | 351 | (session, task) |
| 428 | 352 | })?; |
| 429 | − Self::register_session(this.clone(), session, true, cx).await?; | |
| 430 | − task.await | |
| 353 | + Self::register_session(this.clone(), session.clone(), true, cx).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 | 374 | .detach_and_log_err(cx); |
| 433 | 375 | } |
| @@ -445,12 +387,12 @@ impl DebugPanel { · 11 unmodified lines | ||
| 445 | 387 | }; |
| 446 | 388 | |
| 447 | 389 | let dap_store_handle = self.project.read(cx).dap_store().clone(); |
| 448 | − let mut label = parent_session.read(cx).label().clone(); | |
| 449 | − if !label.ends_with("(child)") { | |
| 450 | − label = format!("{label} (child)").into(); | |
| 451 | − } | |
| 390 | + let label = self.label_for_child_session(&parent_session, request, cx); | |
| 452 | 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 | 396 | binary.request_args = request.clone(); |
| 455 | 397 | cx.spawn_in(window, async move |this, cx| { |
| 456 | 398 | let (session, task) = dap_store_handle.update(cx, |dap_store, cx| { |
| @@ -462,7 +404,11 @@ impl DebugPanel { · 5 unmodified lines | ||
| 462 | 404 | }); |
| 463 | 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 | 412 | task.await |
| 467 | 413 | }) |
| 468 | 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 | 548 | pub(crate) fn top_controls_strip( |
| 583 | 549 | &mut self, |
| 584 | 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 | 584 | Some( |
| 613 | 585 | div.border_b_1() |
| @@ -629,6 +601,8 @@ impl DebugPanel { · 15 unmodified lines | ||
| 629 | 601 | project::debugger::session::ThreadStatus::Exited, |
| 630 | 602 | ); |
| 631 | 603 | let capabilities = running_state.read(cx).capabilities(cx); |
| 604 | + let supports_detach = | |
| 605 | + running_state.read(cx).session().read(cx).is_attached(); | |
| 632 | 606 | this.map(|this| { |
| 633 | 607 | if thread_status == ThreadStatus::Running { |
| 634 | 608 | this.child( |
| @@ -817,33 +791,48 @@ impl DebugPanel { · 182 unmodified lines | ||
| 817 | 791 | } |
| 818 | 792 | }), |
| 819 | 793 | ) |
| 820 | − .child( | |
| 821 | − IconButton::new("debug-disconnect", IconName::DebugDetach) | |
| 822 | − .icon_size(IconSize::XSmall) | |
| 823 | − .on_click(window.listener_for( | |
| 824 | − &running_state, | |
| 825 | − |this, _, _, cx| { | |
| 826 | − this.detach_client(cx); | |
| 827 | − }, | |
| 828 | − )) | |
| 829 | − .tooltip({ | |
| 830 | − let focus_handle = focus_handle.clone(); | |
| 831 | − move |window, cx| { | |
| 832 | − Tooltip::for_action_in( | |
| 833 | − "Detach", | |
| 834 | − &Detach, | |
| 835 | − &focus_handle, | |
| 836 | − window, | |
| 837 | − cx, | |
| 838 | − ) | |
| 839 | − } | |
| 840 | − }), | |
| 794 | + .when( | |
| 795 | + supports_detach, | |
| 796 | + |div| { | |
| 797 | + div.child( | |
| 798 | + IconButton::new( | |
| 799 | + "debug-disconnect", | |
| 800 | + IconName::DebugDetach, | |
| 801 | + ) | |
| 802 | + .disabled( | |
| 803 | + thread_status != ThreadStatus::Stopped | |
| 804 | + && thread_status != ThreadStatus::Running, | |
| 805 | + ) | |
| 806 | + .icon_size(IconSize::XSmall) | |
| 807 | + .on_click(window.listener_for( | |
| 808 | + &running_state, | |
| 809 | + |this, _, _, cx| { | |
| 810 | + this.detach_client(cx); | |
| 811 | + }, | |
| 812 | + )) | |
| 813 | + .tooltip({ | |
| 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 | 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 | 837 | .child( |
| 849 | 838 | h_flex() |
| @@ -884,7 +873,10 @@ impl DebugPanel { · 34 unmodified lines | ||
| 884 | 873 | window, |
| 885 | 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 | 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 | 1058 | async fn register_session_inner( |
| @@ -1069,6 +1080,11 @@ async fn register_session_inner( · 21 unmodified lines | ||
| 1069 | 1080 | .ok(); |
| 1070 | 1081 | let serialized_layout = persistence::get_serialized_layout(adapter_name).await; |
| 1071 | 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 | 1088 | this.sessions.retain(|session| { |
| 1073 | 1089 | !session |
| 1074 | 1090 | .read(cx) |
| @@ -1082,8 +1098,8 @@ async fn register_session_inner( · 7 unmodified lines | ||
| 1082 | 1098 | let debug_session = DebugSession::running( |
| 1083 | 1099 | this.project.clone(), |
| 1084 | 1100 | this.workspace.clone(), |
| 1101 | + parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()), | |
| 1085 | 1102 | session, |
| 1086 | − cx.weak_entity(), | |
| 1087 | 1103 | serialized_layout, |
| 1088 | 1104 | this.position(window, cx).axis(), |
| 1089 | 1105 | window, |
| @@ -1363,6 +1379,7 @@ impl Render for DebugPanel { · 273 unmodified lines | ||
| 1363 | 1379 | }); |
| 1364 | 1380 | cx.notify(); |
| 1365 | 1381 | })) |
| 1382 | + .on_action(cx.listener(Self::copy_debug_adapter_arguments)) | |
| 1366 | 1383 | .when(self.active_session.is_some(), |this| { |
| 1367 | 1384 | this.on_mouse_down( |
| 1368 | 1385 | MouseButton::Right, |
| @@ -1,14 +1,17 @@ | ||
| 1 | +use std::any::TypeId; | |
| 2 | + | |
| 1 | 3 | use dap::debugger_settings::DebuggerSettings; |
| 2 | 4 | use debugger_panel::{DebugPanel, ToggleFocus}; |
| 3 | 5 | use editor::Editor; |
| 4 | 6 | use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt}; |
| 5 | −use gpui::{App, EntityInputHandler, actions}; | |
| 7 | +use gpui::{App, DispatchPhase, EntityInputHandler, actions}; | |
| 6 | 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 | 10 | use session::DebugSession; |
| 9 | 11 | use settings::Settings; |
| 10 | 12 | use stack_trace_view::StackTraceView; |
| 11 | 13 | use tasks_ui::{Spawn, TaskOverrides}; |
| 14 | +use ui::{FluentBuilder, InteractiveElement}; | |
| 12 | 15 | use util::maybe; |
| 13 | 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 | 61 | pub fn init(cx: &mut App) { |
| 57 | 62 | DebuggerSettings::register(cx); |
| 58 | 63 | workspace::FollowableViewRegistry::register::<DebugSession>(cx); |
| @@ -68,107 +73,20 @@ pub fn init(cx: &mut App) { · 9 unmodified lines | ||
| 68 | 73 | .register_action(|workspace, _: &ToggleFocus, window, cx| { |
| 69 | 74 | workspace.toggle_panel_focus::<DebugPanel>(window, cx); |
| 70 | 75 | }) |
| 71 | − .register_action(|workspace, _: &Pause, _, cx| { | |
| 72 | − if let Some(debug_panel) = workspace.panel::<DebugPanel>(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 | − } | |
| 76 | + .register_action(|workspace: &mut Workspace, _: &Start, window, cx| { | |
| 77 | + NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); | |
| 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 | 90 | .register_action( |
| 173 | 91 | |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| { |
| 174 | 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( | |
| 182 | − |workspace: &mut Workspace, _: &ShowStackTrace, window, cx| { | |
| 183 | − let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else { | |
| 184 | − return; | |
| 185 | − }; | |
| 99 | + .register_action_renderer(|div, workspace, _, cx| { | |
| 100 | + let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else { | |
| 101 | + return div; | |
| 102 | + }; | |
| 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) { | |
| 188 | − let is_active = workspace | |
| 189 | − .active_item(cx) | |
| 190 | − .is_some_and(|item| item.item_id() == existing.item_id()); | |
| 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 | − }; | |
| 115 | + let caps = running_state.capabilities(cx); | |
| 116 | + let supports_step_back = caps.supports_step_back.unwrap_or_default(); | |
| 117 | + let supports_detach = running_state.session().read(cx).is_attached(); | |
| 118 | + let status = running_state.thread_status(cx); | |
| 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| { | |
| 200 | − session.stack_trace_view(project, window, cx).clone() | |
| 201 | − }); | |
| 168 | + if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) | |
| 169 | + { | |
| 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( | |
| 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 | − }; | |
| 182 | + let project = workspace.project(); | |
| 221 | 183 | |
| 222 | − debug_panel.update(cx, |debug_panel, cx| { | |
| 223 | − debug_panel.rerun_last_session(workspace, window, cx); | |
| 184 | + let stack_trace_view = | |
| 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 | − }, | |
| 226 | − ); | |
| 207 | + }) | |
| 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 | 233 | .detach(); |
| 230 | 234 | |
| 231 | 235 | cx.observe_new({ |
| 232 | − move |editor: &mut Editor, _, cx| { | |
| 236 | + move |editor: &mut Editor, _, _| { | |
| 233 | 237 | editor |
| 234 | − .register_action(cx.listener( | |
| 235 | − move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| { | |
| 236 | − maybe!({ | |
| 237 | − let debug_panel = | |
| 238 | − editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?; | |
| 239 | − let cursor_point: language::Point = editor.selections.newest(cx).head(); | |
| 240 | − let active_session = debug_panel.read(cx).active_session()?; | |
| 238 | + .register_action_renderer(move |editor, window, cx| { | |
| 239 | + let Some(workspace) = editor.workspace() else { | |
| 240 | + return; | |
| 241 | + }; | |
| 242 | + let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else { | |
| 243 | + return; | |
| 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 | |
| 243 | − .buffer() | |
| 244 | − .read(cx) | |
| 245 | − .point_to_buffer_point(cursor_point, cx)?; | |
| 265 | + editor | |
| 266 | + .buffer() | |
| 267 | + .read(cx) | |
| 268 | + .point_to_buffer_point(cursor_point, cx) | |
| 269 | + }) | |
| 270 | + .ok()??; | |
| 246 | 271 | |
| 247 | − let path = | |
| 272 | + let path = | |
| 248 | 273 | debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer( |
| 249 | 274 | &buffer, cx, |
| 250 | 275 | )?; |
| 251 | 276 | |
| 252 | − let source_breakpoint = SourceBreakpoint { | |
| 253 | − row: position.row, | |
| 254 | − path, | |
| 255 | − message: None, | |
| 256 | − condition: None, | |
| 257 | − hit_condition: None, | |
| 258 | − state: debugger::breakpoint_store::BreakpointState::Enabled, | |
| 259 | − }; | |
| 277 | + let source_breakpoint = SourceBreakpoint { | |
| 278 | + row: position.row, | |
| 279 | + path, | |
| 280 | + message: None, | |
| 281 | + condition: None, | |
| 282 | + hit_condition: None, | |
| 283 | + state: debugger::breakpoint_store::BreakpointState::Enabled, | |
| 284 | + }; | |
| 260 | 285 | |
| 261 | − active_session.update(cx, |session, cx| { | |
| 262 | − session.running_state().update(cx, |state, cx| { | |
| 263 | − if let Some(thread_id) = state.selected_thread_id() { | |
| 264 | − state.session().update(cx, |session, cx| { | |
| 265 | − session.run_to_position( | |
| 266 | − source_breakpoint, | |
| 267 | − thread_id, | |
| 268 | − cx, | |
| 269 | − ); | |
| 270 | − }) | |
| 271 | − } | |
| 286 | + active_session.update(cx, |session, cx| { | |
| 287 | + session.running_state().update(cx, |state, cx| { | |
| 288 | + if let Some(thread_id) = state.selected_thread_id() { | |
| 289 | + state.session().update(cx, |session, cx| { | |
| 290 | + session.run_to_position( | |
| 291 | + source_breakpoint, | |
| 292 | + thread_id, | |
| 293 | + cx, | |
| 294 | + ); | |
| 295 | + }) | |
| 296 | + } | |
| 297 | + }); | |
| 272 | 298 | }); |
| 273 | − }); | |
| 274 | 299 | |
| 275 | − Some(()) | |
| 276 | − }); | |
| 277 | − }, | |
| 278 | − )) | |
| 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()?; | |
| 300 | + Some(()) | |
| 301 | + }); | |
| 302 | + } | |
| 303 | + }); | |
| 288 | 304 | |
| 289 | − let text = editor.text_for_range( | |
| 290 | − editor.selections.newest(cx).range(), | |
| 291 | − &mut None, | |
| 292 | − window, | |
| 293 | − cx, | |
| 294 | − )?; | |
| 305 | + window.on_action( | |
| 306 | + TypeId::of::<editor::actions::EvaluateSelectedText>(), | |
| 307 | + move |_, _, window, cx| { | |
| 308 | + maybe!({ | |
| 309 | + let text = editor | |
| 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| { | |
| 297 | − session.running_state().update(cx, |state, cx| { | |
| 298 | − let stack_id = state.selected_stack_frame_id(cx); | |
| 320 | + active_session.update(cx, |session, cx| { | |
| 321 | + session.running_state().update(cx, |state, cx| { | |
| 322 | + let stack_id = state.selected_stack_frame_id(cx); | |
| 299 | 323 | |
| 300 | − state.session().update(cx, |session, cx| { | |
| 301 | − session.evaluate(text, None, stack_id, None, cx).detach(); | |
| 324 | + state.session().update(cx, |session, cx| { | |
| 325 | + session | |
| 326 | + .evaluate(text, None, stack_id, None, cx) | |
| 327 | + .detach(); | |
| 328 | + }); | |
| 302 | 329 | }); |
| 303 | 330 | }); |
| 304 | − }); | |
| 305 | 331 | |
| 306 | − Some(()) | |
| 307 | − }); | |
| 308 | − }, | |
| 309 | − )) | |
| 332 | + Some(()) | |
| 333 | + }); | |
| 334 | + }, | |
| 335 | + ); | |
| 336 | + }) | |
| 310 | 337 | .detach(); |
| 311 | 338 | } |
| 312 | 339 | }) |
| @@ -1,4 +1,4 @@ | ||
| 1 | −use collections::FxHashMap; | |
| 1 | +use collections::{FxHashMap, HashMap}; | |
| 2 | 2 | use language::LanguageRegistry; |
| 3 | 3 | use paths::local_debug_file_relative_path; |
| 4 | 4 | use std::{ |
| @@ -15,9 +15,9 @@ use dap::{ · 10 unmodified lines | ||
| 15 | 15 | use editor::{Editor, EditorElement, EditorStyle}; |
| 16 | 16 | use fuzzy::{StringMatch, StringMatchCandidate}; |
| 17 | 17 | use gpui::{ |
| 18 | − App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, | |
| 19 | − InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription, | |
| 20 | − TextStyle, UnderlineStyle, WeakEntity, | |
| 18 | + Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, | |
| 19 | + HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, | |
| 20 | + Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, | |
| 21 | 21 | }; |
| 22 | 22 | use itertools::Itertools as _; |
| 23 | 23 | use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; |
| @@ -28,10 +28,10 @@ use theme::ThemeSettings; · 4 unmodified lines | ||
| 28 | 28 | use ui::{ |
| 29 | 29 | ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, |
| 30 | 30 | ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize, |
| 31 | − IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _, | |
| 32 | − ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt, | |
| 33 | − StyledTypography, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, px, | |
| 34 | − relative, rems, v_flex, | |
| 31 | + IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label, | |
| 32 | + LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce, | |
| 33 | + SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable, | |
| 34 | + Tooltip, Window, div, h_flex, px, relative, rems, v_flex, | |
| 35 | 35 | }; |
| 36 | 36 | use util::ResultExt; |
| 37 | 37 | use workspace::{ModalView, Workspace, pane}; |
| @@ -50,7 +50,7 @@ pub(super) struct NewProcessModal { · 12 unmodified lines | ||
| 50 | 50 | mode: NewProcessMode, |
| 51 | 51 | debug_picker: Entity<Picker<DebugDelegate>>, |
| 52 | 52 | attach_mode: Entity<AttachMode>, |
| 53 | − launch_mode: Entity<ConfigureMode>, | |
| 53 | + configure_mode: Entity<ConfigureMode>, | |
| 54 | 54 | task_mode: TaskMode, |
| 55 | 55 | debugger: Option<DebugAdapterName>, |
| 56 | 56 | // save_scenario_state: Option<SaveScenarioState>, |
| @@ -194,26 +194,31 @@ impl NewProcessModal { · 137 unmodified lines | ||
| 194 | 194 | return Ok(()); |
| 195 | 195 | }; |
| 196 | 196 | |
| 197 | − let (used_tasks, current_resolved_tasks) = | |
| 198 | − task_inventory.update(cx, |task_inventory, cx| { | |
| 197 | + let (used_tasks, current_resolved_tasks) = task_inventory | |
| 198 | + .update(cx, |task_inventory, cx| { | |
| 199 | 199 | task_inventory |
| 200 | − .used_and_current_resolved_tasks(&task_contexts, cx) | |
| 201 | − })?; | |
| 202 | − | |
| 203 | − debug_picker | |
| 204 | − .update_in(cx, |picker, window, cx| { | |
| 205 | − picker.delegate.tasks_loaded( | |
| 206 | − task_contexts.clone(), | |
| 207 | − languages, | |
| 208 | − lsp_tasks.clone(), | |
| 209 | − current_resolved_tasks.clone(), | |
| 210 | − add_current_language_tasks, | |
| 211 | − cx, | |
| 212 | − ); | |
| 213 | − picker.refresh(window, cx); | |
| 214 | − cx.notify(); | |
| 215 | − }) | |
| 216 | − .ok(); | |
| 200 | + .used_and_current_resolved_tasks(task_contexts.clone(), cx) | |
| 201 | + })? | |
| 202 | + .await; | |
| 203 | + | |
| 204 | + if let Ok(task) = debug_picker.update(cx, |picker, cx| { | |
| 205 | + picker.delegate.tasks_loaded( | |
| 206 | + task_contexts.clone(), | |
| 207 | + languages, | |
| 208 | + lsp_tasks.clone(), | |
| 209 | + current_resolved_tasks.clone(), | |
| 210 | + add_current_language_tasks, | |
| 211 | + cx, | |
| 212 | + ) | |
| 213 | + }) { | |
| 214 | + task.await; | |
| 215 | + debug_picker | |
| 216 | + .update_in(cx, |picker, window, cx| { | |
| 217 | + picker.refresh(window, cx); | |
| 218 | + cx.notify(); | |
| 219 | + }) | |
| 220 | + .ok(); | |
| 221 | + } | |
| 217 | 222 | |
| 218 | 223 | if let Some(active_cwd) = task_contexts |
| 219 | 224 | .active_context() |
| @@ -253,7 +258,7 @@ impl NewProcessModal { · 33 unmodified lines | ||
| 253 | 258 | Self { |
| 254 | 259 | debug_picker, |
| 255 | 260 | attach_mode, |
| 256 | − launch_mode: configure_mode, | |
| 261 | + configure_mode, | |
| 257 | 262 | task_mode, |
| 258 | 263 | debugger: None, |
| 259 | 264 | mode, |
| @@ -283,7 +288,7 @@ impl NewProcessModal { · 23 unmodified lines | ||
| 283 | 288 | NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| { |
| 284 | 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 | 292 | this.clone().render(dap_menu, window, cx).into_any_element() |
| 288 | 293 | }), |
| 289 | 294 | NewProcessMode::Debug => v_flex() |
| @@ -297,7 +302,7 @@ impl NewProcessModal { · 7 unmodified lines | ||
| 297 | 302 | match self.mode { |
| 298 | 303 | NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx), |
| 299 | 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 | 306 | NewProcessMode::Debug => self.debug_picker.focus_handle(cx), |
| 302 | 307 | } |
| 303 | 308 | } |
| @@ -305,7 +310,7 @@ impl NewProcessModal { · 1 unmodified line | ||
| 305 | 310 | fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> { |
| 306 | 311 | let request = match self.mode { |
| 307 | 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 | 315 | NewProcessMode::Attach => Some(DebugRequest::Attach( |
| 311 | 316 | self.attach_mode.read(cx).debug_request(), |
| @@ -315,7 +320,7 @@ impl NewProcessModal { · 3 unmodified lines | ||
| 315 | 320 | let label = suggested_label(&request, debugger); |
| 316 | 321 | |
| 317 | 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 | 324 | } else { |
| 320 | 325 | None |
| 321 | 326 | }; |
| @@ -476,10 +481,9 @@ impl NewProcessModal { · 154 unmodified lines | ||
| 476 | 481 | .get_index_of(adapter.0.as_ref()) |
| 477 | 482 | .unwrap_or(usize::MAX) |
| 478 | 483 | }); |
| 479 | − } | |
| 480 | − | |
| 481 | − if self.debugger.is_none() { | |
| 482 | − self.debugger = available_adapters.first().cloned(); | |
| 484 | + if self.debugger.is_none() { | |
| 485 | + self.debugger = available_adapters.first().cloned(); | |
| 486 | + } | |
| 483 | 487 | } |
| 484 | 488 | |
| 485 | 489 | let label = self |
| @@ -831,7 +835,7 @@ impl Render for NewProcessModal { · 345 unmodified lines | ||
| 831 | 835 | .disabled( |
| 832 | 836 | self.debugger.is_none() |
| 833 | 837 | || self |
| 834 | − .launch_mode | |
| 838 | + .configure_mode | |
| 835 | 839 | .read(cx) |
| 836 | 840 | .program |
| 837 | 841 | .read(cx) |
| @@ -1143,41 +1147,67 @@ impl DebugDelegate { · 305 unmodified lines | ||
| 1143 | 1147 | current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>, |
| 1144 | 1148 | add_current_language_tasks: bool, |
| 1145 | 1149 | cx: &mut Context<Picker<Self>>, |
| 1146 | − ) { | |
| 1150 | + ) -> Task<()> { | |
| 1147 | 1151 | self.task_contexts = Some(task_contexts.clone()); |
| 1148 | − | |
| 1149 | − let (recent, scenarios) = self | |
| 1150 | − .task_store | |
| 1151 | − .update(cx, |task_store, cx| { | |
| 1152 | − task_store.task_inventory().map(|inventory| { | |
| 1153 | − inventory.update(cx, |inventory, cx| { | |
| 1154 | − inventory.list_debug_scenarios( | |
| 1155 | − &task_contexts, | |
| 1156 | − lsp_tasks, | |
| 1157 | − current_resolved_tasks, | |
| 1158 | − add_current_language_tasks, | |
| 1159 | − cx, | |
| 1160 | − ) | |
| 1161 | − }) | |
| 1152 | + let task = self.task_store.update(cx, |task_store, cx| { | |
| 1153 | + task_store.task_inventory().map(|inventory| { | |
| 1154 | + inventory.update(cx, |inventory, cx| { | |
| 1155 | + inventory.list_debug_scenarios( | |
| 1156 | + &task_contexts, | |
| 1157 | + lsp_tasks, | |
| 1158 | + current_resolved_tasks, | |
| 1159 | + add_current_language_tasks, | |
| 1160 | + cx, | |
| 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() { | |
| 1167 | − self.last_used_candidate_index = Some(recent.len() - 1); | |
| 1168 | − } | |
| 1172 | + this.update(cx, |this, cx| { | |
| 1173 | + if !recent.is_empty() { | |
| 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 | |
| 1173 | − .into_iter() | |
| 1174 | − .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario)) | |
| 1175 | − .chain(scenarios.into_iter().map(|(kind, scenario)| { | |
| 1176 | − let (language, scenario) = | |
| 1177 | − Self::get_scenario_kind(&languages, &dap_registry, scenario); | |
| 1178 | − (language.or(Some(kind)), scenario) | |
| 1179 | − })) | |
| 1180 | − .collect(); | |
| 1187 | + this.delegate.candidates = recent | |
| 1188 | + .into_iter() | |
| 1189 | + .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario)) | |
| 1190 | + .chain( | |
| 1191 | + scenarios | |
| 1192 | + .into_iter() | |
| 1193 | + .filter(|(kind, _)| match kind { | |
| 1194 | + TaskSourceKind::Worktree { | |
| 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 | 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 | 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 | 1408 | fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) { |
| 1269 | 1409 | let debug_scenario = self |
| 1270 | 1410 | .matches |
| @@ -1300,6 +1440,60 @@ impl PickerDelegate for DebugDelegate { · 29 unmodified lines | ||
| 1300 | 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 | 1497 | fn render_match( |
| 1304 | 1498 | &self, |
| 1305 | 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 | 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 | 4 | use dap::client::SessionId; |
| 5 | 5 | use gpui::{ |
| 6 | 6 | App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, |
| @@ -22,7 +22,6 @@ pub struct DebugSession { · 15 unmodified lines | ||
| 22 | 22 | running_state: Entity<RunningState>, |
| 23 | 23 | label: OnceLock<SharedString>, |
| 24 | 24 | stack_trace_view: OnceCell<Entity<StackTraceView>>, |
| 25 | − _debug_panel: WeakEntity<DebugPanel>, | |
| 26 | 25 | _worktree_store: WeakEntity<WorktreeStore>, |
| 27 | 26 | workspace: WeakEntity<Workspace>, |
| 28 | 27 | _subscriptions: [Subscription; 1], |
| @@ -38,8 +37,8 @@ impl DebugSession { · 9 unmodified lines | ||
| 38 | 37 | pub(crate) fn running( |
| 39 | 38 | project: Entity<Project>, |
| 40 | 39 | workspace: WeakEntity<Workspace>, |
| 40 | + parent_terminal: Option<Entity<DebugTerminal>>, | |
| 41 | 41 | session: Entity<Session>, |
| 42 | − _debug_panel: WeakEntity<DebugPanel>, | |
| 43 | 42 | serialized_layout: Option<SerializedLayout>, |
| 44 | 43 | dock_axis: Axis, |
| 45 | 44 | window: &mut Window, |
| @@ -50,6 +49,7 @@ impl DebugSession { · 4 unmodified lines | ||
| 50 | 49 | session.clone(), |
| 51 | 50 | project.clone(), |
| 52 | 51 | workspace.clone(), |
| 52 | + parent_terminal, | |
| 53 | 53 | serialized_layout, |
| 54 | 54 | dock_axis, |
| 55 | 55 | window, |
| @@ -64,7 +64,6 @@ impl DebugSession { · 8 unmodified lines | ||
| 64 | 64 | remote_id: None, |
| 65 | 65 | running_state, |
| 66 | 66 | label: OnceLock::new(), |
| 67 | − _debug_panel, | |
| 68 | 67 | stack_trace_view: OnceCell::new(), |
| 69 | 68 | _worktree_store: project.read(cx).worktree_store().downgrade(), |
| 70 | 69 | workspace, |
| @@ -605,6 +605,7 @@ impl RunningState { · 604 unmodified lines | ||
| 605 | 605 | session: Entity<Session>, |
| 606 | 606 | project: Entity<Project>, |
| 607 | 607 | workspace: WeakEntity<Workspace>, |
| 608 | + parent_terminal: Option<Entity<DebugTerminal>>, | |
| 608 | 609 | serialized_pane_layout: Option<SerializedLayout>, |
| 609 | 610 | dock_axis: Axis, |
| 610 | 611 | window: &mut Window, |
| @@ -617,7 +618,8 @@ impl RunningState { · 6 unmodified lines | ||
| 617 | 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 | 624 | let variable_list = |
| 623 | 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 | 819 | let request_type = dap_registry |
| 818 | 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 | 822 | .and_then(|adapter| adapter.request_kind(&config)); |
| 821 | 823 | |
| 822 | 824 | let config_is_valid = request_type.is_ok(); |
| 823 | 825 | |
| 824 | 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 | 828 | BuildTaskDefinition::Template { |
| 827 | 829 | task_template, |
| 828 | 830 | locator_name, |
| 829 | 831 | } => (task_template, locator_name), |
| 830 | 832 | BuildTaskDefinition::ByName(ref label) => { |
| 831 | − let Some(task) = task_store.update(cx, |this, cx| { | |
| 832 | − this.task_inventory().and_then(|inventory| { | |
| 833 | + let task = task_store.update(cx, |this, cx| { | |
| 834 | + this.task_inventory().map(|inventory| { | |
| 833 | 835 | inventory.read(cx).task_template_by_label( |
| 834 | 836 | buffer, |
| 835 | 837 | worktree_id, |
| @@ -837,14 +839,15 @@ impl RunningState { · 1 unmodified line | ||
| 837 | 839 | cx, |
| 838 | 840 | ) |
| 839 | 841 | }) |
| 840 | − })? | |
| 841 | − else { | |
| 842 | − anyhow::bail!("Couldn't find task template for {:?}", build) | |
| 843 | − }; | |
| 842 | + })?; | |
| 843 | + let task = match task { | |
| 844 | + Some(task) => task.await, | |
| 845 | + None => None, | |
| 846 | + }.with_context(|| format!("Couldn't find task template for {build:?}"))?; | |
| 844 | 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 | 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 | 855 | debug_assert!(!config_is_valid); |
| 853 | 856 | Some(locator_name) |
| 854 | 857 | } else if !config_is_valid { |
| 855 | − dap_store | |
| 858 | + let task = dap_store | |
| 856 | 859 | .update(cx, |this, cx| { |
| 857 | 860 | this.debug_scenario_for_build_task( |
| 858 | 861 | task.original_task().clone(), |
| @@ -860,17 +863,21 @@ impl RunningState { · 1 unmodified line | ||
| 860 | 863 | task.display_label().to_owned().into(), |
| 861 | 864 | cx, |
| 862 | 865 | ) |
| 863 | − .and_then(|scenario| { | |
| 864 | − match scenario.build { | |
| 865 | − Some(BuildTaskDefinition::Template { | |
| 866 | − locator_name, .. | |
| 867 | − }) => locator_name, | |
| 868 | − _ => None, | |
| 869 | − } | |
| 870 | − }) | |
| 866 | + | |
| 867 | + }); | |
| 868 | + if let Ok(t) = task { | |
| 869 | + t.await.and_then(|scenario| { | |
| 870 | + match scenario.build { | |
| 871 | + Some(BuildTaskDefinition::Template { | |
| 872 | + locator_name, .. | |
| 873 | + }) => locator_name, | |
| 874 | + _ => None, | |
| 875 | + } | |
| 871 | 876 | }) |
| 872 | − .ok() | |
| 873 | − .flatten() | |
| 877 | + } else { | |
| 878 | + None | |
| 879 | + } | |
| 880 | + | |
| 874 | 881 | } else { |
| 875 | 882 | None |
| 876 | 883 | }; |
| @@ -929,15 +936,13 @@ impl RunningState { · 52 unmodified lines | ||
| 929 | 936 | }; |
| 930 | 937 | |
| 931 | 938 | if config_is_valid { |
| 932 | − // Ok(DebugTaskDefinition { | |
| 933 | − // label, | |
| 934 | − // adapter: DebugAdapterName(adapter), | |
| 935 | − // config, | |
| 936 | − // tcp_connection, | |
| 937 | − // }) | |
| 938 | 939 | } else if let Some((task, locator_name)) = build_output { |
| 939 | 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 | 946 | let request = dap_store |
| 942 | 947 | .update(cx, |this, cx| { |
| 943 | 948 | this.run_debug_locator(&locator_name, task, cx) |
| @@ -953,7 +958,7 @@ impl RunningState { · 9 unmodified lines | ||
| 953 | 958 | |
| 954 | 959 | let scenario = dap_registry |
| 955 | 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 | 962 | .map(|adapter| adapter.config_from_zed_format(zed_config))??; |
| 958 | 963 | config = scenario.config; |
| 959 | 964 | Self::substitute_variables_in_config(&mut config, &task_context); |
| @@ -992,7 +997,7 @@ impl RunningState { · 32 unmodified lines | ||
| 992 | 997 | let cwd = Some(&request.cwd) |
| 993 | 998 | .filter(|cwd| cwd.len() > 0) |
| 994 | 999 | .map(PathBuf::from) |
| 995 | − .or_else(|| session.binary().cwd.clone()); | |
| 1000 | + .or_else(|| session.binary().unwrap().cwd.clone()); | |
| 996 | 1001 | |
| 997 | 1002 | let mut args = request.args.clone(); |
| 998 | 1003 | |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | use std::{ |
| 2 | + ops::Range, | |
| 2 | 3 | path::{Path, PathBuf}, |
| 3 | 4 | sync::Arc, |
| 4 | 5 | time::Duration, |
| @@ -277,10 +278,9 @@ impl BreakpointList { · 272 unmodified lines | ||
| 277 | 278 | let selected_ix = self.selected_ix; |
| 278 | 279 | let focus_handle = self.focus_handle.clone(); |
| 279 | 280 | uniform_list( |
| 280 | − cx.entity(), | |
| 281 | 281 | "breakpoint-list", |
| 282 | 282 | self.breakpoints.len(), |
| 283 | − move |this, range, window, cx| { | |
| 283 | + cx.processor(move |this, range: Range<usize>, window, cx| { | |
| 284 | 284 | range |
| 285 | 285 | .clone() |
| 286 | 286 | .zip(&mut this.breakpoints[range]) |
| @@ -291,7 +291,7 @@ impl BreakpointList { · 4 unmodified lines | ||
| 291 | 291 | .into_any_element() |
| 292 | 292 | }) |
| 293 | 293 | .collect() |
| 294 | − }, | |
| 294 | + }), | |
| 295 | 295 | ) |
| 296 | 296 | .track_scroll(self.scroll_handle.clone()) |
| 297 | 297 | .flex_grow() |
| @@ -8,7 +8,7 @@ use project::{ · 7 unmodified lines | ||
| 8 | 8 | ProjectItem as _, ProjectPath, |
| 9 | 9 | debugger::session::{Session, SessionEvent}, |
| 10 | 10 | }; |
| 11 | −use std::{path::Path, sync::Arc}; | |
| 11 | +use std::{ops::Range, path::Path, sync::Arc}; | |
| 12 | 12 | use ui::{Scrollbar, ScrollbarState, prelude::*}; |
| 13 | 13 | use workspace::Workspace; |
| 14 | 14 | |
| @@ -20,7 +20,7 @@ pub struct ModuleList { · 5 unmodified lines | ||
| 20 | 20 | focus_handle: FocusHandle, |
| 21 | 21 | scrollbar_state: ScrollbarState, |
| 22 | 22 | entries: Vec<Module>, |
| 23 | − _rebuild_task: Task<()>, | |
| 23 | + _rebuild_task: Option<Task<()>>, | |
| 24 | 24 | _subscription: Subscription, |
| 25 | 25 | } |
| 26 | 26 | |
| @@ -34,14 +34,16 @@ impl ModuleList { · 7 unmodified lines | ||
| 34 | 34 | |
| 35 | 35 | let _subscription = cx.subscribe(&session, |this, _, event, cx| match event { |
| 36 | 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 | 44 | let scroll_handle = UniformListScrollHandle::new(); |
| 43 | 45 | |
| 44 | − let mut this = Self { | |
| 46 | + Self { | |
| 45 | 47 | scrollbar_state: ScrollbarState::new(scroll_handle.clone()), |
| 46 | 48 | scroll_handle, |
| 47 | 49 | session, |
| @@ -50,14 +52,12 @@ impl ModuleList { · 2 unmodified lines | ||
| 50 | 52 | entries: Vec::new(), |
| 51 | 53 | selected_ix: None, |
| 52 | 54 | _subscription, |
| 53 | − _rebuild_task: Task::ready(()), | |
| 54 | − }; | |
| 55 | − this.schedule_rebuild(cx); | |
| 56 | − this | |
| 55 | + _rebuild_task: None, | |
| 56 | + } | |
| 57 | 57 | } |
| 58 | 58 | |
| 59 | 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 | 61 | this.update(cx, |this, cx| { |
| 62 | 62 | let modules = this |
| 63 | 63 | .session |
| @@ -66,7 +66,7 @@ impl ModuleList { · 2 unmodified lines | ||
| 66 | 66 | cx.notify(); |
| 67 | 67 | }) |
| 68 | 68 | .ok(); |
| 69 | − }); | |
| 69 | + })); | |
| 70 | 70 | } |
| 71 | 71 | |
| 72 | 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 | 282 | fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| 283 | 283 | uniform_list( |
| 284 | − cx.entity(), | |
| 285 | 284 | "module-list", |
| 286 | 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 | 290 | .track_scroll(self.scroll_handle.clone()) |
| 290 | 291 | .size_full() |
| @@ -299,6 +300,9 @@ impl Focusable for ModuleList { · 8 unmodified lines | ||
| 299 | 300 | |
| 300 | 301 | impl Render for ModuleList { |
| 301 | 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 | 306 | div() |
| 303 | 307 | .track_focus(&self.focus_handle) |
| 304 | 308 | .on_action(cx.listener(Self::select_last)) |
| @@ -5,9 +5,10 @@ use std::time::Duration; · 4 unmodified lines | ||
| 5 | 5 | use anyhow::{Context as _, Result, anyhow}; |
| 6 | 6 | use dap::StackFrameId; |
| 7 | 7 | use gpui::{ |
| 8 | − AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy, | |
| 9 | − Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list, | |
| 8 | + AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton, | |
| 9 | + Stateful, Subscription, Task, WeakEntity, list, | |
| 10 | 10 | }; |
| 11 | +use util::debug_panic; | |
| 11 | 12 | |
| 12 | 13 | use crate::StackTraceView; |
| 13 | 14 | use language::PointUtf16; |
| @@ -35,13 +36,16 @@ pub struct StackFrameList { · 21 unmodified lines | ||
| 35 | 36 | selected_ix: Option<usize>, |
| 36 | 37 | opened_stack_frame_id: Option<StackFrameId>, |
| 37 | 38 | scrollbar_state: ScrollbarState, |
| 38 | − scroll_handle: UniformListScrollHandle, | |
| 39 | + list_state: ListState, | |
| 40 | + error: Option<SharedString>, | |
| 39 | 41 | _refresh_task: Task<()>, |
| 40 | 42 | } |
| 41 | 43 | |
| 42 | 44 | #[derive(Debug, PartialEq, Eq)] |
| 43 | 45 | pub enum StackFrameEntry { |
| 44 | 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 | 49 | Collapsed(Vec<dap::StackFrame>), |
| 46 | 50 | } |
| 47 | 51 | |
| @@ -54,7 +58,6 @@ impl StackFrameList { · 6 unmodified lines | ||
| 54 | 58 | cx: &mut Context<Self>, |
| 55 | 59 | ) -> Self { |
| 56 | 60 | let focus_handle = cx.focus_handle(); |
| 57 | − let scroll_handle = UniformListScrollHandle::new(); | |
| 58 | 61 | |
| 59 | 62 | let _subscription = |
| 60 | 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 | 82 | let mut this = Self { |
| 71 | − scrollbar_state: ScrollbarState::new(scroll_handle.clone()), | |
| 72 | 83 | session, |
| 73 | 84 | workspace, |
| 74 | 85 | focus_handle, |
| 75 | 86 | state, |
| 76 | 87 | _subscription, |
| 77 | 88 | entries: Default::default(), |
| 89 | + error: None, | |
| 78 | 90 | selected_ix: None, |
| 79 | 91 | opened_stack_frame_id: None, |
| 80 | − scroll_handle, | |
| 92 | + list_state, | |
| 93 | + scrollbar_state, | |
| 81 | 94 | _refresh_task: Task::ready(()), |
| 82 | 95 | }; |
| 83 | 96 | this.schedule_refresh(true, window, cx); |
| @@ -89,37 +102,35 @@ impl StackFrameList { · 5 unmodified lines | ||
| 89 | 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 | 110 | self.entries |
| 94 | 111 | .iter() |
| 95 | 112 | .flat_map(|frame| match frame { |
| 96 | 113 | StackFrameEntry::Normal(frame) => vec![frame.clone()], |
| 97 | − StackFrameEntry::Collapsed(frames) => { | |
| 98 | − if show_collapsed { | |
| 99 | − frames.clone() | |
| 100 | − } else { | |
| 101 | − vec![] | |
| 102 | − } | |
| 103 | − } | |
| 114 | + StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()], | |
| 115 | + StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(), | |
| 116 | + _ => vec![], | |
| 104 | 117 | }) |
| 105 | 118 | .collect::<Vec<_>>() |
| 106 | 119 | } |
| 107 | 120 | |
| 108 | − fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> { | |
| 109 | − self.state | |
| 110 | − .read_with(cx, |state, _| state.thread_id) | |
| 111 | − .ok() | |
| 112 | − .flatten() | |
| 113 | − .map(|thread_id| { | |
| 114 | − self.session | |
| 115 | − .update(cx, |this, cx| this.stack_frames(thread_id, cx)) | |
| 116 | − }) | |
| 117 | − .unwrap_or_default() | |
| 121 | + fn stack_frames(&self, cx: &mut App) -> Result<Vec<StackFrame>> { | |
| 122 | + if let Ok(Some(thread_id)) = self.state.read_with(cx, |state, _| state.thread_id) { | |
| 123 | + self.session | |
| 124 | + .update(cx, |this, cx| this.stack_frames(thread_id, cx)) | |
| 125 | + } else { | |
| 126 | + Ok(Vec::default()) | |
| 127 | + } | |
| 118 | 128 | } |
| 119 | 129 | |
| 120 | 130 | #[cfg(test)] |
| 121 | 131 | pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> { |
| 122 | 132 | self.stack_frames(cx) |
| 133 | + .unwrap_or_default() | |
| 123 | 134 | .into_iter() |
| 124 | 135 | .map(|stack_frame| stack_frame.dap.clone()) |
| 125 | 136 | .collect() |
| @@ -141,7 +152,7 @@ impl StackFrameList { · 15 unmodified lines | ||
| 141 | 152 | let debounce = this |
| 142 | 153 | .update(cx, |this, cx| { |
| 143 | 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 | 157 | .ok() |
| 147 | 158 | .unwrap_or_default(); |
| @@ -168,20 +179,34 @@ impl StackFrameList { · 20 unmodified lines | ||
| 168 | 179 | .and_then(|ix| self.entries.get(ix)) |
| 169 | 180 | .and_then(|entry| match entry { |
| 170 | 181 | StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id), |
| 171 | − StackFrameEntry::Collapsed(stack_frames) => { | |
| 172 | − stack_frames.first().map(|stack_frame| stack_frame.id) | |
| 173 | − } | |
| 182 | + StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => None, | |
| 174 | 183 | }); |
| 175 | 184 | let mut entries = Vec::new(); |
| 176 | 185 | let mut collapsed_entries = Vec::new(); |
| 177 | 186 | let mut first_stack_frame = None; |
| 178 | − | |
| 179 | − let stack_frames = self.stack_frames(cx); | |
| 187 | + let mut first_stack_frame_with_path = None; | |
| 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 | 201 | for stack_frame in &stack_frames { |
| 181 | 202 | match stack_frame.dap.presentation_hint { |
| 182 | − Some(dap::StackFramePresentationHint::Deemphasize) => { | |
| 203 | + Some(dap::StackFramePresentationHint::Deemphasize) | |
| 204 | + | Some(dap::StackFramePresentationHint::Subtle) => { | |
| 183 | 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 | 211 | let collapsed_entries = std::mem::take(&mut collapsed_entries); |
| 187 | 212 | if !collapsed_entries.is_empty() { |
| @@ -189,6 +214,15 @@ impl StackFrameList { · 1 unmodified line | ||
| 189 | 214 | } |
| 190 | 215 | |
| 191 | 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 | 226 | entries.push(StackFrameEntry::Normal(stack_frame.dap.clone())); |
| 193 | 227 | } |
| 194 | 228 | } |
| @@ -198,22 +232,23 @@ impl StackFrameList { · 3 unmodified lines | ||
| 198 | 232 | if !collapsed_entries.is_empty() { |
| 199 | 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); | |
| 203 | − | |
| 204 | − if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) { | |
| 237 | + if let Some(ix) = first_stack_frame_with_path | |
| 238 | + .or(first_stack_frame) | |
| 239 | + .filter(|_| open_first_stack_frame) | |
| 240 | + { | |
| 205 | 241 | self.select_ix(Some(ix), cx); |
| 206 | 242 | self.activate_selected_entry(window, cx); |
| 207 | 243 | } else if let Some(old_selected_frame_id) = old_selected_frame_id { |
| 208 | 244 | let ix = self.entries.iter().position(|entry| match entry { |
| 209 | 245 | StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id, |
| 210 | − StackFrameEntry::Collapsed(frames) => { | |
| 211 | − frames.iter().any(|frame| frame.id == old_selected_frame_id) | |
| 212 | − } | |
| 246 | + StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => false, | |
| 213 | 247 | }); |
| 214 | 248 | self.selected_ix = ix; |
| 215 | 249 | } |
| 216 | 250 | |
| 251 | + self.list_state.reset(self.entries.len()); | |
| 217 | 252 | cx.emit(StackFrameListEvent::BuiltEntries); |
| 218 | 253 | cx.notify(); |
| 219 | 254 | } |
| @@ -228,6 +263,7 @@ impl StackFrameList { · 8 unmodified lines | ||
| 228 | 263 | .entries |
| 229 | 264 | .iter() |
| 230 | 265 | .flat_map(|entry| match entry { |
| 266 | + StackFrameEntry::Label(stack_frame) => std::slice::from_ref(stack_frame), | |
| 231 | 267 | StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame), |
| 232 | 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 | 406 | fn render_normal_entry( |
| 344 | 407 | &self, |
| 345 | 408 | ix: usize, |
| @@ -443,7 +506,7 @@ impl StackFrameList { · 97 unmodified lines | ||
| 443 | 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 | 510 | let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else { |
| 448 | 511 | return; |
| 449 | 512 | }; |
| @@ -452,6 +515,9 @@ impl StackFrameList { · 2 unmodified lines | ||
| 452 | 515 | .map(StackFrameEntry::Normal); |
| 453 | 516 | self.entries.splice(ix..ix + 1, entries); |
| 454 | 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 | 523 | fn render_collapsed_entry( |
| @@ -501,6 +567,7 @@ impl StackFrameList { · 43 unmodified lines | ||
| 501 | 567 | |
| 502 | 568 | fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement { |
| 503 | 569 | match &self.entries[ix] { |
| 570 | + StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx), | |
| 504 | 571 | StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx), |
| 505 | 572 | StackFrameEntry::Collapsed(stack_frames) => { |
| 506 | 573 | self.render_collapsed_entry(ix, stack_frames, cx) |
| @@ -543,10 +610,6 @@ impl StackFrameList { · 36 unmodified lines | ||
| 543 | 610 | |
| 544 | 611 | fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) { |
| 545 | 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 | 613 | cx.notify(); |
| 551 | 614 | } |
| 552 | 615 | |
| @@ -621,7 +684,10 @@ impl StackFrameList { · 68 unmodified lines | ||
| 621 | 684 | self.go_to_stack_frame_inner(stack_frame, window, cx) |
| 622 | 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 | 692 | cx.notify(); |
| 627 | 693 | } |
| @@ -630,15 +696,11 @@ impl StackFrameList { · 2 unmodified lines | ||
| 630 | 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 { | |
| 634 | − uniform_list( | |
| 635 | − cx.entity(), | |
| 636 | − "stack-frame-list", | |
| 637 | − self.entries.len(), | |
| 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() | |
| 699 | + fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { | |
| 700 | + div() | |
| 701 | + .p_1() | |
| 702 | + .size_full() | |
| 703 | + .child(list(self.list_state.clone()).size_full()) | |
| 642 | 704 | } |
| 643 | 705 | } |
| 644 | 706 | |
| @@ -647,12 +709,27 @@ impl Render for StackFrameList { · 2 unmodified lines | ||
| 647 | 709 | div() |
| 648 | 710 | .track_focus(&self.focus_handle) |
| 649 | 711 | .size_full() |
| 650 | − .p_1() | |
| 651 | 712 | .on_action(cx.listener(Self::select_next)) |
| 652 | 713 | .on_action(cx.listener(Self::select_previous)) |
| 653 | 714 | .on_action(cx.listener(Self::select_first)) |
| 654 | 715 | .on_action(cx.listener(Self::select_last)) |
| 655 | 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 | 733 | .child(self.render_list(window, cx)) |
| 657 | 734 | .child(self.render_vertical_scrollbar(cx)) |
| 658 | 735 | } |
| @@ -980,10 +980,11 @@ impl Render for VariableList { · 979 unmodified lines | ||
| 980 | 980 | .on_action(cx.listener(Self::edit_variable)) |
| 981 | 981 | .child( |
| 982 | 982 | uniform_list( |
| 983 | − cx.entity().clone(), | |
| 984 | 983 | "variable-list", |
| 985 | 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 | 989 | .track_scroll(self.list_handle.clone()) |
| 989 | 990 | .gap_1_5() |
| @@ -16,7 +16,7 @@ use ui::{ActiveTheme as _, Context, ParentElement as _, Styled as _, div}; · 15 unmodified lines | ||
| 16 | 16 | use util::ResultExt as _; |
| 17 | 17 | use workspace::{ |
| 18 | 18 | Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, |
| 19 | − item::{BreadcrumbText, ItemEvent}, | |
| 19 | + item::{BreadcrumbText, ItemEvent, SaveOptions}, | |
| 20 | 20 | searchable::SearchableItemHandle, |
| 21 | 21 | }; |
| 22 | 22 | |
| @@ -148,7 +148,7 @@ impl StackTraceView { · 125 unmodified lines | ||
| 148 | 148 | |
| 149 | 149 | let stack_frames = self |
| 150 | 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 | 153 | let frames_to_open: Vec<_> = stack_frames |
| 154 | 154 | .into_iter() |
| @@ -237,7 +237,7 @@ impl StackTraceView { · 82 unmodified lines | ||
| 237 | 237 | |
| 238 | 238 | let stack_frames = self |
| 239 | 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 | 242 | let active_idx = self |
| 243 | 243 | .selected_stack_frame_id |
| @@ -386,12 +386,12 @@ impl Item for StackTraceView { · 142 unmodified lines | ||
| 386 | 386 | |
| 387 | 387 | fn save( |
| 388 | 388 | &mut self, |
| 389 | − format: bool, | |
| 389 | + options: SaveOptions, | |
| 390 | 390 | project: Entity<Project>, |
| 391 | 391 | window: &mut Window, |
| 392 | 392 | cx: &mut Context<Self>, |
| 393 | 393 | ) -> Task<Result<()>> { |
| 394 | − self.editor.save(format, project, window, cx) | |
| 394 | + self.editor.save(options, project, window, cx) | |
| 395 | 395 | } |
| 396 | 396 | |
| 397 | 397 | fn save_as( |
| @@ -33,6 +33,7 @@ use std::{ · 32 unmodified lines | ||
| 33 | 33 | use terminal_view::terminal_panel::TerminalPanel; |
| 34 | 34 | use tests::{active_debug_session_panel, init_test, init_test_workspace}; |
| 35 | 35 | use util::path; |
| 36 | +use workspace::item::SaveOptions; | |
| 36 | 37 | use workspace::{Item, dock::Panel}; |
| 37 | 38 | |
| 38 | 39 | #[gpui::test] |
| @@ -443,18 +444,20 @@ async fn test_handle_start_debugging_request( · 404 unmodified lines | ||
| 443 | 444 | .update(cx, |workspace, _window, cx| { |
| 444 | 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 | 448 | let active_session = debug_panel |
| 448 | 449 | .read(cx) |
| 449 | 450 | .active_session() |
| 450 | 451 | .unwrap() |
| 451 | 452 | .read(cx) |
| 452 | 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 | 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 | 461 | assert_eq!(current_sessions.len(), 2); |
| 459 | 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 | 472 | // We should preserve the original binary (params to spawn process etc.) except for launch params |
| 470 | 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 | 475 | original_binary.request_args = StartDebuggingRequestArguments { |
| 473 | 476 | request: StartDebuggingRequestArgumentsRequest::Launch, |
| 474 | 477 | configuration: fake_config.clone(), |
| 475 | 478 | }; |
| 476 | 479 | |
| 477 | 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 | 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 | 1222 | editor |
| 1215 | 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 | 1234 | .await |
| 1219 | 1235 | .unwrap(); |
| @@ -718,7 +718,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo · 717 unmodified lines | ||
| 718 | 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 | 723 | assert_eq!( |
| 724 | 724 | &vec![ |
| @@ -735,7 +735,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo · 10 unmodified lines | ||
| 735 | 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 | 740 | assert_eq!( |
| 741 | 741 | &vec![ |
| @@ -191,7 +191,10 @@ async fn test_basic_fetch_initial_scope_and_variables( · 190 unmodified lines | ||
| 191 | 191 | running_state.update(cx, |running_state, cx| { |
| 192 | 192 | let (stack_frame_list, stack_frame_id) = |
| 193 | 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 | 200 | assert_eq!(stack_frames, stack_frame_list); |
| @@ -432,7 +435,10 @@ async fn test_fetch_variables_for_multiple_scopes( · 234 unmodified lines | ||
| 432 | 435 | running_state.update(cx, |running_state, cx| { |
| 433 | 436 | let (stack_frame_list, stack_frame_id) = |
| 434 | 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 | 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 | 1465 | running_state.update(cx, |running_state, cx| { |
| 1460 | 1466 | let (stack_frame_list, stack_frame_id) = |
| 1461 | 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 | 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 | 1750 | running_state.update(cx, |running_state, cx| { |
| 1742 | 1751 | let (stack_frame_list, stack_frame_id) = |
| 1743 | 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 | 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 | 1808 | running_state.update(cx, |running_state, cx| { |
| 1797 | 1809 | let (stack_frame_list, stack_frame_id) = |
| 1798 | 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 | 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 | 43 | use util::ResultExt; |
| 44 | 44 | use workspace::{ |
| 45 | 45 | ItemNavHistory, ToolbarItemLocation, Workspace, |
| 46 | − item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, | |
| 46 | + item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, | |
| 47 | 47 | searchable::SearchableItemHandle, |
| 48 | 48 | }; |
| 49 | 49 | |
| @@ -849,12 +849,12 @@ impl Item for ProjectDiagnosticsEditor { · 799 unmodified lines | ||
| 849 | 849 | |
| 850 | 850 | fn save( |
| 851 | 851 | &mut self, |
| 852 | − format: bool, | |
| 852 | + options: SaveOptions, | |
| 853 | 853 | project: Entity<Project>, |
| 854 | 854 | window: &mut Window, |
| 855 | 855 | cx: &mut Context<Self>, |
| 856 | 856 | ) -> Task<Result<()>> { |
| 857 | − self.editor.save(format, project, window, cx) | |
| 857 | + self.editor.save(options, project, window, cx) | |
| 858 | 858 | } |
| 859 | 859 | |
| 860 | 860 | fn save_as( |
| @@ -35,7 +35,6 @@ assets.workspace = true · 34 unmodified lines | ||
| 35 | 35 | client.workspace = true |
| 36 | 36 | clock.workspace = true |
| 37 | 37 | collections.workspace = true |
| 38 | −command_palette_hooks.workspace = true | |
| 39 | 38 | convert_case.workspace = true |
| 40 | 39 | dap.workspace = true |
| 41 | 40 | db.workspace = true |
| @@ -87,6 +87,7 @@ pub struct ToggleCodeActions { · 86 unmodified lines | ||
| 87 | 87 | #[derive(PartialEq, Clone, Debug)] |
| 88 | 88 | pub enum CodeActionSource { |
| 89 | 89 | Indicator(DisplayRow), |
| 90 | + RunMenu(DisplayRow), | |
| 90 | 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 | 249 | actions!( |
| 247 | 250 | editor, |
| 248 | 251 | [ |
| @@ -426,8 +429,6 @@ actions!( · 177 unmodified lines | ||
| 426 | 429 | DisableBreakpoint, |
| 427 | 430 | EnableBreakpoint, |
| 428 | 431 | EditLogBreakpoint, |
| 429 | − DebuggerRunToCursor, | |
| 430 | − DebuggerEvaluateSelectedText, | |
| 431 | 432 | ToggleAutoSignatureHelp, |
| 432 | 433 | ToggleGitBlameInline, |
| 433 | 434 | OpenGitBlameCommit, |
| @@ -1,2382 +1,495 @@ | ||
| 1 | −use crate::{ | |
| 2 | − code_context_menus::{CompletionsMenu, SortableMatch}, | |
| 3 | − editor_settings::SnippetSortOrder, | |
| 4 | −}; | |
| 5 | −use fuzzy::StringMatch; | |
| 1 | +use crate::{code_context_menus::CompletionsMenu, editor_settings::SnippetSortOrder}; | |
| 2 | +use fuzzy::StringMatchCandidate; | |
| 6 | 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 | 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 | 13 | // Case 1: "foo" |
| 11 | − let query: Option<&str> = Some("foo"); | |
| 12 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 13 | − SortableMatch { | |
| 14 | − string_match: StringMatch { | |
| 15 | − candidate_id: 0, | |
| 16 | − score: 0.2727272727272727, | |
| 17 | − positions: vec![], | |
| 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 | − }, | |
| 14 | + let completions = vec![ | |
| 15 | + CompletionBuilder::constant("foo_bar_baz", "7fffffff"), | |
| 16 | + CompletionBuilder::variable("foo_bar_qux", "7ffffffe"), | |
| 17 | + CompletionBuilder::constant("floorf64", "80000000"), | |
| 18 | + CompletionBuilder::constant("floorf32", "80000000"), | |
| 19 | + CompletionBuilder::constant("floorf16", "80000000"), | |
| 20 | + CompletionBuilder::constant("floorf128", "80000000"), | |
| 85 | 21 | ]; |
| 86 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 87 | − assert_eq!( | |
| 88 | − matches[0].string_match.string.as_str(), | |
| 89 | − "foo_bar_qux", | |
| 90 | − "Match order not expected" | |
| 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 | − ); | |
| 22 | + let matches = sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await; | |
| 23 | + assert_eq!(matches[0], "foo_bar_qux"); | |
| 24 | + assert_eq!(matches[1], "foo_bar_baz"); | |
| 25 | + assert_eq!(matches[2], "floorf16"); | |
| 26 | + assert_eq!(matches[3], "floorf32"); | |
| 107 | 27 | |
| 108 | 28 | // Case 2: "foobar" |
| 109 | − let query: Option<&str> = Some("foobar"); | |
| 110 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 111 | − SortableMatch { | |
| 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 | − }, | |
| 29 | + let completions = vec![ | |
| 30 | + CompletionBuilder::constant("foo_bar_baz", "7fffffff"), | |
| 31 | + CompletionBuilder::variable("foo_bar_qux", "7ffffffe"), | |
| 135 | 32 | ]; |
| 136 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 137 | − assert_eq!( | |
| 138 | − matches[0].string_match.string.as_str(), | |
| 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 | − ); | |
| 33 | + let matches = sort_matches("foobar", &completions, SnippetSortOrder::default(), cx).await; | |
| 34 | + assert_eq!(matches[0], "foo_bar_qux"); | |
| 35 | + assert_eq!(matches[1], "foo_bar_baz"); | |
| 147 | 36 | } |
| 148 | 37 | |
| 149 | 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 | 40 | // Case 1: "ele" |
| 152 | − let query: Option<&str> = Some("ele"); | |
| 153 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 154 | − SortableMatch { | |
| 155 | − string_match: StringMatch { | |
| 156 | − candidate_id: 0, | |
| 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 | − }, | |
| 41 | + let completions = vec![ | |
| 42 | + CompletionBuilder::constant("ElementType", "7fffffff"), | |
| 43 | + CompletionBuilder::variable("element_type", "7ffffffe"), | |
| 44 | + CompletionBuilder::constant("simd_select", "80000000"), | |
| 45 | + CompletionBuilder::keyword("while let", "7fffffff"), | |
| 202 | 46 | ]; |
| 203 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 204 | − assert_eq!( | |
| 205 | − matches[0].string_match.string.as_str(), | |
| 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 | − ); | |
| 47 | + let matches = sort_matches("ele", &completions, SnippetSortOrder::default(), cx).await; | |
| 48 | + assert_eq!(matches[0], "element_type"); | |
| 49 | + assert_eq!(matches[1], "ElementType"); | |
| 214 | 50 | |
| 215 | 51 | // Case 2: "eleme" |
| 216 | − let query: Option<&str> = Some("eleme"); | |
| 217 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 218 | − SortableMatch { | |
| 219 | − string_match: StringMatch { | |
| 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 | − }, | |
| 52 | + let completions = vec![ | |
| 53 | + CompletionBuilder::constant("ElementType", "7fffffff"), | |
| 54 | + CompletionBuilder::variable("element_type", "7ffffffe"), | |
| 55 | + CompletionBuilder::constant("REPLACEMENT_CHARACTER", "80000000"), | |
| 254 | 56 | ]; |
| 255 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 256 | − assert_eq!( | |
| 257 | − matches[0].string_match.string.as_str(), | |
| 258 | − "element_type", | |
| 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 | − ); | |
| 57 | + let matches = sort_matches("eleme", &completions, SnippetSortOrder::default(), cx).await; | |
| 58 | + assert_eq!(matches[0], "element_type"); | |
| 59 | + assert_eq!(matches[1], "ElementType"); | |
| 60 | +} | |
| 266 | 61 | |
| 267 | − // Case 3: "Elem" | |
| 268 | − let query: Option<&str> = Some("Elem"); | |
| 269 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 270 | − SortableMatch { | |
| 271 | − string_match: StringMatch { | |
| 272 | − candidate_id: 0, | |
| 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 | − }, | |
| 62 | +#[gpui::test] | |
| 63 | +async fn test_sort_matches_capitalization(cx: &mut TestAppContext) { | |
| 64 | + // Case 1: "Elem" | |
| 65 | + let completions = vec![ | |
| 66 | + CompletionBuilder::constant("ElementType", "7fffffff"), | |
| 67 | + CompletionBuilder::variable("element_type", "7ffffffe"), | |
| 294 | 68 | ]; |
| 295 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 296 | − assert_eq!( | |
| 297 | − matches[0].string_match.string.as_str(), | |
| 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 | − ); | |
| 69 | + let matches = sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await; | |
| 70 | + assert_eq!(matches[0], "ElementType"); | |
| 71 | + assert_eq!(matches[1], "element_type"); | |
| 306 | 72 | } |
| 307 | 73 | |
| 308 | 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 | 76 | // Case 1: "unre" |
| 311 | − let query: Option<&str> = Some("unre"); | |
| 312 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 313 | − SortableMatch { | |
| 314 | − string_match: StringMatch { | |
| 315 | − candidate_id: 0, | |
| 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 | − }, | |
| 77 | + let completions = vec![ | |
| 78 | + CompletionBuilder::function("unreachable", "80000000"), | |
| 79 | + CompletionBuilder::function("unreachable!(…)", "7fffffff"), | |
| 80 | + CompletionBuilder::function("unchecked_rem", "80000000"), | |
| 81 | + CompletionBuilder::function("unreachable_unchecked", "80000000"), | |
| 361 | 82 | ]; |
| 362 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 363 | − assert_eq!( | |
| 364 | − matches[0].string_match.string.as_str(), | |
| 365 | − "unreachable!(…)", | |
| 366 | − "Match order not expected" | |
| 367 | − ); | |
| 83 | + let matches = sort_matches("unre", &completions, SnippetSortOrder::default(), cx).await; | |
| 84 | + assert_eq!(matches[0], "unreachable!(…)"); | |
| 368 | 85 | |
| 369 | 86 | // Case 2: "unrea" |
| 370 | − let query: Option<&str> = Some("unrea"); | |
| 371 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 372 | − SortableMatch { | |
| 373 | − string_match: StringMatch { | |
| 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 | − }, | |
| 87 | + let completions = vec![ | |
| 88 | + CompletionBuilder::function("unreachable", "80000000"), | |
| 89 | + CompletionBuilder::function("unreachable!(…)", "7fffffff"), | |
| 90 | + CompletionBuilder::function("unreachable_unchecked", "80000000"), | |
| 408 | 91 | ]; |
| 409 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 410 | − assert_eq!( | |
| 411 | − matches[0].string_match.string.as_str(), | |
| 412 | − "unreachable!(…)", | |
| 413 | − "Match order not expected" | |
| 414 | − ); | |
| 92 | + let matches = sort_matches("unrea", &completions, SnippetSortOrder::default(), cx).await; | |
| 93 | + assert_eq!(matches[0], "unreachable!(…)"); | |
| 415 | 94 | |
| 416 | 95 | // Case 3: "unreach" |
| 417 | − let query: Option<&str> = Some("unreach"); | |
| 418 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 419 | − SortableMatch { | |
| 420 | − string_match: StringMatch { | |
| 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 | − }, | |
| 96 | + let completions = vec![ | |
| 97 | + CompletionBuilder::function("unreachable", "80000000"), | |
| 98 | + CompletionBuilder::function("unreachable!(…)", "7fffffff"), | |
| 99 | + CompletionBuilder::function("unreachable_unchecked", "80000000"), | |
| 455 | 100 | ]; |
| 456 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 457 | − assert_eq!( | |
| 458 | − matches[0].string_match.string.as_str(), | |
| 459 | − "unreachable!(…)", | |
| 460 | − "Match order not expected" | |
| 461 | − ); | |
| 101 | + let matches = sort_matches("unreach", &completions, SnippetSortOrder::default(), cx).await; | |
| 102 | + assert_eq!(matches[0], "unreachable!(…)"); | |
| 462 | 103 | |
| 463 | 104 | // Case 4: "unreachabl" |
| 464 | − let query: Option<&str> = Some("unreachable"); | |
| 465 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 466 | − SortableMatch { | |
| 467 | − string_match: StringMatch { | |
| 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 | − }, | |
| 105 | + let completions = vec![ | |
| 106 | + CompletionBuilder::function("unreachable", "80000000"), | |
| 107 | + CompletionBuilder::function("unreachable!(…)", "7fffffff"), | |
| 108 | + CompletionBuilder::function("unreachable_unchecked", "80000000"), | |
| 502 | 109 | ]; |
| 503 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 504 | − assert_eq!( | |
| 505 | − matches[0].string_match.string.as_str(), | |
| 506 | − "unreachable!(…)", | |
| 507 | − "Match order not expected" | |
| 508 | − ); | |
| 110 | + let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await; | |
| 111 | + assert_eq!(matches[0], "unreachable!(…)"); | |
| 509 | 112 | |
| 510 | 113 | // Case 5: "unreachable" |
| 511 | − let query: Option<&str> = Some("unreachable"); | |
| 512 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 513 | − SortableMatch { | |
| 514 | − string_match: StringMatch { | |
| 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 | − }, | |
| 114 | + let completions = vec![ | |
| 115 | + CompletionBuilder::function("unreachable", "80000000"), | |
| 116 | + CompletionBuilder::function("unreachable!(…)", "7fffffff"), | |
| 117 | + CompletionBuilder::function("unreachable_unchecked", "80000000"), | |
| 549 | 118 | ]; |
| 550 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 551 | − assert_eq!( | |
| 552 | − matches[0].string_match.string.as_str(), | |
| 553 | − "unreachable!(…)", | |
| 554 | − "LSP should take over even when fuzzy perfect matches" | |
| 555 | − ); | |
| 119 | + let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await; | |
| 120 | + assert_eq!(matches[0], "unreachable!(…)"); | |
| 556 | 121 | } |
| 557 | 122 | |
| 558 | 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 | 125 | // Case 1: "var" as variable |
| 561 | − let query: Option<&str> = Some("var"); | |
| 562 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 563 | − SortableMatch { | |
| 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 | − }, | |
| 126 | + let completions = vec![ | |
| 127 | + CompletionBuilder::function("var", "7fffffff"), | |
| 128 | + CompletionBuilder::variable("var", "7fffffff"), | |
| 587 | 129 | ]; |
| 588 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 589 | − assert_eq!( | |
| 590 | − matches[0].string_match.candidate_id, 1, | |
| 591 | − "Match order not expected" | |
| 592 | − ); | |
| 593 | − assert_eq!( | |
| 594 | − matches[1].string_match.candidate_id, 0, | |
| 595 | − "Match order not expected" | |
| 596 | − ); | |
| 130 | + let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await; | |
| 131 | + assert_eq!(matches[0], "var"); | |
| 132 | + assert_eq!(matches[1], "var"); | |
| 597 | 133 | |
| 598 | − // Case 2: "var" as constant | |
| 599 | − let query: Option<&str> = Some("var"); | |
| 600 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 601 | − SortableMatch { | |
| 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 | − }, | |
| 134 | + // Case 2: "var" as constant | |
| 135 | + let completions = vec![ | |
| 136 | + CompletionBuilder::function("var", "7fffffff"), | |
| 137 | + CompletionBuilder::constant("var", "7fffffff"), | |
| 625 | 138 | ]; |
| 626 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 627 | − assert_eq!( | |
| 628 | − matches[0].string_match.candidate_id, 1, | |
| 629 | − "Match order not expected" | |
| 630 | − ); | |
| 631 | − assert_eq!( | |
| 632 | − matches[1].string_match.candidate_id, 0, | |
| 633 | − "Match order not expected" | |
| 634 | − ); | |
| 139 | + let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await; | |
| 140 | + assert_eq!(matches[0], "var"); | |
| 141 | + assert_eq!(matches[1], "var"); | |
| 635 | 142 | } |
| 636 | 143 | |
| 637 | 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 | 146 | // Case 1: "on" |
| 640 | − let query: Option<&str> = Some("on"); | |
| 641 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 642 | − SortableMatch { | |
| 643 | − string_match: StringMatch { | |
| 644 | − candidate_id: 0, | |
| 645 | − score: 0.3333333333333333, | |
| 646 | − positions: vec![], | |
| 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 | − }, | |
| 147 | + let completions = vec![ | |
| 148 | + CompletionBuilder::function("onCut?", "12"), | |
| 149 | + CompletionBuilder::function("onPlay?", "12"), | |
| 150 | + CompletionBuilder::function("color?", "12"), | |
| 151 | + CompletionBuilder::function("defaultValue?", "12"), | |
| 152 | + CompletionBuilder::function("style?", "12"), | |
| 153 | + CompletionBuilder::function("className?", "12"), | |
| 714 | 154 | ]; |
| 715 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 716 | − assert_eq!( | |
| 717 | − matches[0].string_match.string, "onCut?", | |
| 718 | − "Match order not expected" | |
| 719 | − ); | |
| 720 | − assert_eq!( | |
| 721 | − matches[1].string_match.string, "onPlay?", | |
| 722 | − "Match order not expected" | |
| 723 | − ); | |
| 155 | + let matches = sort_matches("on", &completions, SnippetSortOrder::default(), cx).await; | |
| 156 | + assert_eq!(matches[0], "onCut?"); | |
| 157 | + assert_eq!(matches[1], "onPlay?"); | |
| 724 | 158 | |
| 725 | 159 | // Case 2: "ona" |
| 726 | − let query: Option<&str> = Some("ona"); | |
| 727 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 728 | − SortableMatch { | |
| 729 | − string_match: StringMatch { | |
| 730 | − candidate_id: 0, | |
| 731 | − score: 0.375, | |
| 732 | − positions: vec![], | |
| 733 | − string: "onAbort?".to_string(), | |
| 734 | − }, | |
| 735 | − is_snippet: false, | |
| 736 | − sort_text: Some("12"), | |
| 737 | − sort_kind: 3, | |
| 738 | − sort_label: "onAbort?", | |
| 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 | − }, | |
| 160 | + let completions = vec![ | |
| 161 | + CompletionBuilder::function("onAbort?", "12"), | |
| 162 | + CompletionBuilder::function("onAuxClick?", "12"), | |
| 163 | + CompletionBuilder::function("onPlay?", "12"), | |
| 164 | + CompletionBuilder::function("onLoad?", "12"), | |
| 165 | + CompletionBuilder::function("onDrag?", "12"), | |
| 166 | + CompletionBuilder::function("onPause?", "12"), | |
| 167 | + CompletionBuilder::function("onPaste?", "12"), | |
| 168 | + CompletionBuilder::function("onAnimationEnd?", "12"), | |
| 169 | + CompletionBuilder::function("onAbortCapture?", "12"), | |
| 170 | + CompletionBuilder::function("onChange?", "12"), | |
| 171 | + CompletionBuilder::function("onWaiting?", "12"), | |
| 172 | + CompletionBuilder::function("onCanPlay?", "12"), | |
| 1064 | 173 | ]; |
| 1065 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 1066 | − assert_eq!( | |
| 1067 | − matches | |
| 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 | − ); | |
| 174 | + let matches = sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await; | |
| 175 | + assert_eq!(matches[0], "onAbort?"); | |
| 176 | + assert_eq!(matches[1], "onAuxClick?"); | |
| 1087 | 177 | } |
| 1088 | 178 | |
| 1089 | 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 | 181 | // Case 1: "prin" |
| 1092 | − let query: Option<&str> = Some("prin"); | |
| 1093 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 1094 | − SortableMatch { | |
| 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 | − }, | |
| 182 | + let completions = vec![ | |
| 183 | + CompletionBuilder::constant("println", "80000000"), | |
| 184 | + CompletionBuilder::snippet("println!(…)", "80000000"), | |
| 1118 | 185 | ]; |
| 1119 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | |
| 1120 | − assert_eq!( | |
| 1121 | − matches[0].string_match.string.as_str(), | |
| 1122 | − "println!(…)", | |
| 1123 | − "Match order not expected" | |
| 1124 | − ); | |
| 186 | + let matches = sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await; | |
| 187 | + assert_eq!(matches[0], "println!(…)"); | |
| 1125 | 188 | } |
| 1126 | 189 | |
| 1127 | 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 | 192 | // Case 1: "set_text" |
| 1130 | − let query: Option<&str> = Some("set_text"); | |
| 1131 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 1132 | − SortableMatch { | |
| 1133 | − string_match: StringMatch { | |
| 1134 | − candidate_id: 0, | |
| 1135 | − score: 1.0, | |
| 1136 | − positions: vec![], | |
| 1137 | − string: "set_text".to_string(), | |
| 1138 | − }, | |
| 1139 | − is_snippet: false, | |
| 1140 | − sort_text: Some("7fffffff"), | |
| 1141 | − sort_kind: 3, | |
| 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 | − }, | |
| 193 | + let completions = vec![ | |
| 194 | + CompletionBuilder::function("set_text", "7fffffff"), | |
| 195 | + CompletionBuilder::function("set_placeholder_text", "7fffffff"), | |
| 196 | + CompletionBuilder::function("set_text_style_refinement", "7fffffff"), | |
| 197 | + CompletionBuilder::function("set_context_menu_options", "7fffffff"), | |
| 198 | + CompletionBuilder::function("select_to_next_word_end", "7fffffff"), | |
| 199 | + CompletionBuilder::function("select_to_next_subword_end", "7fffffff"), | |
| 200 | + CompletionBuilder::function("set_custom_context_menu", "7fffffff"), | |
| 201 | + CompletionBuilder::function("select_to_end_of_excerpt", "7fffffff"), | |
| 202 | + CompletionBuilder::function("select_to_start_of_excerpt", "7fffffff"), | |
| 203 | + CompletionBuilder::function("select_to_start_of_next_excerpt", "7fffffff"), | |
| 204 | + CompletionBuilder::function("select_to_end_of_previous_excerpt", "7fffffff"), | |
| 1264 | 205 | ]; |
| 1265 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | |
| 1266 | − assert_eq!( | |
| 1267 | − matches | |
| 1268 | − .iter() | |
| 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 | − ); | |
| 206 | + let matches = sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await; | |
| 207 | + assert_eq!(matches[0], "set_text"); | |
| 208 | + assert_eq!(matches[1], "set_text_style_refinement"); | |
| 209 | + assert_eq!(matches[2], "set_context_menu_options"); | |
| 1285 | 210 | } |
| 1286 | 211 | |
| 1287 | 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 | 214 | // Case 1: "set" |
| 1290 | − let query: Option<&str> = Some("set"); | |
| 1291 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 1292 | − SortableMatch { | |
| 1293 | − string_match: StringMatch { | |
| 1294 | − candidate_id: 0, | |
| 1295 | − score: 0.12631578947368421, | |
| 1296 | − positions: vec![], | |
| 1297 | − string: "select_to_beginning".to_string(), | |
| 1298 | − }, | |
| 1299 | − is_snippet: false, | |
| 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 | − }, | |
| 215 | + let completions = vec![ | |
| 216 | + CompletionBuilder::function("select_to_beginning", "7fffffff"), | |
| 217 | + CompletionBuilder::function("set_collapse_matches", "7fffffff"), | |
| 218 | + CompletionBuilder::function("set_autoindent", "7fffffff"), | |
| 219 | + CompletionBuilder::function("set_all_diagnostics_active", "7fffffff"), | |
| 220 | + CompletionBuilder::function("select_to_end_of_line", "7fffffff"), | |
| 221 | + CompletionBuilder::function("select_all", "7fffffff"), | |
| 222 | + CompletionBuilder::function("select_line", "7fffffff"), | |
| 223 | + CompletionBuilder::function("select_left", "7fffffff"), | |
| 224 | + CompletionBuilder::function("select_down", "7fffffff"), | |
| 1400 | 225 | ]; |
| 1401 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | |
| 1402 | − assert_eq!( | |
| 1403 | − matches | |
| 1404 | − .iter() | |
| 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 | − ); | |
| 226 | + let matches = sort_matches("set", &completions, SnippetSortOrder::Top, cx).await; | |
| 227 | + assert_eq!(matches[0], "set_autoindent"); | |
| 228 | + assert_eq!(matches[1], "set_collapse_matches"); | |
| 229 | + assert_eq!(matches[2], "set_all_diagnostics_active"); | |
| 1419 | 230 | } |
| 1420 | 231 | |
| 1421 | 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 | 234 | // Case 1: "awa" |
| 1424 | − let query: Option<&str> = Some("awa"); | |
| 1425 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 1426 | − SortableMatch { | |
| 1427 | − string_match: StringMatch { | |
| 1428 | − candidate_id: 0, | |
| 1429 | − score: 0.6000000000000001, | |
| 1430 | − positions: vec![], | |
| 1431 | − string: "await".to_string(), | |
| 1432 | − }, | |
| 1433 | − is_snippet: false, | |
| 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 | − }, | |
| 235 | + let completions = vec![ | |
| 236 | + CompletionBuilder::keyword("await", "7fffffff"), | |
| 237 | + CompletionBuilder::function("await.ne", "80000010"), | |
| 238 | + CompletionBuilder::function("await.eq", "80000010"), | |
| 239 | + CompletionBuilder::function("await.or", "7ffffff8"), | |
| 240 | + CompletionBuilder::function("await.zip", "80000006"), | |
| 241 | + CompletionBuilder::function("await.xor", "7ffffff8"), | |
| 242 | + CompletionBuilder::function("await.and", "80000006"), | |
| 243 | + CompletionBuilder::function("await.map", "80000006"), | |
| 244 | + CompletionBuilder::function("await.take", "7ffffff8"), | |
| 1534 | 245 | ]; |
| 1535 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | |
| 1536 | − assert_eq!( | |
| 1537 | − matches | |
| 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 | − ); | |
| 246 | + let matches = sort_matches("awa", &completions, SnippetSortOrder::Top, cx).await; | |
| 247 | + assert_eq!(matches[0], "await"); | |
| 248 | + | |
| 1553 | 249 | // Case 2: "await" |
| 1554 | − let query: Option<&str> = Some("await"); | |
| 1555 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 1556 | − SortableMatch { | |
| 1557 | − string_match: StringMatch { | |
| 1558 | − candidate_id: 0, | |
| 1559 | − score: 1.0, | |
| 1560 | − positions: vec![], | |
| 1561 | − string: "await".to_string(), | |
| 1562 | − }, | |
| 1563 | − is_snippet: false, | |
| 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 | − }, | |
| 250 | + let completions = vec![ | |
| 251 | + CompletionBuilder::keyword("await", "7fffffff"), | |
| 252 | + CompletionBuilder::function("await.ne", "80000010"), | |
| 253 | + CompletionBuilder::function("await.eq", "80000010"), | |
| 254 | + CompletionBuilder::function("await.or", "7ffffff8"), | |
| 255 | + CompletionBuilder::function("await.zip", "80000006"), | |
| 256 | + CompletionBuilder::function("await.xor", "7ffffff8"), | |
| 257 | + CompletionBuilder::function("await.and", "80000006"), | |
| 258 | + CompletionBuilder::function("await.map", "80000006"), | |
| 259 | + CompletionBuilder::function("await.take", "7ffffff8"), | |
| 1664 | 260 | ]; |
| 1665 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | |
| 1666 | − assert_eq!( | |
| 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 | − ); | |
| 261 | + let matches = sort_matches("await", &completions, SnippetSortOrder::Top, cx).await; | |
| 262 | + assert_eq!(matches[0], "await"); | |
| 1683 | 263 | } |
| 1684 | 264 | |
| 1685 | 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 | 267 | // Case 1: "__in" |
| 1688 | − let query: Option<&str> = Some("__in"); | |
| 1689 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 1690 | − SortableMatch { | |
| 1691 | − string_match: StringMatch { | |
| 1692 | − candidate_id: 211, | |
| 1693 | − score: 0.5, | |
| 1694 | − positions: vec![], | |
| 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 | − }, | |
| 268 | + let completions = vec![ | |
| 269 | + CompletionBuilder::function("__init__", "05.0003.__init__"), | |
| 270 | + CompletionBuilder::function("__init__", "05.0003"), | |
| 271 | + CompletionBuilder::function("__instancecheck__", "05.0005.__instancecheck__"), | |
| 272 | + CompletionBuilder::function("__init_subclass__", "05.0004.__init_subclass__"), | |
| 273 | + CompletionBuilder::function("__instancecheck__", "05.0005"), | |
| 274 | + CompletionBuilder::function("__init_subclass__", "05.0004"), | |
| 1762 | 275 | ]; |
| 1763 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | |
| 1764 | − assert_eq!( | |
| 1765 | − matches | |
| 1766 | − .iter() | |
| 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 | − ); | |
| 276 | + let matches = sort_matches("__in", &completions, SnippetSortOrder::Top, cx).await; | |
| 277 | + assert_eq!(matches[0], "__init__"); | |
| 278 | + assert_eq!(matches[1], "__init__"); | |
| 279 | + | |
| 1778 | 280 | // Case 2: "__ini" |
| 1779 | − let query: Option<&str> = Some("__ini"); | |
| 1780 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 1781 | − SortableMatch { | |
| 1782 | − string_match: StringMatch { | |
| 1783 | − candidate_id: 9, | |
| 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 | − }, | |
| 281 | + let completions = vec![ | |
| 282 | + CompletionBuilder::function("__init__", "05.0004.__init__"), | |
| 283 | + CompletionBuilder::function("__init__", "05.0004"), | |
| 284 | + CompletionBuilder::function("__init_subclass__", "05.0003.__init_subclass__"), | |
| 285 | + CompletionBuilder::function("__init_subclass__", "05.0003"), | |
| 1829 | 286 | ]; |
| 1830 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | |
| 1831 | − assert_eq!( | |
| 1832 | − matches | |
| 1833 | − .iter() | |
| 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 | − ); | |
| 287 | + let matches = sort_matches("__ini", &completions, SnippetSortOrder::Top, cx).await; | |
| 288 | + assert_eq!(matches[0], "__init__"); | |
| 289 | + assert_eq!(matches[1], "__init__"); | |
| 290 | + | |
| 1843 | 291 | // Case 3: "__init" |
| 1844 | − let query: Option<&str> = Some("__init"); | |
| 1845 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 1846 | − SortableMatch { | |
| 1847 | − string_match: StringMatch { | |
| 1848 | − candidate_id: 7, | |
| 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 | − }, | |
| 292 | + let completions = vec![ | |
| 293 | + CompletionBuilder::function("__init__", "05.0000.__init__"), | |
| 294 | + CompletionBuilder::function("__init__", "05.0000"), | |
| 295 | + CompletionBuilder::function("__init_subclass__", "05.0001.__init_subclass__"), | |
| 296 | + CompletionBuilder::function("__init_subclass__", "05.0001"), | |
| 1894 | 297 | ]; |
| 1895 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | |
| 1896 | − assert_eq!( | |
| 1897 | − matches | |
| 1898 | − .iter() | |
| 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 | − ); | |
| 298 | + let matches = sort_matches("__init", &completions, SnippetSortOrder::Top, cx).await; | |
| 299 | + assert_eq!(matches[0], "__init__"); | |
| 300 | + assert_eq!(matches[1], "__init__"); | |
| 301 | + | |
| 1908 | 302 | // Case 4: "__init_" |
| 1909 | − let query: Option<&str> = Some("__init_"); | |
| 1910 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 1911 | − SortableMatch { | |
| 1912 | − string_match: StringMatch { | |
| 1913 | − candidate_id: 4, | |
| 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 | − }, | |
| 303 | + let completions = vec![ | |
| 304 | + CompletionBuilder::function("__init__", "11.9999.__init__"), | |
| 305 | + CompletionBuilder::function("__init__", "11.9999"), | |
| 306 | + CompletionBuilder::function("__init_subclass__", "05.0000.__init_subclass__"), | |
| 307 | + CompletionBuilder::function("__init_subclass__", "05.0000"), | |
| 1959 | 308 | ]; |
| 1960 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); | |
| 1961 | − assert_eq!( | |
| 1962 | − matches | |
| 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 | − ); | |
| 309 | + let matches = sort_matches("__init_", &completions, SnippetSortOrder::Top, cx).await; | |
| 310 | + assert_eq!(matches[0], "__init__"); | |
| 311 | + assert_eq!(matches[1], "__init__"); | |
| 1973 | 312 | } |
| 1974 | 313 | |
| 1975 | 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 | 316 | // Case 1: "int" |
| 1978 | − let query: Option<&str> = Some("int"); | |
| 1979 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 1980 | − SortableMatch { | |
| 1981 | − string_match: StringMatch { | |
| 1982 | − candidate_id: 67, | |
| 1983 | − score: 0.75, | |
| 1984 | − positions: vec![], | |
| 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 | − }, | |
| 317 | + let completions = vec![ | |
| 318 | + CompletionBuilder::function("into", "80000004"), | |
| 319 | + CompletionBuilder::function("try_into", "80000004"), | |
| 320 | + CompletionBuilder::snippet("println", "80000004"), | |
| 321 | + CompletionBuilder::function("clone_into", "80000004"), | |
| 322 | + CompletionBuilder::function("into_searcher", "80000000"), | |
| 323 | + CompletionBuilder::snippet("eprintln", "80000004"), | |
| 2052 | 324 | ]; |
| 2053 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 2054 | − assert_eq!( | |
| 2055 | − matches[0].string_match.string.as_str(), | |
| 2056 | − "into", | |
| 2057 | − "Match order not expected" | |
| 2058 | − ); | |
| 325 | + let matches = sort_matches("int", &completions, SnippetSortOrder::default(), cx).await; | |
| 326 | + assert_eq!(matches[0], "into"); | |
| 327 | + | |
| 2059 | 328 | // Case 2: "into" |
| 2060 | − let query: Option<&str> = Some("into"); | |
| 2061 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 2062 | − SortableMatch { | |
| 2063 | − string_match: StringMatch { | |
| 2064 | − candidate_id: 65, | |
| 2065 | − score: 1.0, | |
| 2066 | − positions: vec![], | |
| 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 | − }, | |
| 329 | + let completions = vec![ | |
| 330 | + CompletionBuilder::function("into", "80000004"), | |
| 331 | + CompletionBuilder::function("try_into", "80000004"), | |
| 332 | + CompletionBuilder::function("clone_into", "80000004"), | |
| 333 | + CompletionBuilder::function("into_searcher", "80000000"), | |
| 334 | + CompletionBuilder::function("split_terminator", "7fffffff"), | |
| 335 | + CompletionBuilder::function("rsplit_terminator", "7fffffff"), | |
| 2134 | 336 | ]; |
| 2135 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 2136 | − assert_eq!( | |
| 2137 | − matches[0].string_match.string.as_str(), | |
| 2138 | − "into", | |
| 2139 | − "Match order not expected" | |
| 2140 | − ); | |
| 337 | + let matches = sort_matches("into", &completions, SnippetSortOrder::default(), cx).await; | |
| 338 | + assert_eq!(matches[0], "into"); | |
| 2141 | 339 | } |
| 2142 | 340 | |
| 2143 | 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 | 343 | // Case 1: "serial" |
| 2146 | − let query: Option<&str> = Some("serial"); | |
| 2147 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 2148 | − SortableMatch { | |
| 2149 | − string_match: StringMatch { | |
| 2150 | − candidate_id: 33, | |
| 2151 | − score: 0.6666666666666666, | |
| 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 | − }, | |
| 344 | + let completions = vec![ | |
| 345 | + CompletionBuilder::function("serialize", "80000000"), | |
| 346 | + CompletionBuilder::function("serialize", "80000000"), | |
| 347 | + CompletionBuilder::variable("serialization_key", "7ffffffe"), | |
| 348 | + CompletionBuilder::function("serialize_version", "80000000"), | |
| 349 | + CompletionBuilder::function("deserialize", "80000000"), | |
| 2208 | 350 | ]; |
| 2209 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 2210 | − assert_eq!( | |
| 2211 | − matches | |
| 2212 | − .iter() | |
| 2213 | − .map(|m| m.string_match.string.as_str()) | |
| 2214 | − .collect::<Vec<&str>>(), | |
| 2215 | − vec![ | |
| 2216 | − "serialization_key", | |
| 2217 | − "serialize", | |
| 2218 | − "serialize", | |
| 2219 | − "serialize_version", | |
| 2220 | − "deserialize" | |
| 2221 | − ] | |
| 2222 | − ); | |
| 351 | + let matches = sort_matches("serial", &completions, SnippetSortOrder::default(), cx).await; | |
| 352 | + assert_eq!(matches[0], "serialization_key"); | |
| 353 | + assert_eq!(matches[1], "serialize"); | |
| 354 | + assert_eq!(matches[2], "serialize"); | |
| 355 | + assert_eq!(matches[3], "serialize_version"); | |
| 356 | + assert_eq!(matches[4], "deserialize"); | |
| 2223 | 357 | } |
| 2224 | 358 | |
| 2225 | 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 | 361 | // Case 1: "setis" |
| 2228 | − let query: Option<&str> = Some("setis"); | |
| 2229 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 2230 | − SortableMatch { | |
| 2231 | − string_match: StringMatch { | |
| 2232 | − candidate_id: 1200, | |
| 2233 | − score: 0.5555555555555556, | |
| 2234 | − positions: vec![], | |
| 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 | − }, | |
| 362 | + let completions = vec![ | |
| 363 | + CompletionBuilder::variable("setISODay", "16"), | |
| 364 | + CompletionBuilder::variable("setISOWeek", "16"), | |
| 365 | + CompletionBuilder::variable("setISOWeekYear", "16"), | |
| 366 | + CompletionBuilder::function("setISOWeekYear", "16"), | |
| 367 | + CompletionBuilder::variable("setIsRefreshing", "11"), | |
| 368 | + CompletionBuilder::function("setFips", "16"), | |
| 2302 | 369 | ]; |
| 2303 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 2304 | − assert_eq!( | |
| 2305 | − matches | |
| 2306 | − .iter() | |
| 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 | − ); | |
| 370 | + let matches = sort_matches("setis", &completions, SnippetSortOrder::default(), cx).await; | |
| 371 | + assert_eq!(matches[0], "setIsRefreshing"); | |
| 372 | + assert_eq!(matches[1], "setISODay"); | |
| 373 | + assert_eq!(matches[2], "setISOWeek"); | |
| 2318 | 374 | } |
| 2319 | 375 | |
| 2320 | 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 | 378 | // Case 1: "item" |
| 2323 | − let query: Option<&str> = Some("item"); | |
| 2324 | − let mut matches: Vec<SortableMatch<'_>> = vec![ | |
| 2325 | − SortableMatch { | |
| 2326 | − string_match: StringMatch { | |
| 2327 | − candidate_id: 1115, | |
| 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 | − }, | |
| 379 | + let completions = vec![ | |
| 380 | + CompletionBuilder::function("Item", "16"), | |
| 381 | + CompletionBuilder::variable("Item", "16"), | |
| 382 | + CompletionBuilder::variable("items", "11"), | |
| 383 | + CompletionBuilder::function("ItemText", "16"), | |
| 2373 | 384 | ]; |
| 2374 | − CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); | |
| 2375 | − assert_eq!( | |
| 2376 | − matches | |
| 2377 | − .iter() | |
| 2378 | − .map(|m| m.string_match.string.as_str()) | |
| 2379 | − .collect::<Vec<&str>>(), | |
| 2380 | − vec!["items", "Item", "Item", "ItemText"] | |
| 2381 | − ); | |
| 385 | + let matches = sort_matches("item", &completions, SnippetSortOrder::default(), cx).await; | |
| 386 | + assert_eq!(matches[0], "items"); | |
| 387 | + assert_eq!(matches[1], "Item"); | |
| 388 | + assert_eq!(matches[2], "Item"); | |
| 389 | + assert_eq!(matches[3], "ItemText"); | |
| 390 | +} | |
| 391 | + | |
| 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 | 722 | let last_rendered_range = self.last_rendered_range.clone(); |
| 723 | 723 | let style = style.clone(); |
| 724 | 724 | let list = uniform_list( |
| 725 | − cx.entity().clone(), | |
| 726 | 725 | "completions", |
| 727 | 726 | self.entries.borrow().len(), |
| 728 | − move |_editor, range, _window, cx| { | |
| 727 | + cx.processor(move |_editor, range: Range<usize>, _window, cx| { | |
| 729 | 728 | last_rendered_range.borrow_mut().replace(range.clone()); |
| 730 | 729 | let start_ix = range.start; |
| 731 | 730 | let completions_guard = completions.borrow_mut(); |
| @@ -837,7 +836,7 @@ impl CompletionsMenu { · 105 unmodified lines | ||
| 837 | 836 | ) |
| 838 | 837 | }) |
| 839 | 838 | .collect() |
| 840 | − }, | |
| 839 | + }), | |
| 841 | 840 | ) |
| 842 | 841 | .occlude() |
| 843 | 842 | .max_h(max_height_in_lines as f32 * window.line_height()) |
| @@ -1045,59 +1044,19 @@ impl CompletionsMenu { · 201 unmodified lines | ||
| 1045 | 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 | 1048 | matches: Vec<StringMatch>, |
| 1050 | 1049 | query: Option<&str>, |
| 1051 | 1050 | snippet_sort_order: SnippetSortOrder, |
| 1052 | 1051 | completions: &[Completion], |
| 1053 | 1052 | ) -> Vec<StringMatch> { |
| 1054 | − let mut sortable_items: Vec<SortableMatch<'_>> = 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 | − } | |
| 1053 | + let mut matches = matches; | |
| 1091 | 1054 | |
| 1092 | − pub fn sort_matches( | |
| 1093 | − matches: &mut Vec<SortableMatch<'_>>, | |
| 1094 | − query: Option<&str>, | |
| 1095 | − snippet_sort_order: SnippetSortOrder, | |
| 1096 | − ) { | |
| 1097 | 1055 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] |
| 1098 | 1056 | enum MatchTier<'a> { |
| 1099 | 1057 | WordStartMatch { |
| 1100 | − sort_mixed_case_prefix_length: Reverse<usize>, | |
| 1058 | + sort_capitalize: Reverse<usize>, | |
| 1059 | + sort_positions: Vec<usize>, | |
| 1101 | 1060 | sort_snippet: Reverse<i32>, |
| 1102 | 1061 | sort_kind: usize, |
| 1103 | 1062 | sort_fuzzy_bracket: Reverse<usize>, |
| @@ -1115,10 +1074,7 @@ impl CompletionsMenu { · 11 unmodified lines | ||
| 1115 | 1074 | |
| 1116 | 1075 | // In a fuzzy bracket, matches with a score of 1.0 are prioritized. |
| 1117 | 1076 | // The remaining matches are partitioned into two groups at 3/5 of the max_score. |
| 1118 | − let max_score = matches | |
| 1119 | − .iter() | |
| 1120 | − .map(|mat| mat.string_match.score) | |
| 1121 | − .fold(0.0, f64::max); | |
| 1077 | + let max_score = matches.iter().map(|mat| mat.score).fold(0.0, f64::max); | |
| 1122 | 1078 | let fuzzy_bracket_threshold = max_score * (3.0 / 5.0); |
| 1123 | 1079 | |
| 1124 | 1080 | let query_start_lower = query |
| @@ -1126,13 +1082,30 @@ impl CompletionsMenu { · 1 unmodified line | ||
| 1126 | 1082 | .and_then(|q| q.chars().next()) |
| 1127 | 1083 | .and_then(|c| c.to_lowercase().next()); |
| 1128 | 1084 | |
| 1129 | − matches.sort_unstable_by_key(|mat| { | |
| 1130 | − let score = mat.string_match.score; | |
| 1085 | + matches.sort_unstable_by_key(|string_match| { | |
| 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 | 1104 | let sort_score = Reverse(OrderedFloat(score)); |
| 1132 | 1105 | |
| 1133 | 1106 | let query_start_doesnt_match_split_words = query_start_lower |
| 1134 | 1107 | .map(|query_char| { |
| 1135 | − !split_words(&mat.string_match.string).any(|word| { | |
| 1108 | + !split_words(&string_match.string).any(|word| { | |
| 1136 | 1109 | word.chars() |
| 1137 | 1110 | .next() |
| 1138 | 1111 | .and_then(|c| c.to_lowercase().next()) |
| @@ -1150,41 +1123,34 @@ impl CompletionsMenu { · 11 unmodified lines | ||
| 1150 | 1123 | 0 |
| 1151 | 1124 | }); |
| 1152 | 1125 | let sort_snippet = match snippet_sort_order { |
| 1153 | − SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }), | |
| 1154 | − SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }), | |
| 1126 | + SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }), | |
| 1127 | + SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }), | |
| 1155 | 1128 | SnippetSortOrder::Inline => Reverse(0), |
| 1156 | 1129 | }; |
| 1157 | − let sort_mixed_case_prefix_length = Reverse( | |
| 1130 | + let sort_capitalize = Reverse( | |
| 1158 | 1131 | query |
| 1159 | 1132 | .as_ref() |
| 1160 | − .map(|q| { | |
| 1161 | − q.chars() | |
| 1162 | − .zip(mat.string_match.string.chars()) | |
| 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 | − }) | |
| 1133 | + .and_then(|q| q.chars().next()) | |
| 1134 | + .zip(string_match.string.chars().next()) | |
| 1135 | + .map(|(q_char, s_char)| if q_char == s_char { 1 } else { 0 }) | |
| 1175 | 1136 | .unwrap_or(0), |
| 1176 | 1137 | ); |
| 1138 | + let sort_positions = string_match.positions.clone(); | |
| 1139 | + | |
| 1177 | 1140 | MatchTier::WordStartMatch { |
| 1178 | − sort_mixed_case_prefix_length, | |
| 1141 | + sort_capitalize, | |
| 1142 | + sort_positions, | |
| 1179 | 1143 | sort_snippet, |
| 1180 | − sort_kind: mat.sort_kind, | |
| 1144 | + sort_kind, | |
| 1181 | 1145 | sort_fuzzy_bracket, |
| 1182 | − sort_text: mat.sort_text, | |
| 1146 | + sort_text, | |
| 1183 | 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 | 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 | 1185 | #[derive(Clone)] |
| 1229 | 1186 | pub struct AvailableCodeAction { |
| 1230 | 1187 | pub excerpt_id: ExcerptId, |
| @@ -1436,7 +1393,9 @@ impl CodeActionsMenu { · 205 unmodified lines | ||
| 1436 | 1393 | |
| 1437 | 1394 | fn origin(&self) -> ContextMenuOrigin { |
| 1438 | 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 | 1399 | Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar, |
| 1441 | 1400 | None => ContextMenuOrigin::Cursor, |
| 1442 | 1401 | } |
| @@ -1452,10 +1411,9 @@ impl CodeActionsMenu { · 9 unmodified lines | ||
| 1452 | 1411 | let actions = self.actions.clone(); |
| 1453 | 1412 | let selected_item = self.selected_item; |
| 1454 | 1413 | let list = uniform_list( |
| 1455 | − cx.entity().clone(), | |
| 1456 | 1414 | "code_actions_menu", |
| 1457 | 1415 | self.actions.len(), |
| 1458 | − move |_this, range, _, cx| { | |
| 1416 | + cx.processor(move |_this, range: Range<usize>, _, cx| { | |
| 1459 | 1417 | actions |
| 1460 | 1418 | .iter() |
| 1461 | 1419 | .skip(range.start) |
| @@ -1518,7 +1476,7 @@ impl CodeActionsMenu { · 56 unmodified lines | ||
| 1518 | 1476 | ) |
| 1519 | 1477 | }) |
| 1520 | 1478 | .collect() |
| 1521 | − }, | |
| 1479 | + }), | |
| 1522 | 1480 | ) |
| 1523 | 1481 | .occlude() |
| 1524 | 1482 | .max_h(max_height_in_lines as f32 * window.line_height()) |
| @@ -464,7 +464,7 @@ impl BlockMap { · 463 unmodified lines | ||
| 464 | 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 | 468 | self.sync(&wrap_snapshot, edits); |
| 469 | 469 | *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone(); |
| 470 | 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 | 483 | self.sync(&wrap_snapshot, edits); |
| 484 | 484 | *self.wrap_snapshot.borrow_mut() = wrap_snapshot; |
| 485 | 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 | 1331 | let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); |
| 1332 | 1332 | cursor.seek(&start_row, Bias::Right, &()); |
| 1333 | 1333 | let (output_start, input_start) = cursor.start(); |
| @@ -357,7 +357,7 @@ impl FoldMap { · 356 unmodified lines | ||
| 357 | 357 | &mut self, |
| 358 | 358 | inlay_snapshot: InlaySnapshot, |
| 359 | 359 | edits: Vec<InlayEdit>, |
| 360 | − ) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) { | |
| 360 | + ) -> (FoldMapWriter<'_>, FoldSnapshot, Vec<FoldEdit>) { | |
| 361 | 361 | let (snapshot, edits) = self.read(inlay_snapshot, edits); |
| 362 | 362 | (FoldMapWriter(self), snapshot, edits) |
| 363 | 363 | } |
| @@ -730,7 +730,7 @@ impl FoldSnapshot { · 366 unmodified lines | ||
| 730 | 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 | 734 | if start_row > self.transforms.summary().output.lines.row { |
| 735 | 735 | panic!("invalid display row {}", start_row); |
| 736 | 736 | } |
| @@ -726,7 +726,7 @@ impl WrapSnapshot { · 725 unmodified lines | ||
| 726 | 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 | 730 | let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); |
| 731 | 731 | transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); |
| 732 | 732 | let mut input_row = transforms.start().1.row(); |
| @@ -209,7 +209,7 @@ use workspace::{ · 208 unmodified lines | ||
| 209 | 209 | CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, |
| 210 | 210 | RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, |
| 211 | 211 | ViewId, Workspace, WorkspaceId, WorkspaceSettings, |
| 212 | − item::{ItemHandle, PreviewTabsSettings}, | |
| 212 | + item::{ItemHandle, PreviewTabsSettings, SaveOptions}, | |
| 213 | 213 | notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, |
| 214 | 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 | 242 | pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction"; |
| 243 | 243 | pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict"; |
| 244 | −pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4; | |
| 245 | 244 | pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.)); |
| 246 | 245 | |
| 247 | 246 | pub type RenderDiffHunkControlsFn = Arc< |
| @@ -450,6 +449,7 @@ pub enum SelectPhase { · 202 unmodified lines | ||
| 450 | 449 | BeginColumnar { |
| 451 | 450 | position: DisplayPoint, |
| 452 | 451 | reset: bool, |
| 452 | + mode: ColumnarMode, | |
| 453 | 453 | goal_column: u32, |
| 454 | 454 | }, |
| 455 | 455 | Extend { |
| @@ -464,6 +464,12 @@ pub enum SelectPhase { · 8 unmodified lines | ||
| 464 | 464 | End, |
| 465 | 465 | } |
| 466 | 466 | |
| 467 | +#[derive(Clone, Debug, PartialEq)] | |
| 468 | +pub enum ColumnarMode { | |
| 469 | + FromMouse, | |
| 470 | + FromSelection, | |
| 471 | +} | |
| 472 | + | |
| 467 | 473 | #[derive(Clone, Debug)] |
| 468 | 474 | pub enum SelectMode { |
| 469 | 475 | Character, |
| @@ -478,6 +484,7 @@ pub enum EditorMode { · 8 unmodified lines | ||
| 478 | 484 | auto_width: bool, |
| 479 | 485 | }, |
| 480 | 486 | AutoHeight { |
| 487 | + min_lines: usize, | |
| 481 | 488 | max_lines: usize, |
| 482 | 489 | }, |
| 483 | 490 | Full { |
| @@ -700,7 +707,7 @@ impl EditorActionId { · 216 unmodified lines | ||
| 700 | 707 | // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>; |
| 701 | 708 | |
| 702 | 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 | 712 | #[derive(Default)] |
| 706 | 713 | struct ScrollbarMarkerState { |
| @@ -902,7 +909,6 @@ struct InlineBlamePopoverState { · 195 unmodified lines | ||
| 902 | 909 | |
| 903 | 910 | struct InlineBlamePopover { |
| 904 | 911 | position: gpui::Point<Pixels>, |
| 905 | − show_task: Option<Task<()>>, | |
| 906 | 912 | hide_task: Option<Task<()>>, |
| 907 | 913 | popover_bounds: Option<Bounds<Pixels>>, |
| 908 | 914 | popover_state: InlineBlamePopoverState, |
| @@ -915,17 +921,29 @@ enum SelectionDragState { · 6 unmodified lines | ||
| 915 | 921 | ReadyToDrag { |
| 916 | 922 | selection: Selection<Anchor>, |
| 917 | 923 | click_position: gpui::Point<Pixels>, |
| 924 | + mouse_down_time: Instant, | |
| 918 | 925 | }, |
| 919 | 926 | /// State when the mouse is dragging the selection in the editor. |
| 920 | 927 | Dragging { |
| 921 | 928 | selection: Selection<Anchor>, |
| 922 | 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 | 944 | /// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have |
| 927 | 945 | /// a breakpoint on them. |
| 928 | −#[derive(Clone, Copy, Debug)] | |
| 946 | +#[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 929 | 947 | struct PhantomBreakpointIndicator { |
| 930 | 948 | display_row: DisplayRow, |
| 931 | 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 | 951 | is_active: bool, |
| 934 | 952 | collides_with_existing_breakpoint: bool, |
| 935 | 953 | } |
| 954 | + | |
| 936 | 955 | /// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. |
| 937 | 956 | /// |
| 938 | 957 | /// See the [module level documentation](self) for more information. |
| @@ -949,8 +968,7 @@ pub struct Editor { · 10 unmodified lines | ||
| 949 | 968 | /// When inline assist editors are linked, they all render cursors because |
| 950 | 969 | /// typing enters text into each of them, even the ones that aren't focused. |
| 951 | 970 | pub(crate) show_cursor_when_unfocused: bool, |
| 952 | − columnar_selection_tail: Option<Anchor>, | |
| 953 | − columnar_display_point: Option<DisplayPoint>, | |
| 971 | + columnar_selection_state: Option<ColumnarSelectionState>, | |
| 954 | 972 | add_selections_state: Option<AddSelectionsState>, |
| 955 | 973 | select_next_state: Option<SelectNextState>, |
| 956 | 974 | select_prev_state: Option<SelectNextState>, |
| @@ -1007,6 +1025,7 @@ pub struct Editor { · 50 unmodified lines | ||
| 1007 | 1025 | mouse_context_menu: Option<MouseContextMenu>, |
| 1008 | 1026 | completion_tasks: Vec<(CompletionId, Task<()>)>, |
| 1009 | 1027 | inline_blame_popover: Option<InlineBlamePopover>, |
| 1028 | + inline_blame_popover_show_task: Option<Task<()>>, | |
| 1010 | 1029 | signature_help_state: SignatureHelpState, |
| 1011 | 1030 | auto_signature_help: Option<bool>, |
| 1012 | 1031 | find_all_references_task_sources: Vec<Anchor>, |
| @@ -1054,8 +1073,9 @@ pub struct Editor { · 41 unmodified lines | ||
| 1054 | 1073 | style: Option<EditorStyle>, |
| 1055 | 1074 | text_style_refinement: Option<TextStyleRefinement>, |
| 1056 | 1075 | next_editor_action_id: EditorActionId, |
| 1057 | − editor_actions: | |
| 1058 | − Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut Window, &mut Context<Self>)>>>>, | |
| 1076 | + editor_actions: Rc< | |
| 1077 | + RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&Editor, &mut Window, &mut Context<Self>)>>>, | |
| 1078 | + >, | |
| 1059 | 1079 | use_autoclose: bool, |
| 1060 | 1080 | use_auto_surround: bool, |
| 1061 | 1081 | auto_replace_emoji_shortcode: bool, |
| @@ -1087,6 +1107,7 @@ pub struct Editor { · 25 unmodified lines | ||
| 1087 | 1107 | tasks_update_task: Option<Task<()>>, |
| 1088 | 1108 | breakpoint_store: Option<Entity<BreakpointStore>>, |
| 1089 | 1109 | gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>), |
| 1110 | + hovered_diff_hunk_row: Option<DisplayRow>, | |
| 1090 | 1111 | pull_diagnostics_task: Task<()>, |
| 1091 | 1112 | in_project_search: bool, |
| 1092 | 1113 | previous_search_ranges: Option<Arc<[Range<Anchor>]>>, |
| @@ -1201,10 +1222,12 @@ struct SelectionHistoryEntry { · 108 unmodified lines | ||
| 1201 | 1222 | add_selections_state: Option<AddSelectionsState>, |
| 1202 | 1223 | } |
| 1203 | 1224 | |
| 1225 | +#[derive(Copy, Clone, Debug, PartialEq, Eq)] | |
| 1204 | 1226 | enum SelectionHistoryMode { |
| 1205 | 1227 | Normal, |
| 1206 | 1228 | Undoing, |
| 1207 | 1229 | Redoing, |
| 1230 | + Skipping, | |
| 1208 | 1231 | } |
| 1209 | 1232 | |
| 1210 | 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 | 1291 | struct DeferredSelectionEffectsState { |
| 1223 | 1292 | changed: bool, |
| 1224 | − should_update_completions: bool, | |
| 1225 | − autoscroll: Option<Autoscroll>, | |
| 1293 | + effects: SelectionEffects, | |
| 1226 | 1294 | old_cursor_position: Anchor, |
| 1227 | 1295 | history_entry: SelectionHistoryEntry, |
| 1228 | 1296 | } |
| @@ -1238,11 +1306,19 @@ struct SelectionHistory { · 9 unmodified lines | ||
| 1238 | 1306 | } |
| 1239 | 1307 | |
| 1240 | 1308 | impl SelectionHistory { |
| 1309 | + #[track_caller] | |
| 1241 | 1310 | fn insert_transaction( |
| 1242 | 1311 | &mut self, |
| 1243 | 1312 | transaction_id: TransactionId, |
| 1244 | 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 | 1322 | self.selections_by_transaction |
| 1247 | 1323 | .insert(transaction_id, (selections, None)); |
| 1248 | 1324 | } |
| @@ -1272,6 +1348,7 @@ impl SelectionHistory { · 23 unmodified lines | ||
| 1272 | 1348 | } |
| 1273 | 1349 | SelectionHistoryMode::Undoing => self.push_redo(entry), |
| 1274 | 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 | 1565 | pub enum FormatTarget { |
| 1489 | − Buffers, | |
| 1566 | + Buffers(HashSet<Entity<Buffer>>), | |
| 1490 | 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 | 1637 | let buffer = cx.new(|cx| Buffer::local("", cx)); |
| 1556 | 1638 | let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); |
| 1557 | 1639 | Self::new( |
| 1558 | − EditorMode::AutoHeight { max_lines }, | |
| 1640 | + EditorMode::AutoHeight { | |
| 1641 | + min_lines, | |
| 1642 | + max_lines, | |
| 1643 | + }, | |
| 1559 | 1644 | buffer, |
| 1560 | 1645 | None, |
| 1561 | 1646 | window, |
| @@ -1817,8 +1902,7 @@ impl Editor { · 255 unmodified lines | ||
| 1817 | 1902 | display_map: display_map.clone(), |
| 1818 | 1903 | selections, |
| 1819 | 1904 | scroll_manager: ScrollManager::new(cx), |
| 1820 | − columnar_selection_tail: None, | |
| 1821 | − columnar_display_point: None, | |
| 1905 | + columnar_selection_state: None, | |
| 1822 | 1906 | add_selections_state: None, |
| 1823 | 1907 | select_next_state: None, |
| 1824 | 1908 | select_prev_state: None, |
| @@ -1872,6 +1956,7 @@ impl Editor { · 47 unmodified lines | ||
| 1872 | 1956 | mouse_context_menu: None, |
| 1873 | 1957 | completion_tasks: Vec::new(), |
| 1874 | 1958 | inline_blame_popover: None, |
| 1959 | + inline_blame_popover_show_task: None, | |
| 1875 | 1960 | signature_help_state: SignatureHelpState::default(), |
| 1876 | 1961 | auto_signature_help: None, |
| 1877 | 1962 | find_all_references_task_sources: Vec::new(), |
| @@ -1949,6 +2034,7 @@ impl Editor { · 71 unmodified lines | ||
| 1949 | 2034 | |
| 1950 | 2035 | breakpoint_store, |
| 1951 | 2036 | gutter_breakpoint_indicator: (None, None), |
| 2037 | + hovered_diff_hunk_row: None, | |
| 1952 | 2038 | _subscriptions: vec![ |
| 1953 | 2039 | cx.observe(&buffer, Self::on_buffer_changed), |
| 1954 | 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 | 2173 | editor.end_selection(window, cx); |
| 2174 | + editor.selection_history.mode = SelectionHistoryMode::Normal; | |
| 2175 | + | |
| 2086 | 2176 | editor.scroll_manager.show_scrollbars(window, cx); |
| 2087 | 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 | 2772 | &mut self, |
| 2683 | 2773 | local: bool, |
| 2684 | 2774 | old_cursor_position: &Anchor, |
| 2685 | − should_update_completions: bool, | |
| 2775 | + effects: SelectionEffects, | |
| 2686 | 2776 | window: &mut Window, |
| 2687 | 2777 | cx: &mut Context<Self>, |
| 2688 | 2778 | ) { |
| @@ -2740,12 +2830,14 @@ impl Editor { · 51 unmodified lines | ||
| 2740 | 2830 | let new_cursor_position = newest_selection.head(); |
| 2741 | 2831 | let selection_start = newest_selection.start; |
| 2742 | 2832 | |
| 2743 | − self.push_to_nav_history( | |
| 2744 | − *old_cursor_position, | |
| 2745 | − Some(new_cursor_position.to_point(buffer)), | |
| 2746 | − false, | |
| 2747 | − cx, | |
| 2748 | − ); | |
| 2833 | + if effects.nav_history { | |
| 2834 | + self.push_to_nav_history( | |
| 2835 | + *old_cursor_position, | |
| 2836 | + Some(new_cursor_position.to_point(buffer)), | |
| 2837 | + false, | |
| 2838 | + cx, | |
| 2839 | + ); | |
| 2840 | + } | |
| 2749 | 2841 | |
| 2750 | 2842 | if local { |
| 2751 | 2843 | if let Some(buffer_id) = new_cursor_position.buffer_id { |
| @@ -2776,7 +2868,7 @@ impl Editor { · 24 unmodified lines | ||
| 2776 | 2868 | let completion_position = completion_menu.map(|menu| menu.initial_position); |
| 2777 | 2869 | drop(context_menu); |
| 2778 | 2870 | |
| 2779 | − if should_update_completions { | |
| 2871 | + if effects.completions { | |
| 2780 | 2872 | if let Some(completion_position) = completion_position { |
| 2781 | 2873 | let start_offset = selection_start.to_offset(buffer); |
| 2782 | 2874 | let position_matches = start_offset == completion_position.to_offset(buffer); |
| @@ -2785,7 +2877,8 @@ impl Editor { · 2 unmodified lines | ||
| 2785 | 2877 | buffer.char_kind_before(start_offset, true) == Some(CharKind::Word) |
| 2786 | 2878 | } else { |
| 2787 | 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 | 2882 | true |
| 2790 | 2883 | } |
| 2791 | 2884 | } else { |
| @@ -2851,22 +2944,22 @@ impl Editor { · 59 unmodified lines | ||
| 2851 | 2944 | let background_executor = cx.background_executor().clone(); |
| 2852 | 2945 | let editor_id = cx.entity().entity_id().as_u64() as ItemId; |
| 2853 | 2946 | self.serialize_selections = cx.background_spawn(async move { |
| 2854 | − background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; | |
| 2855 | − let db_selections = selections | |
| 2856 | − .iter() | |
| 2857 | − .map(|selection| { | |
| 2858 | − ( | |
| 2859 | − selection.start.to_offset(&snapshot), | |
| 2860 | − selection.end.to_offset(&snapshot), | |
| 2861 | − ) | |
| 2862 | − }) | |
| 2863 | − .collect(); | |
| 2947 | + background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; | |
| 2948 | + let db_selections = selections | |
| 2949 | + .iter() | |
| 2950 | + .map(|selection| { | |
| 2951 | + ( | |
| 2952 | + selection.start.to_offset(&snapshot), | |
| 2953 | + selection.end.to_offset(&snapshot), | |
| 2954 | + ) | |
| 2955 | + }) | |
| 2956 | + .collect(); | |
| 2864 | 2957 | |
| 2865 | − DB.save_editor_selections(editor_id, workspace_id, db_selections) | |
| 2866 | − .await | |
| 2867 | − .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) | |
| 2868 | − .log_err(); | |
| 2869 | − }); | |
| 2958 | + DB.save_editor_selections(editor_id, workspace_id, db_selections) | |
| 2959 | + .await | |
| 2960 | + .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) | |
| 2961 | + .log_err(); | |
| 2962 | + }); | |
| 2870 | 2963 | } |
| 2871 | 2964 | } |
| 2872 | 2965 | } |
| @@ -2983,43 +3076,23 @@ impl Editor { · 110 unmodified lines | ||
| 2983 | 3076 | /// effects of selection change occur at the end of the transaction. |
| 2984 | 3077 | pub fn change_selections<R>( |
| 2985 | 3078 | &mut self, |
| 2986 | − autoscroll: Option<Autoscroll>, | |
| 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>, | |
| 3079 | + effects: impl Into<SelectionEffects>, | |
| 3008 | 3080 | window: &mut Window, |
| 3009 | 3081 | cx: &mut Context<Self>, |
| 3010 | 3082 | change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, |
| 3011 | 3083 | ) -> R { |
| 3084 | + let effects = effects.into(); | |
| 3012 | 3085 | if let Some(state) = &mut self.deferred_selection_effects_state { |
| 3013 | − state.autoscroll = autoscroll.or(state.autoscroll); | |
| 3014 | − state.should_update_completions = should_update_completions; | |
| 3086 | + state.effects.scroll = effects.scroll.or(state.effects.scroll); | |
| 3087 | + state.effects.completions = effects.completions; | |
| 3088 | + state.effects.nav_history |= effects.nav_history; | |
| 3015 | 3089 | let (changed, result) = self.selections.change_with(cx, change); |
| 3016 | 3090 | state.changed |= changed; |
| 3017 | 3091 | return result; |
| 3018 | 3092 | } |
| 3019 | 3093 | let mut state = DeferredSelectionEffectsState { |
| 3020 | 3094 | changed: false, |
| 3021 | − should_update_completions, | |
| 3022 | − autoscroll, | |
| 3095 | + effects, | |
| 3023 | 3096 | old_cursor_position: self.selections.newest_anchor().head(), |
| 3024 | 3097 | history_entry: SelectionHistoryEntry { |
| 3025 | 3098 | selections: self.selections.disjoint_anchors(), |
| @@ -3069,19 +3142,13 @@ impl Editor { · 43 unmodified lines | ||
| 3069 | 3142 | if state.changed { |
| 3070 | 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 | 3146 | self.request_autoscroll(autoscroll, cx); |
| 3074 | 3147 | } |
| 3075 | 3148 | |
| 3076 | 3149 | let old_cursor_position = &state.old_cursor_position; |
| 3077 | 3150 | |
| 3078 | − self.selections_did_change( | |
| 3079 | − true, | |
| 3080 | − &old_cursor_position, | |
| 3081 | − state.should_update_completions, | |
| 3082 | − window, | |
| 3083 | − cx, | |
| 3084 | − ); | |
| 3151 | + self.selections_did_change(true, &old_cursor_position, state.effects, window, cx); | |
| 3085 | 3152 | |
| 3086 | 3153 | if self.should_open_signature_help_automatically(&old_cursor_position, cx) { |
| 3087 | 3154 | self.show_signature_help(&ShowSignatureHelp, window, cx); |
| @@ -3156,7 +3223,8 @@ impl Editor { · 68 unmodified lines | ||
| 3156 | 3223 | position, |
| 3157 | 3224 | goal_column, |
| 3158 | 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 | 3228 | SelectPhase::Extend { |
| 3161 | 3229 | position, |
| 3162 | 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 | 3279 | s.set_pending(pending_selection, pending_mode) |
| 3208 | 3280 | }); |
| 3209 | 3281 | } |
| @@ -3309,6 +3381,7 @@ impl Editor { · 99 unmodified lines | ||
| 3309 | 3381 | position: DisplayPoint, |
| 3310 | 3382 | goal_column: u32, |
| 3311 | 3383 | reset: bool, |
| 3384 | + mode: ColumnarMode, | |
| 3312 | 3385 | window: &mut Window, |
| 3313 | 3386 | cx: &mut Context<Self>, |
| 3314 | 3387 | ) { |
| @@ -3331,26 +3404,30 @@ impl Editor { · 16 unmodified lines | ||
| 3331 | 3404 | SelectMode::Character, |
| 3332 | 3405 | ); |
| 3333 | 3406 | }); |
| 3334 | − if position.column() != goal_column { | |
| 3335 | − self.columnar_display_point = Some(DisplayPoint::new(position.row(), goal_column)); | |
| 3336 | − } else { | |
| 3337 | − self.columnar_display_point = None; | |
| 3338 | − } | |
| 3339 | − } | |
| 3407 | + }; | |
| 3340 | 3408 | |
| 3341 | 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 | 3429 | if !reset { |
| 3345 | − self.columnar_display_point = None; | |
| 3346 | − self.select_columns( | |
| 3347 | − tail.to_display_point(&display_map), | |
| 3348 | − position, | |
| 3349 | − goal_column, | |
| 3350 | − &display_map, | |
| 3351 | − window, | |
| 3352 | − cx, | |
| 3353 | − ); | |
| 3430 | + self.select_columns(position, goal_column, &display_map, window, cx); | |
| 3354 | 3431 | } |
| 3355 | 3432 | } |
| 3356 | 3433 | |
| @@ -3364,11 +3441,8 @@ impl Editor { · 7 unmodified lines | ||
| 3364 | 3441 | ) { |
| 3365 | 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() { | |
| 3368 | − let tail = self | |
| 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); | |
| 3444 | + if self.columnar_selection_state.is_some() { | |
| 3445 | + self.select_columns(position, goal_column, &display_map, window, cx); | |
| 3372 | 3446 | } else if let Some(mut pending) = self.selections.pending_anchor() { |
| 3373 | 3447 | let buffer = self.buffer.read(cx).snapshot(cx); |
| 3374 | 3448 | let head; |
| @@ -3455,7 +3529,7 @@ impl Editor { · 80 unmodified lines | ||
| 3455 | 3529 | } |
| 3456 | 3530 | |
| 3457 | 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 | 3533 | if self.selections.pending_anchor().is_some() { |
| 3460 | 3534 | let selections = self.selections.all::<usize>(cx); |
| 3461 | 3535 | self.change_selections(None, window, cx, |s| { |
| @@ -3467,13 +3541,26 @@ impl Editor { · 5 unmodified lines | ||
| 3467 | 3541 | |
| 3468 | 3542 | fn select_columns( |
| 3469 | 3543 | &mut self, |
| 3470 | − tail: DisplayPoint, | |
| 3471 | 3544 | head: DisplayPoint, |
| 3472 | 3545 | goal_column: u32, |
| 3473 | 3546 | display_map: &DisplaySnapshot, |
| 3474 | 3547 | window: &mut Window, |
| 3475 | 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 | 3564 | let start_row = cmp::min(tail.row(), head.row()); |
| 3478 | 3565 | let end_row = cmp::max(tail.row(), head.row()); |
| 3479 | 3566 | let start_column = cmp::min(tail.column(), goal_column); |
| @@ -3483,7 +3570,10 @@ impl Editor { · 3 unmodified lines | ||
| 3483 | 3570 | let selection_ranges = (start_row.0..=end_row.0) |
| 3484 | 3571 | .map(DisplayRow) |
| 3485 | 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 | 3577 | let start = display_map |
| 3488 | 3578 | .clip_point(DisplayPoint::new(row, start_column), Bias::Left) |
| 3489 | 3579 | .to_point(display_map); |
| @@ -3501,15 +3591,19 @@ impl Editor { · 11 unmodified lines | ||
| 3501 | 3591 | }) |
| 3502 | 3592 | .collect::<Vec<_>>(); |
| 3503 | 3593 | |
| 3504 | − let mut non_empty_ranges = selection_ranges | |
| 3505 | − .iter() | |
| 3506 | − .filter(|selection_range| selection_range.start != selection_range.end) | |
| 3507 | − .peekable(); | |
| 3508 | − | |
| 3509 | − let ranges = if non_empty_ranges.peek().is_some() { | |
| 3510 | − non_empty_ranges.cloned().collect() | |
| 3511 | − } else { | |
| 3512 | − selection_ranges | |
| 3594 | + let ranges = match columnar_state { | |
| 3595 | + ColumnarSelectionState::FromMouse { .. } => { | |
| 3596 | + let mut non_empty_ranges = selection_ranges | |
| 3597 | + .iter() | |
| 3598 | + .filter(|selection_range| selection_range.start != selection_range.end) | |
| 3599 | + .peekable(); | |
| 3600 | + if non_empty_ranges.peek().is_some() { | |
| 3601 | + non_empty_ranges.cloned().collect() | |
| 3602 | + } else { | |
| 3603 | + selection_ranges | |
| 3604 | + } | |
| 3605 | + } | |
| 3606 | + _ => selection_ranges, | |
| 3513 | 3607 | }; |
| 3514 | 3608 | |
| 3515 | 3609 | self.change_selections(None, window, cx, |s| { |
| @@ -3532,11 +3626,11 @@ impl Editor { · 16 unmodified lines | ||
| 3532 | 3626 | }; |
| 3533 | 3627 | |
| 3534 | 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 | 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 | 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 | 4086 | let had_active_inline_completion = this.has_active_inline_completion(); |
| 3993 | − this.change_selections_without_updating_completions( | |
| 3994 | − Some(Autoscroll::fit()), | |
| 4087 | + this.change_selections( | |
| 4088 | + SelectionEffects::scroll(Autoscroll::fit()).completions(false), | |
| 3995 | 4089 | window, |
| 3996 | 4090 | cx, |
| 3997 | 4091 | |s| s.select(new_selections), |
| @@ -5114,10 +5208,13 @@ impl Editor { · 1116 unmodified lines | ||
| 5114 | 5208 | .as_ref() |
| 5115 | 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 | 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 | 5218 | // If the new query is a suffix of the old query (typing more characters) and |
| 5122 | 5219 | // the previous result was complete, the existing completions can be filtered. |
| 5123 | 5220 | // |
| @@ -5135,7 +5232,6 @@ impl Editor { · 11 unmodified lines | ||
| 5135 | 5232 | menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot) |
| 5136 | 5233 | }; |
| 5137 | 5234 | if position_matches { |
| 5138 | − menu.filter(query.clone(), provider.clone(), window, cx); | |
| 5139 | 5235 | return; |
| 5140 | 5236 | } |
| 5141 | 5237 | } |
| @@ -5684,70 +5780,60 @@ impl Editor { · 542 unmodified lines | ||
| 5684 | 5780 | drop(context_menu); |
| 5685 | 5781 | let snapshot = self.snapshot(window, cx); |
| 5686 | 5782 | let deployed_from = action.deployed_from.clone(); |
| 5687 | − let mut task = self.code_actions_task.take(); | |
| 5688 | 5783 | let action = action.clone(); |
| 5689 | − cx.spawn_in(window, async move |editor, cx| { | |
| 5690 | − while let Some(prev_task) = task { | |
| 5691 | − prev_task.await.log_err(); | |
| 5692 | − task = editor.update(cx, |this, _| this.code_actions_task.take())?; | |
| 5784 | + self.completion_tasks.clear(); | |
| 5785 | + self.discard_inline_completion(false, cx); | |
| 5786 | + | |
| 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| { | |
| 5696 | − if editor.focus_handle.is_focused(window) { | |
| 5697 | − let multibuffer_point = match &action.deployed_from { | |
| 5698 | − Some(CodeActionSource::Indicator(row)) => { | |
| 5699 | − DisplayPoint::new(*row, 0).to_point(&snapshot) | |
| 5700 | − } | |
| 5701 | − _ => editor.selections.newest::<Point>(cx).head(), | |
| 5702 | − }; | |
| 5703 | − let (buffer, buffer_row) = snapshot | |
| 5704 | − .buffer_snapshot | |
| 5705 | − .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) | |
| 5706 | − .and_then(|(buffer_snapshot, range)| { | |
| 5707 | − editor | |
| 5708 | − .buffer | |
| 5709 | − .read(cx) | |
| 5710 | − .buffer(buffer_snapshot.remote_id()) | |
| 5711 | − .map(|buffer| (buffer, range.start.row)) | |
| 5712 | − })?; | |
| 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; | |
| 5811 | + if !self.focus_handle.is_focused(window) { | |
| 5812 | + return; | |
| 5813 | + } | |
| 5814 | + let project = self.project.clone(); | |
| 5815 | + | |
| 5816 | + let code_actions_task = match deployed_from { | |
| 5817 | + Some(CodeActionSource::RunMenu(_)) => Task::ready(None), | |
| 5818 | + _ => self.code_actions(buffer_row, window, cx), | |
| 5819 | + }; | |
| 5820 | + | |
| 5821 | + let runnable_task = match deployed_from { | |
| 5822 | + Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())), | |
| 5823 | + _ => { | |
| 5824 | + let mut task_context_task = Task::ready(None); | |
| 5825 | + if let Some(tasks) = &tasks { | |
| 5826 | + if let Some(project) = project { | |
| 5827 | + task_context_task = | |
| 5828 | + Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); | |
| 5734 | 5829 | } |
| 5830 | + } | |
| 5735 | 5831 | |
| 5736 | − editor.completion_tasks.clear(); | |
| 5737 | − editor.discard_inline_completion(false, cx); | |
| 5738 | − let task_context = | |
| 5739 | − tasks | |
| 5740 | − .as_ref() | |
| 5741 | − .zip(editor.project.clone()) | |
| 5742 | − .map(|(tasks, project)| { | |
| 5743 | − Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) | |
| 5744 | − }); | |
| 5832 | + cx.spawn_in(window, { | |
| 5833 | + let buffer = buffer.clone(); | |
| 5834 | + async move |editor, cx| { | |
| 5835 | + let task_context = task_context_task.await; | |
| 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 | 5837 | let resolved_tasks = |
| 5752 | 5838 | tasks |
| 5753 | 5839 | .zip(task_context.clone()) |
| @@ -5758,104 +5844,144 @@ impl Editor { · 4 unmodified lines | ||
| 5758 | 5844 | tasks.column, |
| 5759 | 5845 | )), |
| 5760 | 5846 | }); |
| 5761 | − let debug_scenarios = editor.update(cx, |editor, cx| { | |
| 5762 | − if cx.has_flag::<DebuggerFeatureFlag>() { | |
| 5763 | − maybe!({ | |
| 5764 | − let project = editor.project.as_ref()?; | |
| 5765 | − let dap_store = project.read(cx).dap_store(); | |
| 5766 | − let mut scenarios = vec![]; | |
| 5767 | − let resolved_tasks = resolved_tasks.as_ref()?; | |
| 5768 | − let buffer = buffer.read(cx); | |
| 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?; | |
| 5847 | + let debug_scenarios = editor | |
| 5848 | + .update(cx, |editor, cx| { | |
| 5849 | + editor.debug_scenarios(&resolved_tasks, &buffer, cx) | |
| 5850 | + })? | |
| 5851 | + .await; | |
| 5852 | + anyhow::Ok((resolved_tasks, debug_scenarios, task_context)) | |
| 5853 | + } | |
| 5854 | + }) | |
| 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 | 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 | 5985 | pub fn confirm_code_action( |
| 5860 | 5986 | &mut self, |
| 5861 | 5987 | action: &ConfirmCodeAction, |
| @@ -6193,71 +6319,65 @@ impl Editor { · 331 unmodified lines | ||
| 6193 | 6319 | ) { |
| 6194 | 6320 | if let Some(state) = &mut self.inline_blame_popover { |
| 6195 | 6321 | state.hide_task.take(); |
| 6196 | − cx.notify(); | |
| 6197 | 6322 | } else { |
| 6198 | 6323 | let delay = EditorSettings::get_global(cx).hover_popover_delay; |
| 6324 | + let blame_entry = blame_entry.clone(); | |
| 6199 | 6325 | let show_task = cx.spawn(async move |editor, cx| { |
| 6200 | 6326 | cx.background_executor() |
| 6201 | 6327 | .timer(std::time::Duration::from_millis(delay)) |
| 6202 | 6328 | .await; |
| 6203 | 6329 | editor |
| 6204 | 6330 | .update(cx, |editor, cx| { |
| 6205 | − if let Some(state) = &mut editor.inline_blame_popover { | |
| 6206 | − state.show_task = None; | |
| 6207 | − cx.notify(); | |
| 6208 | − } | |
| 6331 | + editor.inline_blame_popover_show_task.take(); | |
| 6332 | + let Some(blame) = editor.blame.as_ref() else { | |
| 6333 | + return; | |
| 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 | 6360 | .ok(); |
| 6211 | 6361 | }); |
| 6212 | − let Some(blame) = self.blame.as_ref() else { | |
| 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 | − }); | |
| 6362 | + self.inline_blame_popover_show_task = Some(show_task); | |
| 6239 | 6363 | } |
| 6240 | 6364 | } |
| 6241 | 6365 | |
| 6242 | 6366 | fn hide_blame_popover(&mut self, cx: &mut Context<Self>) { |
| 6367 | + self.inline_blame_popover_show_task.take(); | |
| 6243 | 6368 | if let Some(state) = &mut self.inline_blame_popover { |
| 6244 | − if state.show_task.is_some() { | |
| 6245 | − self.inline_blame_popover.take(); | |
| 6246 | − cx.notify(); | |
| 6247 | − } else { | |
| 6248 | − let hide_task = cx.spawn(async move |editor, cx| { | |
| 6249 | − cx.background_executor() | |
| 6250 | − .timer(std::time::Duration::from_millis(100)) | |
| 6251 | − .await; | |
| 6252 | − editor | |
| 6253 | − .update(cx, |editor, cx| { | |
| 6254 | − editor.inline_blame_popover.take(); | |
| 6255 | − cx.notify(); | |
| 6256 | − }) | |
| 6257 | − .ok(); | |
| 6258 | − }); | |
| 6259 | − state.hide_task = Some(hide_task); | |
| 6260 | − } | |
| 6369 | + let hide_task = cx.spawn(async move |editor, cx| { | |
| 6370 | + cx.background_executor() | |
| 6371 | + .timer(std::time::Duration::from_millis(100)) | |
| 6372 | + .await; | |
| 6373 | + editor | |
| 6374 | + .update(cx, |editor, cx| { | |
| 6375 | + editor.inline_blame_popover.take(); | |
| 6376 | + cx.notify(); | |
| 6377 | + }) | |
| 6378 | + .ok(); | |
| 6379 | + }); | |
| 6380 | + state.hide_task = Some(hide_task); | |
| 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( | |
| 7102 | − cursor_event: bool, | |
| 7103 | − modifiers: &Modifiers, | |
| 7104 | − cx: &mut Context<Self>, | |
| 7105 | − ) -> bool { | |
| 7221 | + fn multi_cursor_modifier(invert: bool, modifiers: &Modifiers, cx: &mut Context<Self>) -> bool { | |
| 7106 | 7222 | let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier; |
| 7107 | − if cursor_event { | |
| 7223 | + if invert { | |
| 7108 | 7224 | match multi_cursor_setting { |
| 7109 | 7225 | MultiCursorModifier::Alt => modifiers.alt, |
| 7110 | 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 { | |
| 7121 | − modifiers.shift && multi_cursor_modifier && modifiers.number_of_modifiers() == 2 | |
| 7236 | + fn columnar_selection_mode( | |
| 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 | 7253 | fn update_selection_mode( |
| @@ -7128,10 +7257,10 @@ impl Editor { · 3 unmodified lines | ||
| 7128 | 7257 | window: &mut Window, |
| 7129 | 7258 | cx: &mut Context<Self>, |
| 7130 | 7259 | ) { |
| 7131 | − let multi_cursor_modifier = Self::multi_cursor_modifier(true, modifiers, cx); | |
| 7132 | − if !Self::columnar_selection_modifiers(multi_cursor_modifier, modifiers) | |
| 7133 | − || self.selections.pending.is_none() | |
| 7134 | − { | |
| 7260 | + let Some(mode) = Self::columnar_selection_mode(modifiers, cx) else { | |
| 7261 | + return; | |
| 7262 | + }; | |
| 7263 | + if self.selections.pending.is_none() { | |
| 7135 | 7264 | return; |
| 7136 | 7265 | } |
| 7137 | 7266 | |
| @@ -7143,6 +7272,7 @@ impl Editor { · 5 unmodified lines | ||
| 7143 | 7272 | SelectPhase::BeginColumnar { |
| 7144 | 7273 | position, |
| 7145 | 7274 | reset: false, |
| 7275 | + mode, | |
| 7146 | 7276 | goal_column: point_for_position.exact_unclipped.column(), |
| 7147 | 7277 | }, |
| 7148 | 7278 | window, |
| @@ -7519,8 +7649,7 @@ impl Editor { · 370 unmodified lines | ||
| 7519 | 7649 | "Set Breakpoint" |
| 7520 | 7650 | }; |
| 7521 | 7651 | |
| 7522 | − let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx) | |
| 7523 | − .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor)); | |
| 7652 | + let run_to_cursor = window.is_action_available(&RunToCursor, cx); | |
| 7524 | 7653 | |
| 7525 | 7654 | let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state { |
| 7526 | 7655 | BreakpointState::Enabled => Some("Disable"), |
| @@ -7544,7 +7673,7 @@ impl Editor { · 17 unmodified lines | ||
| 7544 | 7673 | }) |
| 7545 | 7674 | .ok(); |
| 7546 | 7675 | |
| 7547 | − window.dispatch_action(Box::new(DebuggerRunToCursor), cx); | |
| 7676 | + window.dispatch_action(Box::new(RunToCursor), cx); | |
| 7548 | 7677 | }) |
| 7549 | 7678 | .separator() |
| 7550 | 7679 | }) |
| @@ -7899,7 +8028,7 @@ impl Editor { · 348 unmodified lines | ||
| 7899 | 8028 | window.focus(&editor.focus_handle(cx)); |
| 7900 | 8029 | editor.toggle_code_actions( |
| 7901 | 8030 | &ToggleCodeActions { |
| 7902 | − deployed_from: Some(CodeActionSource::Indicator(row)), | |
| 8031 | + deployed_from: Some(CodeActionSource::RunMenu(row)), | |
| 7903 | 8032 | quick_launch, |
| 7904 | 8033 | }, |
| 7905 | 8034 | window, |
| @@ -14076,7 +14205,8 @@ impl Editor { · 6170 unmodified lines | ||
| 14076 | 14205 | prefer_lsp && !lsp_tasks_by_rows.is_empty(), |
| 14077 | 14206 | new_rows, |
| 14078 | 14207 | cx.clone(), |
| 14079 | − ); | |
| 14208 | + ) | |
| 14209 | + .await; | |
| 14080 | 14210 | editor |
| 14081 | 14211 | .update(cx, |editor, _| { |
| 14082 | 14212 | editor.clear_tasks(); |
| @@ -14106,35 +14236,40 @@ impl Editor { · 23 unmodified lines | ||
| 14106 | 14236 | snapshot: DisplaySnapshot, |
| 14107 | 14237 | prefer_lsp: bool, |
| 14108 | 14238 | runnable_ranges: Vec<RunnableRange>, |
| 14109 | − mut cx: AsyncWindowContext, | |
| 14110 | − ) -> Vec<((BufferId, BufferRow), RunnableTasks)> { | |
| 14111 | − runnable_ranges | |
| 14112 | − .into_iter() | |
| 14113 | − .filter_map(|mut runnable| { | |
| 14114 | − let mut tasks = cx | |
| 14239 | + cx: AsyncWindowContext, | |
| 14240 | + ) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> { | |
| 14241 | + cx.spawn(async move |cx| { | |
| 14242 | + let mut runnable_rows = Vec::with_capacity(runnable_ranges.len()); | |
| 14243 | + for mut runnable in runnable_ranges { | |
| 14244 | + let Some(tasks) = cx | |
| 14115 | 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 | 14252 | if prefer_lsp { |
| 14118 | 14253 | tasks.retain(|(task_kind, _)| { |
| 14119 | 14254 | !matches!(task_kind, TaskSourceKind::Language { .. }) |
| 14120 | 14255 | }); |
| 14121 | 14256 | } |
| 14122 | 14257 | if tasks.is_empty() { |
| 14123 | − return None; | |
| 14258 | + continue; | |
| 14124 | 14259 | } |
| 14125 | 14260 | |
| 14126 | 14261 | let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot); |
| 14127 | − | |
| 14128 | − let row = snapshot | |
| 14262 | + let Some(row) = snapshot | |
| 14129 | 14263 | .buffer_snapshot |
| 14130 | − .buffer_line_for_row(MultiBufferRow(point.row))? | |
| 14131 | − .1 | |
| 14132 | − .start | |
| 14133 | − .row; | |
| 14264 | + .buffer_line_for_row(MultiBufferRow(point.row)) | |
| 14265 | + .map(|(_, range)| range.start.row) | |
| 14266 | + else { | |
| 14267 | + continue; | |
| 14268 | + }; | |
| 14134 | 14269 | |
| 14135 | 14270 | let context_range = |
| 14136 | 14271 | BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end); |
| 14137 | − Some(( | |
| 14272 | + runnable_rows.push(( | |
| 14138 | 14273 | (runnable.buffer_id, row), |
| 14139 | 14274 | RunnableTasks { |
| 14140 | 14275 | templates: tasks, |
| @@ -14145,16 +14280,17 @@ impl Editor { · 4 unmodified lines | ||
| 14145 | 14280 | column: point.column, |
| 14146 | 14281 | extra_variables: runnable.extra_captures, |
| 14147 | 14282 | }, |
| 14148 | − )) | |
| 14149 | − }) | |
| 14150 | − .collect() | |
| 14283 | + )); | |
| 14284 | + } | |
| 14285 | + runnable_rows | |
| 14286 | + }) | |
| 14151 | 14287 | } |
| 14152 | 14288 | |
| 14153 | 14289 | fn templates_with_tags( |
| 14154 | 14290 | project: &Entity<Project>, |
| 14155 | 14291 | runnable: &mut Runnable, |
| 14156 | 14292 | cx: &mut App, |
| 14157 | − ) -> Vec<(TaskSourceKind, TaskTemplate)> { | |
| 14293 | + ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> { | |
| 14158 | 14294 | let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { |
| 14159 | 14295 | let (worktree_id, file) = project |
| 14160 | 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) | |
| 14173 | − .into_iter() | |
| 14174 | − .flat_map(|RunnableTag(tag)| { | |
| 14175 | − inventory | |
| 14176 | − .as_ref() | |
| 14177 | − .into_iter() | |
| 14178 | − .flat_map(|inventory| { | |
| 14179 | − inventory.read(cx).list_tasks( | |
| 14180 | − file.clone(), | |
| 14181 | − Some(runnable.language.clone()), | |
| 14182 | − worktree_id, | |
| 14183 | − cx, | |
| 14184 | − ) | |
| 14185 | − }) | |
| 14186 | − .filter(move |(_, template)| { | |
| 14187 | − template.tags.iter().any(|source_tag| source_tag == &tag) | |
| 14188 | − }) | |
| 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); | |
| 14308 | + let tags = mem::take(&mut runnable.tags); | |
| 14309 | + let language = runnable.language.clone(); | |
| 14310 | + cx.spawn(async move |cx| { | |
| 14311 | + let mut templates_with_tags = Vec::new(); | |
| 14312 | + if let Some(inventory) = inventory { | |
| 14313 | + for RunnableTag(tag) in tags { | |
| 14314 | + let Ok(new_tasks) = inventory.update(cx, |inventory, cx| { | |
| 14315 | + inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx) | |
| 14316 | + }) else { | |
| 14317 | + return templates_with_tags; | |
| 14318 | + }; | |
| 14319 | + templates_with_tags.extend(new_tasks.await.into_iter().filter( | |
| 14320 | + move |(_, template)| { | |
| 14321 | + template.tags.iter().any(|source_tag| source_tag == &tag) | |
| 14322 | + }, | |
| 14323 | + )); | |
| 14324 | + } | |
| 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 | 14344 | pub fn move_to_enclosing_bracket( |
| @@ -14268,18 +14405,20 @@ impl Editor { · 60 unmodified lines | ||
| 14268 | 14405 | cx: &mut Context<Self>, |
| 14269 | 14406 | ) { |
| 14270 | 14407 | self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); |
| 14271 | − self.end_selection(window, cx); | |
| 14272 | − self.selection_history.mode = SelectionHistoryMode::Undoing; | |
| 14273 | 14408 | if let Some(entry) = self.selection_history.undo_stack.pop_back() { |
| 14274 | − self.change_selections(None, window, cx, |s| { | |
| 14275 | − s.select_anchors(entry.selections.to_vec()) | |
| 14409 | + self.selection_history.mode = SelectionHistoryMode::Undoing; | |
| 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 | 14418 | self.select_next_state = entry.select_next_state; |
| 14278 | 14419 | self.select_prev_state = entry.select_prev_state; |
| 14279 | 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 | 14424 | pub fn redo_selection( |
| @@ -14289,18 +14428,20 @@ impl Editor { · 3 unmodified lines | ||
| 14289 | 14428 | cx: &mut Context<Self>, |
| 14290 | 14429 | ) { |
| 14291 | 14430 | self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); |
| 14292 | − self.end_selection(window, cx); | |
| 14293 | − self.selection_history.mode = SelectionHistoryMode::Redoing; | |
| 14294 | 14431 | if let Some(entry) = self.selection_history.redo_stack.pop_back() { |
| 14295 | − self.change_selections(None, window, cx, |s| { | |
| 14296 | − s.select_anchors(entry.selections.to_vec()) | |
| 14432 | + self.selection_history.mode = SelectionHistoryMode::Redoing; | |
| 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 | 14441 | self.select_next_state = entry.select_next_state; |
| 14299 | 14442 | self.select_prev_state = entry.select_prev_state; |
| 14300 | 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 | 14447 | pub fn expand_excerpts( |
| @@ -15605,7 +15746,7 @@ impl Editor { · 1298 unmodified lines | ||
| 15605 | 15746 | Some(self.perform_format( |
| 15606 | 15747 | project, |
| 15607 | 15748 | FormatTrigger::Manual, |
| 15608 | − FormatTarget::Buffers, | |
| 15749 | + FormatTarget::Buffers(self.buffer.read(cx).all_buffers()), | |
| 15609 | 15750 | window, |
| 15610 | 15751 | cx, |
| 15611 | 15752 | )) |
| @@ -15650,13 +15791,7 @@ impl Editor { · 38 unmodified lines | ||
| 15650 | 15791 | ) -> Task<Result<()>> { |
| 15651 | 15792 | let buffer = self.buffer.clone(); |
| 15652 | 15793 | let (buffers, target) = match target { |
| 15653 | − FormatTarget::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 | − } | |
| 15794 | + FormatTarget::Buffers(buffers) => (buffers, LspFormatTarget::Buffers), | |
| 15660 | 15795 | FormatTarget::Ranges(selection_ranges) => { |
| 15661 | 15796 | let multi_buffer = buffer.read(cx); |
| 15662 | 15797 | let snapshot = multi_buffer.read(cx); |
| @@ -16113,13 +16248,16 @@ impl Editor { · 450 unmodified lines | ||
| 16113 | 16248 | window: &Window, |
| 16114 | 16249 | cx: &mut Context<Self>, |
| 16115 | 16250 | ) -> Option<()> { |
| 16116 | − let project = self.project.as_ref()?.downgrade(); | |
| 16251 | + if !self.mode().is_full() { | |
| 16252 | + return None; | |
| 16253 | + } | |
| 16117 | 16254 | let pull_diagnostics_settings = ProjectSettings::get_global(cx) |
| 16118 | 16255 | .diagnostics |
| 16119 | 16256 | .lsp_pull_diagnostics; |
| 16120 | 16257 | if !pull_diagnostics_settings.enabled { |
| 16121 | 16258 | return None; |
| 16122 | 16259 | } |
| 16260 | + let project = self.project.as_ref()?.downgrade(); | |
| 16123 | 16261 | let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms); |
| 16124 | 16262 | let mut buffers = self.buffer.read(cx).all_buffers(); |
| 16125 | 16263 | if let Some(buffer_id) = buffer_id { |
| @@ -16176,7 +16314,13 @@ impl Editor { · 50 unmodified lines | ||
| 16176 | 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 | 16326 | pub fn transact( |
| @@ -17098,7 +17242,16 @@ impl Editor { · 915 unmodified lines | ||
| 17098 | 17242 | } |
| 17099 | 17243 | |
| 17100 | 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 | 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 | 18281 | .selections |
| 18120 | 18282 | .disjoint_anchors() |
| 18121 | 18283 | .iter() |
| 18122 | − .map(|selection| { | |
| 18123 | − let range = if selection.reversed { | |
| 18124 | − selection.end.text_anchor..selection.start.text_anchor | |
| 18125 | − } else { | |
| 18126 | − selection.start.text_anchor..selection.end.text_anchor | |
| 18127 | − }; | |
| 18128 | − Location { | |
| 18129 | − buffer: buffer.clone(), | |
| 18130 | − range, | |
| 18131 | − } | |
| 18284 | + .map(|range| Location { | |
| 18285 | + buffer: buffer.clone(), | |
| 18286 | + range: range.start.text_anchor..range.end.text_anchor, | |
| 18132 | 18287 | }) |
| 18133 | 18288 | .collect::<Vec<_>>(); |
| 18134 | 18289 | |
| @@ -18383,12 +18538,12 @@ impl Editor { · 248 unmodified lines | ||
| 18383 | 18538 | |
| 18384 | 18539 | pub fn highlight_gutter<T: 'static>( |
| 18385 | 18540 | &mut self, |
| 18386 | − ranges: &[Range<Anchor>], | |
| 18541 | + ranges: impl Into<Vec<Range<Anchor>>>, | |
| 18387 | 18542 | color_fetcher: fn(&App) -> Hsla, |
| 18388 | 18543 | cx: &mut Context<Self>, |
| 18389 | 18544 | ) { |
| 18390 | 18545 | self.gutter_highlights |
| 18391 | − .insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges))); | |
| 18546 | + .insert(TypeId::of::<T>(), (color_fetcher, ranges.into())); | |
| 18392 | 18547 | cx.notify(); |
| 18393 | 18548 | } |
| 18394 | 18549 | |
| @@ -18400,6 +18555,65 @@ impl Editor { · 5 unmodified lines | ||
| 18400 | 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 | 18617 | #[cfg(feature = "test-support")] |
| 18404 | 18618 | pub fn all_text_background_highlights( |
| 18405 | 18619 | &self, |
| @@ -19662,7 +19876,7 @@ impl Editor { · 1256 unmodified lines | ||
| 19662 | 19876 | .flatten() |
| 19663 | 19877 | .filter_map(|keystroke| { |
| 19664 | 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 | 19880 | } else { |
| 19667 | 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 | 19971 | pub fn register_action<A: Action>( |
| 19743 | 19972 | &mut self, |
| 19744 | 19973 | listener: impl Fn(&A, &mut Window, &mut App) + 'static, |
| @@ -19747,7 +19976,7 @@ impl Editor { · 2 unmodified lines | ||
| 19747 | 19976 | let listener = Arc::new(listener); |
| 19748 | 19977 | self.editor_actions.borrow_mut().insert( |
| 19749 | 19978 | id, |
| 19750 | − Box::new(move |window, _| { | |
| 19979 | + Box::new(move |_, window, _| { | |
| 19751 | 19980 | let listener = listener.clone(); |
| 19752 | 19981 | window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| { |
| 19753 | 19982 | let action = action.downcast_ref().unwrap(); |
| @@ -19945,12 +20174,15 @@ impl Editor { · 191 unmodified lines | ||
| 19945 | 20174 | if !selections.is_empty() { |
| 19946 | 20175 | let snapshot = |
| 19947 | 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 | 20179 | self.change_selections(None, window, cx, |s| { |
| 19949 | 20180 | s.select_ranges(selections.into_iter().map(|(start, end)| { |
| 19950 | 20181 | snapshot.clip_offset(start, Bias::Left) |
| 19951 | 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 | 20208 | let buffer = buffer.read(cx); |
| 19977 | 20209 | let buffer_snapshot = buffer.snapshot(); |
| 19978 | 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 | 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 | 20218 | if scope.prefers_label_for_snippet_in_completion() { |
| 19982 | 20219 | if let Some(label) = completion.label() { |
| 19983 | 20220 | if matches!( |
| @@ -21440,8 +21677,8 @@ impl EditorSnapshot { · 1456 unmodified lines | ||
| 21440 | 21677 | return None; |
| 21441 | 21678 | } |
| 21442 | 21679 | |
| 21443 | − let em_width = cx.text_system().em_width(font_id, font_size).log_err()?; | |
| 21444 | − let em_advance = cx.text_system().em_advance(font_id, font_size).log_err()?; | |
| 21680 | + let ch_width = cx.text_system().ch_width(font_id, font_size).log_err()?; | |
| 21681 | + let ch_advance = cx.text_system().ch_advance(font_id, font_size).log_err()?; | |
| 21445 | 21682 | |
| 21446 | 21683 | let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| { |
| 21447 | 21684 | matches!( |
| @@ -21454,8 +21691,10 @@ impl EditorSnapshot { · 6 unmodified lines | ||
| 21454 | 21691 | .show_line_numbers |
| 21455 | 21692 | .unwrap_or(gutter_settings.line_numbers); |
| 21456 | 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. | |
| 21458 | − let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32; | |
| 21694 | + // Avoid flicker-like gutter resizes when the line number gains another digit by | |
| 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 | 21698 | max_line_number_width.max(min_width_for_number_on_gutter) |
| 21460 | 21699 | } else { |
| 21461 | 21700 | 0.0.into() |
| @@ -21478,20 +21717,20 @@ impl EditorSnapshot { · 16 unmodified lines | ||
| 21478 | 21717 | + MAX_RELATIVE_TIMESTAMP.len() |
| 21479 | 21718 | + SPACING_WIDTH; |
| 21480 | 21719 | |
| 21481 | − em_advance * max_char_count | |
| 21720 | + ch_advance * max_char_count | |
| 21482 | 21721 | }); |
| 21483 | 21722 | |
| 21484 | 21723 | let is_singleton = self.buffer_snapshot.is_singleton(); |
| 21485 | 21724 | |
| 21486 | 21725 | let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO); |
| 21487 | 21726 | left_padding += if !is_singleton { |
| 21488 | − em_width * 4.0 | |
| 21727 | + ch_width * 4.0 | |
| 21489 | 21728 | } else if show_runnables || show_breakpoints { |
| 21490 | − em_width * 3.0 | |
| 21729 | + ch_width * 3.0 | |
| 21491 | 21730 | } else if show_git_gutter && show_line_numbers { |
| 21492 | − em_width * 2.0 | |
| 21731 | + ch_width * 2.0 | |
| 21493 | 21732 | } else if show_git_gutter || show_line_numbers { |
| 21494 | − em_width | |
| 21733 | + ch_width | |
| 21495 | 21734 | } else { |
| 21496 | 21735 | px(0.) |
| 21497 | 21736 | }; |
| @@ -21499,11 +21738,11 @@ impl EditorSnapshot { · 1 unmodified line | ||
| 21499 | 21738 | let shows_folds = is_singleton && gutter_settings.folds; |
| 21500 | 21739 | |
| 21501 | 21740 | let right_padding = if shows_folds && show_line_numbers { |
| 21502 | − em_width * 4.0 | |
| 21741 | + ch_width * 4.0 | |
| 21503 | 21742 | } else if shows_folds || (!is_singleton && show_line_numbers) { |
| 21504 | − em_width * 3.0 | |
| 21743 | + ch_width * 3.0 | |
| 21505 | 21744 | } else if show_line_numbers { |
| 21506 | − em_width | |
| 21745 | + ch_width | |
| 21507 | 21746 | } else { |
| 21508 | 21747 | px(0.) |
| 21509 | 21748 | }; |
| @@ -21709,7 +21948,7 @@ impl Render for Editor { · 199 unmodified lines | ||
| 21709 | 21948 | |
| 21710 | 21949 | let background = match self.mode { |
| 21711 | 21950 | EditorMode::SingleLine { .. } => cx.theme().system().transparent, |
| 21712 | − EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, | |
| 21951 | + EditorMode::AutoHeight { .. } => cx.theme().system().transparent, | |
| 21713 | 21952 | EditorMode::Full { .. } => cx.theme().colors().editor_background, |
| 21714 | 21953 | EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7), |
| 21715 | 21954 | }; |
| @@ -22367,6 +22606,7 @@ impl BreakpointPromptEditor { · 651 unmodified lines | ||
| 22367 | 22606 | let prompt = cx.new(|cx| { |
| 22368 | 22607 | let mut prompt = Editor::new( |
| 22369 | 22608 | EditorMode::AutoHeight { |
| 22609 | + min_lines: 1, | |
| 22370 | 22610 | max_lines: Self::MAX_LINES as usize, |
| 22371 | 22611 | }, |
| 22372 | 22612 | buffer, |
| @@ -150,6 +150,7 @@ impl Minimap { · 149 unmodified lines | ||
| 150 | 150 | |
| 151 | 151 | #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] |
| 152 | 152 | pub struct Gutter { |
| 153 | + pub min_line_number_digits: usize, | |
| 153 | 154 | pub line_numbers: bool, |
| 154 | 155 | pub runnables: bool, |
| 155 | 156 | pub breakpoints: bool, |
| @@ -609,6 +610,10 @@ pub struct GutterContent { · 453 unmodified lines | ||
| 609 | 610 | /// |
| 610 | 611 | /// Default: true |
| 611 | 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 | 617 | /// Whether to show runnable buttons in the gutter. |
| 613 | 618 | /// |
| 614 | 619 | /// Default: true |
| @@ -56,7 +56,7 @@ use util::{ · 55 unmodified lines | ||
| 56 | 56 | }; |
| 57 | 57 | use workspace::{ |
| 58 | 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 | 62 | #[gpui::test] |
| @@ -1907,7 +1907,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { · 1844 unmodified lines | ||
| 1907 | 1907 | DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4), |
| 1908 | 1908 | ]) |
| 1909 | 1909 | }); |
| 1910 | − | |
| 1911 | 1910 | editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); |
| 1912 | 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 | 1926 | assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx); |
| 1928 | 1927 | |
| 1929 | 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 | 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 | 1934 | editor.move_right(&MoveRight, window, cx); |
| 1936 | 1935 | editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); |
| 1937 | 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 | 1938 | editor, |
| 1940 | 1939 | cx, |
| 1941 | 1940 | ); |
| 1942 | 1941 | |
| 1943 | 1942 | editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); |
| 1944 | 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 | 1945 | editor, |
| 1947 | 1946 | cx, |
| 1948 | 1947 | ); |
| 1949 | 1948 | |
| 1950 | 1949 | editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx); |
| 1951 | 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 | 1952 | editor, |
| 1954 | 1953 | cx, |
| 1955 | 1954 | ); |
| @@ -9042,7 +9041,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 7086 unmodified lines | ||
| 9042 | 9041 | ); |
| 9043 | 9042 | let save = editor |
| 9044 | 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 | 9054 | .unwrap(); |
| 9048 | 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 | 9082 | let save = editor |
| 9076 | 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 | 9094 | .unwrap(); |
| 9080 | 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 | 9104 | // Set rust language override and assert overridden tabsize is sent to language server |
| 9106 | 9105 | update_test_language_settings(cx, |settings| { |
| 9107 | 9106 | settings.languages.insert( |
| @@ -9129,7 +9128,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 21 unmodified lines | ||
| 9129 | 9128 | }); |
| 9130 | 9129 | let save = editor |
| 9131 | 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 | 9141 | .unwrap(); |
| 9135 | 9142 | cx.executor().start_waiting(); |
| @@ -9297,7 +9304,15 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { · 161 unmodified lines | ||
| 9297 | 9304 | cx.executor().start_waiting(); |
| 9298 | 9305 | let save = multi_buffer_editor |
| 9299 | 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 | 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 | 9523 | #[gpui::test] |
| 9345 | 9524 | async fn test_range_format_during_save(cx: &mut TestAppContext) { |
| 9346 | 9525 | init_test(cx, |_| {}); |
| @@ -9384,7 +9563,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 37 unmodified lines | ||
| 9384 | 9563 | |
| 9385 | 9564 | let save = editor |
| 9386 | 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 | 9576 | .unwrap(); |
| 9390 | 9577 | fake_server |
| @@ -9427,7 +9614,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 36 unmodified lines | ||
| 9427 | 9614 | ); |
| 9428 | 9615 | let save = editor |
| 9429 | 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 | 9627 | .unwrap(); |
| 9433 | 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 | 9637 | // For non-dirty buffer, no formatting request should be sent |
| 9443 | 9638 | let save = editor |
| 9444 | 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 | 9650 | .unwrap(); |
| 9448 | 9651 | let _pending_format_request = fake_server |
| 9449 | 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 | 9655 | .next(); |
| 9453 | 9656 | cx.executor().start_waiting(); |
| @@ -9470,7 +9673,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 16 unmodified lines | ||
| 9470 | 9673 | assert!(cx.read(|cx| editor.is_dirty(cx))); |
| 9471 | 9674 | let save = editor |
| 9472 | 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 | 9686 | .unwrap(); |
| 9476 | 9687 | fake_server |
| @@ -9554,7 +9765,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { · 77 unmodified lines | ||
| 9554 | 9765 | editor.perform_format( |
| 9555 | 9766 | project.clone(), |
| 9556 | 9767 | FormatTrigger::Manual, |
| 9557 | − FormatTarget::Buffers, | |
| 9768 | + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), | |
| 9558 | 9769 | window, |
| 9559 | 9770 | cx, |
| 9560 | 9771 | ) |
| @@ -9600,7 +9811,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { · 39 unmodified lines | ||
| 9600 | 9811 | editor.perform_format( |
| 9601 | 9812 | project, |
| 9602 | 9813 | FormatTrigger::Manual, |
| 9603 | − FormatTarget::Buffers, | |
| 9814 | + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), | |
| 9604 | 9815 | window, |
| 9605 | 9816 | cx, |
| 9606 | 9817 | ) |
| @@ -9778,7 +9989,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { · 171 unmodified lines | ||
| 9778 | 9989 | editor.perform_format( |
| 9779 | 9990 | project.clone(), |
| 9780 | 9991 | FormatTrigger::Manual, |
| 9781 | − FormatTarget::Buffers, | |
| 9992 | + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), | |
| 9782 | 9993 | window, |
| 9783 | 9994 | cx, |
| 9784 | 9995 | ) |
| @@ -9814,7 +10025,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { · 29 unmodified lines | ||
| 9814 | 10025 | editor.perform_format( |
| 9815 | 10026 | project.clone(), |
| 9816 | 10027 | FormatTrigger::Manual, |
| 9817 | − FormatTarget::Buffers, | |
| 10028 | + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), | |
| 9818 | 10029 | window, |
| 9819 | 10030 | cx, |
| 9820 | 10031 | ) |
| @@ -15356,7 +15567,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { · 5535 unmodified lines | ||
| 15356 | 15567 | editor.perform_format( |
| 15357 | 15568 | project.clone(), |
| 15358 | 15569 | FormatTrigger::Manual, |
| 15359 | − FormatTarget::Buffers, | |
| 15570 | + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), | |
| 15360 | 15571 | window, |
| 15361 | 15572 | cx, |
| 15362 | 15573 | ) |
| @@ -15376,7 +15587,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { · 13 unmodified lines | ||
| 15376 | 15587 | editor.perform_format( |
| 15377 | 15588 | project.clone(), |
| 15378 | 15589 | FormatTrigger::Manual, |
| 15379 | − FormatTarget::Buffers, | |
| 15590 | + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), | |
| 15380 | 15591 | window, |
| 15381 | 15592 | cx, |
| 15382 | 15593 | ) |
| @@ -21911,6 +22122,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { · 6528 unmodified lines | ||
| 21911 | 22122 | .downcast::<Editor>() |
| 21912 | 22123 | .unwrap(); |
| 21913 | 22124 | let fake_server = fake_servers.next().await.unwrap(); |
| 22125 | + let server_id = fake_server.server.server_id(); | |
| 21914 | 22126 | let mut first_request = fake_server |
| 21915 | 22127 | .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| { |
| 21916 | 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 | 22154 | .expect("created a singleton buffer") |
| 21943 | 22155 | .read(cx) |
| 21944 | 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 | 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 | 22203 | "Cursor movement should not trigger diagnostic request" |
| 21989 | 22204 | ); |
| 21990 | 22205 | ensure_result_id(Some("2".to_string()), cx); |
| 21991 | − | |
| 21992 | 22206 | // Multiple rapid edits should be debounced |
| 21993 | 22207 | for _ in 0..5 { |
| 21994 | 22208 | editor.update_in(cx, |editor, window, cx| { |
| @@ -1,15 +1,15 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | 2 | ActiveDiagnostic, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement, |
| 3 | − CodeActionSource, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, | |
| 4 | − ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, | |
| 5 | − DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, | |
| 6 | − Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, | |
| 7 | − FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, | |
| 8 | − InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, | |
| 9 | − MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, | |
| 10 | − OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, | |
| 11 | − SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, | |
| 12 | − StickyHeaderExcerpt, ToPoint, ToggleFold, | |
| 3 | + CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, | |
| 4 | + ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, | |
| 5 | + DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, | |
| 6 | + EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, | |
| 7 | + FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, | |
| 8 | + HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, | |
| 9 | + LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, | |
| 10 | + PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, | |
| 11 | + SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint, | |
| 12 | + ToggleFold, | |
| 13 | 13 | code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, |
| 14 | 14 | display_map::{ |
| 15 | 15 | Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk, |
| @@ -75,7 +75,7 @@ use std::{ · 59 unmodified lines | ||
| 75 | 75 | ops::{Deref, Range}, |
| 76 | 76 | rc::Rc, |
| 77 | 77 | sync::Arc, |
| 78 | − time::Duration, | |
| 78 | + time::{Duration, Instant}, | |
| 79 | 79 | }; |
| 80 | 80 | use sum_tree::Bias; |
| 81 | 81 | use text::{BufferId, SelectionGoal}; |
| @@ -87,6 +87,7 @@ use util::{RangeExt, ResultExt, debug_panic}; · 5 unmodified lines | ||
| 87 | 87 | use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; |
| 88 | 88 | |
| 89 | 89 | const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; |
| 90 | +const SELECTION_DRAG_DELAY: Duration = Duration::from_millis(300); | |
| 90 | 91 | |
| 91 | 92 | /// Determines what kinds of highlights should be applied to a lines background. |
| 92 | 93 | #[derive(Clone, Copy, Default)] |
| @@ -107,6 +108,12 @@ struct SelectionLayout { · 14 unmodified lines | ||
| 107 | 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 | 117 | impl SelectionLayout { |
| 111 | 118 | fn new<T: ToPoint + ToDisplayPoint + Clone>( |
| 112 | 119 | selection: Selection<T>, |
| @@ -187,7 +194,7 @@ impl EditorElement { · 74 unmodified lines | ||
| 187 | 194 | let editor = &self.editor; |
| 188 | 195 | editor.update(cx, |editor, cx| { |
| 189 | 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 | 649 | editor.selection_drag_state = SelectionDragState::ReadyToDrag { |
| 643 | 650 | selection: newest_anchor.clone(), |
| 644 | 651 | click_position: event.position, |
| 652 | + mouse_down_time: Instant::now(), | |
| 645 | 653 | }; |
| 646 | 654 | cx.stop_propagation(); |
| 647 | 655 | return; |
| @@ -692,12 +700,15 @@ impl EditorElement { · 44 unmodified lines | ||
| 692 | 700 | } |
| 693 | 701 | |
| 694 | 702 | let position = point_for_position.previous_valid; |
| 695 | − let multi_cursor_modifier = Editor::multi_cursor_modifier(true, &modifiers, cx); | |
| 696 | − if Editor::columnar_selection_modifiers(multi_cursor_modifier, &modifiers) { | |
| 703 | + if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) { | |
| 697 | 704 | editor.select( |
| 698 | 705 | SelectPhase::BeginColumnar { |
| 699 | 706 | position, |
| 700 | − reset: true, | |
| 707 | + reset: match mode { | |
| 708 | + ColumnarMode::FromMouse => true, | |
| 709 | + ColumnarMode::FromSelection => false, | |
| 710 | + }, | |
| 711 | + mode: mode, | |
| 701 | 712 | goal_column: point_for_position.exact_unclipped.column(), |
| 702 | 713 | }, |
| 703 | 714 | window, |
| @@ -717,7 +728,7 @@ impl EditorElement { · 13 unmodified lines | ||
| 717 | 728 | editor.select( |
| 718 | 729 | SelectPhase::Begin { |
| 719 | 730 | position, |
| 720 | − add: multi_cursor_modifier, | |
| 731 | + add: Editor::multi_cursor_modifier(true, &modifiers, cx), | |
| 721 | 732 | click_count, |
| 722 | 733 | }, |
| 723 | 734 | window, |
| @@ -814,6 +825,7 @@ impl EditorElement { · 90 unmodified lines | ||
| 814 | 825 | SelectPhase::BeginColumnar { |
| 815 | 826 | position, |
| 816 | 827 | reset: true, |
| 828 | + mode: ColumnarMode::FromMouse, | |
| 817 | 829 | goal_column: point_for_position.exact_unclipped.column(), |
| 818 | 830 | }, |
| 819 | 831 | window, |
| @@ -837,6 +849,7 @@ impl EditorElement { · 17 unmodified lines | ||
| 837 | 849 | SelectionDragState::ReadyToDrag { |
| 838 | 850 | selection: _, |
| 839 | 851 | ref click_position, |
| 852 | + mouse_down_time: _, | |
| 840 | 853 | } => { |
| 841 | 854 | if event.position == *click_position { |
| 842 | 855 | editor.select( |
| @@ -851,13 +864,18 @@ impl EditorElement { · 8 unmodified lines | ||
| 851 | 864 | editor.selection_drag_state = SelectionDragState::None; |
| 852 | 865 | cx.stop_propagation(); |
| 853 | 866 | return; |
| 867 | + } else { | |
| 868 | + debug_panic!("drag state can never be in ready state after drag") | |
| 854 | 869 | } |
| 855 | 870 | } |
| 856 | 871 | SelectionDragState::Dragging { ref selection, .. } => { |
| 857 | 872 | let snapshot = editor.snapshot(window, cx); |
| 858 | 873 | let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot)); |
| 859 | − if !point_for_position.intersects_selection(&selection_display) { | |
| 860 | − let is_cut = !event.modifiers.control; | |
| 874 | + if !point_for_position.intersects_selection(&selection_display) | |
| 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 | 879 | editor.move_selection_on_drop( |
| 862 | 880 | &selection.clone(), |
| 863 | 881 | point_for_position.previous_valid, |
| @@ -865,10 +883,11 @@ impl EditorElement { · 1 unmodified line | ||
| 865 | 883 | window, |
| 866 | 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 | 960 | return; |
| 942 | 961 | } |
| 943 | 962 | |
| 944 | − let text_bounds = position_map.text_hitbox.bounds; | |
| 945 | 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(); | |
| 948 | − let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); | |
| 949 | − let top = text_bounds.origin.y + vertical_margin; | |
| 950 | − let bottom = text_bounds.bottom_left().y - vertical_margin; | |
| 951 | − if event.position.y < top { | |
| 952 | − scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); | |
| 953 | − } | |
| 954 | − if event.position.y > bottom { | |
| 955 | − scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); | |
| 956 | − } | |
| 966 | + let scroll_delta = { | |
| 967 | + let text_bounds = text_hitbox.bounds; | |
| 968 | + let mut scroll_delta = gpui::Point::<f32>::default(); | |
| 969 | + let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); | |
| 970 | + let top = text_bounds.origin.y + vertical_margin; | |
| 971 | + let bottom = text_bounds.bottom_left().y - vertical_margin; | |
| 972 | + if event.position.y < top { | |
| 973 | + scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); | |
| 974 | + } | |
| 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 | |
| 959 | − let style = editor.style.clone().unwrap_or_default(); | |
| 960 | − let font_id = window.text_system().resolve_font(&style.text.font()); | |
| 961 | − 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(); | |
| 979 | + // We need horizontal width of text | |
| 980 | + let style = editor.style.clone().unwrap_or_default(); | |
| 981 | + let font_id = window.text_system().resolve_font(&style.text.font()); | |
| 982 | + let font_size = style.text.font_size.to_pixels(window.rem_size()); | |
| 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; | |
| 969 | − let right = text_bounds.top_right().x - scroll_space; | |
| 989 | + let left = text_bounds.origin.x + scroll_space; | |
| 990 | + let right = text_bounds.top_right().x - scroll_space; | |
| 970 | 991 | |
| 971 | − if event.position.x < left { | |
| 972 | − scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); | |
| 973 | − } | |
| 974 | − if event.position.x > right { | |
| 975 | − scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); | |
| 976 | − } | |
| 992 | + if event.position.x < left { | |
| 993 | + scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); | |
| 994 | + } | |
| 995 | + if event.position.x > right { | |
| 996 | + scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); | |
| 997 | + } | |
| 998 | + scroll_delta | |
| 999 | + }; | |
| 977 | 1000 | |
| 978 | 1001 | if !editor.has_pending_selection() { |
| 979 | 1002 | let drop_anchor = position_map |
| @@ -982,28 +1005,60 @@ impl EditorElement { · 2 unmodified lines | ||
| 982 | 1005 | match editor.selection_drag_state { |
| 983 | 1006 | SelectionDragState::Dragging { |
| 984 | 1007 | ref mut drop_cursor, |
| 1008 | + ref mut hide_drop_cursor, | |
| 985 | 1009 | .. |
| 986 | 1010 | } => { |
| 987 | 1011 | drop_cursor.start = drop_anchor; |
| 988 | 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, .. } => { | |
| 991 | − let drop_cursor = Selection { | |
| 992 | − id: post_inc(&mut editor.selections.next_selection_id), | |
| 993 | − start: drop_anchor, | |
| 994 | − end: drop_anchor, | |
| 995 | − reversed: false, | |
| 996 | − goal: SelectionGoal::None, | |
| 997 | − }; | |
| 998 | − editor.selection_drag_state = SelectionDragState::Dragging { | |
| 999 | − selection: selection.clone(), | |
| 1000 | − drop_cursor, | |
| 1001 | − }; | |
| 1017 | + SelectionDragState::ReadyToDrag { | |
| 1018 | + ref selection, | |
| 1019 | + ref click_position, | |
| 1020 | + ref mouse_down_time, | |
| 1021 | + } => { | |
| 1022 | + if mouse_down_time.elapsed() >= SELECTION_DRAG_DELAY { | |
| 1023 | + let drop_cursor = Selection { | |
| 1024 | + id: post_inc(&mut editor.selections.next_selection_id), | |
| 1025 | + start: drop_anchor, | |
| 1026 | + end: drop_anchor, | |
| 1027 | + reversed: false, | |
| 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 | 1062 | } else { |
| 1008 | 1063 | editor.select( |
| 1009 | 1064 | SelectPhase::Update { |
| @@ -1027,17 +1082,70 @@ impl EditorElement { · 17 unmodified lines | ||
| 1027 | 1082 | let text_hitbox = &position_map.text_hitbox; |
| 1028 | 1083 | let gutter_hitbox = &position_map.gutter_hitbox; |
| 1029 | 1084 | let modifiers = event.modifiers; |
| 1085 | + let text_hovered = text_hitbox.is_hovered(window); | |
| 1030 | 1086 | let gutter_hovered = gutter_hitbox.is_hovered(window); |
| 1031 | 1087 | editor.set_gutter_hovered(gutter_hovered, cx); |
| 1032 | 1088 | editor.mouse_cursor_hidden = false; |
| 1033 | 1089 | |
| 1034 | − if gutter_hovered { | |
| 1035 | − let new_point = position_map | |
| 1036 | − .point_for_position(event.position) | |
| 1037 | − .previous_valid; | |
| 1090 | + let point_for_position = position_map.point_for_position(event.position); | |
| 1091 | + let valid_point = point_for_position.previous_valid; | |
| 1092 | + | |
| 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 | 1146 | let buffer_anchor = position_map |
| 1039 | 1147 | .snapshot |
| 1040 | − .display_point_to_anchor(new_point, Bias::Left); | |
| 1148 | + .display_point_to_anchor(valid_point, Bias::Left); | |
| 1041 | 1149 | |
| 1042 | 1150 | if let Some((buffer_snapshot, file)) = position_map |
| 1043 | 1151 | .snapshot |
| @@ -1045,7 +1153,6 @@ impl EditorElement { · 1 unmodified line | ||
| 1045 | 1153 | .buffer_for_excerpt(buffer_anchor.excerpt_id) |
| 1046 | 1154 | .and_then(|buffer| buffer.file().map(|file| (buffer, file))) |
| 1047 | 1155 | { |
| 1048 | − let was_hovered = editor.gutter_breakpoint_indicator.0.is_some(); | |
| 1049 | 1156 | let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot); |
| 1050 | 1157 | |
| 1051 | 1158 | let is_visible = editor |
| @@ -1073,43 +1180,46 @@ impl EditorElement { · 21 unmodified lines | ||
| 1073 | 1180 | .is_some() |
| 1074 | 1181 | }); |
| 1075 | 1182 | |
| 1076 | − editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator { | |
| 1077 | − display_row: new_point.row(), | |
| 1078 | − is_active: is_visible, | |
| 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 { | |
| 1183 | + if !is_visible { | |
| 1184 | + editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| { | |
| 1185 | + cx.spawn(async move |this, cx| { | |
| 1085 | 1186 | cx.background_executor() |
| 1086 | 1187 | .timer(Duration::from_millis(200)) |
| 1087 | 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(); | |
| 1098 | − }) | |
| 1099 | − }); | |
| 1199 | + }); | |
| 1200 | + } | |
| 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 | 1207 | } else { |
| 1101 | − editor.gutter_breakpoint_indicator = (None, None); | |
| 1208 | + editor.gutter_breakpoint_indicator.1 = None; | |
| 1209 | + None | |
| 1102 | 1210 | } |
| 1103 | 1211 | } else { |
| 1104 | − editor.gutter_breakpoint_indicator = (None, None); | |
| 1105 | − } | |
| 1212 | + editor.gutter_breakpoint_indicator.1 = None; | |
| 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 | 1221 | // Don't trigger hover popover if mouse is hovering over context menu |
| 1110 | − if text_hitbox.is_hovered(window) { | |
| 1111 | − let point_for_position = position_map.point_for_position(event.position); | |
| 1112 | − | |
| 1222 | + if text_hovered { | |
| 1113 | 1223 | editor.update_hovered_link( |
| 1114 | 1224 | point_for_position, |
| 1115 | 1225 | &position_map.snapshot, |
| @@ -1247,16 +1357,18 @@ impl EditorElement { · 131 unmodified lines | ||
| 1247 | 1357 | if let SelectionDragState::Dragging { |
| 1248 | 1358 | ref selection, |
| 1249 | 1359 | ref drop_cursor, |
| 1360 | + ref hide_drop_cursor, | |
| 1250 | 1361 | } = editor.selection_drag_state |
| 1251 | 1362 | { |
| 1252 | − if drop_cursor | |
| 1253 | − .start | |
| 1254 | − .cmp(&selection.start, &snapshot.buffer_snapshot) | |
| 1255 | − .eq(&Ordering::Less) | |
| 1256 | − || drop_cursor | |
| 1257 | − .end | |
| 1258 | − .cmp(&selection.end, &snapshot.buffer_snapshot) | |
| 1259 | − .eq(&Ordering::Greater) | |
| 1363 | + if !hide_drop_cursor | |
| 1364 | + && (drop_cursor | |
| 1365 | + .start | |
| 1366 | + .cmp(&selection.start, &snapshot.buffer_snapshot) | |
| 1367 | + .eq(&Ordering::Less) | |
| 1368 | + || drop_cursor | |
| 1369 | + .end | |
| 1370 | + .cmp(&selection.end, &snapshot.buffer_snapshot) | |
| 1371 | + .eq(&Ordering::Greater)) | |
| 1260 | 1372 | { |
| 1261 | 1373 | let drag_cursor_layout = SelectionLayout::new( |
| 1262 | 1374 | drop_cursor.clone(), |
| @@ -1451,7 +1563,7 @@ impl EditorElement { · 188 unmodified lines | ||
| 1451 | 1563 | snapshot |
| 1452 | 1564 | .grapheme_at(cursor_position) |
| 1453 | 1565 | .or_else(|| { |
| 1454 | − if cursor_column == 0 { | |
| 1566 | + if snapshot.is_empty() { | |
| 1455 | 1567 | snapshot.placeholder_text().and_then(|s| { |
| 1456 | 1568 | s.graphemes(true).next().map(|s| s.to_string().into()) |
| 1457 | 1569 | }) |
| @@ -2230,7 +2342,7 @@ impl EditorElement { · 772 unmodified lines | ||
| 2230 | 2342 | text_hitbox: &Hitbox, |
| 2231 | 2343 | window: &mut Window, |
| 2232 | 2344 | cx: &mut App, |
| 2233 | − ) -> Option<AnyElement> { | |
| 2345 | + ) -> Option<InlineBlameLayout> { | |
| 2234 | 2346 | if !self |
| 2235 | 2347 | .editor |
| 2236 | 2348 | .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx)) |
| @@ -2259,13 +2371,13 @@ impl EditorElement { · 22 unmodified lines | ||
| 2259 | 2371 | padding * em_width |
| 2260 | 2372 | }; |
| 2261 | 2373 | |
| 2262 | − let blame_entry = blame | |
| 2374 | + let entry = blame | |
| 2263 | 2375 | .update(cx, |blame, cx| { |
| 2264 | 2376 | blame.blame_for_rows(&[*row_info], cx).next() |
| 2265 | 2377 | }) |
| 2266 | 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 | 2382 | let start_y = content_origin.y |
| 2271 | 2383 | + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); |
| @@ -2294,24 +2406,19 @@ impl EditorElement { · 22 unmodified lines | ||
| 2294 | 2406 | let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); |
| 2295 | 2407 | let bounds = Bounds::new(absolute_offset, size); |
| 2296 | 2408 | |
| 2297 | − self.layout_blame_entry_popover( | |
| 2298 | − bounds, | |
| 2299 | − blame_entry, | |
| 2300 | − blame, | |
| 2301 | − line_height, | |
| 2302 | − text_hitbox, | |
| 2303 | − window, | |
| 2304 | − cx, | |
| 2305 | − ); | |
| 2409 | + self.layout_blame_entry_popover(entry.clone(), blame, line_height, text_hitbox, window, cx); | |
| 2306 | 2410 | |
| 2307 | 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 | 2420 | fn layout_blame_entry_popover( |
| 2313 | 2421 | &self, |
| 2314 | − parent_bounds: Bounds<Pixels>, | |
| 2315 | 2422 | blame_entry: BlameEntry, |
| 2316 | 2423 | blame: Entity<GitBlame>, |
| 2317 | 2424 | line_height: Pixels, |
| @@ -2319,91 +2426,59 @@ impl EditorElement { · 1 unmodified line | ||
| 2319 | 2426 | window: &mut Window, |
| 2320 | 2427 | cx: &mut App, |
| 2321 | 2428 | ) { |
| 2322 | − let mouse_position = window.mouse_position(); | |
| 2323 | − let mouse_over_inline_blame = parent_bounds.contains(&mouse_position); | |
| 2324 | − let mouse_over_popover = self.editor.read_with(cx, |editor, _| { | |
| 2429 | + let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| { | |
| 2325 | 2430 | editor |
| 2326 | 2431 | .inline_blame_popover |
| 2327 | 2432 | .as_ref() |
| 2328 | − .and_then(|state| state.popover_bounds) | |
| 2329 | − .map_or(false, |bounds| bounds.contains(&mouse_position)) | |
| 2330 | − }); | |
| 2331 | − | |
| 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 | − }); | |
| 2433 | + .map(|state| (state.popover_state.clone(), state.position)) | |
| 2434 | + }) else { | |
| 2435 | + return; | |
| 2436 | + }; | |
| 2339 | 2437 | |
| 2340 | − let should_draw = self.editor.read_with(cx, |editor, _| { | |
| 2341 | − editor | |
| 2342 | − .inline_blame_popover | |
| 2343 | − .as_ref() | |
| 2344 | − .map_or(false, |state| state.show_task.is_none()) | |
| 2438 | + let workspace = self | |
| 2439 | + .editor | |
| 2440 | + .read_with(cx, |editor, _| editor.workspace().map(|w| w.downgrade())); | |
| 2441 | + | |
| 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 { | |
| 2348 | − let maybe_element = self.editor.update(cx, |editor, cx| { | |
| 2349 | − editor | |
| 2350 | − .workspace() | |
| 2351 | − .map(|workspace| workspace.downgrade()) | |
| 2352 | − .zip( | |
| 2353 | − editor | |
| 2354 | − .inline_blame_popover | |
| 2355 | − .as_ref() | |
| 2356 | − .map(|p| p.popover_state.clone()), | |
| 2357 | − ) | |
| 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 | − }; | |
| 2455 | + if let Some(mut element) = maybe_element { | |
| 2456 | + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); | |
| 2457 | + let overall_height = size.height + HOVER_POPOVER_GAP; | |
| 2458 | + let popover_origin = if target_point.y > overall_height { | |
| 2459 | + point(target_point.x, target_point.y - size.height) | |
| 2460 | + } else { | |
| 2461 | + point( | |
| 2462 | + target_point.x, | |
| 2463 | + target_point.y + line_height + HOVER_POPOVER_GAP, | |
| 2464 | + ) | |
| 2465 | + }; | |
| 2389 | 2466 | |
| 2390 | − let horizontal_offset = (text_hitbox.top_right().x | |
| 2391 | − - POPOVER_RIGHT_OFFSET | |
| 2392 | − - (popover_origin.x + size.width)) | |
| 2393 | − .min(Pixels::ZERO); | |
| 2467 | + let horizontal_offset = (text_hitbox.top_right().x | |
| 2468 | + - POPOVER_RIGHT_OFFSET | |
| 2469 | + - (popover_origin.x + size.width)) | |
| 2470 | + .min(Pixels::ZERO); | |
| 2394 | 2471 | |
| 2395 | − point(popover_origin.x + horizontal_offset, popover_origin.y) | |
| 2396 | − }); | |
| 2472 | + let origin = point(popover_origin.x + horizontal_offset, popover_origin.y); | |
| 2473 | + let popover_bounds = Bounds::new(origin, size); | |
| 2397 | 2474 | |
| 2398 | − let popover_bounds = Bounds::new(origin, size); | |
| 2399 | − self.editor.update(cx, |editor, _| { | |
| 2400 | − if let Some(state) = &mut editor.inline_blame_popover { | |
| 2401 | − state.popover_bounds = Some(popover_bounds); | |
| 2402 | − } | |
| 2403 | − }); | |
| 2475 | + self.editor.update(cx, |editor, _| { | |
| 2476 | + if let Some(state) = &mut editor.inline_blame_popover { | |
| 2477 | + state.popover_bounds = Some(popover_bounds); | |
| 2478 | + } | |
| 2479 | + }); | |
| 2404 | 2480 | |
| 2405 | − window.defer_draw(element, origin, 2); | |
| 2406 | − } | |
| 2481 | + window.defer_draw(element, origin, 2); | |
| 2407 | 2482 | } |
| 2408 | 2483 | } |
| 2409 | 2484 | |
| @@ -2812,7 +2887,8 @@ impl EditorElement { · 402 unmodified lines | ||
| 2812 | 2887 | let available_width = gutter_dimensions.left_padding - git_gutter_width; |
| 2813 | 2888 | |
| 2814 | 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 | 2892 | && row_info |
| 2817 | 2893 | .buffer_row |
| 2818 | 2894 | .is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length) |
| @@ -4730,7 +4806,6 @@ impl EditorElement { · 1911 unmodified lines | ||
| 4730 | 4806 | row_range: Range<DisplayRow>, |
| 4731 | 4807 | row_infos: &[RowInfo], |
| 4732 | 4808 | text_hitbox: &Hitbox, |
| 4733 | − position_map: &PositionMap, | |
| 4734 | 4809 | newest_cursor_position: Option<DisplayPoint>, |
| 4735 | 4810 | line_height: Pixels, |
| 4736 | 4811 | right_margin: Pixels, |
| @@ -4740,14 +4815,15 @@ impl EditorElement { · 3 unmodified lines | ||
| 4740 | 4815 | editor: Entity<Editor>, |
| 4741 | 4816 | window: &mut Window, |
| 4742 | 4817 | cx: &mut App, |
| 4743 | − ) -> Vec<AnyElement> { | |
| 4818 | + ) -> (Vec<AnyElement>, Vec<(DisplayRow, Bounds<Pixels>)>) { | |
| 4744 | 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 | 4822 | let mut controls = vec![]; |
| 4823 | + let mut control_bounds = vec![]; | |
| 4748 | 4824 | |
| 4749 | 4825 | let active_positions = [ |
| 4750 | − Some(point_for_position.previous_valid), | |
| 4826 | + hovered_diff_hunk_row.map(|row| DisplayPoint::new(row, 0)), | |
| 4751 | 4827 | newest_cursor_position, |
| 4752 | 4828 | ]; |
| 4753 | 4829 | |
| @@ -4792,6 +4868,7 @@ impl EditorElement { · 38 unmodified lines | ||
| 4792 | 4868 | { |
| 4793 | 4869 | continue; |
| 4794 | 4870 | } |
| 4871 | + | |
| 4795 | 4872 | if active_positions |
| 4796 | 4873 | .iter() |
| 4797 | 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 | 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 | 4898 | window.with_absolute_element_offset(gpui::Point::new(x, y), |window| { |
| 4819 | 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 | 4909 | fn layout_signature_help( |
| @@ -5562,6 +5642,12 @@ impl EditorElement { · 732 unmodified lines | ||
| 5562 | 5642 | let editor = self.editor.read(cx); |
| 5563 | 5643 | if editor.mouse_cursor_hidden { |
| 5564 | 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 | 5651 | } else if editor |
| 5566 | 5652 | .hovered_link_state |
| 5567 | 5653 | .as_ref() |
| @@ -6203,9 +6289,9 @@ impl EditorElement { · 635 unmodified lines | ||
| 6203 | 6289 | } |
| 6204 | 6290 | |
| 6205 | 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 | 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 | 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 | 7425 | // For a whitespace to be on a boundary, any of the following conditions need to be met: |
| 7329 | 7426 | // - It is a tab |
| 7330 | 7427 | // - It is adjacent to an edge (start or end) |
| @@ -7537,7 +7634,7 @@ impl Element for EditorElement { · 206 unmodified lines | ||
| 7537 | 7634 | fn request_layout( |
| 7538 | 7635 | &mut self, |
| 7539 | 7636 | _: Option<&GlobalElementId>, |
| 7540 | − __inspector_id: Option<&gpui::InspectorElementId>, | |
| 7637 | + _inspector_id: Option<&gpui::InspectorElementId>, | |
| 7541 | 7638 | window: &mut Window, |
| 7542 | 7639 | cx: &mut App, |
| 7543 | 7640 | ) -> (gpui::LayoutId, ()) { |
| @@ -7588,7 +7685,10 @@ impl Element for EditorElement { · 44 unmodified lines | ||
| 7588 | 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 | 7692 | let editor_handle = cx.entity().clone(); |
| 7593 | 7693 | let max_line_number_width = |
| 7594 | 7694 | self.max_line_number_width(&editor.snapshot(window, cx), window, cx); |
| @@ -7599,6 +7699,7 @@ impl Element for EditorElement { · 4 unmodified lines | ||
| 7599 | 7699 | .update(cx, |editor, cx| { |
| 7600 | 7700 | compute_auto_height_layout( |
| 7601 | 7701 | editor, |
| 7702 | + min_lines, | |
| 7602 | 7703 | max_lines, |
| 7603 | 7704 | max_line_number_width, |
| 7604 | 7705 | known_dimensions, |
| @@ -7673,8 +7774,7 @@ impl Element for EditorElement { · 68 unmodified lines | ||
| 7673 | 7774 | let line_height = style.text.line_height_in_pixels(window.rem_size()); |
| 7674 | 7775 | let em_width = window.text_system().em_width(font_id, font_size).unwrap(); |
| 7675 | 7776 | let em_advance = window.text_system().em_advance(font_id, font_size).unwrap(); |
| 7676 | − | |
| 7677 | − let glyph_grid_cell = size(em_width, line_height); | |
| 7777 | + let glyph_grid_cell = size(em_advance, line_height); | |
| 7678 | 7778 | |
| 7679 | 7779 | let gutter_dimensions = snapshot |
| 7680 | 7780 | .gutter_dimensions( |
| @@ -8233,7 +8333,7 @@ impl Element for EditorElement { · 552 unmodified lines | ||
| 8233 | 8333 | MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row); |
| 8234 | 8334 | |
| 8235 | 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 | 8337 | max_scroll_top, |
| 8238 | 8338 | ); |
| 8239 | 8339 | |
| @@ -8245,7 +8345,7 @@ impl Element for EditorElement { · 5 unmodified lines | ||
| 8245 | 8345 | start_row, |
| 8246 | 8346 | editor_content_width, |
| 8247 | 8347 | scroll_width, |
| 8248 | − em_width, | |
| 8348 | + em_advance, | |
| 8249 | 8349 | &line_layouts, |
| 8250 | 8350 | cx, |
| 8251 | 8351 | ) |
| @@ -8260,10 +8360,9 @@ impl Element for EditorElement { · 8 unmodified lines | ||
| 8260 | 8360 | }); |
| 8261 | 8361 | |
| 8262 | 8362 | let scroll_pixel_position = point( |
| 8263 | − scroll_position.x * em_width, | |
| 8363 | + scroll_position.x * em_advance, | |
| 8264 | 8364 | scroll_position.y * line_height, |
| 8265 | 8365 | ); |
| 8266 | − | |
| 8267 | 8366 | let indent_guides = self.layout_indent_guides( |
| 8268 | 8367 | content_origin, |
| 8269 | 8368 | text_hitbox.origin, |
| @@ -8328,7 +8427,7 @@ impl Element for EditorElement { · 58 unmodified lines | ||
| 8328 | 8427 | cx, |
| 8329 | 8428 | ); |
| 8330 | 8429 | |
| 8331 | − let mut inline_blame = None; | |
| 8430 | + let mut inline_blame_layout = None; | |
| 8332 | 8431 | let mut inline_code_actions = None; |
| 8333 | 8432 | if let Some(newest_selection_head) = newest_selection_head { |
| 8334 | 8433 | let display_row = newest_selection_head.row(); |
| @@ -8350,7 +8449,7 @@ impl Element for EditorElement { · 15 unmodified lines | ||
| 8350 | 8449 | let line_layout = &line_layouts[line_ix]; |
| 8351 | 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 | 8453 | display_row, |
| 8355 | 8454 | row_info, |
| 8356 | 8455 | line_layout, |
| @@ -8362,8 +8461,8 @@ impl Element for EditorElement { · 5 unmodified lines | ||
| 8362 | 8461 | &text_hitbox, |
| 8363 | 8462 | window, |
| 8364 | 8463 | cx, |
| 8365 | − ); | |
| 8366 | − if inline_blame.is_some() { | |
| 8464 | + ) { | |
| 8465 | + inline_blame_layout = Some(layout); | |
| 8367 | 8466 | // Blame overrides inline diagnostics |
| 8368 | 8467 | inline_diagnostics.remove(&display_row); |
| 8369 | 8468 | } |
| @@ -8633,6 +8732,25 @@ impl Element for EditorElement { · 263 unmodified lines | ||
| 8633 | 8732 | |
| 8634 | 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 | 8754 | let position_map = Rc::new(PositionMap { |
| 8637 | 8755 | size: bounds.size, |
| 8638 | 8756 | visible_row_range, |
| @@ -8645,32 +8763,17 @@ impl Element for EditorElement { · 6 unmodified lines | ||
| 8645 | 8763 | snapshot, |
| 8646 | 8764 | gutter_hitbox: gutter_hitbox.clone(), |
| 8647 | 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 | 8773 | self.editor.update(cx, |editor, _| { |
| 8651 | 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 | 8777 | EditorLayout { |
| 8675 | 8778 | mode, |
| 8676 | 8779 | position_map, |
| @@ -8692,7 +8795,7 @@ impl Element for EditorElement { · 15 unmodified lines | ||
| 8692 | 8795 | line_numbers, |
| 8693 | 8796 | blamed_display_rows, |
| 8694 | 8797 | inline_diagnostics, |
| 8695 | − inline_blame, | |
| 8798 | + inline_blame_layout, | |
| 8696 | 8799 | inline_code_actions, |
| 8697 | 8800 | blocks, |
| 8698 | 8801 | cursors, |
| @@ -8718,7 +8821,7 @@ impl Element for EditorElement { · 19 unmodified lines | ||
| 8718 | 8821 | fn paint( |
| 8719 | 8822 | &mut self, |
| 8720 | 8823 | _: Option<&GlobalElementId>, |
| 8721 | − __inspector_id: Option<&gpui::InspectorElementId>, | |
| 8824 | + _inspector_id: Option<&gpui::InspectorElementId>, | |
| 8722 | 8825 | bounds: Bounds<gpui::Pixels>, |
| 8723 | 8826 | _: &mut Self::RequestLayoutState, |
| 8724 | 8827 | layout: &mut Self::PrepaintState, |
| @@ -8873,7 +8976,7 @@ pub struct EditorLayout { · 148 unmodified lines | ||
| 8873 | 8976 | display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>, |
| 8874 | 8977 | blamed_display_rows: Option<Vec<AnyElement>>, |
| 8875 | 8978 | inline_diagnostics: HashMap<DisplayRow, AnyElement>, |
| 8876 | − inline_blame: Option<AnyElement>, | |
| 8979 | + inline_blame_layout: Option<InlineBlameLayout>, | |
| 8877 | 8980 | inline_code_actions: Option<AnyElement>, |
| 8878 | 8981 | blocks: Vec<BlockLayout>, |
| 8879 | 8982 | highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>, |
| @@ -9333,6 +9436,9 @@ pub(crate) struct PositionMap { · 453 unmodified lines | ||
| 9333 | 9436 | pub snapshot: EditorSnapshot, |
| 9334 | 9437 | pub text_hitbox: Hitbox, |
| 9335 | 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 | 9444 | #[derive(Debug, Copy, Clone)] |
| @@ -9388,7 +9494,7 @@ impl PositionMap { · 49 unmodified lines | ||
| 9388 | 9494 | let scroll_position = self.snapshot.scroll_position(); |
| 9389 | 9495 | let position = position - text_bounds.origin; |
| 9390 | 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 | 9498 | let row = ((y / self.line_height) + scroll_position.y) as u32; |
| 9393 | 9499 | |
| 9394 | 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 | 9873 | fn compute_auto_height_layout( |
| 9768 | 9874 | editor: &mut Editor, |
| 9875 | + min_lines: usize, | |
| 9769 | 9876 | max_lines: usize, |
| 9770 | 9877 | max_line_number_width: Pixels, |
| 9771 | 9878 | known_dimensions: Size<Option<Pixels>>, |
| @@ -9813,7 +9920,7 @@ fn compute_auto_height_layout( · 41 unmodified lines | ||
| 9813 | 9920 | |
| 9814 | 9921 | let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height; |
| 9815 | 9922 | let height = scroll_height |
| 9816 | − .max(line_height) | |
| 9923 | + .max(line_height * min_lines as f32) | |
| 9817 | 9924 | .min(line_height * max_lines as f32); |
| 9818 | 9925 | |
| 9819 | 9926 | Some(size(width, height)) |
| @@ -10116,7 +10223,10 @@ mod tests { · 296 unmodified lines | ||
| 10116 | 10223 | |
| 10117 | 10224 | for editor_mode_without_invisibles in [ |
| 10118 | 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 | 10231 | for show_line_numbers in [true, false] { |
| 10122 | 10232 | let invisibles = collect_invisibles_from_new_editor( |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | 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 | 5 | editor_settings::SeedQuerySetting, |
| 5 | 6 | persistence::{DB, SerializedEditor}, |
| 6 | 7 | scroll::ScrollAnchor, |
| @@ -40,7 +41,7 @@ use ui::{IconDecorationKind, prelude::*}; · 33 unmodified lines | ||
| 40 | 41 | use util::{ResultExt, TryFutureExt, paths::PathExt}; |
| 41 | 42 | use workspace::{ |
| 42 | 43 | CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, |
| 43 | − item::{FollowableItem, Item, ItemEvent, ProjectItem}, | |
| 44 | + item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, | |
| 44 | 45 | searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, |
| 45 | 46 | }; |
| 46 | 47 | use workspace::{ |
| @@ -611,12 +612,13 @@ impl Item for Editor { · 564 unmodified lines | ||
| 611 | 612 | if newest_selection.head() == offset { |
| 612 | 613 | false |
| 613 | 614 | } else { |
| 614 | − let nav_history = self.nav_history.take(); | |
| 615 | 615 | self.set_scroll_anchor(scroll_anchor, window, cx); |
| 616 | − self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { | |
| 617 | − s.select_ranges([offset..offset]) | |
| 618 | − }); | |
| 619 | − self.nav_history = nav_history; | |
| 616 | + self.change_selections( | |
| 617 | + SelectionEffects::default().nav_history(false), | |
| 618 | + window, | |
| 619 | + cx, | |
| 620 | + |s| s.select_ranges([offset..offset]), | |
| 621 | + ); | |
| 620 | 622 | true |
| 621 | 623 | } |
| 622 | 624 | } else { |
| @@ -805,7 +807,7 @@ impl Item for Editor { · 182 unmodified lines | ||
| 805 | 807 | |
| 806 | 808 | fn save( |
| 807 | 809 | &mut self, |
| 808 | − format: bool, | |
| 810 | + options: SaveOptions, | |
| 809 | 811 | project: Entity<Project>, |
| 810 | 812 | window: &mut Window, |
| 811 | 813 | cx: &mut Context<Self>, |
| @@ -816,13 +818,25 @@ impl Item for Editor { · 4 unmodified lines | ||
| 816 | 818 | .into_iter() |
| 817 | 819 | .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone())) |
| 818 | 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 | 833 | cx.spawn_in(window, async move |this, cx| { |
| 820 | − if format { | |
| 834 | + if options.format { | |
| 821 | 835 | this.update_in(cx, |editor, window, cx| { |
| 822 | 836 | editor.perform_format( |
| 823 | 837 | project.clone(), |
| 824 | 838 | FormatTrigger::Save, |
| 825 | − FormatTarget::Buffers, | |
| 839 | + FormatTarget::Buffers(buffers_to_save.clone()), | |
| 826 | 840 | window, |
| 827 | 841 | cx, |
| 828 | 842 | ) |
| @@ -830,33 +844,28 @@ impl Item for Editor { · 1 unmodified line | ||
| 830 | 844 | .await?; |
| 831 | 845 | } |
| 832 | 846 | |
| 833 | − if buffers.len() == 1 { | |
| 834 | − // Apply full save routine for singleton buffers, to allow to `touch` the file via the editor. | |
| 847 | + if !buffers_to_save.is_empty() { | |
| 835 | 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 | 852 | .await?; |
| 838 | − } else { | |
| 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 | − }); | |
| 853 | + } | |
| 847 | 854 | |
| 848 | − project | |
| 849 | − .update(cx, |project, cx| project.save_buffers(dirty_buffers, cx))? | |
| 850 | − .await?; | |
| 851 | − for buffer in clean_buffers { | |
| 852 | − buffer | |
| 853 | − .update(cx, |buffer, cx| { | |
| 854 | − let version = buffer.saved_version().clone(); | |
| 855 | − let mtime = buffer.saved_mtime(); | |
| 856 | − buffer.did_save(version, mtime, cx); | |
| 857 | − }) | |
| 858 | − .ok(); | |
| 859 | − } | |
| 855 | + // Notify about clean buffers for language server events | |
| 856 | + let buffers_that_were_not_saved: Vec<_> = buffers | |
| 857 | + .into_iter() | |
| 858 | + .filter(|b| !buffers_to_save.contains(b)) | |
| 859 | + .collect(); | |
| 860 | + | |
| 861 | + for buffer in buffers_that_were_not_saved { | |
| 862 | + buffer | |
| 863 | + .update(cx, |buffer, cx| { | |
| 864 | + let version = buffer.saved_version().clone(); | |
| 865 | + let mtime = buffer.saved_mtime(); | |
| 866 | + buffer.did_save(version, mtime, cx); | |
| 867 | + }) | |
| 868 | + .ok(); | |
| 860 | 869 | } |
| 861 | 870 | |
| 862 | 871 | Ok(()) |
| @@ -8,7 +8,7 @@ use util::ResultExt as _; · 7 unmodified lines | ||
| 8 | 8 | use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node}; |
| 9 | 9 | use text::{Anchor, OffsetRangeExt as _}; |
| 10 | 10 | |
| 11 | −use crate::Editor; | |
| 11 | +use crate::{Editor, SelectionEffects}; | |
| 12 | 12 | |
| 13 | 13 | pub struct JsxTagCompletionState { |
| 14 | 14 | edit_index: usize, |
| @@ -600,9 +600,14 @@ pub(crate) fn handle_from( · 585 unmodified lines | ||
| 600 | 600 | }) |
| 601 | 601 | .collect::<Vec<_>>(); |
| 602 | 602 | this.update_in(cx, |this, window, cx| { |
| 603 | − this.change_selections_without_updating_completions(None, window, cx, |s| { | |
| 604 | − s.select(base_selections); | |
| 605 | − }); | |
| 603 | + this.change_selections( | |
| 604 | + SelectionEffects::no_scroll().completions(false), | |
| 605 | + window, | |
| 606 | + cx, | |
| 607 | + |s| { | |
| 608 | + s.select(base_selections); | |
| 609 | + }, | |
| 610 | + ); | |
| 606 | 611 | }) |
| 607 | 612 | .ok()?; |
| 608 | 613 | } |
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint, | |
| 3 | − DisplaySnapshot, Editor, FindAllReferences, GoToDeclaration, GoToDefinition, | |
| 4 | − GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, | |
| 5 | − SelectionExt, ToDisplayPoint, ToggleCodeActions, | |
| 2 | + Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor, | |
| 3 | + EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation, | |
| 4 | + GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt, | |
| 5 | + ToDisplayPoint, ToggleCodeActions, | |
| 6 | 6 | actions::{Format, FormatSelections}, |
| 7 | 7 | selections_collection::SelectionsCollection, |
| 8 | 8 | }; |
| @@ -199,17 +199,14 @@ pub fn deploy_context_menu( · 190 unmodified lines | ||
| 199 | 199 | .is_some() |
| 200 | 200 | }); |
| 201 | 201 | |
| 202 | − let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx) | |
| 203 | − .map_or(false, |filter| { | |
| 204 | − !filter.is_hidden(&DebuggerEvaluateSelectedText) | |
| 205 | − }); | |
| 202 | + let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx); | |
| 206 | 203 | |
| 207 | 204 | ui::ContextMenu::build(window, cx, |menu, _window, _cx| { |
| 208 | 205 | let builder = menu |
| 209 | 206 | .on_blur_subscription(Subscription::new(|| {})) |
| 210 | 207 | .when(evaluate_selection && has_selections, |builder| { |
| 211 | 208 | builder |
| 212 | − .action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText)) | |
| 209 | + .action("Evaluate Selection", Box::new(EvaluateSelectedText)) | |
| 213 | 210 | .separator() |
| 214 | 211 | }) |
| 215 | 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 | 58 | map.clip_point(point, Bias::Left) |
| 59 | 59 | } |
| 60 | 60 | |
| 61 | −/// Returns a column to the right of the current point, doing nothing | |
| 62 | −// if that point is at the end of the line. | |
| 61 | +/// Returns a column to the right of the current point, wrapping | |
| 62 | +/// to the next line if that point is at the end of line. | |
| 63 | 63 | pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { |
| 64 | 64 | if point.column() < map.line_len(point.row()) { |
| 65 | 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 | 267 | let mut is_first_iteration = true; |
| 268 | 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 | 270 | if is_first_iteration |
| 271 | 271 | && classifier.is_punctuation(right) |
| 272 | 272 | && !classifier.is_punctuation(left) |
| 273 | + && left != '\n' | |
| 273 | 274 | { |
| 274 | 275 | is_first_iteration = false; |
| 275 | 276 | return false; |
| @@ -318,10 +319,11 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint · 42 unmodified lines | ||
| 318 | 319 | let classifier = map.buffer_snapshot.char_classifier_at(raw_point); |
| 319 | 320 | let mut is_first_iteration = true; |
| 320 | 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 | 323 | if is_first_iteration |
| 323 | 324 | && classifier.is_punctuation(left) |
| 324 | 325 | && !classifier.is_punctuation(right) |
| 326 | + && right != '\n' | |
| 325 | 327 | { |
| 326 | 328 | is_first_iteration = false; |
| 327 | 329 | return false; |
| @@ -12,7 +12,7 @@ use text::ToOffset; · 11 unmodified lines | ||
| 12 | 12 | use ui::{ButtonLike, KeyBinding, prelude::*}; |
| 13 | 13 | use workspace::{ |
| 14 | 14 | Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, |
| 15 | − searchable::SearchableItemHandle, | |
| 15 | + item::SaveOptions, searchable::SearchableItemHandle, | |
| 16 | 16 | }; |
| 17 | 17 | |
| 18 | 18 | pub struct ProposedChangesEditor { |
| @@ -351,13 +351,13 @@ impl Item for ProposedChangesEditor { · 332 unmodified lines | ||
| 351 | 351 | |
| 352 | 352 | fn save( |
| 353 | 353 | &mut self, |
| 354 | − format: bool, | |
| 354 | + options: SaveOptions, | |
| 355 | 355 | project: Entity<Project>, |
| 356 | 356 | window: &mut Window, |
| 357 | 357 | cx: &mut Context<Self>, |
| 358 | 358 | ) -> Task<anyhow::Result<()>> { |
| 359 | 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 | 669 | return; |
| 670 | 670 | } |
| 671 | 671 | |
| 672 | − let cur_position = self.scroll_position(cx); | |
| 672 | + let mut current_position = self.scroll_position(cx); | |
| 673 | 673 | let Some(visible_line_count) = self.visible_line_count() else { |
| 674 | 674 | return; |
| 675 | 675 | }; |
| 676 | − let new_pos = cur_position + point(0., amount.lines(visible_line_count)); | |
| 677 | − self.set_scroll_position(new_pos, window, cx); | |
| 676 | + | |
| 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 | 691 | /// Returns an ordering. The newest selection is: |
| @@ -1,12 +1,13 @@ | ||
| 1 | 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 | 5 | use gpui::{Bounds, Context, Pixels, Window, px}; |
| 5 | 6 | use language::Point; |
| 6 | 7 | use multi_buffer::Anchor; |
| 7 | 8 | use std::{cmp, f32}; |
| 8 | 9 | |
| 9 | −#[derive(PartialEq, Eq, Clone, Copy)] | |
| 10 | +#[derive(Debug, PartialEq, Eq, Clone, Copy)] | |
| 10 | 11 | pub enum Autoscroll { |
| 11 | 12 | Next, |
| 12 | 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 | 80 | pub enum AutoscrollStrategy { |
| 71 | 81 | Fit, |
| 72 | 82 | Newest, |
| @@ -5,6 +5,8 @@ use ui::{Pixels, px}; · 4 unmodified lines | ||
| 5 | 5 | pub enum ScrollDirection { |
| 6 | 6 | Upwards, |
| 7 | 7 | Downwards, |
| 8 | + Rightwards, | |
| 9 | + Leftwards, | |
| 8 | 10 | } |
| 9 | 11 | |
| 10 | 12 | impl ScrollDirection { |
| @@ -19,6 +21,8 @@ pub enum ScrollAmount { · 8 unmodified lines | ||
| 19 | 21 | Line(f32), |
| 20 | 22 | // Scroll N pages (positive is towards the end of the document) |
| 21 | 23 | Page(f32), |
| 24 | + // Scroll N columns (positive is towards the right of the document) | |
| 25 | + Column(f32), | |
| 22 | 26 | } |
| 23 | 27 | |
| 24 | 28 | impl ScrollAmount { |
| @@ -32,6 +36,15 @@ impl ScrollAmount { · 7 unmodified lines | ||
| 32 | 36 | } |
| 33 | 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 | 52 | match self { |
| 40 | 53 | ScrollAmount::Line(x) => px(line_height.0 * x), |
| 41 | 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 | 72 | match self { |
| 54 | 73 | Self::Line(amount) if amount.is_sign_positive() => ScrollDirection::Downwards, |
| 55 | 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 | 77 | _ => ScrollDirection::Upwards, |
| 57 | 78 | } |
| 58 | 79 | } |
| @@ -411,7 +411,7 @@ impl<'a> MutableSelectionsCollection<'a> { · 410 unmodified lines | ||
| 411 | 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 | 415 | self.collection.buffer(self.cx) |
| 416 | 416 | } |
| 417 | 417 | |
| @@ -659,6 +659,7 @@ impl<'a> MutableSelectionsCollection<'a> { · 241 unmodified lines | ||
| 659 | 659 | .collect(); |
| 660 | 660 | self.select(selections); |
| 661 | 661 | } |
| 662 | + | |
| 662 | 663 | pub fn reverse_selections(&mut self) { |
| 663 | 664 | let map = &self.display_map(); |
| 664 | 665 | let mut new_selections: Vec<Selection<Point>> = Vec::new(); |
| @@ -169,6 +169,12 @@ impl EditorLspTestContext { · 168 unmodified lines | ||
| 169 | 169 | .expect("Opened test file wasn't an editor") |
| 170 | 170 | }); |
| 171 | 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 | 178 | window.focus(&editor.focus_handle(cx)) |
| 173 | 179 | }); |
| 174 | 180 | |
| @@ -36,6 +36,7 @@ impl ExtensionEvents { · 35 unmodified lines | ||
| 36 | 36 | #[derive(Clone)] |
| 37 | 37 | pub enum Event { |
| 38 | 38 | ExtensionInstalled(Arc<ExtensionManifest>), |
| 39 | + ExtensionUninstalled(Arc<ExtensionManifest>), | |
| 39 | 40 | ExtensionsInstalledChanged, |
| 40 | 41 | ConfigureExtensionRequested(Arc<ExtensionManifest>), |
| 41 | 42 | } |
| @@ -50,7 +50,7 @@ interface dap { · 49 unmodified lines | ||
| 50 | 50 | } |
| 51 | 51 | |
| 52 | 52 | record debug-adapter-binary { |
| 53 | − command: string, | |
| 53 | + command: option<string>, | |
| 54 | 54 | arguments: list<string>, |
| 55 | 55 | envs: env-vars, |
| 56 | 56 | cwd: option<string>, |
| @@ -132,6 +132,7 @@ pub enum Event { · 131 unmodified lines | ||
| 132 | 132 | ExtensionsUpdated, |
| 133 | 133 | StartedReloading, |
| 134 | 134 | ExtensionInstalled(Arc<str>), |
| 135 | + ExtensionUninstalled(Arc<str>), | |
| 135 | 136 | ExtensionFailedToLoad(Arc<str>), |
| 136 | 137 | } |
| 137 | 138 | |
| @@ -842,6 +843,8 @@ impl ExtensionStore { · 704 unmodified lines | ||
| 842 | 843 | let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref()); |
| 843 | 844 | let fs = self.fs.clone(); |
| 844 | 845 | |
| 846 | + let extension_manifest = self.extension_manifest_for_id(&extension_id).cloned(); | |
| 847 | + | |
| 845 | 848 | match self.outstanding_operations.entry(extension_id.clone()) { |
| 846 | 849 | btree_map::Entry::Occupied(_) => return, |
| 847 | 850 | btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove), |
| @@ -878,6 +881,17 @@ impl ExtensionStore { · 30 unmodified lines | ||
| 878 | 881 | ) |
| 879 | 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 | 895 | anyhow::Ok(()) |
| 882 | 896 | }) |
| 883 | 897 | .detach_and_log_err(cx) |
| @@ -1682,12 +1696,15 @@ impl ExtensionStore { · 798 unmodified lines | ||
| 1682 | 1696 | |
| 1683 | 1697 | pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) { |
| 1684 | 1698 | let connection_options = client.read(cx).connection_options(); |
| 1685 | − if self.ssh_clients.contains_key(&connection_options.ssh_url()) { | |
| 1686 | − return; | |
| 1699 | + let ssh_url = connection_options.ssh_url(); | |
| 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 | |
| 1690 | − .insert(connection_options.ssh_url(), client.downgrade()); | |
| 1707 | + self.ssh_clients.insert(ssh_url, client.downgrade()); | |
| 1691 | 1708 | self.ssh_registered_tx.unbounded_send(()).ok(); |
| 1692 | 1709 | } |
| 1693 | 1710 | } |
| @@ -8,9 +8,9 @@ use collections::BTreeMap; · 7 unmodified lines | ||
| 8 | 8 | use extension::ExtensionHostProxy; |
| 9 | 9 | use fs::{FakeFs, Fs, RealFs}; |
| 10 | 10 | use futures::{AsyncReadExt, StreamExt, io::BufReader}; |
| 11 | −use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext}; | |
| 11 | +use gpui::{AppContext as _, SemanticVersion, TestAppContext}; | |
| 12 | 12 | use http_client::{FakeHttpClient, Response}; |
| 13 | −use language::{BinaryStatus, LanguageMatcher, LanguageRegistry}; | |
| 13 | +use language::{BinaryStatus, LanguageMatcher, LanguageRegistry, LanguageServerStatusUpdate}; | |
| 14 | 14 | use lsp::LanguageServerName; |
| 15 | 15 | use node_runtime::NodeRuntime; |
| 16 | 16 | use parking_lot::Mutex; |
| @@ -717,9 +717,18 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { · 700 unmodified lines | ||
| 717 | 717 | status_updates.next().await.unwrap(), |
| 718 | 718 | ], |
| 719 | 719 | [ |
| 720 | − (SharedString::new("gleam"), BinaryStatus::CheckingForUpdate), | |
| 721 | − (SharedString::new("gleam"), BinaryStatus::Downloading), | |
| 722 | − (SharedString::new("gleam"), BinaryStatus::None) | |
| 720 | + ( | |
| 721 | + LanguageServerName::new_static("gleam"), | |
| 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 | 378 | .await |
| 379 | 379 | } |
| 380 | + | |
| 380 | 381 | async fn get_dap_binary( |
| 381 | 382 | &self, |
| 382 | 383 | dap_name: Arc<str>, |
| @@ -724,7 +725,7 @@ impl IncrementalCompilationCache { · 341 unmodified lines | ||
| 724 | 725 | } |
| 725 | 726 | |
| 726 | 727 | impl CacheStore for IncrementalCompilationCache { |
| 727 | − fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> { | |
| 728 | + fn get(&self, key: &[u8]) -> Option<Cow<'_, [u8]>> { | |
| 728 | 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 | 555 | .child( |
| 556 | 556 | h_flex() |
| 557 | − .gap_2() | |
| 557 | + .gap_1() | |
| 558 | 558 | .justify_between() |
| 559 | 559 | .child( |
| 560 | 560 | Button::new( |
| 561 | 561 | SharedString::from(format!("rebuild-{}", extension.id)), |
| 562 | 562 | "Rebuild", |
| 563 | 563 | ) |
| 564 | + .color(Color::Accent) | |
| 565 | + .disabled(matches!(status, ExtensionStatus::Upgrading)) | |
| 564 | 566 | .on_click({ |
| 565 | 567 | let extension_id = extension.id.clone(); |
| 566 | 568 | move |_, _, cx| { |
| @@ -568,12 +570,12 @@ impl ExtensionsPage { · 1 unmodified line | ||
| 568 | 570 | store.rebuild_dev_extension(extension_id.clone(), cx) |
| 569 | 571 | }); |
| 570 | 572 | } |
| 571 | − }) | |
| 572 | − .color(Color::Accent) | |
| 573 | − .disabled(matches!(status, ExtensionStatus::Upgrading)), | |
| 573 | + }), | |
| 574 | 574 | ) |
| 575 | 575 | .child( |
| 576 | 576 | Button::new(SharedString::from(extension.id.clone()), "Uninstall") |
| 577 | + .color(Color::Accent) | |
| 578 | + .disabled(matches!(status, ExtensionStatus::Removing)) | |
| 577 | 579 | .on_click({ |
| 578 | 580 | let extension_id = extension.id.clone(); |
| 579 | 581 | move |_, _, cx| { |
| @@ -581,9 +583,7 @@ impl ExtensionsPage { · 1 unmodified line | ||
| 581 | 583 | store.uninstall_extension(extension_id.clone(), cx) |
| 582 | 584 | }); |
| 583 | 585 | } |
| 584 | − }) | |
| 585 | − .color(Color::Accent) | |
| 586 | − .disabled(matches!(status, ExtensionStatus::Removing)), | |
| 586 | + }), | |
| 587 | 587 | ) |
| 588 | 588 | .when(can_configure, |this| { |
| 589 | 589 | this.child( |
| @@ -591,8 +591,8 @@ impl ExtensionsPage { · 1 unmodified line | ||
| 591 | 591 | SharedString::from(format!("configure-{}", extension.id)), |
| 592 | 592 | "Configure", |
| 593 | 593 | ) |
| 594 | − | |
| 595 | − | |
| 594 | + .color(Color::Accent) | |
| 595 | + .disabled(matches!(status, ExtensionStatus::Installing)) | |
| 596 | 596 | .on_click({ |
| 597 | 597 | let manifest = Arc::new(extension.clone()); |
| 598 | 598 | move |_, _, cx| { |
| @@ -609,9 +609,7 @@ impl ExtensionsPage { · 10 unmodified lines | ||
| 609 | 609 | }); |
| 610 | 610 | } |
| 611 | 611 | } |
| 612 | − }) | |
| 613 | − .color(Color::Accent) | |
| 614 | − .disabled(matches!(status, ExtensionStatus::Installing)), | |
| 612 | + }), | |
| 615 | 613 | ) |
| 616 | 614 | }), |
| 617 | 615 | ), |
| @@ -1479,18 +1477,12 @@ impl Render for ExtensionsPage { · 861 unmodified lines | ||
| 1479 | 1477 | return this.py_4().child(self.render_empty_state(cx)); |
| 1480 | 1478 | } |
| 1481 | 1479 | |
| 1482 | − let extensions_page = cx.entity().clone(); | |
| 1483 | 1480 | let scroll_handle = self.list.clone(); |
| 1484 | 1481 | this.child( |
| 1485 | − uniform_list( | |
| 1486 | − extensions_page, | |
| 1487 | − "entries", | |
| 1488 | − count, | |
| 1489 | − Self::render_extensions, | |
| 1490 | − ) | |
| 1491 | − .flex_grow() | |
| 1492 | − .pb_4() | |
| 1493 | − .track_scroll(scroll_handle), | |
| 1482 | + uniform_list("entries", count, cx.processor(Self::render_extensions)) | |
| 1483 | + .flex_grow() | |
| 1484 | + .pb_4() | |
| 1485 | + .track_scroll(scroll_handle), | |
| 1494 | 1486 | ) |
| 1495 | 1487 | .child( |
| 1496 | 1488 | div() |
| @@ -209,7 +209,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { · 208 unmodified lines | ||
| 209 | 209 | "bandana", |
| 210 | 210 | "./bandana", |
| 211 | 211 | ".\\bandana", |
| 212 | − util::separator!("a/bandana"), | |
| 212 | + util::path!("a/bandana"), | |
| 213 | 213 | "b/bandana", |
| 214 | 214 | "b\\bandana", |
| 215 | 215 | " bandana", |
| @@ -230,7 +230,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { · 14 unmodified lines | ||
| 230 | 230 | assert_eq!( |
| 231 | 231 | picker.delegate.matches.len(), |
| 232 | 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 | 234 | 1 |
| 235 | 235 | } else { |
| 236 | 236 | 2 |
| @@ -897,6 +897,7 @@ struct FakeFsState { · 896 unmodified lines | ||
| 897 | 897 | buffered_events: Vec<PathEvent>, |
| 898 | 898 | metadata_call_count: usize, |
| 899 | 899 | read_dir_call_count: usize, |
| 900 | + path_write_counts: std::collections::HashMap<PathBuf, usize>, | |
| 900 | 901 | moves: std::collections::HashMap<u64, PathBuf>, |
| 901 | 902 | home_dir: Option<PathBuf>, |
| 902 | 903 | } |
| @@ -1083,6 +1084,7 @@ impl FakeFs { · 180 unmodified lines | ||
| 1083 | 1084 | events_paused: false, |
| 1084 | 1085 | read_dir_call_count: 0, |
| 1085 | 1086 | metadata_call_count: 0, |
| 1087 | + path_write_counts: Default::default(), | |
| 1086 | 1088 | moves: Default::default(), |
| 1087 | 1089 | home_dir: None, |
| 1088 | 1090 | })), |
| @@ -1173,6 +1175,8 @@ impl FakeFs { · 84 unmodified lines | ||
| 1173 | 1175 | recreate_inode: bool, |
| 1174 | 1176 | ) -> Result<()> { |
| 1175 | 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 | 1180 | let new_inode = state.get_and_increment_inode(); |
| 1177 | 1181 | let new_mtime = state.get_and_increment_mtime(); |
| 1178 | 1182 | let new_len = new_content.len() as u64; |
| @@ -1727,6 +1731,17 @@ impl FakeFs { · 548 unmodified lines | ||
| 1727 | 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 | 1745 | fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> { |
| 1731 | 1746 | self.executor.simulate_random_delay() |
| 1732 | 1747 | } |
| @@ -57,6 +57,7 @@ actions!( · 56 unmodified lines | ||
| 57 | 57 | ExpandCommitEditor, |
| 58 | 58 | GenerateCommitMessage, |
| 59 | 59 | Init, |
| 60 | + OpenModifiedFiles, | |
| 60 | 61 | ] |
| 61 | 62 | ); |
| 62 | 63 | |
| @@ -26,8 +26,8 @@ use std::{ · 25 unmodified lines | ||
| 26 | 26 | }; |
| 27 | 27 | use sum_tree::MapSeekTarget; |
| 28 | 28 | use thiserror::Error; |
| 29 | −use util::ResultExt; | |
| 30 | 29 | use util::command::{new_smol_command, new_std_command}; |
| 30 | +use util::{ResultExt, paths}; | |
| 31 | 31 | use uuid::Uuid; |
| 32 | 32 | |
| 33 | 33 | pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession}; |
| @@ -323,7 +323,7 @@ pub trait GitRepository: Send + Sync { · 289 unmodified lines | ||
| 323 | 323 | /// Resolve a list of refs to SHAs. |
| 324 | 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 | 327 | async move { |
| 328 | 328 | self.revparse_batch(vec!["HEAD".into()]) |
| 329 | 329 | .await |
| @@ -508,6 +508,50 @@ pub struct GitRepositoryCheckpoint { · 178 unmodified lines | ||
| 508 | 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 | 555 | impl GitRepository for RealGitRepository { |
| 512 | 556 | fn reload_index(&self) { |
| 513 | 557 | if let Ok(mut index) = self.repository.lock().index() { |
| @@ -525,7 +569,7 @@ impl GitRepository for RealGitRepository { · 11 unmodified lines | ||
| 525 | 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 | 573 | let working_directory = self.working_directory(); |
| 530 | 574 | self.executor |
| 531 | 575 | .spawn(async move { |
| @@ -561,7 +605,7 @@ impl GitRepository for RealGitRepository { · 29 unmodified lines | ||
| 561 | 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 | 609 | let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned) |
| 566 | 610 | else { |
| 567 | 611 | return future::ready(Err(anyhow!("no working directory"))).boxed(); |
| @@ -668,7 +712,7 @@ impl GitRepository for RealGitRepository { · 100 unmodified lines | ||
| 668 | 712 | commit: String, |
| 669 | 713 | mode: ResetMode, |
| 670 | 714 | env: Arc<HashMap<String, String>>, |
| 671 | − ) -> BoxFuture<Result<()>> { | |
| 715 | + ) -> BoxFuture<'_, Result<()>> { | |
| 672 | 716 | async move { |
| 673 | 717 | let working_directory = self.working_directory(); |
| 674 | 718 | |
| @@ -698,7 +742,7 @@ impl GitRepository for RealGitRepository { · 23 unmodified lines | ||
| 698 | 742 | commit: String, |
| 699 | 743 | paths: Vec<RepoPath>, |
| 700 | 744 | env: Arc<HashMap<String, String>>, |
| 701 | − ) -> BoxFuture<Result<()>> { | |
| 745 | + ) -> BoxFuture<'_, Result<()>> { | |
| 702 | 746 | let working_directory = self.working_directory(); |
| 703 | 747 | let git_binary_path = self.git_binary_path.clone(); |
| 704 | 748 | async move { |
| @@ -723,7 +767,7 @@ impl GitRepository for RealGitRepository { · 18 unmodified lines | ||
| 723 | 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 | 771 | // https://git-scm.com/book/en/v2/Git-Internals-Git-Objects |
| 728 | 772 | const GIT_MODE_SYMLINK: u32 = 0o120000; |
| 729 | 773 | |
| @@ -756,7 +800,7 @@ impl GitRepository for RealGitRepository { · 26 unmodified lines | ||
| 756 | 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 | 804 | let repo = self.repository.clone(); |
| 761 | 805 | self.executor |
| 762 | 806 | .spawn(async move { |
| @@ -777,7 +821,7 @@ impl GitRepository for RealGitRepository { · 14 unmodified lines | ||
| 777 | 821 | path: RepoPath, |
| 778 | 822 | content: Option<String>, |
| 779 | 823 | env: Arc<HashMap<String, String>>, |
| 780 | − ) -> BoxFuture<anyhow::Result<()>> { | |
| 824 | + ) -> BoxFuture<'_, anyhow::Result<()>> { | |
| 781 | 825 | let working_directory = self.working_directory(); |
| 782 | 826 | let git_binary_path = self.git_binary_path.clone(); |
| 783 | 827 | self.executor |
| @@ -841,7 +885,7 @@ impl GitRepository for RealGitRepository { · 57 unmodified lines | ||
| 841 | 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 | 889 | let working_directory = self.working_directory(); |
| 846 | 890 | self.executor |
| 847 | 891 | .spawn(async move { |
| @@ -891,14 +935,14 @@ impl GitRepository for RealGitRepository { · 43 unmodified lines | ||
| 891 | 935 | .boxed() |
| 892 | 936 | } |
| 893 | 937 | |
| 894 | − fn merge_message(&self) -> BoxFuture<Option<String>> { | |
| 938 | + fn merge_message(&self) -> BoxFuture<'_, Option<String>> { | |
| 895 | 939 | let path = self.path().join("MERGE_MSG"); |
| 896 | 940 | self.executor |
| 897 | 941 | .spawn(async move { std::fs::read_to_string(&path).ok() }) |
| 898 | 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 | 946 | let git_binary_path = self.git_binary_path.clone(); |
| 903 | 947 | let working_directory = self.working_directory(); |
| 904 | 948 | let path_prefixes = path_prefixes.to_owned(); |
| @@ -919,7 +963,7 @@ impl GitRepository for RealGitRepository { · 14 unmodified lines | ||
| 919 | 963 | .boxed() |
| 920 | 964 | } |
| 921 | 965 | |
| 922 | − fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> { | |
| 966 | + fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> { | |
| 923 | 967 | let working_directory = self.working_directory(); |
| 924 | 968 | let git_binary_path = self.git_binary_path.clone(); |
| 925 | 969 | self.executor |
| @@ -986,7 +1030,7 @@ impl GitRepository for RealGitRepository { · 60 unmodified lines | ||
| 986 | 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 | 1034 | let repo = self.repository.clone(); |
| 991 | 1035 | self.executor |
| 992 | 1036 | .spawn(async move { |
| @@ -1018,7 +1062,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines | ||
| 1018 | 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 | 1066 | let repo = self.repository.clone(); |
| 1023 | 1067 | self.executor |
| 1024 | 1068 | .spawn(async move { |
| @@ -1030,7 +1074,7 @@ impl GitRepository for RealGitRepository { · 5 unmodified lines | ||
| 1030 | 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 | 1078 | let working_directory = self.working_directory(); |
| 1035 | 1079 | let git_binary_path = self.git_binary_path.clone(); |
| 1036 | 1080 | |
| @@ -1052,7 +1096,7 @@ impl GitRepository for RealGitRepository { · 15 unmodified lines | ||
| 1052 | 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 | 1100 | let working_directory = self.working_directory(); |
| 1057 | 1101 | let git_binary_path = self.git_binary_path.clone(); |
| 1058 | 1102 | self.executor |
| @@ -1083,7 +1127,7 @@ impl GitRepository for RealGitRepository { · 24 unmodified lines | ||
| 1083 | 1127 | &self, |
| 1084 | 1128 | paths: Vec<RepoPath>, |
| 1085 | 1129 | env: Arc<HashMap<String, String>>, |
| 1086 | − ) -> BoxFuture<Result<()>> { | |
| 1130 | + ) -> BoxFuture<'_, Result<()>> { | |
| 1087 | 1131 | let working_directory = self.working_directory(); |
| 1088 | 1132 | let git_binary_path = self.git_binary_path.clone(); |
| 1089 | 1133 | self.executor |
| @@ -1111,7 +1155,7 @@ impl GitRepository for RealGitRepository { · 21 unmodified lines | ||
| 1111 | 1155 | &self, |
| 1112 | 1156 | paths: Vec<RepoPath>, |
| 1113 | 1157 | env: Arc<HashMap<String, String>>, |
| 1114 | − ) -> BoxFuture<Result<()>> { | |
| 1158 | + ) -> BoxFuture<'_, Result<()>> { | |
| 1115 | 1159 | let working_directory = self.working_directory(); |
| 1116 | 1160 | let git_binary_path = self.git_binary_path.clone(); |
| 1117 | 1161 | |
| @@ -1143,7 +1187,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines | ||
| 1143 | 1187 | name_and_email: Option<(SharedString, SharedString)>, |
| 1144 | 1188 | options: CommitOptions, |
| 1145 | 1189 | env: Arc<HashMap<String, String>>, |
| 1146 | − ) -> BoxFuture<Result<()>> { | |
| 1190 | + ) -> BoxFuture<'_, Result<()>> { | |
| 1147 | 1191 | let working_directory = self.working_directory(); |
| 1148 | 1192 | self.executor |
| 1149 | 1193 | .spawn(async move { |
| @@ -1182,7 +1226,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines | ||
| 1182 | 1226 | ask_pass: AskPassDelegate, |
| 1183 | 1227 | env: Arc<HashMap<String, String>>, |
| 1184 | 1228 | cx: AsyncApp, |
| 1185 | − ) -> BoxFuture<Result<RemoteCommandOutput>> { | |
| 1229 | + ) -> BoxFuture<'_, Result<RemoteCommandOutput>> { | |
| 1186 | 1230 | let working_directory = self.working_directory(); |
| 1187 | 1231 | let executor = cx.background_executor().clone(); |
| 1188 | 1232 | async move { |
| @@ -1214,7 +1258,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines | ||
| 1214 | 1258 | ask_pass: AskPassDelegate, |
| 1215 | 1259 | env: Arc<HashMap<String, String>>, |
| 1216 | 1260 | cx: AsyncApp, |
| 1217 | − ) -> BoxFuture<Result<RemoteCommandOutput>> { | |
| 1261 | + ) -> BoxFuture<'_, Result<RemoteCommandOutput>> { | |
| 1218 | 1262 | let working_directory = self.working_directory(); |
| 1219 | 1263 | let executor = cx.background_executor().clone(); |
| 1220 | 1264 | async move { |
| @@ -1239,7 +1283,7 @@ impl GitRepository for RealGitRepository { · 18 unmodified lines | ||
| 1239 | 1283 | ask_pass: AskPassDelegate, |
| 1240 | 1284 | env: Arc<HashMap<String, String>>, |
| 1241 | 1285 | cx: AsyncApp, |
| 1242 | − ) -> BoxFuture<Result<RemoteCommandOutput>> { | |
| 1286 | + ) -> BoxFuture<'_, Result<RemoteCommandOutput>> { | |
| 1243 | 1287 | let working_directory = self.working_directory(); |
| 1244 | 1288 | let remote_name = format!("{}", fetch_options); |
| 1245 | 1289 | let executor = cx.background_executor().clone(); |
| @@ -1257,7 +1301,7 @@ impl GitRepository for RealGitRepository { · 11 unmodified lines | ||
| 1257 | 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 | 1305 | let working_directory = self.working_directory(); |
| 1262 | 1306 | let git_binary_path = self.git_binary_path.clone(); |
| 1263 | 1307 | self.executor |
| @@ -1303,7 +1347,7 @@ impl GitRepository for RealGitRepository { · 39 unmodified lines | ||
| 1303 | 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 | 1351 | let working_directory = self.working_directory(); |
| 1308 | 1352 | let git_binary_path = self.git_binary_path.clone(); |
| 1309 | 1353 | self.executor |
| @@ -1396,7 +1440,7 @@ impl GitRepository for RealGitRepository { · 86 unmodified lines | ||
| 1396 | 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 | 1444 | let working_directory = self.working_directory(); |
| 1401 | 1445 | let git_binary_path = self.git_binary_path.clone(); |
| 1402 | 1446 | |
| @@ -1435,7 +1479,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines | ||
| 1435 | 1479 | &self, |
| 1436 | 1480 | left: GitRepositoryCheckpoint, |
| 1437 | 1481 | right: GitRepositoryCheckpoint, |
| 1438 | − ) -> BoxFuture<Result<bool>> { | |
| 1482 | + ) -> BoxFuture<'_, Result<bool>> { | |
| 1439 | 1483 | let working_directory = self.working_directory(); |
| 1440 | 1484 | let git_binary_path = self.git_binary_path.clone(); |
| 1441 | 1485 | |
| @@ -1474,7 +1518,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines | ||
| 1474 | 1518 | &self, |
| 1475 | 1519 | base_checkpoint: GitRepositoryCheckpoint, |
| 1476 | 1520 | target_checkpoint: GitRepositoryCheckpoint, |
| 1477 | − ) -> BoxFuture<Result<String>> { | |
| 1521 | + ) -> BoxFuture<'_, Result<String>> { | |
| 1478 | 1522 | let working_directory = self.working_directory(); |
| 1479 | 1523 | let git_binary_path = self.git_binary_path.clone(); |
| 1480 | 1524 | |
| @@ -21,6 +21,7 @@ agent_settings.workspace = true · 20 unmodified lines | ||
| 21 | 21 | anyhow.workspace = true |
| 22 | 22 | askpass.workspace = true |
| 23 | 23 | buffer_diff.workspace = true |
| 24 | +call.workspace = true | |
| 24 | 25 | chrono.workspace = true |
| 25 | 26 | collections.workspace = true |
| 26 | 27 | command_palette_hooks.workspace = true |
| @@ -148,6 +148,7 @@ impl CommitModal { · 147 unmodified lines | ||
| 148 | 148 | } |
| 149 | 149 | } |
| 150 | 150 | git_panel.set_modal_open(true, cx); |
| 151 | + git_panel.load_local_committer(cx); | |
| 151 | 152 | }); |
| 152 | 153 | |
| 153 | 154 | let dock = workspace.dock_at_position(git_panel.position(window, cx)); |
| @@ -248,6 +248,8 @@ fn conflicts_updated( · 247 unmodified lines | ||
| 248 | 248 | removed_block_ids.insert(block_id); |
| 249 | 249 | } |
| 250 | 250 | |
| 251 | + editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx); | |
| 252 | + | |
| 251 | 253 | editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx); |
| 252 | 254 | editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx); |
| 253 | 255 | editor |
| @@ -325,8 +327,7 @@ fn update_conflict_highlighting( · 71 unmodified lines | ||
| 325 | 327 | cx: &mut Context<Editor>, |
| 326 | 328 | ) { |
| 327 | 329 | log::debug!("update conflict highlighting for {conflict:?}"); |
| 328 | − let theme = cx.theme().clone(); | |
| 329 | − let colors = theme.colors(); | |
| 330 | + | |
| 330 | 331 | let outer_start = buffer |
| 331 | 332 | .anchor_in_excerpt(excerpt_id, conflict.range.start) |
| 332 | 333 | .unwrap(); |
| @@ -346,26 +347,29 @@ fn update_conflict_highlighting( · 13 unmodified lines | ||
| 346 | 347 | .anchor_in_excerpt(excerpt_id, conflict.theirs.end) |
| 347 | 348 | .unwrap(); |
| 348 | 349 | |
| 349 | − let ours_background = colors.version_control_conflict_ours_background; | |
| 350 | − let ours_marker = colors.version_control_conflict_ours_marker_background; | |
| 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; | |
| 350 | + let ours_background = cx.theme().colors().version_control_conflict_marker_ours; | |
| 351 | + let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs; | |
| 354 | 352 | |
| 355 | 353 | let options = RowHighlightOptions { |
| 356 | − include_gutter: false, | |
| 354 | + include_gutter: true, | |
| 357 | 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 | 364 | // Prevent diff hunk highlighting within the entire conflict region. |
| 361 | − editor.highlight_rows::<ConflictsOuter>( | |
| 362 | − outer_start..outer_end, | |
| 363 | − divider_background, | |
| 365 | + editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx); | |
| 366 | + editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx); | |
| 367 | + editor.highlight_rows::<ConflictsOursMarker>( | |
| 368 | + outer_start..our_start, | |
| 369 | + ours_background, | |
| 364 | 370 | options, |
| 365 | 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 | 373 | editor.highlight_rows::<ConflictsTheirs>( |
| 370 | 374 | their_start..their_end, |
| 371 | 375 | theirs_background, |
| @@ -374,7 +378,7 @@ fn update_conflict_highlighting( · 2 unmodified lines | ||
| 374 | 378 | ); |
| 375 | 379 | editor.highlight_rows::<ConflictsTheirsMarker>( |
| 376 | 380 | their_end..outer_end, |
| 377 | − theirs_marker, | |
| 381 | + theirs_background, | |
| 378 | 382 | options, |
| 379 | 383 | cx, |
| 380 | 384 | ); |
| @@ -512,6 +516,9 @@ pub(crate) fn resolve_conflict( · 131 unmodified lines | ||
| 512 | 516 | let end = snapshot |
| 513 | 517 | .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end) |
| 514 | 518 | .unwrap(); |
| 519 | + | |
| 520 | + editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx); | |
| 521 | + | |
| 515 | 522 | editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx); |
| 516 | 523 | editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx); |
| 517 | 524 | editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx); |
| @@ -20,18 +20,20 @@ use editor::{ · 19 unmodified lines | ||
| 20 | 20 | use futures::StreamExt as _; |
| 21 | 21 | use git::blame::ParsedCommitMessage; |
| 22 | 22 | use git::repository::{ |
| 23 | − Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, PushOptions, | |
| 24 | − Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, | |
| 23 | + Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter, | |
| 24 | + PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, | |
| 25 | + UpstreamTrackingStatus, get_git_committer, | |
| 25 | 26 | }; |
| 26 | 27 | use git::status::StageStatus; |
| 27 | 28 | use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus}; |
| 28 | 29 | use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll}; |
| 29 | 30 | use gpui::{ |
| 30 | − Action, Animation, AnimationExt as _, Axis, ClickEvent, Corner, DismissEvent, Entity, | |
| 31 | − EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, | |
| 32 | − ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point, | |
| 33 | − PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle, | |
| 34 | − WeakEntity, actions, anchored, deferred, percentage, uniform_list, | |
| 31 | + Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, | |
| 32 | + DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, | |
| 33 | + ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent, | |
| 34 | + MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task, | |
| 35 | + Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage, | |
| 36 | + uniform_list, | |
| 35 | 37 | }; |
| 36 | 38 | use itertools::Itertools; |
| 37 | 39 | use language::{Buffer, File}; |
| @@ -54,6 +56,7 @@ use project::{ · 16 unmodified lines | ||
| 54 | 56 | use serde::{Deserialize, Serialize}; |
| 55 | 57 | use settings::{Settings as _, SettingsStore}; |
| 56 | 58 | use std::future::Future; |
| 59 | +use std::ops::Range; | |
| 57 | 60 | use std::path::{Path, PathBuf}; |
| 58 | 61 | use std::{collections::HashSet, sync::Arc, time::Duration, usize}; |
| 59 | 62 | use strum::{IntoEnumIterator, VariantNames}; |
| @@ -63,11 +66,11 @@ use ui::{ · 3 unmodified lines | ||
| 63 | 66 | Tooltip, prelude::*, |
| 64 | 67 | }; |
| 65 | 68 | use util::{ResultExt, TryFutureExt, maybe}; |
| 66 | −use workspace::AppState; | |
| 69 | + | |
| 67 | 70 | use workspace::{ |
| 68 | 71 | Workspace, |
| 69 | 72 | dock::{DockPosition, Panel, PanelEvent}, |
| 70 | − notifications::DetachAndPromptErr, | |
| 73 | + notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId}, | |
| 71 | 74 | }; |
| 72 | 75 | use zed_llm_client::CompletionIntent; |
| 73 | 76 | |
| @@ -356,6 +359,8 @@ pub struct GitPanel { · 282 unmodified lines | ||
| 356 | 359 | context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, |
| 357 | 360 | modal_open: bool, |
| 358 | 361 | show_placeholders: bool, |
| 362 | + local_committer: Option<GitCommitter>, | |
| 363 | + local_committer_task: Option<Task<()>>, | |
| 359 | 364 | _settings_subscription: Subscription, |
| 360 | 365 | } |
| 361 | 366 | |
| @@ -372,7 +377,10 @@ pub(crate) fn commit_message_editor( · 10 unmodified lines | ||
| 372 | 377 | let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx)); |
| 373 | 378 | let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 }; |
| 374 | 379 | let mut commit_editor = Editor::new( |
| 375 | − EditorMode::AutoHeight { max_lines }, | |
| 380 | + EditorMode::AutoHeight { | |
| 381 | + min_lines: 1, | |
| 382 | + max_lines, | |
| 383 | + }, | |
| 376 | 384 | buffer, |
| 377 | 385 | None, |
| 378 | 386 | window, |
| @@ -389,144 +397,150 @@ pub(crate) fn commit_message_editor( · 10 unmodified lines | ||
| 389 | 397 | } |
| 390 | 398 | |
| 391 | 399 | impl GitPanel { |
| 392 | − pub fn new( | |
| 393 | − workspace: Entity<Workspace>, | |
| 394 | − project: Entity<Project>, | |
| 395 | − app_state: Arc<AppState>, | |
| 400 | + fn new( | |
| 401 | + workspace: &mut Workspace, | |
| 396 | 402 | window: &mut Window, |
| 397 | − cx: &mut Context<Self>, | |
| 398 | − ) -> Self { | |
| 403 | + cx: &mut Context<Workspace>, | |
| 404 | + ) -> Entity<Self> { | |
| 405 | + let project = workspace.project().clone(); | |
| 406 | + let app_state = workspace.app_state().clone(); | |
| 399 | 407 | let fs = app_state.fs.clone(); |
| 400 | 408 | let git_store = project.read(cx).git_store().clone(); |
| 401 | 409 | let active_repository = project.read(cx).active_repository(cx); |
| 402 | − let workspace = workspace.downgrade(); | |
| 403 | 410 | |
| 404 | − let focus_handle = cx.focus_handle(); | |
| 405 | − cx.on_focus(&focus_handle, window, Self::focus_in).detach(); | |
| 406 | − cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { | |
| 407 | − this.hide_scrollbars(window, cx); | |
| 408 | − }) | |
| 409 | − .detach(); | |
| 411 | + let git_panel = cx.new(|cx| { | |
| 412 | + let focus_handle = cx.focus_handle(); | |
| 413 | + cx.on_focus(&focus_handle, window, Self::focus_in).detach(); | |
| 414 | + cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { | |
| 415 | + this.hide_scrollbars(window, cx); | |
| 416 | + }) | |
| 417 | + .detach(); | |
| 410 | 418 | |
| 411 | − let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; | |
| 412 | − cx.observe_global::<SettingsStore>(move |this, cx| { | |
| 413 | − let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; | |
| 414 | − if is_sort_by_path != was_sort_by_path { | |
| 415 | − this.update_visible_entries(cx); | |
| 416 | − } | |
| 417 | − was_sort_by_path = is_sort_by_path | |
| 418 | − }) | |
| 419 | − .detach(); | |
| 419 | + let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; | |
| 420 | + cx.observe_global::<SettingsStore>(move |this, cx| { | |
| 421 | + let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; | |
| 422 | + if is_sort_by_path != was_sort_by_path { | |
| 423 | + this.update_visible_entries(cx); | |
| 424 | + } | |
| 425 | + was_sort_by_path = is_sort_by_path | |
| 426 | + }) | |
| 427 | + .detach(); | |
| 420 | 428 | |
| 421 | − // just to let us render a placeholder editor. | |
| 422 | − // Once the active git repo is set, this buffer will be replaced. | |
| 423 | − let temporary_buffer = cx.new(|cx| Buffer::local("", cx)); | |
| 424 | − let commit_editor = cx.new(|cx| { | |
| 425 | − commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx) | |
| 426 | − }); | |
| 429 | + // just to let us render a placeholder editor. | |
| 430 | + // Once the active git repo is set, this buffer will be replaced. | |
| 431 | + let temporary_buffer = cx.new(|cx| Buffer::local("", cx)); | |
| 432 | + let commit_editor = cx.new(|cx| { | |
| 433 | + commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx) | |
| 434 | + }); | |
| 427 | 435 | |
| 428 | − commit_editor.update(cx, |editor, cx| { | |
| 429 | − editor.clear(window, cx); | |
| 430 | − }); | |
| 436 | + commit_editor.update(cx, |editor, cx| { | |
| 437 | + editor.clear(window, cx); | |
| 438 | + }); | |
| 431 | 439 | |
| 432 | − let scroll_handle = UniformListScrollHandle::new(); | |
| 440 | + let scroll_handle = UniformListScrollHandle::new(); | |
| 433 | 441 | |
| 434 | − cx.subscribe_in( | |
| 435 | − &git_store, | |
| 436 | − window, | |
| 437 | − move |this, git_store, event, window, cx| match event { | |
| 438 | − GitStoreEvent::ActiveRepositoryChanged(_) => { | |
| 439 | − this.active_repository = git_store.read(cx).active_repository(); | |
| 440 | − this.schedule_update(true, window, cx); | |
| 441 | − } | |
| 442 | − GitStoreEvent::RepositoryUpdated( | |
| 443 | − _, | |
| 444 | − RepositoryEvent::Updated { full_scan }, | |
| 445 | − true, | |
| 446 | − ) => { | |
| 447 | − this.schedule_update(*full_scan, window, cx); | |
| 448 | − } | |
| 442 | + let vertical_scrollbar = ScrollbarProperties { | |
| 443 | + axis: Axis::Vertical, | |
| 444 | + state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), | |
| 445 | + show_scrollbar: false, | |
| 446 | + show_track: false, | |
| 447 | + auto_hide: false, | |
| 448 | + hide_task: None, | |
| 449 | + }; | |
| 449 | 450 | |
| 450 | − GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => { | |
| 451 | − this.schedule_update(false, window, cx); | |
| 452 | − } | |
| 453 | − GitStoreEvent::IndexWriteError(error) => { | |
| 454 | − this.workspace | |
| 455 | − .update(cx, |workspace, cx| { | |
| 456 | − workspace.show_error(error, cx); | |
| 457 | − }) | |
| 458 | − .ok(); | |
| 451 | + let horizontal_scrollbar = ScrollbarProperties { | |
| 452 | + axis: Axis::Horizontal, | |
| 453 | + state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), | |
| 454 | + show_scrollbar: false, | |
| 455 | + show_track: false, | |
| 456 | + auto_hide: false, | |
| 457 | + hide_task: None, | |
| 458 | + }; | |
| 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(_, _, _) => {} | |
| 461 | − GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {} | |
| 462 | − }, | |
| 463 | − ) | |
| 464 | − .detach(); | |
| 466 | + }); | |
| 465 | 467 | |
| 466 | − let vertical_scrollbar = ScrollbarProperties { | |
| 467 | − axis: Axis::Vertical, | |
| 468 | − state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), | |
| 469 | − show_scrollbar: false, | |
| 470 | − show_track: false, | |
| 471 | − auto_hide: false, | |
| 472 | − hide_task: None, | |
| 473 | − }; | |
| 468 | + cx.subscribe_in( | |
| 469 | + &git_store, | |
| 470 | + window, | |
| 471 | + move |this, _git_store, event, window, cx| match event { | |
| 472 | + GitStoreEvent::ActiveRepositoryChanged(_) => { | |
| 473 | + this.active_repository = this.project.read(cx).active_repository(cx); | |
| 474 | + this.schedule_update(true, window, cx); | |
| 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 { | |
| 476 | − axis: Axis::Horizontal, | |
| 477 | − state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), | |
| 478 | − show_scrollbar: false, | |
| 479 | − show_track: false, | |
| 480 | − auto_hide: false, | |
| 481 | − hide_task: None, | |
| 482 | − }; | |
| 484 | + GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => { | |
| 485 | + this.schedule_update(false, window, cx); | |
| 486 | + } | |
| 487 | + GitStoreEvent::IndexWriteError(error) => { | |
| 488 | + this.workspace | |
| 489 | + .update(cx, |workspace, cx| { | |
| 490 | + workspace.show_error(error, cx); | |
| 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; | |
| 485 | − let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| { | |
| 486 | − if assistant_enabled != AgentSettings::get_global(cx).enabled { | |
| 487 | − assistant_enabled = AgentSettings::get_global(cx).enabled; | |
| 488 | − cx.notify(); | |
| 489 | − } | |
| 500 | + let mut this = Self { | |
| 501 | + active_repository, | |
| 502 | + commit_editor, | |
| 503 | + conflicted_count: 0, | |
| 504 | + conflicted_staged_count: 0, | |
| 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 | 544 | git_panel |
| 531 | 545 | } |
| 532 | 546 | |
| @@ -1774,7 +1788,19 @@ impl GitPanel { · 1241 unmodified lines | ||
| 1774 | 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 | 1805 | const ONE_MB: usize = 1_000_000; |
| 1780 | 1806 | if diff_text.len() > ONE_MB { |
| @@ -1812,26 +1838,37 @@ impl GitPanel { · 31 unmodified lines | ||
| 1812 | 1838 | }; |
| 1813 | 1839 | |
| 1814 | 1840 | let stream = model.stream_completion_text(request, &cx); |
| 1815 | − let mut messages = stream.await?; | |
| 1816 | − | |
| 1817 | − if !text_empty { | |
| 1818 | − this.update(cx, |this, cx| { | |
| 1819 | − this.commit_message_buffer(cx).update(cx, |buffer, cx| { | |
| 1820 | − let insert_position = buffer.anchor_before(buffer.len()); | |
| 1821 | − buffer.edit([(insert_position..insert_position, "\n")], None, cx) | |
| 1822 | − }); | |
| 1823 | − })?; | |
| 1824 | − } | |
| 1825 | − | |
| 1826 | − while let Some(message) = messages.stream.next().await { | |
| 1827 | − let text = message?; | |
| 1841 | + match stream.await { | |
| 1842 | + Ok(mut messages) => { | |
| 1843 | + if !text_empty { | |
| 1844 | + this.update(cx, |this, cx| { | |
| 1845 | + this.commit_message_buffer(cx).update(cx, |buffer, cx| { | |
| 1846 | + let insert_position = buffer.anchor_before(buffer.len()); | |
| 1847 | + buffer.edit([(insert_position..insert_position, "\n")], None, cx) | |
| 1848 | + }); | |
| 1849 | + })?; | |
| 1850 | + } | |
| 1828 | 1851 | |
| 1829 | − this.update(cx, |this, cx| { | |
| 1830 | − this.commit_message_buffer(cx).update(cx, |buffer, cx| { | |
| 1831 | − let insert_position = buffer.anchor_before(buffer.len()); | |
| 1832 | − buffer.edit([(insert_position..insert_position, text)], None, cx); | |
| 1833 | − }); | |
| 1834 | − })?; | |
| 1852 | + while let Some(message) = messages.stream.next().await { | |
| 1853 | + match message { | |
| 1854 | + Ok(text) => { | |
| 1855 | + this.update(cx, |this, cx| { | |
| 1856 | + this.commit_message_buffer(cx).update(cx, |buffer, cx| { | |
| 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 | 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 | 2274 | fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> { |
| 2225 | 2275 | let mut new_co_authors = Vec::new(); |
| 2226 | 2276 | let project = self.project.read(cx); |
| @@ -2243,34 +2293,38 @@ impl GitPanel { · 16 unmodified lines | ||
| 2243 | 2293 | let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else { |
| 2244 | 2294 | continue; |
| 2245 | 2295 | }; |
| 2246 | − if participant.can_write() && participant.user.email.is_some() { | |
| 2247 | − let email = participant.user.email.clone().unwrap(); | |
| 2248 | − | |
| 2249 | − new_co_authors.push(( | |
| 2250 | − participant | |
| 2251 | − .user | |
| 2252 | − .name | |
| 2253 | − .clone() | |
| 2254 | − .unwrap_or_else(|| participant.user.github_login.clone()), | |
| 2255 | − email, | |
| 2256 | − )) | |
| 2296 | + if !participant.can_write() { | |
| 2297 | + continue; | |
| 2298 | + } | |
| 2299 | + if let Some(email) = &collaborator.committer_email { | |
| 2300 | + let name = collaborator | |
| 2301 | + .committer_name | |
| 2302 | + .clone() | |
| 2303 | + .or_else(|| participant.user.name.clone()) | |
| 2304 | + .unwrap_or_else(|| participant.user.github_login.clone()); | |
| 2305 | + new_co_authors.push((name.clone(), email.clone())) | |
| 2257 | 2306 | } |
| 2258 | 2307 | } |
| 2259 | 2308 | if !project.is_local() && !project.is_read_only(cx) { |
| 2260 | − if let Some(user) = room.local_participant_user(cx) { | |
| 2261 | − if let Some(email) = user.email.clone() { | |
| 2262 | − new_co_authors.push(( | |
| 2263 | − user.name | |
| 2264 | − .clone() | |
| 2265 | − .unwrap_or_else(|| user.github_login.clone()), | |
| 2266 | − email.clone(), | |
| 2267 | − )) | |
| 2268 | − } | |
| 2309 | + if let Some(local_committer) = self.local_committer(room, cx) { | |
| 2310 | + new_co_authors.push(local_committer); | |
| 2269 | 2311 | } |
| 2270 | 2312 | } |
| 2271 | 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 | 2328 | fn toggle_fill_co_authors( |
| 2275 | 2329 | &mut self, |
| 2276 | 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 | 2766 | fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) { |
| 2693 | 2767 | let Some(workspace) = self.workspace.upgrade() else { |
| 2694 | 2768 | return; |
| @@ -3662,8 +3736,10 @@ impl GitPanel { · 967 unmodified lines | ||
| 3662 | 3736 | .relative() |
| 3663 | 3737 | .overflow_hidden() |
| 3664 | 3738 | .child( |
| 3665 | − uniform_list(cx.entity().clone(), "entries", entry_count, { | |
| 3666 | − move |this, range, window, cx| { | |
| 3739 | + uniform_list( | |
| 3740 | + "entries", | |
| 3741 | + entry_count, | |
| 3742 | + cx.processor(move |this, range: Range<usize>, window, cx| { | |
| 3667 | 3743 | let mut items = Vec::with_capacity(range.end - range.start); |
| 3668 | 3744 | |
| 3669 | 3745 | for ix in range { |
| @@ -3691,8 +3767,8 @@ impl GitPanel { · 21 unmodified lines | ||
| 3691 | 3767 | } |
| 3692 | 3768 | |
| 3693 | 3769 | items |
| 3694 | − } | |
| 3695 | − }) | |
| 3770 | + }), | |
| 3771 | + ) | |
| 3696 | 3772 | .when( |
| 3697 | 3773 | !self.horizontal_scrollbar.show_track |
| 3698 | 3774 | && self.horizontal_scrollbar.show_scrollbar, |
| @@ -4141,6 +4217,32 @@ impl GitPanel { · 442 unmodified lines | ||
| 4141 | 4217 | self.amend_pending = value; |
| 4142 | 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 | 4248 | fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> { |
| @@ -4167,8 +4269,9 @@ impl Render for GitPanel { · 20 unmodified lines | ||
| 4167 | 4269 | let has_write_access = self.has_write_access(cx); |
| 4168 | 4270 | |
| 4169 | 4271 | let has_co_authors = room.map_or(false, |room| { |
| 4170 | − room.read(cx) | |
| 4171 | − .remote_participants() | |
| 4272 | + self.load_local_committer(cx); | |
| 4273 | + let room = room.read(cx); | |
| 4274 | + room.remote_participants() | |
| 4172 | 4275 | .values() |
| 4173 | 4276 | .any(|remote_participant| remote_participant.can_write()) |
| 4174 | 4277 | }); |
| @@ -4852,7 +4955,7 @@ impl Component for PanelRepoFooter { · 677 unmodified lines | ||
| 4852 | 4955 | #[cfg(test)] |
| 4853 | 4956 | mod tests { |
| 4854 | 4957 | use git::status::StatusCode; |
| 4855 | − use gpui::TestAppContext; | |
| 4958 | + use gpui::{TestAppContext, VisualTestContext}; | |
| 4856 | 4959 | use project::{FakeFs, WorktreeSettings}; |
| 4857 | 4960 | use serde_json::json; |
| 4858 | 4961 | use settings::SettingsStore; |
| @@ -4916,8 +5019,9 @@ mod tests { · 57 unmodified lines | ||
| 4916 | 5019 | |
| 4917 | 5020 | let project = |
| 4918 | 5021 | Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await; |
| 4919 | − let (workspace, cx) = | |
| 4920 | − cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); | |
| 5022 | + let workspace = | |
| 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 | 5026 | cx.read(|cx| { |
| 4923 | 5027 | project |
| @@ -4934,10 +5038,7 @@ mod tests { · 10 unmodified lines | ||
| 4934 | 5038 | |
| 4935 | 5039 | cx.executor().run_until_parked(); |
| 4936 | 5040 | |
| 4937 | − let app_state = workspace.read_with(cx, |workspace, _| workspace.app_state().clone()); | |
| 4938 | − let panel = cx.new_window_entity(|window, cx| { | |
| 4939 | − GitPanel::new(workspace.clone(), project.clone(), app_state, window, cx) | |
| 4940 | − }); | |
| 5041 | + let panel = workspace.update(cx, GitPanel::new).unwrap(); | |
| 4941 | 5042 | |
| 4942 | 5043 | let handle = cx.update_window_entity(&panel, |panel, _, _| { |
| 4943 | 5044 | std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) |
| @@ -10,7 +10,7 @@ use git::{ · 9 unmodified lines | ||
| 10 | 10 | status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode}, |
| 11 | 11 | }; |
| 12 | 12 | use git_panel_settings::GitPanelSettings; |
| 13 | −use gpui::{Action, App, FocusHandle, actions}; | |
| 13 | +use gpui::{Action, App, Context, FocusHandle, Window, actions}; | |
| 14 | 14 | use onboarding::GitOnboardingModal; |
| 15 | 15 | use project_diff::ProjectDiff; |
| 16 | 16 | use ui::prelude::*; |
| @@ -142,10 +142,41 @@ pub fn init(cx: &mut App) { · 125 unmodified lines | ||
| 142 | 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 | 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 | 180 | pub fn git_status_icon(status: FileStatus) -> impl IntoElement { |
| 150 | 181 | GitStatusIcon::new(status) |
| 151 | 182 | } |
| @@ -37,7 +37,7 @@ use util::ResultExt as _; · 36 unmodified lines | ||
| 37 | 37 | use workspace::{ |
| 38 | 38 | CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, |
| 39 | 39 | ToolbarItemView, Workspace, |
| 40 | − item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, | |
| 40 | + item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, | |
| 41 | 41 | searchable::SearchableItemHandle, |
| 42 | 42 | }; |
| 43 | 43 | |
| @@ -632,12 +632,12 @@ impl Item for ProjectDiff { · 588 unmodified lines | ||
| 632 | 632 | |
| 633 | 633 | fn save( |
| 634 | 634 | &mut self, |
| 635 | − format: bool, | |
| 635 | + options: SaveOptions, | |
| 636 | 636 | project: Entity<Project>, |
| 637 | 637 | window: &mut Window, |
| 638 | 638 | cx: &mut Context<Self>, |
| 639 | 639 | ) -> Task<Result<()>> { |
| 640 | − self.editor.save(format, project, window, cx) | |
| 640 | + self.editor.save(options, project, window, cx) | |
| 641 | 641 | } |
| 642 | 642 | |
| 643 | 643 | fn save_as( |
| @@ -1565,7 +1565,15 @@ mod tests { · 921 unmodified lines | ||
| 1565 | 1565 | |
| 1566 | 1566 | cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| { |
| 1567 | 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 | 1578 | .await |
| 1571 | 1579 | .unwrap(); |
| @@ -1,8 +1,4 @@ | ||
| 1 | −use std::{ | |
| 2 | − ops::Range, | |
| 3 | − rc::Rc, | |
| 4 | − time::{Duration, Instant}, | |
| 5 | −}; | |
| 1 | +use std::{ops::Range, rc::Rc, time::Duration}; | |
| 6 | 2 | |
| 7 | 3 | use gpui::{ |
| 8 | 4 | App, Application, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, |
| @@ -22,7 +18,7 @@ pub struct Quote { · 13 unmodified lines | ||
| 22 | 18 | open: f64, |
| 23 | 19 | high: f64, |
| 24 | 20 | low: f64, |
| 25 | − timestamp: Instant, | |
| 21 | + timestamp: Duration, | |
| 26 | 22 | volume: i64, |
| 27 | 23 | turnover: f64, |
| 28 | 24 | ttm: f64, |
| @@ -50,8 +46,7 @@ impl Quote { · 21 unmodified lines | ||
| 50 | 46 | let open = prev_close + rng.gen_range(-3.0..3.0); |
| 51 | 47 | let high = (prev_close + rng.gen_range::<f64, _>(0.0..10.0)).max(open); |
| 52 | 48 | let low = (prev_close - rng.gen_range::<f64, _>(0.0..10.0)).min(open); |
| 53 | − // Randomize the timestamp in the past 24 hours | |
| 54 | − let timestamp = Instant::now() - Duration::from_secs(rng.gen_range(0..86400)); | |
| 49 | + let timestamp = Duration::from_secs(rng.gen_range(0..86400)); | |
| 55 | 50 | let volume = rng.gen_range(1_000_000..100_000_000); |
| 56 | 51 | let turnover = last_done * volume as f64; |
| 57 | 52 | let symbol = { |
| @@ -170,7 +165,7 @@ impl TableRow { · 112 unmodified lines | ||
| 170 | 165 | .child(format!("{:.2}%", self.quote.change())), |
| 171 | 166 | "timestamp" => div() |
| 172 | 167 | .text_color(color) |
| 173 | − .child(format!("{:?}", self.quote.timestamp.elapsed().as_secs())), | |
| 168 | + .child(format!("{:?}", self.quote.timestamp.as_secs())), | |
| 174 | 169 | "open" => div() |
| 175 | 170 | .text_color(color) |
| 176 | 171 | .child(format!("{:.2}", self.quote.open)), |
| @@ -378,8 +373,6 @@ impl DataTable { · 201 unmodified lines | ||
| 378 | 373 | |
| 379 | 374 | impl Render for DataTable { |
| 380 | 375 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| 381 | − let entity = cx.entity(); | |
| 382 | − | |
| 383 | 376 | div() |
| 384 | 377 | .font_family(".SystemUIFont") |
| 385 | 378 | .bg(gpui::white()) |
| @@ -431,8 +424,10 @@ impl Render for DataTable { · 45 unmodified lines | ||
| 431 | 424 | .relative() |
| 432 | 425 | .size_full() |
| 433 | 426 | .child( |
| 434 | − uniform_list(entity, "items", self.quotes.len(), { | |
| 435 | − move |this, range, _, _| { | |
| 427 | + uniform_list( | |
| 428 | + "items", | |
| 429 | + self.quotes.len(), | |
| 430 | + cx.processor(move |this, range: Range<usize>, _, _| { | |
| 436 | 431 | this.visible_range = range.clone(); |
| 437 | 432 | let mut items = Vec::with_capacity(range.end - range.start); |
| 438 | 433 | for i in range { |
| @@ -441,8 +436,8 @@ impl Render for DataTable { · 2 unmodified lines | ||
| 441 | 436 | } |
| 442 | 437 | } |
| 443 | 438 | items |
| 444 | − } | |
| 445 | − }) | |
| 439 | + }), | |
| 440 | + ) | |
| 446 | 441 | .size_full() |
| 447 | 442 | .track_scroll(self.scroll_handle.clone()), |
| 448 | 443 | ) |
| @@ -9,10 +9,9 @@ impl Render for UniformListExample { · 8 unmodified lines | ||
| 9 | 9 | fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| 10 | 10 | div().size_full().bg(rgb(0xffffff)).child( |
| 11 | 11 | uniform_list( |
| 12 | − cx.entity().clone(), | |
| 13 | 12 | "entries", |
| 14 | 13 | 50, |
| 15 | − |_this, range, _window, _cx| { | |
| 14 | + cx.processor(|_this, range, _window, _cx| { | |
| 16 | 15 | let mut items = Vec::new(); |
| 17 | 16 | for ix in range { |
| 18 | 17 | let item = ix + 1; |
| @@ -29,7 +28,7 @@ impl Render for UniformListExample { · 10 unmodified lines | ||
| 29 | 28 | ); |
| 30 | 29 | } |
| 31 | 30 | items |
| 32 | − }, | |
| 31 | + }), | |
| 33 | 32 | ) |
| 34 | 33 | .h_full(), |
| 35 | 34 | ) |
| @@ -64,7 +64,7 @@ pub struct AppCell { · 63 unmodified lines | ||
| 64 | 64 | impl AppCell { |
| 65 | 65 | #[doc(hidden)] |
| 66 | 66 | #[track_caller] |
| 67 | − pub fn borrow(&self) -> AppRef { | |
| 67 | + pub fn borrow(&self) -> AppRef<'_> { | |
| 68 | 68 | if option_env!("TRACK_THREAD_BORROWS").is_some() { |
| 69 | 69 | let thread_id = std::thread::current().id(); |
| 70 | 70 | eprintln!("borrowed {thread_id:?}"); |
| @@ -74,7 +74,7 @@ impl AppCell { · 3 unmodified lines | ||
| 74 | 74 | |
| 75 | 75 | #[doc(hidden)] |
| 76 | 76 | #[track_caller] |
| 77 | − pub fn borrow_mut(&self) -> AppRefMut { | |
| 77 | + pub fn borrow_mut(&self) -> AppRefMut<'_> { | |
| 78 | 78 | if option_env!("TRACK_THREAD_BORROWS").is_some() { |
| 79 | 79 | let thread_id = std::thread::current().id(); |
| 80 | 80 | eprintln!("borrowed {thread_id:?}"); |
| @@ -84,7 +84,7 @@ impl AppCell { · 3 unmodified lines | ||
| 84 | 84 | |
| 85 | 85 | #[doc(hidden)] |
| 86 | 86 | #[track_caller] |
| 87 | − pub fn try_borrow_mut(&self) -> Result<AppRefMut, BorrowMutError> { | |
| 87 | + pub fn try_borrow_mut(&self) -> Result<AppRefMut<'_>, BorrowMutError> { | |
| 88 | 88 | if option_env!("TRACK_THREAD_BORROWS").is_some() { |
| 89 | 89 | let thread_id = std::thread::current().id(); |
| 90 | 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 | 240 | /// Run something using this entity and cx, when the returned struct is dropped |
| 229 | 241 | pub fn on_drop( |
| 230 | 242 | &self, |
| @@ -5,10 +5,10 @@ · 4 unmodified lines | ||
| 5 | 5 | //! elements with uniform height. |
| 6 | 6 | |
| 7 | 7 | use crate::{ |
| 8 | − AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, Element, ElementId, Entity, | |
| 9 | − GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, | |
| 10 | − IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Render, ScrollHandle, Size, | |
| 11 | − StyleRefinement, Styled, Window, point, size, | |
| 8 | + AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId, | |
| 9 | + Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId, | |
| 10 | + ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window, | |
| 11 | + point, size, | |
| 12 | 12 | }; |
| 13 | 13 | use smallvec::SmallVec; |
| 14 | 14 | use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; |
| @@ -19,28 +19,23 @@ use super::ListHorizontalSizingBehavior; · 4 unmodified lines | ||
| 19 | 19 | /// When rendered into a container with overflow-y: hidden and a fixed (or max) height, |
| 20 | 20 | /// uniform_list will only render the visible subset of items. |
| 21 | 21 | #[track_caller] |
| 22 | −pub fn uniform_list<I, R, V>( | |
| 23 | − view: Entity<V>, | |
| 24 | − id: I, | |
| 22 | +pub fn uniform_list<R>( | |
| 23 | + id: impl Into<ElementId>, | |
| 25 | 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 | 26 | ) -> UniformList |
| 28 | 27 | where |
| 29 | − I: Into<ElementId>, | |
| 30 | 28 | R: IntoElement, |
| 31 | − V: Render, | |
| 32 | 29 | { |
| 33 | 30 | let id = id.into(); |
| 34 | 31 | let mut base_style = StyleRefinement::default(); |
| 35 | 32 | base_style.overflow.y = Some(Overflow::Scroll); |
| 36 | 33 | |
| 37 | − let render_range = move |range, window: &mut Window, cx: &mut App| { | |
| 38 | − view.update(cx, |this, cx| { | |
| 39 | − f(this, range, window, cx) | |
| 40 | − .into_iter() | |
| 41 | − .map(|component| component.into_any_element()) | |
| 42 | − .collect() | |
| 43 | − }) | |
| 34 | + let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| { | |
| 35 | + f(range, window, cx) | |
| 36 | + .into_iter() | |
| 37 | + .map(|component| component.into_any_element()) | |
| 38 | + .collect() | |
| 44 | 39 | }; |
| 45 | 40 | |
| 46 | 41 | UniformList { |
| @@ -255,7 +255,7 @@ pub trait VisualContext: AppContext { · 254 unmodified lines | ||
| 255 | 255 | update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R, |
| 256 | 256 | ) -> Self::Result<R>; |
| 257 | 257 | |
| 258 | − /// Update a view with the given callback | |
| 258 | + /// Create a new entity, with access to `Window`. | |
| 259 | 259 | fn new_window_entity<T: 'static>( |
| 260 | 260 | &mut self, |
| 261 | 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 | 143 | #[cfg(target_os = "windows")] |
| 144 | 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 | 152 | pub(crate) trait Platform: 'static { |
| @@ -718,7 +722,7 @@ impl<T> ops::Index<usize> for AtlasTextureList<T> { · 569 unmodified lines | ||
| 718 | 722 | |
| 719 | 723 | impl<T> AtlasTextureList<T> { |
| 720 | 724 | #[allow(unused)] |
| 721 | − fn drain(&mut self) -> std::vec::Drain<Option<T>> { | |
| 725 | + fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> { | |
| 722 | 726 | self.free_list.clear(); |
| 723 | 727 | self.textures.drain(..) |
| 724 | 728 | } |
| @@ -342,7 +342,7 @@ impl BladeRenderer { · 341 unmodified lines | ||
| 342 | 342 | let surface = context |
| 343 | 343 | .gpu |
| 344 | 344 | .create_surface_configured(window, surface_config) |
| 345 | − .unwrap(); | |
| 345 | + .map_err(|err| anyhow::anyhow!("Failed to create surface: {err:?}"))?; | |
| 346 | 346 | |
| 347 | 347 | let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc { |
| 348 | 348 | name: "main", |
| @@ -52,7 +52,7 @@ impl LinuxClient for HeadlessClient { · 51 unmodified lines | ||
| 52 | 52 | } |
| 53 | 53 | |
| 54 | 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 | 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 | 4 | pub(crate) struct LinuxKeyboardLayout { |
| 4 | − id: String, | |
| 5 | + name: SharedString, | |
| 5 | 6 | } |
| 6 | 7 | |
| 7 | 8 | impl PlatformKeyboardLayout for LinuxKeyboardLayout { |
| 8 | 9 | fn id(&self) -> &str { |
| 9 | − &self.id | |
| 10 | + &self.name | |
| 10 | 11 | } |
| 11 | 12 | |
| 12 | 13 | fn name(&self) -> &str { |
| 13 | − &self.id | |
| 14 | + &self.name | |
| 14 | 15 | } |
| 15 | 16 | } |
| 16 | 17 | |
| 17 | 18 | impl LinuxKeyboardLayout { |
| 18 | − pub(crate) fn new(id: String) -> Self { | |
| 19 | − Self { id } | |
| 19 | + pub(crate) fn new(name: SharedString) -> Self { | |
| 20 | + Self { name } | |
| 20 | 21 | } |
| 21 | 22 | } |
| @@ -71,17 +71,6 @@ use super::{ · 70 unmodified lines | ||
| 71 | 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 | 74 | use crate::platform::{PlatformWindow, blade::BladeContext}; |
| 86 | 75 | use crate::{ |
| 87 | 76 | AnyWindowHandle, Bounds, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId, |
| @@ -91,10 +80,26 @@ use crate::{ · 3 unmodified lines | ||
| 91 | 80 | PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScreenCaptureSource, |
| 92 | 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 | 98 | /// Used to convert evdev scancode to xkb scancode |
| 96 | 99 | const MIN_KEYCODE: u32 = 8; |
| 97 | 100 | |
| 101 | +const UNKNOWN_KEYBOARD_LAYOUT_NAME: SharedString = SharedString::new_static("unknown"); | |
| 102 | + | |
| 98 | 103 | #[derive(Clone)] |
| 99 | 104 | pub struct Globals { |
| 100 | 105 | pub qh: QueueHandle<WaylandClientStatePtr>, |
| @@ -205,6 +210,7 @@ pub(crate) struct WaylandClientState { · 104 unmodified lines | ||
| 205 | 210 | // Output to scale mapping |
| 206 | 211 | outputs: HashMap<ObjectId, Output>, |
| 207 | 212 | in_progress_outputs: HashMap<ObjectId, InProgressOutput>, |
| 213 | + keyboard_layout: LinuxKeyboardLayout, | |
| 208 | 214 | keymap_state: Option<xkb::State>, |
| 209 | 215 | compose_state: Option<xkb::compose::State>, |
| 210 | 216 | drag: DragState, |
| @@ -335,6 +341,35 @@ impl WaylandClientStatePtr { · 124 unmodified lines | ||
| 335 | 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 | 373 | pub fn drop_window(&self, surface_id: &ObjectId) { |
| 339 | 374 | let mut client = self.get_client(); |
| 340 | 375 | let mut state = client.borrow_mut(); |
| @@ -533,6 +568,7 @@ impl WaylandClient { · 192 unmodified lines | ||
| 533 | 568 | in_progress_outputs, |
| 534 | 569 | windows: HashMap::default(), |
| 535 | 570 | common, |
| 571 | + keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME), | |
| 536 | 572 | keymap_state: None, |
| 537 | 573 | compose_state: None, |
| 538 | 574 | drag: DragState { |
| @@ -590,17 +626,7 @@ impl WaylandClient { · 51 unmodified lines | ||
| 590 | 626 | |
| 591 | 627 | impl LinuxClient for WaylandClient { |
| 592 | 628 | fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> { |
| 593 | − let state = self.0.borrow(); | |
| 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)) | |
| 629 | + Box::new(self.0.borrow().keyboard_layout.clone()) | |
| 604 | 630 | } |
| 605 | 631 | |
| 606 | 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 | 1208 | state.keymap_state = Some(xkb::State::new(&keymap)); |
| 1183 | 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() { | |
| 1186 | − drop(state); | |
| 1187 | − callback(); | |
| 1188 | − state = client.borrow_mut(); | |
| 1189 | − state.common.callbacks.keyboard_layout_change = Some(callback); | |
| 1190 | − } | |
| 1212 | + this.handle_keyboard_layout_change(); | |
| 1191 | 1213 | } |
| 1192 | 1214 | wl_keyboard::Event::Enter { surface, .. } => { |
| 1193 | 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 | 1252 | keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); |
| 1231 | 1253 | state.modifiers = Modifiers::from_xkb(keymap_state); |
| 1232 | 1254 | |
| 1233 | − if group != old_layout { | |
| 1234 | − if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() | |
| 1235 | − { | |
| 1236 | − drop(state); | |
| 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 | − }; | |
| 1255 | + if let Some(focused_window) = focused_window { | |
| 1256 | + let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent { | |
| 1257 | + modifiers: state.modifiers, | |
| 1258 | + }); | |
| 1246 | 1259 | |
| 1247 | − let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent { | |
| 1248 | − modifiers: state.modifiers, | |
| 1249 | − }); | |
| 1260 | + drop(state); | |
| 1261 | + focused_window.handle_input(input); | |
| 1262 | + } | |
| 1250 | 1263 | |
| 1251 | − drop(state); | |
| 1252 | − focused_window.handle_input(input); | |
| 1264 | + if group != old_layout { | |
| 1265 | + this.handle_keyboard_layout_change(); | |
| 1266 | + } | |
| 1253 | 1267 | } |
| 1254 | 1268 | wl_keyboard::Event::Key { |
| 1255 | 1269 | serial, |
| @@ -1374,6 +1388,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { · 118 unmodified lines | ||
| 1374 | 1388 | } |
| 1375 | 1389 | } |
| 1376 | 1390 | } |
| 1391 | + | |
| 1377 | 1392 | impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr { |
| 1378 | 1393 | fn event( |
| 1379 | 1394 | this: &mut Self, |
| @@ -28,7 +28,7 @@ use x11rb::{ · 27 unmodified lines | ||
| 28 | 28 | protocol::xkb::ConnectionExt as _, |
| 29 | 29 | protocol::xproto::{ |
| 30 | 30 | AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent, |
| 31 | − ConnectionExt as _, EventMask, KeyPressEvent, | |
| 31 | + ConnectionExt as _, EventMask, KeyPressEvent, Visibility, | |
| 32 | 32 | }, |
| 33 | 33 | protocol::{Event, randr, render, xinput, xkb, xproto}, |
| 34 | 34 | resource_manager::Database, |
| @@ -78,7 +78,10 @@ pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1; · 43 unmodified lines | ||
| 78 | 78 | |
| 79 | 79 | pub(crate) struct WindowRef { |
| 80 | 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 | 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 | 111 | #[derive(Debug)] |
| 99 | 112 | #[non_exhaustive] |
| 100 | 113 | pub enum EventHandlerError { |
| @@ -185,6 +198,7 @@ pub struct X11ClientState { · 84 unmodified lines | ||
| 185 | 198 | pub(crate) keyboard_focused_window: Option<xproto::Window>, |
| 186 | 199 | pub(crate) xkb: xkbc::State, |
| 187 | 200 | previous_xkb_state: XKBStateNotiy, |
| 201 | + keyboard_layout: LinuxKeyboardLayout, | |
| 188 | 202 | pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>, |
| 189 | 203 | pub(crate) xim_handler: Option<XimHandler>, |
| 190 | 204 | pub modifiers: Modifiers, |
| @@ -220,7 +234,14 @@ impl X11ClientStatePtr { · 29 unmodified lines | ||
| 220 | 234 | let mut state = client.0.borrow_mut(); |
| 221 | 235 | |
| 222 | 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 | 246 | if state.mouse_focused_window == Some(x_window) { |
| 226 | 247 | state.mouse_focused_window = None; |
| @@ -349,13 +370,21 @@ impl X11Client { · 122 unmodified lines | ||
| 349 | 370 | let events = xkb::EventType::STATE_NOTIFY |
| 350 | 371 | | xkb::EventType::MAP_NOTIFY |
| 351 | 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 | 381 | xcb_connection |
| 353 | 382 | .xkb_select_events( |
| 354 | 383 | xkb::ID::USE_CORE_KBD.into(), |
| 355 | 384 | 0u8.into(), |
| 356 | 385 | events, |
| 357 | − 0u8.into(), | |
| 358 | − 0u8.into(), | |
| 386 | + map_notify_parts, | |
| 387 | + map_notify_parts, | |
| 359 | 388 | &xkb::SelectEventsAux::new(), |
| 360 | 389 | ) |
| 361 | 390 | .unwrap(); |
| @@ -373,17 +402,22 @@ impl X11Client { · 11 unmodified lines | ||
| 373 | 402 | xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id) |
| 374 | 403 | }; |
| 375 | 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 | 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 | 415 | let scale_factor = resource_database |
| 381 | 416 | .get_value("Xft.dpi", "Xft.dpi") |
| 382 | 417 | .ok() |
| 383 | 418 | .flatten() |
| 384 | 419 | .map(|dpi: f32| dpi / 96.0) |
| 385 | 420 | .unwrap_or(1.0); |
| 386 | − | |
| 387 | 421 | let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database) |
| 388 | 422 | .unwrap() |
| 389 | 423 | .reply() |
| @@ -461,6 +495,7 @@ impl X11Client { · 71 unmodified lines | ||
| 461 | 495 | keyboard_focused_window: None, |
| 462 | 496 | xkb: xkb_state, |
| 463 | 497 | previous_xkb_state: XKBStateNotiy::default(), |
| 498 | + keyboard_layout, | |
| 464 | 499 | ximc, |
| 465 | 500 | xim_handler, |
| 466 | 501 | |
| @@ -492,6 +527,10 @@ impl X11Client { · 25 unmodified lines | ||
| 492 | 527 | let mut last_key_release = None; |
| 493 | 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 | 534 | loop { |
| 496 | 535 | match xcb_connection.poll_for_event() { |
| 497 | 536 | Ok(Some(event)) => { |
| @@ -500,9 +539,29 @@ impl X11Client { · 2 unmodified lines | ||
| 500 | 539 | windows_to_refresh.insert(expose_event.window); |
| 501 | 540 | } |
| 502 | 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 | 552 | last_key_release = Some(event); |
| 504 | 553 | } |
| 505 | 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 | 565 | if let Some(last_press) = last_key_press.as_ref() { |
| 507 | 566 | if last_press.detail == key_press.detail { |
| 508 | 567 | continue; |
| @@ -523,6 +582,12 @@ impl X11Client { · 14 unmodified lines | ||
| 523 | 582 | events.push(Event::KeyPress(key_press)); |
| 524 | 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 | 592 | if let Some(release_event) = last_key_release.take() { |
| 528 | 593 | events.push(release_event); |
| @@ -532,10 +597,6 @@ impl X11Client { · 3 unmodified lines | ||
| 532 | 597 | } |
| 533 | 598 | } |
| 534 | 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 | 600 | break; |
| 540 | 601 | } |
| 541 | 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 | 616 | if events.is_empty() && windows_to_refresh.is_empty() { |
| 549 | 617 | break; |
| 550 | 618 | } |
| 551 | 619 | |
| 552 | 620 | for window in windows_to_refresh.into_iter() { |
| 553 | − if let Some(window) = self.get_window(window) { | |
| 554 | − window.refresh(RequestFrameOptions { | |
| 555 | − require_presentation: true, | |
| 556 | − }); | |
| 621 | + let mut state = self.0.borrow_mut(); | |
| 622 | + if let Some(window) = state.windows.get_mut(&window) { | |
| 623 | + window.expose_event_received = true; | |
| 557 | 624 | } |
| 558 | 625 | } |
| 559 | 626 | |
| @@ -661,6 +728,27 @@ impl X11Client { · 101 unmodified lines | ||
| 661 | 728 | |
| 662 | 729 | fn handle_event(&self, event: Event) -> Option<()> { |
| 663 | 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 | 752 | Event::ClientMessage(event) => { |
| 665 | 753 | let window = self.get_window(event.window)?; |
| 666 | 754 | let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32(); |
| @@ -826,7 +914,7 @@ impl X11Client { · 159 unmodified lines | ||
| 826 | 914 | self.reset_ime(); |
| 827 | 915 | window.handle_ime_delete(); |
| 828 | 916 | } |
| 829 | − Event::XkbNewKeyboardNotify(_) | Event::MapNotify(_) => { | |
| 917 | + Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { | |
| 830 | 918 | let mut state = self.0.borrow_mut(); |
| 831 | 919 | let xkb_state = { |
| 832 | 920 | let xkb_keymap = xkbc::x11::keymap_new_from_device( |
| @@ -850,6 +938,8 @@ impl X11Client { · 17 unmodified lines | ||
| 850 | 938 | locked_layout, |
| 851 | 939 | }; |
| 852 | 940 | state.xkb = xkb_state; |
| 941 | + drop(state); | |
| 942 | + self.handle_keyboard_layout_change(); | |
| 853 | 943 | } |
| 854 | 944 | Event::XkbStateNotify(event) => { |
| 855 | 945 | let mut state = self.0.borrow_mut(); |
| @@ -869,16 +959,6 @@ impl X11Client { · 13 unmodified lines | ||
| 869 | 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 | 962 | let modifiers = Modifiers::from_xkb(&state.xkb); |
| 883 | 963 | if state.last_modifiers_changed_event == modifiers { |
| 884 | 964 | drop(state); |
| @@ -893,6 +973,10 @@ impl X11Client { · 8 unmodified lines | ||
| 893 | 973 | ModifiersChangedEvent { modifiers }, |
| 894 | 974 | )); |
| 895 | 975 | } |
| 976 | + | |
| 977 | + if new_layout != old_layout { | |
| 978 | + self.handle_keyboard_layout_change(); | |
| 979 | + } | |
| 896 | 980 | } |
| 897 | 981 | Event::KeyPress(event) => { |
| 898 | 982 | let window = self.get_window(event.event)?; |
| @@ -1281,6 +1365,22 @@ impl X11Client { · 382 unmodified lines | ||
| 1281 | 1365 | drop(state); |
| 1282 | 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 | 1386 | impl LinuxClient for X11Client { |
| @@ -1294,14 +1394,7 @@ impl LinuxClient for X11Client { · 7 unmodified lines | ||
| 1294 | 1394 | |
| 1295 | 1395 | fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> { |
| 1296 | 1396 | let state = self.0.borrow(); |
| 1297 | − let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); | |
| 1298 | − Box::new(LinuxKeyboardLayout::new( | |
| 1299 | − state | |
| 1300 | − .xkb | |
| 1301 | − .get_keymap() | |
| 1302 | − .layout_get_name(layout_idx) | |
| 1303 | − .to_string(), | |
| 1304 | − )) | |
| 1397 | + Box::new(state.keyboard_layout.clone()) | |
| 1305 | 1398 | } |
| 1306 | 1399 | |
| 1307 | 1400 | fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> { |
| @@ -1383,61 +1476,12 @@ impl LinuxClient for X11Client { · 75 unmodified lines | ||
| 1383 | 1476 | ) |
| 1384 | 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.